【堆利用】heap利用技术
总结一下自己遇到过的ctf heap 类的利用方法,把脚本和思路收集一下。
Unlink技术
第一次接触是在强网杯2018,题目:silent
unlink从最初的没有检查机制,到现在的检查前后chunk,使得unlink的利用变得更加复杂,具体内容参见ctfwiki。检查内容如下:
1 | // fd bk |
如下图所示,要绕过检查,必须有一个已知指针P保存了被unlink的chunk,
举个比方。想要在free(chunk2)时unlink chunk1,必须有一个已知指针P保存了chunk1的地址,并把chunk1的fd指针和bk指针都指向与P有关的位置。原理如下图:
最后,P中存储的值会被更改为 P-0x18。回到题目上来。
程序功能:
- malloc一个块,可以指定size大小,读入字符串
- free一个块,指针没有清空
- 更新一个块的信息,覆盖之前的数据,覆盖的长度取决于strlen
题目分析/利用步骤
用unlink的思路来解决这道题。
1 | add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#0 |
先申请3个块,占据bss段中s的前三个8B,构造 s - 0x18 + 3*0x8 = s
在这里我们将P指向 s+3*0x8 (0x6020C0 + 3*0x8),是为了避免P指向 s时,对其进行修改操作时 strlen()返回值为 0,即无法修改。
1 | add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#0 |
先申请3个块,占据bss段中s的前三个8B,构造 s - 0x18 + 3*0x8 = s
1 | add(0x100,"a"*0xff)#3 |
申请块3,4(释放后属于unsorted bin,可以合并),申请5的目的是防止3和4合并后一路合并到topchunk
1 | bss = 0x6020c0 + 0x18 |
这一段非常重要,涉及到伪造chunk
free掉3,4块之后,会被合并为一个大chunk,其大小为(包括chunkhead):0x100+0x10+0x100+0x10 = 0x220。所以我们申请0x210大小的chunk时,会申请到之前3,4合并的地址。
我们在这个0x220大小的chunk中,伪造两个chunk,分隔恰好制造为 s[4]指针指向的地方。再布置好s[3]chunk里的fd,bk,就可以进行一次unlink。
解释一长串的字符串:
p64(0x0) + p64(0x101) + p64(fd) + p64(bk) :构造第一个假chunk,其大小为之前的大小(0x100+0x10)减去0x10字节(损失了一个chunk head)。构造0x101表示前一堆块正在使用,不合并。
"b" * (0x100 - 0x20)
:充填。
p64(0x100) + p64(0x90) + "c"*0x80
:构造prev_size和size位,添加充填。
p64(0) + p64(0x21) + "a"*0x10
:构造第三个chunk
p64(0) + p64(0x21) + "d"*(0x210-0x1c0-1)
:构造第四个chunk
为啥要构造第四个chunk?
为了检查第三个chunk是否在使用。
1 | free(4) |
释放掉chunk4,unlink完成,s[3]的指针被修改为&s
0x602018是free函数的got表地址。
update(3) 将s[0]的指针修改为free函数的got表地址
update(0) 将free函数的got表值修改为system函数值
这样之后释放 chunk2时,调用free(s[2])就等于调用system(s[2]),
而 s[2] 中存字符串 “/bin/sh\x00”,所以调用delete(2)会弹shell。
exp如下:
1 |
|
Fastbin Attack
一个简单粗暴的技术,可以让fastbin分配到(伪)任意位置,从而实现(伪)任意地址写。然而2.27版本的tcache毒化比这个更加无脑,显得fastbin有些鸡肋了,但并不影响fastbin attack的重要地位。
这里随便找了个题演示。题目链接
题目分析
利用unsortedbin泄漏libc地址,将fake chunk布置在libc的malloc_hook之前
unsorted bin只有一个块时,其前后项指针都会指向位于libc中的main_arena中。我们增加一个advertisement之后再释放,将其置入unsortedbin,然后增加一个poster,此时会从unsortedbin中切出poster大小的chunk。由于系统并未清除chunk中的信息,我们读出后可以得到libc的地址信息。
利用fastbin的double free,我们在libc的malloc_hook之前的位置构造一个伪chunk,欺骗系统将其分配给我们,从而可以修改malloc_hook,改为one_gadget的地址。在调用malloc之前执行,从而控制程序流,getshell。
利用步骤
确定main_arena的偏移,获得libc地址:
unsortedbin只有一个块时,其前后指针都会指向main_arena,位于libc中。我们先确定main_arena在libc中的偏移。
寻找malloc_trim函数,ida中反汇编如下:
即偏移地址是0x3c4b20
1
2
3
4
5
6
7
8
9
10#申请一个ad,放入unsortedbin中
ad('abcdefgh') #0
post('abcdefgh') #1
delete(0)
#读出libc地址
main_arena = 0x3c4b20
post('deadbeef')
show()
p.recvuntil('deadbeef')
arena_addr = u64(p.recv(6)+'\x00\x00')但是在实际调试的过程中发现算出的地址和实际的地址相差0x100个字节
经过调试,在申请unsortedbin的chunk之前,该chunk保存的前后指针如下,是main_arena+88
但是申请之后,得到的chunk内部数据是:
增加了0x100。可能是因为这里对unsortedbin进行了切割,因此修改了内部的数据。
所以在泄漏地址时额外减去0x100即可。
1
libc_base = arena_addr - main_arena - 88 - 0x100
确定fake chunk应该布置的位置
找到malloc_hook的位置,前面有libc的地址,高位字节是0x7f,可以利用fastbinattack来申请到该位置的内存。
在malloc_hook-0x3-0x10的位置作为fastbin的chunkhead
1
2
3
4
5
6
7
8
9#进行fastbin double free
post('2222222') #2
post('3333333') #3
delete(2)
delete(3)
delete(2)
#将fastbin的fd指针修改为malloc_hook-0x3-0x10
post(p64(libc_base + libc.symbols['__malloc_hook'] - 0x10 - 0x3)) #4
确定one_gadget的地址
one_gadget1 = 0x45216 + libc_base
one_gadget2 = 0x4526a + libc_base
one_gadget3 = 0xf02a4 + libc_base
one_gadget4 = 0xf1147 + libc_base
依次尝试,最后发现one_gadget3可以用。
1
2
3
4
5
6post('/bin/sh')#5
post('/bin/sh')#6
post('\x00\x00\x00'+p64(one_gadget3))
delete(5)
p.interactive()
完整exp如下
1 | # -*- coding:utf-8 -*- |
off by null
一般情况下,需要能申请的 chunk 大于 0x100,这样 off by null 可以溢出覆盖 prev_inuse
标志位,否则直接将整个 chunk size 覆盖为 0 了。
先上一道特殊的题:hctf2018 heapstorm_zero
利用分析
本题限制了申请 chunk 必须小于等于 0x38,因此必定是 fastbin,此时只有 offbynull 不足以利用。
tip1:
scanf()
函数在读取字符串时,如果字符串较大,会调用 malloc 申请一个块大内存来暂时保存。tip2:malloc 分配内存时,先检查 fastbin, smallbin, unsortedbin, largebin,如果都没有合适的 chunk,则会分割 top_chunk。然而此时会触发堆块合并,会尝试将相邻的 fastbin 合并,保存至 unsortedbin 中。
所以,申请 10 个 fastbin chunk,释放后通过 scanf 分配一个超大 chunk,触发malloc_consolidate 将 fastbin chunk 合并,从而在 unsortedbin 中得到一个大 chunk。
1 | ... |
触发 consolidate 之前:
理论上,合并后的 chunk 大小为 10 * 0x8 + (0x28*3 + 0x18*2 + 0x38*5) = 0x210