第 7 章 符号执行与模拟执行
静态分析费时费力时,让"机器替你暴力分析"——这是符号执行(angr)和模拟执行(Qiling/Unicorn)的舞台。解决 flag 校验器、复杂算法、跨架构样本的捷径。
📍 知识点地图 | 主题:符号执行与模拟 | 前置:第4-5章 | 后续:第39章 | 核心概念:angr、Z3、Qiling、Unicorn
7.1 概念对比
| 技术 | 原理 | 擅长 | 代表工具 |
|---|---|---|---|
| 符号执行 | 把输入当符号变量,沿路径求解满足条件的值 | 自动解 flag 校验、逆向算法 | angr, Triton, Manticore, Z3 |
| 模拟执行 | 用引擎执行指令,不依赖真实 OS | 跑跨架构/脏环境样本 | Qiling, Unicorn, QEMU |
| 传统执行 | 真 OS 跑真进程 | 需要环境 | 直接运行 |
7.2 angr(符号执行入门)
经典场景:自动解 flag
import angr
p = angr.Project("./binary", auto_load_libs=False)
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)
simgr.explore(find=0x401234, avoid=0x401111) # find=成功打印地址, avoid=失败地址
if simgr.found:
found = simgr.found[0]
flag = found.posix.dumps(0) # 拿到让程序走成功分支的输入
print(flag)
带约束的符号输入(更贴近实战)
import angr, claripy
p = angr.Project("./checker", auto_load_libs=False)
state = p.factory.entry_state()
# 创建 32 字节符号输入,约束为可打印 ASCII + flag{ 前缀
flag = claripy.BVS("flag", 32 * 8)
for i in range(32):
state.solver.add(flag.get_byte(i) >= 0x20)
state.solver.add(flag.get_byte(i) <= 0x7e)
state.solver.add(flag.get_byte(0) == ord('f'))
state.solver.add(flag.get_byte(1) == ord('l'))
simgr = p.factory.simulation_manager(state)
simgr.explore(find=0x401234, avoid=0x401111)
if simgr.found:
print(simgr.found[0].solver.eval(flag, cast_to=bytes))
Hook 函数降低路径爆炸
# 把耗时/复杂函数替换成摘要,避免路径爆炸
p.hook(0x401000, angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained']())
实战提示
- DFS 替代 BFS(flag checker 路径深、分叉多):
simgr.use_technique(angr.exploration_techniques.DFS()) - 限制符号内存操作:减少
claripy复杂度 - 超时保护:
simgr.run(n=1000)或探索技术LengthLimiter - 从中间地址开始:跳过初始化,直接设置寄存器/内存状态
常见模式
| 模式 | 做法 |
|---|---|
| argv 输入 | state = p.factory.full_init_state(args=["./x", claripy.BVS("a", n)]) |
| 找不到 find 地址 | 用 find=lambda s: b"Correct" in s.posix.dumps(1) |
| 有 sleep/反调试 | Hook 掉再探索 |
| 路径爆炸 | DFS + Hook 昂贵函数 |
7.3 Z3(约束求解器)
当逻辑简单、只需解方程时,直接用 Z3 更快:
from z3 import *
flag = [BitVec(f"f{i}", 8) for i in range(8)]
s = Solver()
for i in range(8):
s.add(flag[i] >= 0x20, flag[i] <= 0x7e)
# 程序逻辑转约束,例如 flag[3] + flag[7] == 0xAB (mod 256)
s.add(flag[3] + flag[7] == 0xAB)
if s.check() == sat:
m = s.model()
print(bytes(m.eval(f).as_long() for f in flag))
7.4 Qiling(跨平台模拟执行)
比 Unicorn 高一层:它模拟整个系统环境(加载器、libc、系统调用),可以直接"跑"ELF/PE/固件,且能 Hook 任意地址/syscall。
from qiling import Qiling
# Linux ELF
ql = Qiling(["./binary"], rootfs="/path/to/linux_rootfs")
# Windows PE(不需要真 Windows!)
ql = Qiling(["C:/x.exe"], rootfs="C:/qiling/examples/rootfs/x8664_windows")
# 固件 ARM(IoT)
ql = Qiling(["./router_bin"], rootfs="rootfs", arch="arm")
# Hook 反调试: ptrace 直接返回 0
ql.set_syscall("ptrace", lambda ql, *args: 0)
# Hook 任意地址
ql.hook_address(lambda ql: print("hit!"), 0x401000)
ql.run()
Qiling 反调试绕过的威力
反调试最狠的招之一:
ptrace(PTRACE_TRACEME)检测。Qiling 模拟环境中直接让该 syscall 返回 0,程序以为自己没有被调试——无需 patch 二进制。
输入 fuzz
跑 N 次不同输入,观察输出/崩溃,找正确输入。
7.5 Unicorn(CPU 级引擎)
最底层的模拟引擎,常被嵌进工具链:脱壳、反混淆、按需模拟某个函数。
from unicorn import *
from unicorn.x86_const import *
# 映射代码段 + 栈
uc = Uc(UC_ARCH_X86, UC_MODE_64)
uc.mem_map(0x400000, 0x1000)
uc.mem_map(0x700000, 0x1000) # 栈
uc.mem_write(0x400000, code)
uc.mem_write(0x700000, b"\x00" * 0x1000)
uc.reg_write(UC_X86_REG_RSP, 0x701000)
# 指令级 Hook(跟踪寄存器变化)
def hook_code(uc, address, size, user_data):
rip = uc.reg_read(UC_X86_REG_RIP)
print(f"[*] 0x{rip:x}")
uc.hook_add(UC_HOOK_CODE, hook_code)
uc.emu_start(0x400000, 0x400100) # 从 0x400000 跑到 0x400100
常见用法
- 脱壳:跟踪自解密代码,dump 解密后的内存
- 验证反编译猜想:模拟执行一段函数,对照输出
- 混合模式(64↔32):
retf切换模式时复制寄存器/内存 - 指令计数侧信道:movfuscated 程序里统计指令数推断行为
7.6 Triton(动态符号执行)
在指令执行过程中做符号化+约束收集:
import triton
ctx = triton.TritonContext(triton.ARCH.X86_64)
# 符号化输入缓冲区
ctx.setConcreteMemoryAreaValue(buf_addr, b"\x00" * 32)
for i in range(32):
ctx.symbolizeMemory(triton.MemoryAccess(buf_addr + i, 8), f"flag_{i}")
# 跑指令, 在比较点收集约束, 用 z3 求解
7.7 选型决策树
要自动解 flag 校验 → angr / Z3
要跑异架构或脏环境样本 → Qiling(整系统)/ Unicorn(纯 CPU)
要精确指令行为/脱壳 → Unicorn + Hook
要执行中符号约束 → Triton
7.8 完整实战演示:angr 自动解 flag 校验器
以一道典型 CTF 题 flag_check(逐字节校验 32 字符 flag,比较点地址 0x401350,错误输出在 0x4013a0)为例。
题目形态(静态分析已确认)
int main(int argc, char **argv) {
if (argc != 2) return 1;
char *in = argv[1];
if (strlen(in) != 32) return 1; // 长度检查
for (int i = 0; i < 32; i++) {
if (check_table[i] != transform(in[i], i)) { // 逐字节校验
puts("Wrong!"); // 0x4013a0
return 0;
}
}
puts("Correct!"); // 0x401350
}
angr 解法(3 行核心)
import angr
p = angr.Project("./flag_check", auto_load_libs=False)
s = p.factory.full_init_state(args=["./flag_check", "A"*32]) # 先给 32 字符占位
sm = p.factory.simulation_manager(s)
sm.explore(find=0x401350, avoid=0x4013a0) # find=正确输出, avoid=错误输出
if sm.found:
print(sm.found[0].posix.dumps(1)) # 打印程序输出(含 flag)
为什么不用符号输入?很多题只需要程序自己打印 flag(
printf("%s", flag)分支)。如果程序不打印,才需要符号化 argv(见下)。
变体 1:程序不打印,需要符号化 argv
import angr, claripy
p = angr.Project("./flag_check", auto_load_libs=False)
argv1 = claripy.BVS("argv1", 33*8) # 32 字符 + NUL
s = p.factory.full_init_state(args=["./flag_check", argv1])
# 约束: 可打印 ASCII
for i in range(32):
s.solver.add(argv1.get_byte(i) >= 0x20)
s.solver.add(argv1.get_byte(i) <= 0x7e)
s.solver.add(argv1.get_byte(32) == 0) # NUL
sm = p.factory.simulation_manager(s)
sm.explore(find=0x401350, avoid=0x4013a0)
print(sm.found[0].solver.eval(argv1, cast_to=bytes))
变体 2:路径爆炸怎么办
sm.use_technique(angr.exploration_techniques.DFS()) # 深度优先,flag checker 更有效
sm.use_technique(angr.exploration_techniques.LengthLimiter(10000)) # 限制探索量
# 或 Hook 掉昂贵的库函数
p.hook_symbol("memcmp", angr.SIM_PROCEDURES["stubs"]["ReturnUnconstrained"]())
变体 3:完全不知道地址,按输出找
sm.explore(find=lambda s_: b"Correct" in s_.posix.dumps(1),
avoid=lambda s_: b"Wrong" in s_.posix.dumps(1))
什么时候 angr 会失败(要换手段)
□ 程序有系统调用/网络/随机数(非确定性)→ 换 Qiling + 观察
□ 数学运算复杂(乘除/浮点)→ 约束求解变慢 → Z3 手动建模
□ 反调试(读 /proc、ptrace)→ 先 Qiling 绕反调试,再符号化
□ 输入走 scanf 而非 argv → 用 full_init_state + 符号 stdin
动手练习
- 写一个简单的 flag 校验 C 程序(strcmp 或逐字节比较),用 angr 自动解出。
- 用 Z3 解一个逐字节异或校验的题。
- 找一个带
sleep/ptrace反调试的样本,用 Qiling 跑通。 - 用 Unicorn 模拟执行一个函数的 20 条指令,观察寄存器变化。
深入阅读
- 仓库:
skills/reverse-engineering/tools-dynamic.md(angr 全章节:路径爆炸处理、CFG、Hook;Triton;Qiling fuzz) - 仓库:
skills/reverse-engineering/tools.md(Unicorn 全章节:混合模式、寄存器跟踪) - 仓库:
skills/reverse-engineering/tools-advanced.md(Manticore、Triton、自定义 VM 字节码提升到 LLVM IR) - 仓库:
skills/reverse-engineering/patterns-ctf-3.md(Z3 电路、指令计数器状态等案例)