【翻译】站在巨人的肩膀上:用 LLVM 反混淆 WebAssembly
本文最后更新于:2026年8月14日 下午
本文为翻译 / 转载内容
- 原标题:Standing on the Shoulders of Giants: De-Obfuscating WebAssembly Using LLVM
- 原作者:Vikas Gupta、Peter Garba(Thales Cybersecurity & Digital Identity, CDI)
- 原始出处:Black Hat Asia 2025 议题演讲(同一议题也曾在 RE//verse 2025 上分享)
- 原始幻灯片:https://i.blackhat.com/Asia-25/Asia-25-Gupta-StandingOnTheShouldersOfGiants.pdf
- 演讲视频:https://www.youtube.com/watch?v=Z-udrjM7Z78
- 作者议题合集:https://github.com/su-vikas/Presentations
本文由本人根据 Black Hat Asia 2025 公开幻灯片翻译整理成中文,并对幻灯片中过于精简的要点做了必要的行文补全,便于中文读者阅读。版权归原作者所有,翻译仅供学习交流使用。原始幻灯片以图示为主,部分演示截图(反混淆前后的代码对比)无法在文字中呈现,建议结合原始 PDF 与视频一起看。
关于作者
Vikas Gupta
- Thales CDI 高级安全研究员,此前就职于 Google
- 信息安全硕士,OSCP 认证
- OWASP 移动安全测试指南(MSTG)合著者
- 关注方向:逆向工程、移动安全
Peter Garba
- Thales CDI(新加坡)首席软件安全工程师、产品负责人
- Thales 内部混淆工具的作者
- 夜里是个狂热的逆向工程爱好者
动机与问题陈述
这次议题想回答的其实是四个递进的问题:
- WebAssembly 对我们来说足够安全吗?
- 如何混淆 Wasm 二进制?
- 如何把 Wasm 提升(lift)到 LLVM IR?
- 如何反混淆 Wasm 二进制、恢复出原始逻辑?
需要说明的是,现有关于 Wasm 安全的文献非常多,但绝大多数是从漏洞利用视角出发的——挖漏洞、写 exploit、安全编码实践。而这次议题讨论的是逆向工程视角下的 Wasm 安全,这是一个明显缺乏公开研究的方向。
本次工作的成果
- 演示如何复用现有工具链完成 Wasm 的混淆与反混淆
- 提出并实现了把 Wasm 提升到 LLVM IR 的工具——Squanchy
- 实现了 Wasm 的自动化反混淆
WebAssembly 基础
在进入正题前,先过一遍从逆向角度需要了解的 Wasm 内部机制。
- WebAssembly 于 2015 年公布,是一个高性能、安全、可移植的编译目标。
- 生成的二进制体积紧凑、解析速度快。
- 运行在一个基于栈的虚拟机上(可以类比 JVM),通过定义良好的 imports / exports 与宿主程序通信。
- 应用面很广:游戏、大型 Web 应用(如 Google Earth)、区块链智能合约等等。
结构上:
- 每个 Wasm 程序是单个代码文件,称为 Module(模块)。
- 模块按 Section(段) 组织:export、import、globals、functions 等。
- 采用索引空间(Indexed Spaces):条目通过从 0 开始的整数索引访问。
- 代码空间与数据空间是分离的,这带来了几个后果:
- 编译后的程序无法破坏自身的执行环境
- 无法跳转到任意位置
- 无法执行其他未定义行为
最后这条对逆向来说很关键:Wasm 的控制流天然比原生二进制”规整”,这既是它作为混淆载体的限制,也是我们做反混淆时可以利用的结构性前提。
Wasm 工具链现状
- WABT(WebAssembly Binary Toolkit)
- wasm-tools
- Ghidra:通过 Wasm 插件,可以查看反编译后的 Wasm
- IDA Pro v9:对 Wasm 的支持时好时坏(hit-n-miss),这里主要用它来查看目标文件
- JEB Pro
Wasm 混淆
什么是混淆
混淆:在保持程序原有行为的前提下,让程序变得更难理解的过程。
把代码变得不可读,动机通常有这么几类:
- 恶意软件作者想规避逆向分析
- 商业应用中防止 IP 被窃取或被逆向
- 数字版权管理(DRM)
基于 LLVM 的混淆
混淆 Wasm 的一条主流路径是在 LLVM 中端(middle-end)做文章。这种做法的最大好处是与源语言无关——不管前端是 C、C++ 还是 Rust,只要能进到 LLVM IR,混淆 pass 就能生效。
开源的 LLVM 混淆器有:O-LLVM、Hikari、Polaris。三者支持的能力大致如下:
| O-LLVM | Hikari | Polaris |
|---|---|---|
| 指令替换(Instruction Substitution) | 虚假控制流(Bogus control flow) | 别名访问(Alias Access) |
| 控制流平坦化(Control Flow Flattening) | 控制流平坦化 | 平坦化(Flattening) |
| 基本块分割(Basic block splitting) | 函数调用混淆 | 间接跳转(Indirect Branch) |
| 虚假控制流 | 函数包装(Function wrapper) | 间接调用(Indirect Call) |
| 基本块分割 | 字符串加密 | |
| 字符串加密 | 虚假控制流 | |
| 指令替换 | 指令替换 | |
| 间接跳转 | 函数合并(Merge Function) | |
| 线性 MBA(Linear MBA) |
混淆带来的复杂度膨胀
反复施加混淆之后,情况会迅速恶化:
- 二进制体积急剧膨胀,例如涨到 12MB
- 反编译出来的代码超过 2000 行
- 工具开始崩溃
这就是反混淆真正的起点:不是”看不懂”,而是”工具直接跑不动”。
Wasm 反混淆
反混淆的两个目标
- 还原那些被施加的变换(有时候这是不可能的)
- 简化代码,让后续分析能够继续下去
经典方法 vs 现代方法
经典混淆依赖混淆模式、常量展开、垃圾代码插入;相应地,经典反混淆靠的是模式匹配。
现代混淆发生在源码层面或中间表示(IR)层面;现代反混淆则依托于多个不同抽象层次的中间语言,并基于通用优化工具来做——这正是本文路线的核心思想。
SATURN:基于编译器的反混淆
SATURN 提出了一种基于 LLVM 编译器基础设施的通用反混淆方法:把二进制代码提升回 LLVM IR,再用自定义的反混淆 pass 去处理。它能削弱某些混淆,最理想的情况下可以把混淆彻底移除。
这也是本次工作的思想来源——所谓”站在巨人的肩膀上”。
先试试 Binaryen(失败了)
Binaryen 是 Wasm 的编译器与工具链基础设施库,它的优化器有大量能改善代码体积和速度的 pass,输入 Wasm、输出 Wasm,看起来是最自然的选择。
结果是:不行,完全没有反混淆效果。 😞😡
提升到 LLVM IR
为什么选 LLVM
- LLVM 是目标无关的优化器和代码生成器
- LLVM 有语言无关的中间表示(IR)
- 用 LLVM IR 的好处:
- 世界级的优化与分析 pass
- 表达能力丰富的中间语言
- 易用的 API
- 归一化(Normalization)
- 多种后端可用于重新编译
- 快!
提升的挑战
想用上 LLVM 的优化 pass,前提是把 Wasm 提升到 LLVM IR。这件事的难点在于:
- 正确性
- 要捕获副作用与表达能力
- 运行时环境的表示
- 栈式机器到寄存器机器的转换
最后两条是核心矛盾:Wasm 是栈机,LLVM IR 是 SSA 寄存器形式;而 Wasm 的运行时状态(内存、全局变量、函数表)如果不建模出来,优化器根本无从下手。
方案一:WAMRC(不理想)
WAMR(WebAssembly Micro Runtime)是一个轻量、独立的 Wasm 运行时,其中的 WAMRC 是把 Wasm 编译成 AOT 文件的 AOT 编译器。
但它的缺点很致命:
- 符号信息丢失
- 生成的 LLVM IR 不包含各种表(全局变量表、函数表)⇒ LLVM 优化根本跑不起来
方案二:wasm2c(选中)
wasm2c 是把 Wasm 提升到 C 的优秀工具,它的关键优势在于提供了一个定义良好的 Wasm 运行时,这在反混淆时非常有用:
- 提供初始化 Wasm 实例和内存的辅助函数
- 提供初始化、修改全局变量的辅助函数
- 提供内存 load/store 的辅助函数,而且这些访问都经由 helper 完成,可以被覆写(override)
- 不修改原始的控制流图(CFG)
最后一点非常重要——反混淆的过程中必须保住 CFG,否则你没法判断自己还原出来的东西对不对。
它的不足是:
- 每个函数拿不到运行时信息(比如各种表)
- 代码不会折叠(fold)
wasm2c 生成的代码结构大致是这样:
1 | |
w2c_instance会传递给所有函数,用来在函数之间保持执行状态w2c_env_instance可以自由用来跟踪重要的值Memory结构体保存当前已初始化内存的状态,会用表内存来初始化- 函数表用于间接函数调用
- 全局变量是动态生成的
w2c_instance由 helper 函数实例化,完成内存、全局变量等的初始化
于是反混淆的思路就清晰了:既然运行时状态是由 helper 初始化并且可被覆写的,那就把运行时”建模”进 IR 里,让 LLVM 的优化器能看见这些常量事实。
Squanchy:把想法自动化
Squanchy 是本次工作实现的工具,用来自动化多个反混淆步骤:
- 建模并注入运行时:把运行时 helper 注入到模块/函数中
- 相应地内联函数
- 优化函数/模块:使用定制的优化流水线以保留控制流图
- 移除 wasm2c 运行时
- 把目标函数及其依赖提取到一个新的干净模块中
项目地址:https://github.com/pgarba/Squanchy
优化阶段的关键取舍
Squanchy 会施加 LLVM 的 O3 流水线,同时保留控制流图。这里作者给出的洞察很有意思:
混淆流水线是人写出来的。
也就是说,混淆的施加顺序和组合是有套路的:
- 控制流保护:控制流平坦化
- 代码保护:指令替换
- 强化保护:不透明谓词(Opaque Predicates)、混合布尔算术(MBA)
既然施加是有结构的,拆解也就可以有针对性。
此外,Squanchy 还会覆写 LLVM 的各种阈值(thresholds)——默认的内联阈值、展开阈值这些是为正常代码调优的,面对混淆后的膨胀代码需要放宽,否则优化器会直接放弃。
最后是 Brightening(提亮) 与重编译:从 LLVM IR 出发,可以直接重新编译到 ARM64 等目标。
单靠 LLVM 优化不够
跑完 LLVM 优化之后确实有进展,但也暴露了明确的边界:
- LLVM 只能削弱某些混淆
- 有些技术 LLVM 根本破不掉:
- 控制流平坦化(*)
- 虚假控制流
- 求解复杂的 MBA(多轮替换叠加之后尤其困难)
- ……
超越 LLVM:求解 MBA
$$(x \oplus y) + 2 \times (x \wedge y) = x + y$$
MBA(Mixed Boolean Arithmetic,混合布尔算术) 表达式:
- 把算术运算符(
+、-、×)和布尔运算符(¬、⊕、∧、∨)混在一起 - 难以分析——这些运算符之间没有通用规则可用(没有分配律、没有结合律等等)
- 面对复杂的 MBA,SMT 求解器可能也解不出来
- 基于模式的 MBA 求解可以通过串联(chaining)多个 MBA 来绕过
专门用来解 MBA 的工具:
- SiMBA:处理线性 MBA
- GAMBA:处理非线性 MBA 表达式
- SiMBA++:在 LLVM IR 层面简化 MBA,https://github.com/pgarba/SiMBA-
SiMBA++ 的工作方式是:
- 在 LLVM IR 中检测候选表达式
- 调用 SiMBA 或 GAMBA 执行简化(支持调用外部简化器)
- 用简化结果替换 IR 中的原表达式
于是流水线变成了:LLVM Opt + SiMBA + GAMBA,效果继续推进。
再进一步:SOUPER
Souper 是一个基于综合(synthesis)的超优化器(superoptimizer),它针对的是一种领域特定的 IR——可以理解为 LLVM IR 中一个纯函数式、无控制流的子集。
- 可以作为 LLVM 优化 pass 运行
- 能够综合出优化:
- CEGIS(反例引导的归纳综合):生成多个候选 RHS,从中挑选代价最小的
- 数据流分析
- 能够解掉不透明谓词
- 对控制流混淆效果很好
至此,工具链拼图基本完整:LLVM + SiMBA + GAMBA + SOUPER。面对 Hikari(Sub=1, bogus=1, split=1)产生的冗长复杂混淆代码,这套组合能给出可读的还原结果。
真实世界的应用
Wasm 恶意软件
浏览器中用 Wasm 做挖矿的情况在稳步增加:
- 相比 JS,Wasm 执行哈希运算快得多
- **门罗币(Monero)**是挖矿中最常被使用的加密货币
恶意软件多样化:wasm-mutate
Carbera-Arteaga 等人演示了用 wasm-mutate 来规避检测。
wasm-mutate 会把二进制变换成一个保持原有功能的变体程序,包含三类变换:
- 窥孔(Peephole):约 135 条重写规则
- 模块结构变换:添加新类型、新函数、新导出等
- 控制流图变换:循环展开、交换条件分支
需要注意 wasm-mutate 的输出需要用 wasm-validate 验证——有些变换会破坏 WASM 文件。
Squanchy 的效果(对 wasm-mutate):
- 施加了 3000 次(真实)迭代
- 100% 的变异被移除
- 代码被归一化,与原始代码 100% 匹配
- 也就是说,这套方法完整恢复了函数(顺带还优化了它)
反混淆真实恶意软件
针对 CryptoNight 及其混淆版本:
- Squanchy 反混淆出来的函数与未混淆版本的函数相匹配
- 参考:
用例:hCaptcha
- hCaptcha 使用了混淆过的 Wasm
- 中小规模的混淆函数可以在 1–2 分钟内完成简化
- 效果:解开控制流平坦化、简化并内联函数
这一条对做爬虫/风控对抗的同学大概是最有直接价值的:hCaptcha 这类商业验证码的 Wasm 核心逻辑,并非不可分析。
结论
Wasm 混淆
- 基于 LLVM IR 的工具:O-LLVM、Polaris
Squanchy:把 Wasm 提升到 LLVM IR
- wasm2c + Squanchy 的组合效果很好
用 LLVM 做反混淆
- LLVM + SiMBA + GAMBA + SOUPER + ……
真实世界应用
- 恶意软件归一化:wasm-mutate 的输出可以被简化;CryptoNight 恶意软件被成功简化
- hCaptcha 二进制的反混淆
工具汇总
- 现有工具链可以复用
- 混淆:Polaris、O-LLVM、Wasmixer
- 反混淆:LLVM、SiMBA++、SOUPER
- 符号执行:KLEE、Manticore、SeeWasm
相关资源
- 幻灯片 + 白皮书:https://github.com/su-vikas/Presentations
- Squanchy:https://github.com/pgarba/Squanchy
- SiMBA++:https://github.com/pgarba/SiMBA-
- Black Hat Asia 2025 原始 PDF:https://i.blackhat.com/Asia-25/Asia-25-Gupta-StandingOnTheShouldersOfGiants.pdf
- 演讲视频:https://www.youtube.com/watch?v=Z-udrjM7Z78
再次声明:本文为 Black Hat Asia 2025 议题 Standing on the Shoulders of Giants: De-Obfuscating WebAssembly Using LLVM 的中文翻译整理,原作者为 Vikas Gupta 与 Peter Garba,版权归原作者所有。如有翻译不准确之处,以原始幻灯片和演讲视频为准。