#逆向工程基本原理06
混淆技术
混淆(Obfuscation)是通过对二进制代码进行变换和修改,使得分析者难以理解程序的逻辑和结构,从而增加逆向工程的难度
混淆的目的:
- 防止软件的非法复用与复现
- 隐藏加密算法、数据结构或代码逻辑
- 保护代码免受未授权的逆向工程
布局混淆
布局混淆通过对代码中命名、注释等信息进行修改/删除,破坏代码的可读性:
- 删除源代码中的注释/调试信息
- 删除未使用的方法、类和数据结构
- 重命名常量、类变量、方法名称等
布局混淆是最简单、基础的混淆方法,不涉及代码逻辑实现方面的修改
标识符重命名
将标识符名称替换为随机生成的名称,这些名称看起来毫无意义,因此难以理解。这是一种非常简单但有效的混淆技术
也可以将标识符替换为视觉上难以区分的名称(如 l1l1l1l1 和 llll1ll1),人眼难以分辨
标识符重载
将标识符名称替换为当前代码中已经使用的名称。在很多编程语言中,不同的命名空间可以使用相同的变量名(例如各个类都有相同名称的函数、各个函数都有相同名称的局部变量),在代码中尽可能使用相同的标识符可以提高代码理解的难度
例如,f1, f2, f3 被同时用于参数变量和全局变量命名
数据混淆
对代码中使用的数据进行变换,从而增加代码的理解难度
(1) 标量变量合并
“标量变量”指单一的、不可再分的数据单元(如整数、浮点数、字符等)。在精度允许范围内,多个标量变量 \(V_1 \cdots V_n\) 可以合并为一个变量 \(V_0\)
例如,将两个 32 位整型变量 X 和 Y 合并为一个 64 位整型变量 Z:
\[Z(X, Y) = 2^{32} \times Y + X\]
(2) 变量分割
布尔变量和其他取值范围有限的变量可以拆分为两个或多个变量
例如布尔变量 V 可以分割成两个变量 p 和 q:
- 当 p=q=0 或 p=q=1 时,V=False
- 否则,V=True
(3) 数组重构
将一个数组拆分成几个子数组、将两个或多个数组合并成一个数组、折叠数组(增加维数)或扁平化数组(减少维数)
(4) 混合布尔运算(MBA)
Mixed Boolean-Arithmetic(MBA)将一个表达式替换成等价的复杂的布尔运算表达式
示例:证明 \(y = -(x \lor \lnot y) + (x \land y) - 1\)
- 根据按位取反:\(\lnot z = -z - 1\),所以 \(-(x \lor \lnot y) - 1 = \lnot(x \lor \lnot y)\) 当前右式:\(\lnot(x \lor \lnot y) + (x \land y)\)
- 根据德·摩根律:\(\lnot(a \lor b) = \lnot a \land \lnot b\),所以 \(\lnot(x \lor \lnot y) = (\lnot x \land y)\) 当前右式:\((\lnot x \land y) + (x \land y)\)
- 根据位逻辑恒等式:\((\lnot x \land y) + (x \land y) = y\)
所以最终右式等于 \(y\),证毕
基础位运算恒等式:
| 恒等式 | 说明 |
|---|---|
| \(\lnot A = -A - 1\) | 按位取反 |
| \(\lnot(A \land B) = \lnot A \lor \lnot B\) | 德·摩根律 |
| \(\lnot(A \lor B) = \lnot A \land \lnot B\) | 德·摩根律 |
| \(A \lor (A \land B) = A\) | 吸收律 |
| \(A = (A \land B) + (A \land \lnot B)\) | 位逻辑恒等式 |
| \(A \oplus B = (A \lor B) - (A \land B)\) | 异或的补码表示 |
| \(A \lor B = A + B - (A \land B)\) | OR/AND 与加法关系 |
| \(A \oplus B = (A + B) - 2(A \land B)\) | XOR 与加法关系 |
MBA 混淆在 CTF 比赛和恶意软件分析中非常常见,掌握基础位运算恒等式有助于手动或自动化简化 MBA 表达式
(5) 字符串加密
将代码中的明文字符串替换成加密后的随机字符,执行时运行解密程序还原字符串。字符串加密可以有效阻止硬编码静态扫描
控制流混淆
修改程序的控制流程来混淆逻辑结构,使其难以理解和分析。控制流混淆分类:
- 控制流扁平化:扁平化源代码结构,使分支目标不容易确定
- 控制聚合变换:拆分同一逻辑代码块,或合并不同逻辑代码块
- 代码变换:插入冗余/无效代码,或修改原程序的算法逻辑
(1) 控制流扁平化
现实世界中的程序往往具有易于理解的控制流。控制流扁平化的原理是将易于理解的程序控制流变成依赖数据值进行跳转的程序控制流
实现步骤:
- 将高级语言的控制结构分解为 if-then-goto 结构
- 修改 goto 语句,使得跳转地址在动态运行时才能确定(通过 switch-case 状态机实现)
1 | 原始代码: 扁平化后: |
(2) 控制聚合变换
通过拆散/合并逻辑上属于/不属于同一个函数的代码块来实现混淆
函数内联与外联
- 内联(Inline):将函数调用直接替换为函数体,减少函数调用的开销
- 外联(Outline):提取重复代码片段形成独立函数,将原来的重复代码替换为函数调用
函数交错
打乱程序原有代码结构,将多个实现不同功能的代码块合并到一起
函数克隆
对原始代码应用不同的混淆变换,创建多个不同版本的函数
循环变换
循环变换可提升程序性能,但也增加了代码复杂性,达到混淆效果:
- 循环分块:调整循环内部迭代空间,提升缓存命中率
- 循环展开:复制循环体内代码来减少循环迭代次数
- 循环分裂:将一个大循环分割成多个小循环
(3) 代码变换
插入冗余代码
插入不影响原始程序功能的无用代码:
- P 永真,分支判断语句冗余
- P 值未知,但 True 和 False 两个分支代码等效(可采用不同的混淆)
- P 永真,True 分支对应真实逻辑,False 分支为混淆的虚假逻辑
拓展循环条件
在原有的循环判断条件中添加冗余的判断语句,不影响循环执行次数但增加了代码复杂度
移除库函数调用
C/C++ 比较依赖标准库调用,而库函数的语义是众所周知的且名称很难被混淆。通过自定义相似功能的函数来替代库函数进行混淆(例如自定义 base64 函数替换 openssl 的调用)
添加冗余操作数
在原始表达式上利用代数法则添加一些结果为恒定值的变量表达式,不影响原始计算逻辑
自修改代码
代码在执行时修改自己的指令:程序动态生成自己的代码来提高速度、压缩代码以节省空间,同时能防止软件扫描与检测
1 | _start: |
反混淆技术
反混淆是指对代码进行分析和理解,还原被混淆的代码或数据——将代码和数据还原成(原始)易于理解的形式。需要注意的是,大部分混淆技术是不可逆的
布局反混淆
布局混淆主要是对命名、注释等信息进行删除和修改,这些信息的修改与破坏本质上无法彻底还原
主要思路:分析代码语义并结合代码上下文,对无意义标识符进行重命名或添加注释
(1) 基于统计机器翻译的标识符恢复(从 0 到 1)
统计机器翻译(SMT)通过对大量平行语料统计分析,构建统计翻译模型
- 从开源仓库中下载大量未混淆的代码作为语料库
- 源代码与反编译之后的代码经过对齐之后互为平行语料
- 输入混淆代码片段,翻译模型通过预测概率的方式对标识符进行”翻译”
参考论文:Suggesting Meaningful Variable Names for Decompiled Code: A Machine Translation Approach (ICPC’18)
(2) 基于静态代码分析的标识符恢复(从 1 到 n)
本质是基于已恢复符号迭代恢复:
- 根据赋值关系还原标识符(例如
MD5(data, length, &A)→md5Value) - 根据上下文线索推断(例如 base64 算法的字符集
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"可以推断函数名为base64)
参考:Recommending Rename Refactorings (RSSE’10),以及 findcrypt-yara 工具
数据反混淆
数据反混淆的难点:
- 难以检测识别:涉及的指令较少、不会显著改变程序的控制流
- 混淆方式多样:数据编码、数据分割、数据合并等
- 对数据有依赖性:有时修改数据的值(如字符串加密),需要知道混淆后的数值才能反混淆
主要方法:
(1) MBA 简化
应用场景:经过混合布尔运算混淆的数据
核心概念:
- 真值表矩阵 \(M\):行代表所有变量取值组合,列代表每个位运算子表达式,元素值为该子表达式在对应变量取值下的真假值
- 系数向量 \(\vec{v}\):表达式中各项位运算子表达式系数的有序集合
- 签名向量 \(\vec{s} = M\vec{v}\):唯一地捕捉和表示 MBA 表达式的完整数学语义。如果两个表达式的签名向量相同,那么它们在数学上等价
简化流程:
- 预定义基向量:人工选择一组基向量(优先选择仅包含少量多项式位运算的基向量,确保简化后的式子不包含复杂位运算)
- 计算签名向量:计算待简化 MBA 表达式的签名向量
- 求解线性组合:找一个由基向量线性组合的表达式,使其签名与待简化 MBA 表达式相同
示例:简化 \(E = 2(x \lor y) - (\lnot x \land y) - (x \land \lnot y)\)
选择基向量 \(\{x, y, x \land y, 1\}\),对于表达式 \(E' = a \cdot x + b \cdot y + c \cdot (x \land y) + d \cdot 1\):
通过计算签名向量并求解线性组合参数 \((a, b, c, d)\),得到结果 \(E = x + y\)
参考论文:Boosting SMT solver performance on mixed-bitwise-arithmetic expressions (PLDI’21)
(2) 模拟执行反混淆
应用场景:字符串加密
原始字符串被加密后,程序中一般会有相应的解密代码用于恢复原始数据。模拟执行混淆代码,使用解密代码的返回值替换原来的加密字符串
步骤:
- 对目标文件进行反汇编,得到静态汇编代码
- 模拟执行代码,提取实际执行的代码片段与返回值
- 筛选数据类型为字符串的返回值
- 建立模拟执行的代码和静态汇编代码的映射关系
- 将字符串返回值插入到相应静态汇编代码的返回点
参考论文:String Deobfuscation Scheme based on Dynamic Code Extraction for Mobile Malwares (INPRA’16)
控制流反混淆
控制聚合变换仅是打乱代码顺序和函数组成,并不显著改变控制流结构,难以识别和反混淆。这里主要针对控制流扁平化和代码变换介绍反混淆思路
(1) 控制流去扁平化
基本思路:
- 识别调度器和状态变量
- 识别每个跳转块执行末尾状态变量的值
- 根据调度器和状态变量的值,确定下一跳的地址
- 用跳转到下一个块来替换跳转到调度器
- 根据 CFG 重构代码
自动化控制流去扁平化涉及符号执行、模拟执行等多种复杂的技术
参考资料: - D810: A journey into control flow unflattening - Deobfuscation: recovering an OLLVM-protected program - Hex-Rays Microcode API vs Obfuscating Compiler
(2) 去除虚假控制流
许多控制流混淆技术在真实代码逻辑中嵌入虚假逻辑(虚假控制流、冗余代码、冗余操作数等)。反混淆思路主要是自动识别虚假代码逻辑并移除
不透明谓词(Opaque Predicate)是防止虚假代码被轻易识别和移除的主要手段——值确定的表达式,但编译器和静态分析工具难以发现
不透明谓词的构造分为两类:
- 局部:代码包含在一个基本块中,例如
if (x*x == 7*y*y - 1) - 全局:代码分布在整个程序中,例如
R = x*x; ...; S = 7*y*y - 1; ...; if (R == S)
识别不透明谓词的方法
(1)基于数据流寻值
通过追踪变量的值判断谓词取值。例如 x=5; ...; y=7; ...; if (x*x == (7*y*y-1)),可以直接算出 \(x^2 = 25\),\(7y^2 - 1 = 342\),当前 if 恒为假
不足之处:如果 x、y 非固定值则失效
(2)基于模式匹配
利用已知混淆器采用的策略来识别不透明谓词:根据混淆代码构建模式匹配规则,以识别常用的不透明谓词(如 \(x^2 \neq 7y^2 - 1\) 或 \(random(1,5) < 0\))
对抗思路:混淆器应避免使用已知或较为通用的不透明谓词,构造的不透明谓词在语法上应与真实程序相似
(3)基于符号执行
提取所有影响可疑谓词取值的变量和语句(程序切片),构造谓词表达式 \(P(x, y)\),通过约束求解器(如 Z3)判断 \(P(x, y) = true\) 或 \(false\) 是否有解
例如:\(0 < x < 10\) 且 \(y > 10\),则 \(x^2 < 100\),\(7y^2 - 1 > 699 > 100 > x^2\),因此 \(x^2 < 7y^2 - 1\) 恒成立
对抗思路:
- 提高程序切片难度(控制流扁平化、引入垃圾运算造成路径爆炸)
- 提高约束求解难度(引入 MBA、大整数模运算、冗余变量依赖等)
一些常见的不透明谓词: - \(random(1, 5) < 0\):永假(随机数 ≥ 1) - \((7x^2 + 1) \mod 7 == 0\):当 \(x^2 \mod 7 \neq 6/7\) 时为假(实际上对任意整数 \(x\),\((7x^2 + 1) \mod 7 = 1\),恒非零,所以恒假) - \(x \mod 2 == 0 \lor ((x^2 - 1) \mod 8 == 0)\):永真(当 \(x\) 为偶数第一项成立,当 \(x\) 为奇数则 \(x^2 - 1 = (x-1)(x+1)\),其中必有一个被 2 整除另一个被 4 整除,所以 \(8 | (x^2-1)\))
混淆与反混淆工具
混淆工具
OLLVM
OLLVM 是瑞士西北应用科技大学安全实验室于 2010 年发起的一个项目,提供一个基于 LLVM 编译套件的开源分支,通过代码混淆和防篡改提高软件安全性
OLLVM 的加固技术以 LLVM Passes 的形式实现对 IR 的混淆,具有很好的兼容性,支持大多数编程语言(C, C++, Objective-C, Ada, Fortran)和目标平台(x86, x86-64, ARM, Thumb, MIPS 等)
OLLVM 包含 3 种独立的 LLVM Passes:
| Pass | 参数 | 说明 |
|---|---|---|
| 指令替换 | -mllvm -sub | 将一条运算指令替换为多条等价的运算指令(如 a = a ^ b → a = (~a & b) \| (a & ~b)) |
| 控制流扁平化 | -mllvm -fla | 利用 case-switch 结构将程序控制流扁平化 |
| 虚假控制流 | -mllvm -bcf | 加入含有不透明谓词的条件跳转和不可达的基本块 |
Hikari
相比于 OLLVM,Hikari 添加了更多的 LLVM Pass 并修复了部分 bug
新增的 LLVM Passes:
| Pass | 参数 | 说明 |
|---|---|---|
| 函数调用混淆 | -enable-fco | 使用 JSON 配置解析符号,将函数调用替换为 dlopen + dlsym 的形式 |
| 函数包装 | — | 创建虚假函数来包装实际的函数调用(如 foo(1) → DummyA(1)→DummyB(1)→foo(1)) |
| 间接分支 | -enable-indibran | 分支指令被转换为基于寄存器的间接调用 |
| 字符串加密 | -enable-strcry | 将常量替换成含有加解密和编码运算的表达式形式 |
Tigress
Tigress 是 C 语言的多样化仿真器/混淆器,提供多种针对动静态逆向工程以及去虚拟化攻击的防御手段,可以生成具有任意复杂性和多样性的虚拟指令集
Tigress 提供 32 种转化方式,包括但不限于:
- Virtualize、JitJit Dynamic
- Flatten、Split、Merge
- Add Opaque、Encode Literals、Encode Data
- Enc Arithmetic、Encode External、Encode Branches
- Anti Alias Analysis、Anti Taint Analysis
- Self Modify、Checksum、Inline、Optimize
反混淆工具
AntiOllvm
AntiOllvm 是一款付费的反混淆工具,原理是利用 retdec 将二进制代码还原为 LLVM IR,经过各种优化去除混淆后再编译回原文件
优点:
- 在还原回 LLVM IR 后能最大限度保持原有代码特征
- 利用现有的 LLVM 中的优化手段
- 统一架构的分析方法较为通用,重建 CFG 非常容易
- 分析过程更加直观,不接触汇编代码,更容易分析出混淆规则
反混淆流程:
- 使用 retdec 将二进制文件还原为 LLVM IR
- 使用 LLVM 内部优化将 IR 代码优化为较高可读性的代码(部分指令替换及不可达代码在此阶段被优化掉)
- 定制 Pass 去除函数内部的虚假控制流和扁平化
- 通过 LLVM 重新生成汇编代码并回填文件