tcache stashing unlink attack这种攻击利用有一个稍微绕的点,就是small bin中的空闲块挂进tcache bin这块。弯不大,仔细想想就好了
往期回顾:
好好说话之Tcache Attack(2):tcache dup与tcache house of spirit
好好说话之Tcache Attack(1):tcache基础与tcache poisoning
好好说话之Large Bin Attack
好好说话之Unsorted Bin Attack
好好说话之Fastbin Attack(4):Arbitrary Alloc
(补题)2015 9447 CTF : Search Engine
好好说话之Fastbin Attack(3):Alloc to Stack
好好说话之Fastbin Attack(2):House Of Spirit
好好说话之Fastbin Attack(1):Fastbin Double Free
好好说话之Use After Free
好好说话之unlink
…
编写不易,如果能够帮助到你,希望能够点赞收藏加关注哦Thanks♪(・ω・)ノ
tcache stashing unlink attack
首先从名字就可以看出这种方法与unlink有关,这种攻击利用的是tcache bin中有剩余(数量小于TCACHE_MAX_BINS)时,同大小的small bin会放进tcache中,这种情况可以使用calloc
分配同大小堆块触发,因为calloc分配堆块时不从tcache bin中选取。在获取到一个smallbin中的一个chunk后,如果tcache任由足够空闲位置,会将剩余的smallbin挂进tcache中,在这个过程中只对第一个bin进行了完整性检查,后面的堆块的检查缺失。当攻击者可以修改一个small bin的bk时,就可以实现在任意地址上写一个libc地址。构造得当的情况下也可以分配fake_chunk到任意地址
例题:how2heap中的tcache stashing unlink attack
例题源码如下,稍作改动,去掉了一些不影响执行流程的输出代码:
1 //gcc -g -no-pie hollk.c -o hollk
2 //patchelf --set-rpath 路径/2.27-3ubuntu1_amd64/ hollk
3 //patchelf --set-interpreter 路径/2.27-3ubuntu1_amd64/ld-linux-x86-64.so.2 hollk
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <assert.h>
7
8 int main(){
9 unsigned long stack_var[0x10] = {0};
10 unsigned long *chunk_lis[0x10] = {0};
11 unsigned long *target;
12
13 setbuf(stdout, NULL);
14
15 printf("stack_var addr is:%p\n",&stack_var[0]);
16 printf("chunk_lis addr is:%p\n",&chunk_lis[0]);
17 printf("target addr is:%p\n",(void*)target);
18
19 stack_var[3] = (unsigned long)(&stack_var[2]);
20
21 for(int i = 0;i < 9;i++){
22 chunk_lis[i] = (unsigned long*)malloc(0x90);
23 }
24
25 for(int i = 3;i < 9;i++){
26 free(chunk_lis[i]);
27 }
28
29 free(chunk_lis[1]);
30 free(chunk_lis[0]);
31 free(chunk_lis[2]);
32
33 malloc(0xa0);
34 malloc(0x90);
35 malloc(0x90);
36
37 chunk_lis[2][1] = (unsigned long)stack_var;
38 calloc(1,0x90);
39
40 target = malloc(0x90);
41
42 printf("target now: %p\n",(void*)target);
43
44 assert(target == &stack_var[2]);
45 return 0;
46 }
简单的描述一下这个程序的执行流程:首先创建了一个数组stack_var[0x10]
,一个指针数组chunk_lis[0x10]
,一个指针target
。接下来调用setbuf()函数进行初始化。接着调用printf()函数打印stack_var、chunk_lis首地址及target的地址。接下来将stack_var[2]
所在地址放在stack_var[3]
中。接着循环创建8
个size为0xa0
大小的chunk,并将八个chunk的malloc指针依序放进chunk_lis[]中。然后根据chunk_lis[]中的堆块malloc指针循环释放6
个已创建的chunk。接下来依序释放chunk_lis[1]、chunk_lis[0]、chunk_lis[2]
中malloc指针指向的chunk。然后连续创建三个chunk,第一个size为0xb0
,第二个size为0xa0
,三个size为0xa0
。接下来将chunk_lis\[2]\[1]
位置中的内容修改成stack_var
的起始地址,接着调用calloc()
函数申请一个size为0xa0
大小的chunk。最后申请一个size为0xa0
大小的chunk,并将其malloc指针赋给target变量,并打印target
由于我们在编译的时候使用了-g
参数,所以在使用gdb进行调试的时候可以在代码行下断点。首先我们在第19
行下断点,查看一下打印出来的各个变量的地址:
可以看到stack_var的起始地址为0x7fffffffdde0
,chunk_lis的起始地址为0x7fffffffde60
,target的起始地址为0x7ffff7de01ef
。这里建议拿个小本本记一下这三处地址,后面在查看内存变化的时候会反复查看,也可以现在就看一下三个地址内部的情况。接下来我们将断点下在第21
行,使程序执行stack_var[3] = (unsigned long)(&stack_var[2]);
这段代码:
这里我们看一下stack_var数组内部的情况,在stack_var[3]位置中的内容被修改成了stack_var[2]的地址,接下来我们将断点下在第29
行,执行两个for循环:
这里强调一下,第二个for循环中起始释放的chunk的下标为3
,所以释放是从第4
个chunk开始的。上图是释放之后bin中的情况,由于我们使用patchelf将程序执行glibc的版本修改为2.27
,所以存在tcache机制。被释放的chunk会进入tcache bin中,由于在第一个for循环中我们创建的都是size为0xa0
大小的chunk,所以释放之后都会进入tcache bin中0xa0
这条单项链表中。这里注意看链表中其实只有6
个被释放块,但是tcache链表存放被释放块数量的最大值为7
,所以此时tcache并不是满状态
接下来将断点下在第33
行,依序释放chunk_lis[1]、chunk_lis[0]、chunk_lis[2],我们再来看一下bin中的情况:
可以看到在释放chunk_lis[1]的时候chunk2
作为最后一个进入tcache的chunk填满了整条链表,接下来再继续释放size为0xa0
的堆块的话就不会在进入此条单向链表了。由于chunk_lis[0]、chunk_lis[2]中malloc指向的chunk的size都为0xa0
,所以超过fastbin max size
,所以会进入unsorted bin中,上图可以看到此时chunk1与chunk3已经进入了unsorted bin中。接下来我们将断点下在第34
行,申请一块size为0xb0大小的chunk,我们在看一下bin中的情况:
由于unsorted bin存取机制的原因,如果此时申请一个size为0xb0
大小的chunk,unsorted bin中如果没有符合chunk size的空闲块(chunk3、chunk1的size小于0xb0),那么unsorted bin中的空闲块chunk3和chunk1会按照size落在small bin的0xa0
链表中。接下来我们将断点下在第37
行,完成两次申请size为0xa0
大小的chunk
这样一来由于tcache bin中又满足size为0xa0
的空闲块,所以chunk2和chunk4就被重新启用了。那么此时bin中就形成了tcache bin中存在5
个空闲块,small bin中存在2
个空闲块的情况了,后面去讲为什么这样去部署。接下来我们将断点下在第38
行,执行chunk_lis[2][1] = (unsigned long)stack_var;
这条语句:
chunk_lis[2][1] = (unsigned long)stack_var;
这条语句是这样执行的,首先chunk_lis[2]的位置就是存放chunk3的malloc指针的位置,那么chunk_lis[2][1]指的就是以chunk3头指针为起始位置,向后第二个地址位宽的位置,即chunk3的bk
位置。chunk3_bk中的内容就被修改成了stack_var
的头指针,这个时候我们可以看一下bin中的状况:
由于chunk3是unsorted bin中最后一个chunk,且chunk3的bk被修改成了stack_var的头指针,所以,stack_var会被认为是紧跟着chunk3之后释放的一个chunk:
那么接下来我们将断点下在第40行,调用calloc函数申请一个size为0xa0大小的chunk:
这里说明一下为什么要使用calloc
进行申请chunk,这是因为calloc在申请chunk的时候不会从tcache bin中摘取空闲块
,如果这里使用malloc的话就会直接从tcache bin中获得空闲块了。那么在calloc申请size为0xa0
大小的chunk的时候就会直接从small bin
中获取,那么由于small bin是FIFO
先进先出机制,所以这里被重新启用的是chunk1
这个时候就到了前面理论部分描述的内容了:在获取到一个smallbin
中的一个 chunk 后会如果 tcache 仍有足够空闲位置(tcache中有两个位置,chunk3和stack_var刚好够落在这两个位置),剩下的 smallbin 从最后一个 stack_var
(0x7ffffffddf0)开始顺着bk
链接到 tcachebin 中 ,在这个过程中只对第一个 chunk3
进行了完整性检查,后面的stack_var的检查缺失
。这样一来就造成上图的效果,stack_var就被挂进了tcache bin的链表中
接下来我们将断点下在第44行,最后申请一个size为0xa0
大小的chunk,并将其malloc指针赋给target变量,并打印target:
由于stack_var处于tcache链表的最后一个,所以在申请size为0xa0大小的chunk的时候,stack_var就会被重新启用
例题后补~