Unlink
释放堆
释放堆时会判断当前 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 */ |
流程
- 检查指针 p 指向的 this_chunk 的 size 字段的 PREV_INUSE 位 是否为 0, 为 0 表示 pre_chunk 已经被 free, 则进入向后合并流程
- 获取 pre_chunk 的 size, 加到 size 中, 以此来表示 size 已经合并
- 根据 this_chunk 的 PREV_SIZE 位来获得 pre_chunk 的指针
- 将这个指针传入 unlink 宏, 即让 pre_chunk 进入 unlink 流程
向前合并
代码
malloc.c 中向前合并的代码如下:
4284 if (nextchunk != av->top) { |
流程
检查 next_chunk 的信息
如果 next_chunk 不是 top chunk 且 next_chunk 的 next_chunk 的 PREV_INUSE 为 0, 则进入向前合并流程
如果 next_chunk 不是 free 的, 则修改他的 size 字段的 PRE_INUSE 位
如果 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 }检测 next_chunk 是否 INUSE, 是通过inuse_bit_at_offset(nextchunk, nextsize) 来获得next_chunk 的 next_chunk 的size字段的presize位实现的。
让 next_chunk 进入 unlink 流程
size += next_size , 表示大小上两个 chunk 已经合并
Unlink
代码
unlink是个宏, 但是在读代码的时候请把bk和fd当作变量。
- p 是指向当前执行 unlink 操作的 chunk 的指针
- 以下为 glibc-2.19 中 unlink 宏的代码
|
流程
检查 P 的 size 字段 和 P 的 next_chunk 中记录的 PREV_SIZE 是否一致, 不一致则出现corrupted size vs. prev_size的错误
检查是否满足 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 可任意命令执行。解链:FD->bk = bk 和 BK->fd = fd
以下是CTF-Wiki的图
- 如果此时 P 的 Fd 和 BK 被我们修改为 FD 的地址
接着对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 /* |
Demo
libc version: 2.23
代码
|
编译与运行
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
运行结果:
分析
开启 gdb 调试 , 用
--silent
去掉开头一堆免责信息gdb unlink_demo --silent
由于有源文件, 因此我们可以直接对行号进行下断, 15行为
int main()
所在位置pwndbg> b main
我们用
ni
指令(表示单步步过)运行到第26行, 即 malloc 两块chunk后, 查看heap
信息然后在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
然后修改好 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;这时候 free 掉 chunk2 的话就会触发向后合并。
free(chunk2)
此时 unsorted bin 中
本来应该只有 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 的基址
此时指向 chunk1 的指针内容为 chunk1 的地址, 而chunk1[3] 即 chunk1->fd 的位置的值为该指针的地址, 相当一个二级指针(unlink 的解链操作造成的)
此时修改 chunk1[3] 的内容为 data 的地址, 即是修改指向 chunk1 的指针的值
chunk[3] = data
strcpy(data, "victim's data")可以看到此时指向 chunk1 的指针已经被修改为指向 data
此时修改 chunk1[0] 其实就是在修改 data 的值
chunk1[0] = 0x002164656b636168LL;
这便是使用 unlink 机制实现任意地址写操作
总结
我们可以利用 unlink 机制来实现:
- leak main_arena 的地址从而计算 libc 的地址
- 泄露 heap address 的地址
- 实现任意地址写操作
实现以上操作需要绕过 unlink 的安全检查机制, 即当我们 unlink this_chunk 时我们需要
程序中存在一个全局指针变量 ptr
ptr 指向的对内存可由用户控制, 使其变为 this_chunk 的地址
然后使得 this_chunk 的 fd 和 bk 变为
ptr - 0x10
和ptr - 0x18