[机翻] [ABD] 09_SAT_SMT与符号执行

09 SAT/SMT 与符号执行

https://github.com/malrev/ABD

⚠️ API 版本说明:本文档中的 Miasm API 代码示例已于 2026-07 更新至 Miasm 0.1.5+ 版本。课程原始材料使用的是 0.1.3 版本(2020年),部分 API 已发生 breaking change。更新处以 【更新】 标注,旧版用法以 【旧版 v0.1.3】 标注。

本文档讲解 SAT/SMT 求解基础、基于 Miasm 的符号执行路径探索(基本路径探索与 SMT 增强路径探索),以及不透明谓词检测的实战应用。


目录

  1. SAT/SMT 求解
  2. 符号执行路径探索
  3. 不透明谓词检测
  4. 关键设计模式

一、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 = 2x^2 - x - 2 = 0,因式分解为 (x-2)(x+1) = 0
  • 解为 x = 2x = -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),第一个参数 irlifter,不再传 symbols 参数。
  • symbols 包含各寄存器/内存的符号值(可能是符号变量或已求值的具体值)。

步骤 2:执行基本块

  • sb.run_block_at(ircfg, addr) 从地址 addr 执行一个 IR 基本块。
  • 返回值 pc 为下一条要执行的基本块地址。
  • pc 可能是:
    • 确定地址ExprIntExprLoc):无条件跳转或顺序执行。
    • 条件表达式ExprCond):cond ? true_target : false_target,表示条件分支。

步骤 3:检查停止条件

  • pc == lbl_stop (0xdeadbeef),说明到达预设的停止点,记录最终状态。

步骤 4:处理条件分支

  • pcExprCond 时,程序存在两条可能路径。
  • 构造 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_limituncond_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"

工作流

  1. 自动化分析:Miasm 符号执行自动识别未执行块。
  2. 结果导出:生成 IDC 脚本,将地址列表转化为着色命令。
  3. 人工审查:在 IDA Pro 中执行脚本,分析人员直观查看着色结果,人工确认死代码与不透明谓词。

总结

技术 核心工具 反混淆应用
SAT/SMT 求解 Z3 求解器 路径可行性检查、不透明谓词检测、输入生成
基本路径探索 Miasm SymbolicExecutionEngine 枚举所有路径(含不可行路径)
SMT 增强路径探索 Miasm + Z3 Translator 剪除不可行路径,识别不透明谓词
不透明谓词检测 路径覆盖分析 + IDC 导出 在真实恶意软件中发现被保护的死代码

核心流程

Z3 约束求解 → 路径可行性判定 → 剪除不可行路径 → 识别未覆盖块 → 不透明谓词/死代码检测 → IDC 着色导出

Miasm 的 Translator.to_language("z3") 是连接符号执行与 SMT 求解的关键桥梁,使得 Miasm IR 表达式能够被 Z3 求解器处理,实现从"路径枚举"到"路径可行性验证"的升级。

posted @ 2026-08-05 15:55  DirWangK  阅读(7)  评论(0)    收藏  举报