[机翻] [ABD] 09_SAT_SMT与符号执行
09 SAT/SMT 与符号执行
⚠️ API 版本说明:本文档中的 Miasm API 代码示例已于 2026-07 更新至 Miasm 0.1.5+ 版本。课程原始材料使用的是 0.1.3 版本(2020年),部分 API 已发生 breaking change。更新处以
【更新】标注,旧版用法以【旧版 v0.1.3】标注。
本文档讲解 SAT/SMT 求解基础、基于 Miasm 的符号执行路径探索(基本路径探索与 SMT 增强路径探索),以及不透明谓词检测的实战应用。
目录
一、SAT/SMT 求解
1. sat_smt.ipynb — Z3 基础
Z3 是微软开发的高性能 SMT(Satisfiability Modulo Theories)求解器,支持布尔、整数、实数、位向量等多种理论。以下通过 Python 接口(z3-solver)展示其基本用法。
1.1 纯布尔约束求解
场景:判定一组布尔约束是否可满足,并获取满足模型。
import z3
# 创建两个布尔变量
malicious, benign = z3.Bools('malicious benign')
# 创建求解器
s = z3.Solver()
# 添加约束
s.add(z3.Or(malicious, benign)) # 约束1:malicious 或 benign 至少一个为真
s.add(z3.Not(z3.And(malicious, benign))) # 约束2:不能同时为真(互斥)
s.add(z3.Not(malicious)) # 约束3:malicious 为假
# 求解
print(s.check()) # 输出: sat(可满足)
print(s.model()) # 输出: [benign = True, malicious = False]
约束分析:
- 约束1 + 约束3:malicious 为假,故 benign 必须为真(满足约束1的 Or)。
- 约束2:malicious=False, benign=True 不违反互斥约束。
- 结论:唯一解为
benign=True, malicious=False。
1.2 混合布尔 + 整数约束
Z3 的强大之处在于混合多种理论的约束求解:
import z3
# 创建整数变量
x = z3.Int('x')
# 创建求解器
s = z3.Solver()
# 添加整数约束:x^2 - x = 2
s.add(x * x - x == 2)
# 求解
print(s.check()) # 输出: sat(可满足)
print(s.model()) # 输出: [x = 2] 或 [x = -1]
方程分析:
x^2 - x = 2即x^2 - x - 2 = 0,因式分解为(x-2)(x+1) = 0。- 解为
x = 2或x = -1,Z3 返回其中一个解。
1.3 在反混淆中的意义
SAT/SMT 求解是符号执行的核心引擎:
- 路径可行性检查:给定一条路径的约束条件,用 SMT 判定该路径是否可行(是否存在满足约束的输入)。
- 不透明谓词检测:若某条件在所有输入下恒为真/假,SMT 可证明其否定不可满足。
- 输入生成:求解路径约束,生成能触发特定路径的具体输入。
二、符号执行路径探索
1. simple_explore.ipynb — 基本路径探索
本节展示不依赖 SMT 的基本符号执行路径探索——枚举所有路径,但不检查路径可行性。
1.1 核心数据结构:FinalState
FinalState 类用于统一表示路径探索的最终状态:
class FinalState:
"""路径探索的最终状态,记录路径的执行结果与历史信息。"""
def __init__(self, result, sym, path_conds, path_history):
self.result = result # 是否可行(基本探索中始终为 True,SMT 增强探索中可能为 False)
self.sb = sym # 符号执行引擎状态(SymbolicExecutionEngine 实例)
self.path_conds = path_conds # 路径条件列表:沿路径累积的条件约束
self.path_history = path_history # 路径历史:已执行的基本块地址序列
字段说明:
| 字段 | 类型 | 含义 |
|---|---|---|
result |
bool |
路径是否可行。基本探索中始终为 True;SMT 增强探索中,不可行路径标记为 False |
sb |
SymbolicExecutionEngine |
符号执行引擎的最终状态,包含所有寄存器/内存的符号值 |
path_conds |
list |
路径条件列表,记录沿路径遇到的每个分支条件及其取值 |
path_history |
list |
路径历史,记录已执行的基本块地址序列,用于可视化与调试 |
1.2 explore() 函数 — 递归路径探索算法
explore() 是核心的递归路径探索函数,采用深度优先搜索(DFS)枚举所有执行路径:
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.expression.expression import ExprInt, ExprCond
# 停止标签地址(遇到此地址时记录最终状态)
lbl_stop = 0xdeadbeef
# 全局列表,存储所有最终状态
final_states = []
# 【旧版 v0.1.3】def explore(ir, ircfg, addr, symbols, path_conds, path_history,
# 【更新 v0.1.5+】参数 ir 重命名为 lifter(IR 分析器重命名为 Lifter)
def explore(lifter, ircfg, addr, symbols, path_conds, path_history,
cond_limit=30, uncond_limit=100):
"""
递归路径探索算法。
参数:
lifter: IR Lifter(【更新】旧版 v0.1.3 中为 ir: IR 分析器)
ircfg: IR 控制流图
addr: 当前基本块地址
symbols: 当前符号状态(寄存器/内存的符号值)
path_conds: 已累积的路径条件列表
path_history: 已执行的基本块地址历史
cond_limit: 条件分支深度限制(防止路径爆炸)
uncond_limit: 无条件块执行上限(防止无限循环)
"""
# 1. 创建符号执行实例
# 【旧版 v0.1.3】sb = SymbolicExecutionEngine(ir, symbols)
# 【更新 v0.1.5+】SymbolicExecutionEngine 第一个参数 ir→lifter,不再传 symbols
sb = SymbolicExecutionEngine(lifter)
# 2. 执行当前基本块,返回 PC(程序计数器)值
pc = sb.run_block_at(ircfg, addr)
# 3. 检查是否到达停止标签
if int(pc) == lbl_stop:
# 记录最终状态
final_states.append(FinalState(
result=True,
sym=sb,
path_conds=path_conds,
path_history=path_history + [addr]
))
return
# 4. 检查 PC 是否为条件表达式(ExprCond)——即条件分支
if isinstance(pc, ExprCond):
# 4a. 检查条件分支深度限制
if cond_limit <= 0:
return # 超过深度限制,停止探索
# 构造 true / false 分支的条件映射
# cond_true: 条件为真时,pc = pc.src1(true 分支目标)
# cond_false: 条件为假时,pc = pc.src2(false 分支目标)
cond_true = {pc.cond: ExprInt(1, 32)} # 条件 = 1(真)
cond_false = {pc.cond: ExprInt(0, 32)} # 条件 = 0(假)
# 计算两条分支的目标地址
addr_true = sb.eval_expr(pc.src1, cond_true) # true 分支地址
addr_false = sb.eval_expr(pc.src2, cond_false) # false 分支地址
# 4b. 递归探索 true 分支
# 注意:传递 sb.symbols.copy() 实现状态隔离(快照)
# 【更新 v0.1.5+】参数 ir→lifter
explore(
lifter, ircfg, int(addr_true),
sb.symbols.copy(), # 符号状态快照(隔离)
path_conds + [(pc.cond, ExprInt(1, 32))], # 累积路径条件
path_history + [addr],
cond_limit - 1, # 条件深度 -1
uncond_limit
)
# 4c. 递归探索 false 分支
# 【更新 v0.1.5+】参数 ir→lifter
explore(
lifter, ircfg, int(addr_false),
sb.symbols.copy(), # 符号状态快照(隔离)
path_conds + [(pc.cond, ExprInt(0, 32))], # 累积路径条件
path_history + [addr],
cond_limit - 1,
uncond_limit
)
else:
# 5. PC 为确定地址(无条件跳转/顺序执行)
if uncond_limit <= 0:
return # 超过执行上限,停止探索
# 继续执行下一个基本块
# 【更新 v0.1.5+】参数 ir→lifter
explore(
lifter, ircfg, int(pc),
sb.symbols, # 无分支,状态无需拷贝
path_conds,
path_history + [addr],
cond_limit,
uncond_limit - 1 # 无条件深度 -1
)
1.3 算法详解
步骤 1:创建符号执行实例
SymbolicExecutionEngine(ir, symbols)接收 IR 分析器与当前符号状态。(【旧版 v0.1.3】)- 【更新 v0.1.5+】
SymbolicExecutionEngine(lifter),第一个参数ir→lifter,不再传symbols参数。 symbols包含各寄存器/内存的符号值(可能是符号变量或已求值的具体值)。
步骤 2:执行基本块
sb.run_block_at(ircfg, addr)从地址addr执行一个 IR 基本块。- 返回值
pc为下一条要执行的基本块地址。 pc可能是:- 确定地址(
ExprInt或ExprLoc):无条件跳转或顺序执行。 - 条件表达式(
ExprCond):cond ? true_target : false_target,表示条件分支。
- 确定地址(
步骤 3:检查停止条件
- 若
pc == lbl_stop (0xdeadbeef),说明到达预设的停止点,记录最终状态。
步骤 4:处理条件分支
- 当
pc为ExprCond时,程序存在两条可能路径。 - 构造
cond_true = {pc.cond: 1}和cond_false = {pc.cond: 0},分别代入求值,得到两条分支的目标地址。 - 递归探索两条路径,每条路径传递符号状态的拷贝(
sb.symbols.copy()),实现状态隔离。
步骤 5:处理无条件转移
- 当
pc为确定地址时,直接继续执行下一个基本块,无需分支。
1.4 探索参数
| 参数 | 默认值 | 作用 |
|---|---|---|
cond_limit |
30 | 条件分支深度限制。每遇到一个条件分支,深度减 1;减至 0 时停止探索该路径。防止路径爆炸。 |
uncond_limit |
100 | 无条件块执行上限。每执行一个无条件基本块,上限减 1;减至 0 时停止。防止无限循环。 |
2. simple_explore_smt.ipynb — SMT 增强路径探索
本节在基本路径探索的基础上,引入 SMT 路径可行性检查——在递归探索前,用 Z3 判定路径约束是否可满足,剪除不可行路径。
2.1 新增路径可行性检查函数
import z3
from miasm.ir.translators import Translator
def check_path_feasibility(conds):
"""
检查路径条件列表是否可满足(即路径是否可行)。
参数:
conds: 路径条件列表,每个元素为 (lval, rval) 元组
lval: Miasm 条件表达式(ExprCond 的 cond 部分)
rval: 条件取值(ExprInt(1, 32) 表示真,ExprInt(0, 32) 表示假)
返回:
True — 路径可行(约束可满足)
False — 路径不可行(约束不可满足)
"""
solver = z3.Solver()
for lval, rval in conds:
# 将 Miasm 表达式翻译为 Z3 表达式
# Translator.to_language("z3") 获取 Z3 翻译器
# from_expr() 将 Miasm Expr 转换为 Z3 表达式
z3_cond = Translator.to_language("z3").from_expr(lval)
# 添加约束:条件表达式 == 其取值
# rval.arg 为 ExprInt 的整数值(1 或 0)
solver.add(z3_cond == int(rval.arg))
# 检查可满足性
rslt = solver.check()
return rslt == z3.sat
关键翻译:
Translator.to_language("z3").from_expr(lval)是 Miasm IR → Z3 表达式的桥梁。- Miasm 的条件表达式(如
EAX == EBX)被翻译为 Z3 的布尔表达式。 - 路径条件列表中,每个
(lval, rval)表示"条件 lval 取值为 rval"。
2.2 在递归调用前检查路径可行性
修改 explore() 函数,在递归探索前调用 check_path_feasibility():
# 【旧版 v0.1.3】def explore_smt(ir, ircfg, addr, symbols, path_conds, path_history,
# 【更新 v0.1.5+】参数 ir 重命名为 lifter
def explore_smt(lifter, ircfg, addr, symbols, path_conds, path_history,
cond_limit=30, uncond_limit=100):
# 【旧版 v0.1.3】sb = SymbolicExecutionEngine(ir, symbols)
# 【更新 v0.1.5+】SymbolicExecutionEngine 第一个参数 ir→lifter
sb = SymbolicExecutionEngine(lifter)
pc = sb.run_block_at(ircfg, addr)
if int(pc) == lbl_stop:
# 到达停止点前,检查整条路径是否可行
if check_path_feasibility(path_conds):
final_states.append(FinalState(
result=True, # 路径可行
sym=sb,
path_conds=path_conds,
path_history=path_history + [addr]
))
else:
final_states.append(FinalState(
result=False, # 路径不可行
sym=sb,
path_conds=path_conds,
path_history=path_history + [addr]
))
return
if isinstance(pc, ExprCond):
if cond_limit <= 0:
return
cond_true = {pc.cond: ExprInt(1, 32)}
cond_false = {pc.cond: ExprInt(0, 32)}
addr_true = sb.eval_expr(pc.src1, cond_true)
addr_false = sb.eval_expr(pc.src2, cond_false)
# 在递归前检查 true 分支的路径可行性
new_conds_true = path_conds + [(pc.cond, ExprInt(1, 32))]
if check_path_feasibility(new_conds_true):
# 【更新 v0.1.5+】参数 ir→lifter
explore_smt(lifter, ircfg, int(addr_true),
sb.symbols.copy(),
new_conds_true,
path_history + [addr],
cond_limit - 1, uncond_limit)
else:
# true 分支不可行,记录为 False 状态
final_states.append(FinalState(False, sb, new_conds_true,
path_history + [addr]))
# 在递归前检查 false 分支的路径可行性
new_conds_false = path_conds + [(pc.cond, ExprInt(0, 32))]
if check_path_feasibility(new_conds_false):
# 【更新 v0.1.5+】参数 ir→lifter
explore_smt(lifter, ircfg, int(addr_false),
sb.symbols.copy(),
new_conds_false,
path_history + [addr],
cond_limit - 1, uncond_limit)
else:
# false 分支不可行,记录为 False 状态
final_states.append(FinalState(False, sb, new_conds_false,
path_history + [addr]))
else:
if uncond_limit <= 0:
return
# 【更新 v0.1.5+】参数 ir→lifter
explore_smt(lifter, ircfg, int(pc),
sb.symbols, path_conds,
path_history + [addr],
cond_limit, uncond_limit - 1)
2.3 SMT 增强的价值
- 剪除不可行路径:基本探索会枚举所有分支组合,包括逻辑上不可能的路径。SMT 增强通过约束求解,提前剪除不可行路径,减少无效探索。
- 不透明谓词识别:若某条件在所有输入下恒为真,则其 false 分支的路径条件不可满足,
check_path_feasibility()返回False,该分支被标记为不可行。 - 效率提升:虽然每次分支检查增加 SMT 求解开销,但剪枝带来的路径数减少通常远超此开销。
2.4 验证断言
探索完成后,通过断言验证结果符合预期:
# 验证断言
# 1. 最终状态总数应为 2(true 分支 + false 分支)
assert len(final_states) == 2
# 2. 其中恰好 1 个状态为可行(result=True)
assert len([x for x in final_states if x.result == True]) == 1
# 3. 其中恰好 1 个状态为不可行(result=False)
assert len([x for x in final_states if x.result == False]) == 1
断言含义:
len(final_states) == 2:探索到达了停止点的两条路径(true 和 false 分支各一条)。- 1 个
True+ 1 个False:说明存在一个不透明谓词——一条分支可行,另一条不可行(恒不可达)。这正是不透明谓词的特征。
三、不透明谓词检测
1. opfind.ipynb — 真实恶意软件中的不透明谓词检测
本节在真实恶意软件样本 x-tunnel.bin 中检测不透明谓词保护下的死代码。
1.1 样本准备
# 解压样本(密码: infected)
unzip -n -P infected x-tunnel.zip
-n:不覆盖已存在的文件。-P infected:指定解压密码。- 解压后得到
x-tunnel.bin恶意软件样本。
1.2 分析流程
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.core.locationdb import LocationDB
# 1. 加载恶意软件样本
loc_db = LocationDB()
with open('x-tunnel.bin', 'rb') as f:
cont = Container.from_stream(f, loc_db)
machine = Machine(cont.arch)
# 2. 反汇编目标函数 0x405710
target_func = 0x405710
mdis = machine.dis_engine(cont.bin_stream, loc_db=cont.loc_db)
asmcfg = mdis.dis_multiblock(target_func)
# 3. 生成 IRCFG
# 【旧版 v0.1.3】ir_arch = machine.ir(cont.loc_db)
# 【更新 v0.1.5+】machine.ir() → machine.lifter(),变量 ir_arch → lifter
lifter = machine.lifter(cont.loc_db)
# 【旧版 v0.1.3】ircfg = ir_arch.new_ircfg_from_asmcfg(asmcfg)
# 【更新 v0.1.5+】ir_arch.new_ircfg_from_asmcfg() → lifter.new_ircfg_from_asmcfg()
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg)
# 4. 初始化所有寄存器为符号变量
# 这是关键步骤:将所有寄存器符号化,使符号执行能探索所有可能的输入
symbols = {}
# 【旧版 v0.1.3】for reg in ir_arch.arch.regs.all_regs:
# 【更新 v0.1.5+】ir_arch → lifter
for reg in lifter.arch.regs.all_regs:
symbols[reg] = reg # 将寄存器初始化为其自身的符号表示
# 5. 执行 SMT 增强探索
final_states = []
# 【旧版 v0.1.3】explore_smt(ir_arch, ircfg, target_func, symbols,
# 【更新 v0.1.5+】ir_arch → lifter
explore_smt(lifter, ircfg, target_func, symbols,
path_conds=[], path_history=[],
cond_limit=30, uncond_limit=100)
1.3 基本块分类
探索完成后,根据路径覆盖情况对基本块进行分类:
# 收集所有基本块地址
all_blocks = set(ircfg.blocks.keys())
# 分类
executed_lockey = set() # 被至少一条可行路径覆盖的块
unexecuted_lockey = set() # 未被任何路径覆盖的块(疑似死代码)
# 遍历所有可行状态(result=True),收集其路径历史
for state in final_states:
if state.result == True:
executed_lockey.update(state.path_history)
# 未被执行的块 = 所有块 - 被执行的块
unexecuted_lockey = all_blocks - executed_lockey
print(f"被执行的基本块: {len(executed_lockey)}")
print(f"未被执行的基本块: {len(unexecuted_lockey)}(疑似不透明谓词保护的死代码)")
分类逻辑:
| 类别 | 定义 | 含义 |
|---|---|---|
executed_lockey |
被至少一条可行路径(result=True)覆盖的基本块 |
正常代码,在某种输入下会执行 |
unexecuted_lockey |
未被任何可行路径覆盖的基本块 | 疑似不透明谓词保护的死代码——在所有可能输入下都不可达 |
1.4 生成 IDC 脚本进行 IDA Pro 着色
将分析结果导出为 IDC 脚本,在 IDA Pro 中对未执行块着色,便于人工审查:
# 生成 IDC 脚本
idc_script = """# Auto-generated IDC script for highlighting opaque predicate protected dead code
# 颜色定义
COLOR_DEFAULT = 0xFFFFFFFF # 默认颜色
COLOR_DEAD = 0x0000FF # 红色:未执行块(疑似死代码)
COLOR_LIVE = 0x00FF00 # 绿色:已执行块
import idc
# 未执行块着色(红色)
"""
# 为未执行块添加着色命令
for addr in sorted(unexecuted_lockey):
idc_script += f"idc.set_color({hex(addr)}, idc.CIC_ITEM, COLOR_DEAD)\n"
# 为已执行块添加着色命令
for addr in sorted(executed_lockey):
idc_script += f"idc.set_color({hex(addr)}, idc.CIC_ITEM, COLOR_LIVE)\n"
idc_script += '\nprint("Opaque predicate analysis complete.")\n'
# 写入 IDC 脚本文件
with open('op-color.idc', 'w') as f:
f.write(idc_script)
print("IDC 脚本已生成: op-color.idc")
print("请在 IDA Pro 中: File → Script File → 选择 op-color.idc 执行")
着色效果:
- 红色:未执行的基本块——疑似不透明谓词保护的死代码,需人工审查确认。
- 绿色:已执行的基本块——正常代码路径。
- 分析人员可在 IDA Pro 中直观地看到哪些代码块被不透明谓词"隐藏"。
四、关键设计模式
1. FinalState 类:统一状态表示
FinalState 将路径探索的多种信息(可行性、符号状态、路径条件、路径历史)封装为统一对象,便于后续分析:
class FinalState:
def __init__(self, result, sym, path_conds, path_history):
self.result = result # 可行性
self.sb = sym # 符号状态
self.path_conds = path_conds # 路径约束
self.path_history = path_history # 执行历史
设计优势:
- 统一接口:无论路径是否可行,都以
FinalState表示,便于批量处理。 - 信息完整:保留完整的路径信息,支持后续的可行性分析、死代码检测、输入生成等。
2. 递归路径探索:codepath_walk 实现 DFS 式路径枚举
explore() / explore_smt() 函数采用递归实现深度优先的路径枚举:
入口基本块
/ \
true 分支 false 分支
/ \ / \
... ... ... ...
- DFS 特性:先深入一条路径到底(到达停止点或超限),再回溯探索其他分支。
- 递归结构:每次条件分支产生两次递归调用,形成二叉探索树。
- 终止条件:到达
lbl_stop、超过cond_limit或uncond_limit。
3. 状态隔离:sb.symbols.copy() 实现符号状态快照
在分支探索时,传递 sb.symbols.copy() 而非 sb.symbols 本身:
# 正确:传递拷贝,两条路径的状态互不影响
# 【更新 v0.1.5+】参数 ir→lifter
explore(lifter, ircfg, addr_true, sb.symbols.copy(), ...)
explore(lifter, ircfg, addr_false, sb.symbols.copy(), ...)
# 错误:传递引用,两条路径共享状态,会互相污染
# explore(lifter, ircfg, addr_true, sb.symbols, ...)
# explore(lifter, ircfg, addr_false, sb.symbols, ...) # 此时 symbols 已被 true 路径修改!
为什么需要状态隔离:
- 符号执行在 true 分支执行后,会更新
sb.symbols(如设置条件为真后的寄存器值)。 - 若不拷贝,false 分支会继承 true 分支的状态修改,导致错误。
.copy()创建符号状态的深拷贝快照,确保每条路径从独立的初始状态出发。
4. IDC 脚本生成:将分析结果导出为 IDA Pro 着色脚本
将自动化分析结果导出为 IDA Pro 可执行的 IDC 脚本,实现"自动化分析 + 人工审查"的协作工作流:
# 生成模式:分析结果 → IDC 命令列表 → 脚本文件
for addr in unexecuted_lockey:
idc_script += f"idc.set_color({hex(addr)}, idc.CIC_ITEM, COLOR_DEAD)\n"
工作流:
- 自动化分析:Miasm 符号执行自动识别未执行块。
- 结果导出:生成 IDC 脚本,将地址列表转化为着色命令。
- 人工审查:在 IDA Pro 中执行脚本,分析人员直观查看着色结果,人工确认死代码与不透明谓词。
总结
| 技术 | 核心工具 | 反混淆应用 |
|---|---|---|
| SAT/SMT 求解 | Z3 求解器 | 路径可行性检查、不透明谓词检测、输入生成 |
| 基本路径探索 | Miasm SymbolicExecutionEngine | 枚举所有路径(含不可行路径) |
| SMT 增强路径探索 | Miasm + Z3 Translator | 剪除不可行路径,识别不透明谓词 |
| 不透明谓词检测 | 路径覆盖分析 + IDC 导出 | 在真实恶意软件中发现被保护的死代码 |
核心流程:
Z3 约束求解 → 路径可行性判定 → 剪除不可行路径 → 识别未覆盖块 → 不透明谓词/死代码检测 → IDC 着色导出
Miasm 的 Translator.to_language("z3") 是连接符号执行与 SMT 求解的关键桥梁,使得 Miasm IR 表达式能够被 Z3 求解器处理,实现从"路径枚举"到"路径可行性验证"的升级。

浙公网安备 33010602011771号