unlink

1. 核心原理:glibc 的堆块管理机制

glibc 中,空闲堆块通过双向链表组织(fdbk 指针)

1
2
3
4
5
6
struct malloc_chunk {
size_t prev_size; // 前一块大小
size_t size; // 当前块大小 + 标志位
struct malloc_chunk *fd; // 前向指针(指向链表中前一个空闲块)
struct malloc_chunk *bk; // 后向指针(指向链表中后一个空闲块)
};

当释放堆块时,glibc 会执行 unlink 操作将其从空闲链表移除:

1
2
3
4
5
6
7
// 简化版 unlink 宏
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK; // 关键操作:任意地址写
BK->fd = FD; // 关键操作:任意地址写
}

2. 攻击条件

  • 堆溢出漏洞:可覆盖相邻堆块的头部数据(prev_sizesize
  • 可控内存:能伪造堆块结构(控制 fdbk 指针)
  • 触发 unlink:需通过 free() 或堆合并触发目标堆块的 unlink 操作

3. 攻击步骤图解

步骤 1:伪造堆块结构

假设存在堆块 A(易溢出)和 B(目标),在 A 中伪造一个空闲堆块:

1
2
3
4
5
6
7
8
     伪造的堆块 P
+----------------+
A->data: | prev_size |
| size (含 PREV_INUSE=0) | -- 标记前一块为空闲
| fd = target - 3*sizeof(void*) |
| bk = target - 2*sizeof(void*) |
+----------------+

步骤 2:修改相邻堆块头

通过堆溢出修改 B 的头部:

1
2
B->prev_size = 伪造堆块大小  // 使系统认为 P 是空闲块
B->size &= ~PREV_INUSE // 清除 PREV_INUSE 标志位

释放堆块 B 时,glibc 会:

  1. 检查 B->prev_inuse=0,认为前一块 P 空闲
  2. 尝试合并 PB,触发 unlink(P)

执行 unlink 操作时:

1
2
3
4
5
6
7
8
9
FD = P->fd = target - 0x18
BK = P->bk = target - 0x10

// 关键写操作:
FD->bk = BK --> *(target - 0x18 + 0x18) = target - 0x10
即 *target = target - 0x10

BK->fd = FD --> *(target - 0x10 + 0x10) = target - 0x18
即 *target = target - 0x18

最终 *target 被修改为 target - 0x18

4. 现代 glibc 的防护与绕过

防护机制(Safe-Unlinking)

1
2
3
// glibc 2.3.6+ 的检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");

绕过方法

构造满足检查的伪造指针

1
2
3
4
5
6
P->fd = target - 0x18
P->bk = target - 0x10

// 提前在内存中布置:
*(target - 0x18 + 0x18) = P // 使 FD->bk == P
*(target - 0x10 + 0x10) = P // 使 BK->fd == P

5. 实战利用场景

场景:修改 GOT 表执行 shellcode

  1. 选择目标:free@got.plt
  2. 构造 target = free@got.plt
  3. 触发 unlink 后:*free@got.plt = free@got.plt - 0x18
  4. 通过堆操作写 free@got.plt 区域:
1
2
# 此时 free@got.plt 指向自身 -0x18
write(free@got.plt + 0x18, shellcode_addr)