• 熟悉了解tcache_bin的运行过程之后就可以开始学习house of spirittcachebin中的利用进行学习了

  • 之前以及介绍过了house of spiritfastbin中的利用,知道了house of spirit就是通过伪造堆块,将这个伪造的堆块通过free函数,链接到fastbin中。而本篇文章所介绍的是使用house of spirit将伪造的堆块链在tcachebin中。

利用方式

  • glibc2.27中比较之前的版本,在free()一个堆块后,如果该堆块放入的是tcache中,是没有做一些检查的。这时就可以对通过伪造一个堆块。然后修改free(p)中的指针p,使得指针p指向的是我们伪造的堆块地址。

  • 这样我们就可以将伪造的堆块放入tcache_bin中,当我们下次使用malloc()函数申请适当的内存空间时,这个堆块就会被我们申请回来。这样我们就可以进行我们伪造的堆块,这一内存空间进行任意的写。如果是在栈上伪造堆块的话,

  • 常见的伪造区域就是:在上伪造、.bss段上伪造堆块、在malloc_hook的位置伪造堆块

  • 利用过程如图所示:

image-20250124164617495

  • 伪造了一个fake_chunk,此时我们可以通过溢出或者写入,可以修改free(ptr)这个ptr的指针,使其指向我们伪造的堆块fake_chunk

image-20250124164637586

  • 之后我们调用free(ptr),这样我们的fake_chunk就可以被放入tcache_bin,放入后的tcache_bin的结构图如下:

image-20250124164911336

  • 下次调用malloc函数时,申请适当大小的堆块,就可以将fake_chunk给申请回来,就可以向fake_chunk中写入数据。

image-20250124165206165

伪造条件

  • prev_size位的大小是任意的,因为prev_size要有效,就要size中的p位为0,这样才使得prev_size位有效,而在tcache_bin中是不关心size中的p位。
  • 对于size位和size位中的三个标志位:
    • 对于size位,这个需要将该位的范围修改在0x200x410直接,这个size位必须与0x10对齐,size位一定要是我们下次能使用malloc申请的大小。
    • 对与P位,tcache_chunk会忽略该位,此时该位可以0也可以1
    • 对于M位和N位(第2、第3标志位),这两个标志位一定要伪造成0
  • 对于prev_size的起始地址,要满足0x10对齐。

image-20250124175957370

实验

  • 该实验代码是来自how2heapglibc2.27tcache_house_of_spirit.c
源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
setbuf(stdout, NULL);

printf("This file demonstrates the house of spirit attack on tcache.\n");
printf("It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n");
printf("You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n");
printf("(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");

printf("Ok. Let's start with the example!.\n\n");


printf("Calling malloc() once so that it sets up its memory.\n");
malloc(1);

printf("Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n");
unsigned long long *a; //pointer that will be overwritten
unsigned long long fake_chunks[10]; //fake chunk region

printf("This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);

printf("This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
printf("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
fake_chunks[1] = 0x40; // this is the size


printf("Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
printf("... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");

a = &fake_chunks[2];

printf("Freeing the overwritten pointer.\n");
free(a);

printf("Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
void *b = malloc(0x30);
printf("malloc(0x30): %p\n", b);

assert((long)b == (long)&fake_chunks[2]);
}
  • 接下来翻译一下这段代码,顺便审一下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
setbuf(stdout, NULL);

printf("这个代码展示了house of spirit 在tcache中的攻击.\n");
printf("它的利用方式和原来的house of spirit利用相似,但是你并不需要伪造一个将被释放的假堆块.\n");
printf("你能在malloc.c的源码中看到,在函数_int_free中调用tcache_put时并没有检查next_chunk的size位和prev_inuse位是否合理.\n");
printf("(搜索字符串\"invalid next size\" 和 \"double free or corruption\")\n\n");

printf("现在来展示一个例子.\n\n");


printf("先调用malloc()函数Calling malloc()防止之后释放的堆块合并入内存.\n");
malloc(1);

printf("让我们想象一下我们将覆盖1个指向伪造堆块区域的指针.\n");
unsigned long long *a; //将被覆盖的指针
unsigned long long fake_chunks[10]; //伪造堆块的区域

printf("这个区域包含了一个伪造的堆块. 它的size位所在位置为 %p\n", &fake_chunks[1]);

printf("这个堆块的size一定会落在tcache的范围(chunk.size <= 0x410; malloc arg <= 0x408 on x64). PREV_INUSE(lsb)这一位会被tcache_chunks忽略, 然而IS_MMAPPED bits(second lsb)和NON_MAIN_ARENA (third lsb)会导致一些问题.\n");
printf("... 请注意这个位必须是之后将堆块使用malloc申请回来的大小,并且这个大小应该为malloc内部实现的大小(也就是0x10对齐在x64中,或者是在x86中0x8对齐),任何我们申请的非对齐的内存大小,都会在调用malloc时进位(即申请更大的一个能满足对齐的堆块). 例如,在x64下,0x30-0x38malloc都将会申请0x40大小的堆块,所以他们最后都会以malloc函数内部的参数运行. \n");
fake_chunks[1] = 0x40; // this is the size


printf("现在我们将覆盖我们的指针,将使用伪造堆块内部的的第二个假区域块地址(也就是第一个索引的地址)覆盖我们的指针,该堆块的地址为 %p.\n", &fake_chunks[2]);
printf("... 注意这块关联的内存地址必须与16字节对齐.\n");

a = &fake_chunks[2];

printf("释放被覆盖的指针.\n");
free(a);

printf("现在下一个申请堆块将返回的堆块的区域是我们所伪造的堆块,位置如下 %p, 用户使用的区域为 %p!\n", &fake_chunks[1], &fake_chunks[2]);
void *b = malloc(0x30);
printf("malloc(0x30): %p\n", b);

assert((long)b == (long)&fake_chunks[2]);
}
  • ubuntu18.04glibc2.27版本,使用gcc编译该程序,对该程序进行动态调试。
  • 直接使用ni将程序执行到free(a)这边(free(a)还没有执行完),在执行free(a)之前,我们先使用heap查看堆块,再使用tcachebin查看tcachebin中管理的空闲堆块,并且使用stack命令查看栈上伪造的chunk

image-20250124180202557

image-20250124180124281

image-20250124180136631

  • 栈上伪造的chunk位置和内容如下:

image-20250124180317417

image-20250124180532506

  • 之后执行free(a),再使用tcachebin查看tcachebin管理的空闲链表。

image-20250124180633016

  • 再使用heap命令,看到如下堆块内容。

image-20250124180732338

  • 这时我们再执行完malloc,查看tcachebinheap

Snipaste_2025-01-24_18-08-37

tcachebin_spirit_level_1