前阵子接了个加固需求,App 上线后被 class-dump 把类结构扒了个干净,功能逻辑被人照着抄了个马甲包。团队定了两个方向:上 Obfuscator-LLVM 做源码级混淆,或者找能直接处理 IPA 的方案。我先把 Obfuscator-LLVM 的源码读了一遍,搞明白它到底怎么混淆的,再决定走哪条路。这篇把源码层面的实现思路和工程上的取舍一起说清楚。

Obfuscator-LLVM 的结构

Obfuscator-LLVM 是 LLVM 官方主干的一个分支,把混淆做成一个个独立的优化 pass,挂在编译流程里跑。源码结构上,混淆相关代码集中在 lib/Transforms/Obfuscation/ 目录下,每个 pass 一个源文件,职责分得清楚,读起来不费劲。

Substitution:指令替换

Substitution.cpp 做指令替换。思路是拿等价运算替换原指令,比如 a+b 可以替换成 a-(-b),a*b 替换成 (a^b)+((a&b)«1) 这类组合,让逆向的人从指令层面看不出原始运算。它分模块处理加法、减法、布尔运算这些类别,每类的替换规则是写死在代码里的模式列表,启用后随机挑选模式应用。

Bogus Control Flow:虚假控制流

BogusControlFlow.cpp 做虚假控制流。做法是在基本块之间插入永假的条件分支,条件本身用不透明谓词构造——比如 x*x-x 恒等于 0 这类代数恒等式,静态分析很难一眼判断它恒假,分析工具就被这些死代码分支引偏。混淆强度分几个等级,等级越高插入的假块越多,二进制膨胀也越明显。

Flattening:控制流平坦化

Flattening.cpp 做控制流平坦化,三个 pass 里对逆向影响最大的一个。它把 if-else、循环这些有天然结构的控制流,改造成一个 switch 加状态变量的分发循环:原来散落的每个基本块变成分发循环里的一个 case,真实执行顺序由状态变量驱动。改造完,反编译工具看到的是一大块扁平结构,原始的层次关系全部丢失,把执行路径对应回源码逻辑要费很大劲。

字符串加密与集成方式

字符串加密有独立实现,把全局字符串常量替换成加密后的数据,运行时再解密还原。集成方式上,Obfuscator-LLVM 在编译期介入:用它的 clang 替换系统编译器,xcodebuild 传 -mllvm -fla -mllvm -sub -mllvm -bcf 这类参数启用对应 pass,整个编译链都要改。

读源码读出来的限制

读源码的过程也把它的限制摸清了。第一,它要求手里有完整源码和一套能跑通的编译环境,接手别人代码库、或者只想保护某个现成 SDK 的场景就用不上;第二,Swift 混编支持有限,OC 和 C++ 效果好,Swift 代码部分能做的混淆动作少;第三,混淆后的二进制在 Xcode 里调试困难,crash 堆栈的符号对应关系会乱,排查问题成本上去了。

不碰源码的另一条路

相比之下,直接处理 IPA 的路线门槛低很多。用 Ipa Guard 的话,不碰源码,直接对编译好的产物做混淆:扫描二进制里的类和方法,按风险等级标注,勾选后把类名、方法名、变量名替换成无意义乱码,还能处理资源文件名称、修改 MD5、清理调试信息,Ipa Guard 处理完直接重签名装机验证,整个发布流程不用动。适合拿不到源码、或者不想为了混淆改造编译链的团队。
代码混淆

两条路线不冲突:有条件改造编译链、对 OC/C++ 代码保护要求高的,Obfuscator-LLVM 值得上;流程不能动、只要把暴露面压下去的,Ipa Guard 更快见效。按自己团队的条件选,别为了混淆把发布流程搞复杂。