前言
有幸和校队一起参加21年5月的xctf-final。笔者爆肝连续战斗十小时,在与队友各种讨论下艰难做出了house of pig这一题。事后看出题人的wp,方法不尽相同但也是收获良多,特此记录。
笔者的这题解法是没有large bin attack的(知识盲区),利用tcache stashing unlink写global_max_fast,使用fast bin attack申请到_rtld_global附近的内存,劫持exit_hook为one_gadget。其中_rtld_global的地址偏移是个巨坑。
题目的漏洞与功能
这里为了方便阅读,我简化了一个c语言版本。程序可以UAF,可以泄露libc与堆地址。问题在于没有malloc()只有calloc(),并且其大小受到限制。libc版本2.31。
注: calloc()是不走tcache的。无论tcache有无chunk,都会走到_int_malloc()中。
char* ptrs [100] = {0}; int sizes[100] = {0}; int freeable[100] = {0};
int main(){
int choice = 0, index=0, size=0;
while(1){
puts("menu"); scanf("%d", &choice);
switch(choice){
case 1: // malloc no out_of_bound
scanf("%d%d", &index, &size);
if(index>=0 && index<100 && size>=0x90 && size<=0x450){
ptrs[index] = (char*)calloc(1, size);
sizes[index] = size;
freeable[index] = 1;
read(0, ptrs[index], size-1);
ptrs[index][size-1] = 0;
}
break;
case 2: //edit use_after_free, no out_of_bound
scanf("%d%d", &index, &size);
if(index>=0 && index<100 && size>0 && size<sizes[index]){
read(0, ptrs[index], size);
}
break;
case 3: //free no double_free
scanf("%d", &index);
if(index>=0 && index<100 && sizes[index]>0){
free(ptrs[index]);
freeable[index] = 0;
puts(ptrs[index]); //can leak libc
}
break;
default: exit(0);
}
}
}
tcache stashing unlink
正经的参考文档,heap_exploit_2.31,其中有poc。学习poc的好办法就是用gdb自己走一遍。这里我主要在libc源码中分析此漏洞的利用的逻辑。
在_int_malloc()中,tcache有这样一段逻辑:
设需求的size为nb个字节;
如果nb大小的tcache不满( 小于7 ),并且有2个以上nb大小的freed chunk 在smallbin中;
在_int_malloc(av, nb)过程中,会尝试把剩下的nb大小的smallbin放到tcache中
。
static void *
_int_malloc (mstate av, size_t bytes) {
...
if (in_smallbin_range (nb)) {
...
if ((victim = last (bin)) != bin) {
...
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
//①: 如果tchace不满
if (tcache && tc_idx < mp_.tcache_bins) {
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
//②: tcache不满且smallbin还有剩,则进入循环
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin) {
if (tc_victim != 0) {
//③: bk是攻击者控制的,故bck是目标地址附近的内存。这里没有double link check
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
//④: 一个目标地址的写操作
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
}
}
...
}
tcache stashing unlink还有一些变种,但是出问题的逻辑在此。简单来说,在这里的类似unlink操作,没有像smallbin的unlink操作检查前驱或后继节点的合法性。
这样的攻击至少在2019年就已经出现在国外的ctf赛事中了,但是至今仍未打上补丁。
large bin attack
正经的参考文档,heap_exploit_2.31,其中有poc。学习poc的好办法就是用gdb自己走一遍。这里我主要在libc源码中分析此漏洞的利用的逻辑。
当unsorted bin的一个chunk进入large bin时,large bin的链表就尝试加入这个bin
于是就有
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
其中fwd->fd
的chunk是攻击者控制的,其bk_nextsize可以设成(targetAddr-0x20)的位置
static void *
_int_malloc (mstate av, size_t bytes) {
...
//① 遍历unsorted bin,依次取出放入对应的位置
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) {
...
//② 注意main_area->last_remainder,不然直接split了
...
if (in_smallbin_range (size)) {
...
}
else {
/* 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));
//③ 需要走到这个分支,另一个分支已经有double link检查了,无法利用
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk)) {
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
//④ 利用的关键两行代码 fwd->fd指向的是可控的内存
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
...
}
...
}
}
}
...
}
有了上述方法,然后做什么
- 思路1
出题人思路:
- 使用large bin attack把free_hook附近写入一个堆上的地址
- 在1的条件下可以使用tcache stashing unlink+,把free_hook这块chunk放入tcache中
- 第二次使用large bin attack覆盖_IO_list_all为一个堆上地址,并在这个堆上构造好_IO_FILE
- 触发_IO_str_overflow(),依次执行malloc@plt, memcpy@plt, free@plt。其中三个参数都是可控的,与_IO_FILE内部数据有关。于是效果成为:
free_hook = malloc()
memcpy(free_hook, system_addr , n)
free("/bin/sh")
- 思路2
一个参赛选手的思路:
- 使用large bin attack 把 _rtld_global_ptr覆盖成堆上的地址, 并在这个堆上构造好rtld_global结构体数据。(又被称为 house of banana)
- 利用
__rtld_lock_lock_recursive
或者_dl_rtld_unlock_recursive
(俗称exit_hook)为one_gadget。
- 思路3
笔者的思路:
- 使用tcache stashing unlink,把global_max_fast覆盖成一个很大的数。为了绕开大小限制
- fast bin attack,calloc()到rtld_global附近,覆盖__rtld_lock_lock_recursive为one_gadget。
关于fastbin attack。因为fastbin会有chunk size的检查,但是不检查对齐。但是题目大小限制,不能常规的malloc()到0x7f如此的chunk。但是可以malloc到_IO_FILE内,_IO_FILE内有0xff的chunk size。当然也可以calloc()到rtld_global附近。
这个有个大坑就是rtld_global的地址与libc的偏移,在libc-2.31是与环境相关的,而libc-2.27及以下似乎没有,原因不明。故实际解题的操作是,先打stdout泄露出线上环境的偏移,再第二次打rtld_global劫持为one_gadget。
- 思路4
笔者在赛场上未完成的思路,实际上应该是可以的。同样不需要large bin attack。
- 使用tcache stashing unlink,把global_max_fast覆盖成一个很大的数。
- fast bin attack,calloc()到stdout附近,完成第3步后,使用edit()功能,修改jump table为_IO_str_jump
- fast bin attack,calloc()到malloc_hook附近,设置为setcontext+61的位置。(libc-2.27及以前是setcontext+53)。
- 触发_IO_str_overflow, 进行srop。
其中关键是,_IO_str_overflow()中malloc@plt之前,rdx寄存器是与_IO_FILE结构体有关的,受攻击者控制。