Tcache stashing unlink attack:
这个利用手法就比前面三种要复杂一点,但是并不是非常复杂,它利用的是在small bin中如果有chunk被分配走后(利用calloc分配的话就可以避开tcache进行分配)在tcache中对应大小的链表内若有空闲位置(少于7个),则会将small bin中的链表按照顺序挂进tcache的对应链表内,由于这个small bin转tcache的过程中只对small bin链表中的第一个元素进行了完整性检查,而对其顺位下的其chunk却没有进行检查,所以我们可以通过修改第一个chunk的bk指针来将我们目标地址的内存挂进tcache中。
那么我们可以总结出这种利用手法的一个思路:
- 拥有一个未满的tcache列表(大小要与small bin的大小匹配)
- 在small bins中挂进两个以上的chunk
- 修改small bin中非第一个chunk的bk指向目标地址
- 利用calloc等函数的机制绕开tcache分配small bins中的chunk
- 触发tcache和unlink机制将目标内存挂进 tcache中
上面的这段阐述可能有点拗口也不好理解,下面给出wiki上的这个例子,用gdb跟踪运行一下就能理解了(做了一些简化):
#include <stdio.h>
#include <stdlib.h>
int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;
printf("stack_var addr is:%p\n",&stack_var[0]);
printf("chunk_lis addr is:%p\n",&chunk_lis[0]);
printf("target addr is:%p\n",(void*)target);
stack_var[3] = (unsigned long)(&stack_var[2]);
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}
for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}
free(chunk_lis[1]);
free(chunk_lis[0]);
free(chunk_lis[2]);
malloc(0xa0);
malloc(0x90);
malloc(0x90);
chunk_lis[2][1] = (unsigned long)stack_var;
calloc(1,0x90);
target = malloc(0x90);
return 0;
}
这个程序的流程稍显复杂,下面来捋一下:
首先是定义了一个stack_var[0x10]的数组作为我们需要完成分配的目标地址,然后定义了一个 chunk_list[0x10]的指针数组,还有一个target指针。
然后将stack_var[3] (也就是这个将被作为fake_chunk的bk参数)置为stack_var[2]的地址。
接着是两个for循环:第一个for循环创建9个0x90大小的chunk,并用这些malloc的指针填充进chunk_list之中(所以chunk_list里面装着的就是这个9个chunk的指针)
第二个for循环释放了chunk_list的后面6个chunk,之后在循环外按1,0,2的顺序释放了其他三个chunk。
之后连续申请(malloc)一个0xa0,两个0x90的chunk。
最后修改chunk_lis[2] [1] 为stack_var的地址后申请一个0x90的chunk(calloc)
让我们进入gdb跟踪一下:
首先将断点下在12行,看一下分配给我们前三个变量的地址:
然后修改stack_var[3]的值:
这里说一下为什么要改这个东西,因为如果将这段内存作为small bin chunk来看的话,这个红框位置上就是bk指针的参数,而后面它作为fake_chunk挂进tcache的时候后面如果bk没有置为一个类似循环链表的情况的话,就会出现错误,所以这个bk是一定要指向自身的。
然后我们执行完两个for循环看一下bins中的情况:
可以看见tcache里面有了6个free_chunk,但是对应链表还没有满(7个是最大数量)
然后我们再按1,0,2的顺序释放剩余的三个chunk,再看一下bins中的情况:
可以看见chunk 1作为最后一个tcache free_chunk挂进了tcache中,而chunk 0和chunk 2由于tcache满了,只能挂进unsortedbin当中,此时我们再次步进,让系统为我们分配一个0xb0大小的内存空间。
由于tcache中没有大小匹配的chunk,unsortedbin中也没有,所以程序只能在top chunk里面割一块出来给我们。而我们前面申请释放的那些chunk也是保持原状,只有一个地方出现了变动:
unsortedbin由于机制问题,在这一次分配后会将自己链表内的chunk归于对应large bin或small bin中,而这个例子中的unsortedbin中的两个chunk就会被挂进small bin中:
之后我们执行第34行代码将chunk_lis[2] [1]修改为我们的目标地址:stack_var的地址。
这里说一下这个chunk_lis[2] [1]是什么意思:其实就是chunk_list中的第3个元素的第2个地址上的内容。这一步也就是完成了上面说到的修改small bins中最后一个chunk的bk参数。来看一下效果:
可以发现bk已经指向了我们的目标地址并且由于我们前面改了fake_chunk的bk指针完成了一个像“收口”一样的操作,之后我们通过calloc来分配一个0x90的chunk,而由于calloc并不会在tcache中分配chunk,所以会直接从small bin中分配。
之后关键的就来了:由于前面我们malloc了两个0x90的chunk,tcache它的链表又不满了,所以这一次分配后会将small bin中的剩余的chunk挂进tcache之中,而small bin是FIFO的进出原则,所以chunk 0就被分配出去了,剩下的chunk 2和它后面连着的fake_chunk就被挂进tcache了。
由于这个fake_chunk正好在tcache第一个位置上,所以我们只要再分配一个0x90的chunk就可以再次启用并控制这个stack_var上的内存
可以看见这个stack_var已经被在启用,指针也赋给了target。这样我们就完成了一次tcache stashing unlink。