【翻译】站在巨人的肩膀上:用 LLVM 反混淆 WebAssembly

本文最后更新于:2026年8月14日 下午

本文为翻译 / 转载内容

本文由本人根据 Black Hat Asia 2025 公开幻灯片翻译整理成中文,并对幻灯片中过于精简的要点做了必要的行文补全,便于中文读者阅读。版权归原作者所有,翻译仅供学习交流使用。原始幻灯片以图示为主,部分演示截图(反混淆前后的代码对比)无法在文字中呈现,建议结合原始 PDF 与视频一起看。

关于作者

Vikas Gupta

  • Thales CDI 高级安全研究员,此前就职于 Google
  • 信息安全硕士,OSCP 认证
  • OWASP 移动安全测试指南(MSTG)合著者
  • 关注方向:逆向工程、移动安全

Peter Garba

  • Thales CDI(新加坡)首席软件安全工程师、产品负责人
  • Thales 内部混淆工具的作者
  • 夜里是个狂热的逆向工程爱好者

动机与问题陈述

这次议题想回答的其实是四个递进的问题:

  1. WebAssembly 对我们来说足够安全吗?
  2. 如何混淆 Wasm 二进制?
  3. 如何把 Wasm 提升(lift)到 LLVM IR?
  4. 如何反混淆 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-LLVMHikariPolaris。三者支持的能力大致如下:

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
u32 w2c_add(w2c* instance, …)
  • 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++ 的工作方式是:

  1. 在 LLVM IR 中检测候选表达式
  2. 调用 SiMBA 或 GAMBA 执行简化(支持调用外部简化器)
  3. 用简化结果替换 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 及其混淆版本:

用例: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

相关资源


再次声明:本文为 Black Hat Asia 2025 议题 Standing on the Shoulders of Giants: De-Obfuscating WebAssembly Using LLVM 的中文翻译整理,原作者为 Vikas Gupta 与 Peter Garba,版权归原作者所有。如有翻译不准确之处,以原始幻灯片和演讲视频为准。


【翻译】站在巨人的肩膀上:用 LLVM 反混淆 WebAssembly
https://kingjem.github.io/2026/08/14/逆向/【翻译】站在巨人的肩膀上-用LLVM反混淆WebAssembly/
作者
Ruhai
发布于
2026年8月14日
许可协议