1. 项目概述从“金币修改”切入 WebAssembly 逆向世界最近在技术社区和游戏修改圈子里WebAssembly简称 Wasm逆向分析的热度越来越高。很多朋友尤其是对游戏安全、客户端逻辑分析感兴趣的开发者都遇到了一个典型场景一个网页游戏或者用 Wasm 技术打包的 H5 应用其核心逻辑比如角色的金币数量、攻击力数值被编译成了.wasm二进制文件。面对这一堆“天书”如何把它变回我们能看懂、甚至能修改的代码就成了一个非常实际的需求。我最近就花了不少时间研究这个目标很明确反编译一个 Wasm 文件找到存储或计算“金币数量”的逻辑并尝试修改它。这个过程涉及静态分析、动态调试、代码修改与回编译是一套完整的逆向工程实践。无论你是想学习 Wasm 底层机制的安全研究员还是对游戏机制好奇的开发者甚至是希望加固自己 Wasm 应用的前端工程师这套方法论都能给你带来不少启发。接下来我就把自己趟过的路、踩过的坑以及最终成功的步骤毫无保留地分享出来。2. 核心思路与工具选型为什么是这套组合拳面对一个 Wasm 二进制文件我们的目标是将机器友好的指令转换回人类可理解至少是程序员可理解的代码形式并定位到关键数据或函数。直接阅读 Wasm 字节码是不现实的因此我们需要借助工具链。经过多次实践对比我最终确定了以WABTWebAssembly Binary Toolkit为核心结合静态分析工具如 Ghidra、IDA和浏览器动态调试的三位一体策略。这里详细解释一下为什么是它们以及各自扮演的角色。首先WABT 是官方维护的工具集它提供了wasm2wat和wasm2c这两个关键工具。wasm2wat能将二进制.wasm文件转换为文本格式.watWebAssembly Text Format。.wat是一种基于 S-表达式的、人类可读的中间表示它清晰地展示了模块结构、函数定义、内存布局和指令流。虽然.wat依然接近汇编但它的可读性比纯二进制强了不止一个数量级是我们理解程序结构的基石。而wasm2c则更进一步它能将 Wasm 模块转换为等价的 C 代码。这个 C 代码虽然看起来有些“机械”因为它是从栈式虚拟机指令翻译过来的但它提供了完整的函数框架、类型定义和内存访问逻辑极大地便利了我们在高级语言层面进行逻辑分析。尤其是当我们需要搜索特定的常量如初始金币数 100或字符串时在 C 代码里进行文本搜索的效率远高于在二进制或.wat中摸索。然而转换得到的 C 代码并非完美的、可直接编译运行的原生 C 程序。它严重依赖 WABT 运行时库wasm-rt.h等来模拟 Wasm 的执行环境特别是线性内存和函数表。这意味着如果你想把它编译成一个独立的可执行文件进行调试会非常麻烦。所以静态分析工具的介入就至关重要了。Ghidra和IDA Pro这类成熟的逆向工程平台对 Wasm 的支持日益完善。它们不仅能解析 Wasm 文件格式还能进行反汇编、控制流图生成、数据类型分析和交叉引用查找。例如在 Ghidra 中加载由wasm2c生成的.c文件编译出的.o目标文件或者直接加载.wasm文件需要相应插件可以让你以更接近传统二进制逆向的视角来审视代码结构快速定位到可能进行数值比较、赋值的关键函数。但静态分析有时会碰到逻辑复杂或混淆过的代码这时就需要动态调试来验证猜想。现代浏览器Chrome/Edge/Firefox的开发者工具内置了强大的 Wasm 调试支持。你可以在 Wasm 指令级别设置断点、单步执行、查看和修改线性内存以及全局变量的值。这对于验证“某个内存地址是否存储着金币数值”或者“某个函数是否在每次消费时被调用”至关重要。动态调试是连接静态分析猜想与实际程序行为的桥梁。注意工具链的选择没有银弹。对于简单的、未混淆的 Wasmwasm2wat直接阅读可能就够了。对于复杂的、涉及加解密或大量间接调用的可能需要结合 Ghidra 的反编译能力和浏览器的动态调试。我推荐的这套组合覆盖了从初级到高级的分析需求。2.1 工具链的安装与准备工欲善其事必先利其器。下面是在 Ubuntu Linux 环境下搭建这套工具链的详细步骤。Windows 和 macOS 用户也可以找到对应的安装包或编译方法。1. 编译安装 WABTWABT 是基石我们需要从源码编译以获取所有工具和必要的头文件。# 1. 克隆仓库 git clone --recursive https://github.com/WebAssembly/wabt.git cd wabt # 2. 创建构建目录并编译 mkdir build cd build cmake .. make -j$(nproc) # 3. 编译完成后工具位于 build 目录下如 ./wasm2wat, ./wasm2c # 为了方便可以将其添加到系统 PATH或者后续使用绝对路径。编译过程可能会需要 CMake 和 C 编译器如 g请确保系统已安装。编译成功后你会得到一系列可执行文件我们主要用到wasm2wat,wat2wasm,wasm2c,wasm-objdump。2. 准备静态分析环境Ghidra:从 Ghidra 官网 下载最新版本。它是一个 Java 应用解压后运行ghidraRun即可。Ghidra 自带的 Wasm 加载器已经相当不错。IDA Pro:如果你有 IDA Pro建议 7.7 以上版本需要安装 Wasm 插件例如wasm2ida脚本。但根据我的经验Ghidra 对 Wasm 的支持已经非常友好且免费对于大多数任务Ghidra 是首选。3. 浏览器调试环境任何基于 Chromium 的浏览器Chrome, Edge, Brave或 Firefox 都可以。确保开发者工具F12是开启的。后续我们会详细讲解如何在其中调试 Wasm。3. 静态分析实战从二进制到可读代码假设我们有一个名为game.wasm的文件我们的目标是找到并理解其中管理“金币”的逻辑。第一步初步探查与格式转换首先使用wasm-objdump对文件有个整体认识/path/to/wabt/build/wasm-objdump -x game.wasm这个命令会输出 Wasm 模块的段section信息包括导入/导出函数、内存定义、全局变量、数据段等。你需要特别关注Export部分看看有哪些函数是导出给 JavaScript 调用的比如getGold,setGold,addGold之类的。游戏逻辑通常通过导出函数与 JS 交互。Data部分这里可能存放着初始化的字符串或数值常量。如果金币的初始值是硬编码的可能会在这里找到线索。Memory部分了解 Wasm 模块定义了多少内存通常至少一个。金币数值很可能存储在某个固定的内存地址中。接下来进行核心的转换。我们将.wasm转换为.wat和.c。# 转换为文本格式 .wat /path/to/wabt/build/wasm2wat game.wasm -o game.wat # 转换为 C 代码 /path/to/wabt/build/wasm2c game.wasm -o game.c转换完成后你会得到game.wat和game.c两个文件。先打开game.wat它的结构类似 Lisp你会看到很多(func ...)、(i32.const ...)、(i32.store ...)这样的表达式。虽然不直观但你可以搜索像gold、coin、money这样的导出函数名或全局变量名如果开发者没有混淆的话。如果找到了相关的函数名就可以在文件中定位到该函数的定义观察其指令逻辑。第二步深入分析 C 代码game.c文件才是我们分析的主力。用文本编辑器或 IDE 打开它。这个文件会包含很多以Z_开头的函数它们对应着 Wasm 模块中的各个函数。文件开头会包含#include wasm-rt.h并定义了一个代表整个模块的结构体比如Z_game_instance。现在开始我们的“侦查”工作搜索常量在game.c中搜索你认为的金币初始值比如100、1000、9999。注意Wasm 中的整数常量可能会以十六进制形式出现在 C 代码中比如0x64对应十进制 100。使用编辑器的“在文件中查找”功能。搜索字符串搜索可能相关的字符串如gold、coins、currency。这些字符串可能作为数据段内容在 C 代码中会以字符数组的形式出现例如u8 data_segment_data_0[] {0x67, 0x6f, 0x6c, 0x64, 0x00};即 gold\0。分析函数逻辑如果你通过导出表或字符串搜索定位到了疑似函数例如找到了一个名为Z_get_gold的函数就仔细阅读其 C 代码。典型的金币获取函数可能包含从某个固定内存地址加载数据的操作在wasm2c生成的代码中这通常表现为调用WASM_RT_LOAD_I32宏或直接访问instance-memory.data数组。例如static u32 Z_get_gold(struct Z_game_instance* instance) { // 假设金币存储在内存地址 0x5000 处 return WASM_RT_LOAD_I32(instance-memory, 0x5000); }而设置金币的函数则可能使用WASM_RT_STORE_I32宏。理解内存布局在game.c文件的开头部分或靠近数据段定义的地方可能会有一个大的u8 data_segment_data_0[]数组里面存放了初始化数据。你需要结合wasm-objdump输出的数据段信息确定这些初始化数据被加载到了内存的什么地址。金币的初始值很可能就在这里。实操心得wasm2c生成的代码可读性关键取决于原始 Wasm 的复杂度和优化级别。如果原始代码经过高级编译器如 Emscripten 的-O2、-O3优化生成的 C 代码可能会包含很多内联、循环展开和难以理解的临时变量分析起来会困难很多。此时不要纠结于理解每一行 C 代码而是抓住核心模式加载LOAD、存储STORE、运算加减乘除、比较分支。找到对疑似金币地址的访问模式就是成功的关键。第三步使用 Ghidra 进行辅助分析当 C 代码过于复杂时将game.c编译成一个对象文件然后导入 Ghidra可以利用其强大的反编译引擎得到更易读的伪代码。# 首先确保你有 wabt 的 include 目录。假设 wabt 在 /home/user/wabt # 编译 game.c 为 game.o gcc -c game.c -o game.o -I/home/user/wabt/wasm2c -I/home/user/wabt -O2编译时可能会报错找不到wasm-rt.h等头文件你需要正确指定 WABT 源码中的wasm2c和根目录路径。编译成功后在 Ghidra 中新建项目将game.o文件导入。Ghidra 会进行分析。分析完成后你可以在Symbol Tree中看到所有Z_开头的函数。双击函数名Ghidra 会在反编译窗口显示伪代码。Ghidra 的优势在于它能进行数据流分析和类型推断有时能将混乱的指针访问重构为清晰的数组或结构体访问。你可以利用它的“搜索”功能快捷键CtrlShiftF在整个程序中搜索特定的数值常量或字符串并直接跳转到引用该常量的所有位置这比在文本中搜索更高效。4. 动态调试验证在浏览器中“现场抓包”静态分析给了我们假设例如“金币存储在内存地址 0x5000”。动态调试则是验证假设的终极手段。我们需要让这个 Wasm 模块在浏览器中运行起来并能够观察和修改其状态。第一步搭建本地调试环境由于浏览器安全限制直接通过file://协议打开包含 Wasm 的 HTML 文件可能无法进行完整的 Wasm 调试或者会遇到跨域问题。最可靠的方法是启动一个简单的本地 HTTP 服务器。# 在包含 game.wasm 和其宿主 HTML/JS 文件的目录下 python3 -m http.server 8080然后在浏览器中访问http://localhost:8080来打开你的页面。第二步使用浏览器开发者工具打开 Chrome 开发者工具F12切换到“源代码”Sources标签页。在左侧文件导航栏中你应该能找到你的.wasm文件。点击它浏览器会将其反汇编为.wat格式并显示在中央编辑器区域。设置断点你可以直接在显示的 Wasm 指令行号上点击来设置断点。更有效的方法是如果你通过静态分析知道了某个关键函数的导出名例如get_gold你可以在“调试器”面板右侧的“作用域”或“事件监听器断点”中尝试在 Wasm 函数被调用时暂停。但更直接的是在 Wasm 代码中寻找关键操作。定位关键内存访问根据静态分析如果我们怀疑0x5000地址存放金币那么就在 Wasm 代码中搜索涉及该地址的指令。在 Wasm 中内存存储指令是i32.store加载指令是i32.load。你可以在源代码视图里搜索i32.store或(i32.store ...)。找到后在相应的行设置断点。运行与观察触发游戏中的相关操作比如点击获取金币的按钮。当程序执行到你设置的断点时执行会暂停。此时在开发者工具的“内存”Memory面板中你可以查看 Wasm 实例的线性内存。在内存面板中输入你怀疑的地址如0x5000并选择适当的格式如“32位有符号整数”或“32位无符号整数”进行查看。如果这个位置的值正好是你当前的金币数量并且在你进行游戏操作花费金币后这个值发生了变化那么恭喜你找对了修改内存值在“内存”面板中你可以直接双击内存值进行修改。将0x5000处的值从 100 改成 9999然后继续执行程序。如果游戏界面显示的金币数也随之改变那么你的修改就成功了。第三步高级调试技巧与 JS 辅助有时Wasm 内存地址不是固定的或者需要通过基址加偏移来计算。这时可以在包含 Wasm 的 HTML 页面中注入一些辅助调试的 JavaScript 代码就像你在资料中看到的那样。这段 JS 代码可以将 Wasm 实例的导出对象包括内存挂载到全局并提供了方便查看内存的函数viewDWORD,viewString,search等。这样你就能在浏览器控制台Console里直接调用这些函数来扫描内存、搜索特定数值或字符串模式极大地提升了动态分析的效率。注意事项动态调试时浏览器的开发者工具可能会对 Wasm 进行优化或重新编译导致行号或指令与原始.wat文件略有不同这是正常现象。关键在于把握核心指令load/store和内存地址。另外一些游戏可能会对内存中的数值进行加密或校验直接修改内存可能导致游戏逻辑错误或触发反作弊机制。这属于更高级的对抗需要结合静态分析理解其加密算法。5. 修改与回编译让修改持久化动态调试修改内存只是临时的刷新页面就没了。我们的终极目标是通过修改.wasm二进制文件本身实现永久性的“金币自由”。方法一修改 .wat 并回编译如果我们通过静态分析在game.wat中找到了初始化金币值的位置比如在一个data段中或者在某个初始化函数的i32.const指令后跟着i32.store我们可以直接修改.wat文件。在game.wat中找到对应位置。例如数据段初始化(data (i32.const 0x5000) \64\00\00\00) ;; 0x64 100, 小端序将其中的\64修改为更大的值的十六进制表示例如\10\27代表 0x2710 10000。注意 Wasm 使用小端序。或者在函数中找到设置金币的指令(func $init_gold (param i32) i32.const 0x5000 i32.const 100 ;; 这里是十进制表示 i32.store )将i32.const 100改为i32.const 9999。保存修改后的game.wat使用wat2wasm工具将其编译回.wasm文件。/path/to/wabt/build/wat2wasm modified_game.wat -o modified_game.wasm用修改后的modified_game.wasm替换原来的文件刷新页面测试。方法二直接 Patch .wasm 二进制文件对于简单的数值修改有时直接编辑二进制文件更快捷。你需要一个十六进制编辑器如hexedit,010 Editor, 或 VSCode 的 Hex Editor 插件。使用wasm-objdump -d game.wasm反汇编找到对应指令的文件偏移量file offset。例如你发现i32.const 100对应的字节码序列是41 640x41是i32.const的操作码0x64是 LEB128 编码的 100。在十六进制编辑器中定位到该偏移量将代表数值 100 的字节64修改为对应新值的 LEB128 编码。9999 的 LEB128 编码是CF 9F 01你可以用 Python 等工具计算list((9999).to_bytes((9999.bit_length()7)//8, little))。保存文件。这种方法要求你对 Wasm 二进制格式和 LEB128 编码有一定了解适合小范围的精确修改。实操心得修改前务必备份原文件。回编译或 Patch 后一定要用wasm-validateWABT 工具之一检查修改后的文件是否有效。./wasm-validate modified_game.wasm。如果验证通过再替换测试。有时简单的数值修改会导致内存访问越界或其他逻辑错误如果游戏崩溃或行为异常需要重新审视你的修改是否影响了其他依赖此值的计算。6. 常见问题与排查技巧实录在这一路上我遇到了不少坑。这里总结几个典型问题和解决方法希望能帮你节省时间。问题1wasm2c转换失败或生成的 C 代码编译报错。可能原因使用的 WABT 版本与 Wasm 文件使用的某些新特性或尾调用tail call等指令不兼容。解决尝试更新到最新版本的 WABT 源码并重新编译。如果问题依旧可以尝试先用wasm2wat转换再使用wat2wasm重新编译为.wasm这有时会规范化格式然后再进行wasm2c。问题2在 Ghidra 中分析.o文件时函数名杂乱找不到入口。可能原因wasm2c生成的 C 代码函数名都是Z_前缀Ghidra 可能无法识别 Wasm 特有的调用约定和内存模型导致分析不完整。解决优先尝试 Ghidra 直接导入原始的.wasm文件。新版本的 Ghidra 对 Wasm 的支持越来越好能够自动识别函数、内存和数据段。如果必须分析.o文件可以在 Ghidra 的Symbol Table中手动查找和重命名你认为重要的函数。问题3浏览器开发者工具中看不到 Wasm 文件或者无法设置断点。可能原因1Wasm 模块是通过WebAssembly.instantiate()动态创建或流式编译的源代码列表中没有独立的.wasm文件。解决在开发者工具的“源代码”标签页注意顶部偏右有一个“更多选项”图标三个点点击后确保“Wasm 调试”是启用的。同时在“网络”Network标签页过滤 Wasm 请求找到对应的文件然后右键选择“以源形式打开”。可能原因2页面是通过file://协议打开的存在安全限制。解决务必使用本地 HTTP 服务器如python -m http.server来提供页面。问题4找到了金币的内存地址但修改后游戏没有立即更新显示或者数值很快被改回去了。可能原因1金币值有多个副本或缓存。游戏逻辑可能在内存中多个位置存储了同一数值例如一个用于显示一个用于计算。你只修改了一处。解决在修改内存后继续单步执行或触发一次金币更新操作如捡一个金币观察是否有其他指令将旧值重新写回。或者搜索所有向该地址进行i32.store的指令。可能原因2存在客户端校验或加密。金币值在存储前可能经过了某种变换如 XOR 一个密钥或加上一个随机偏移。直接修改存储值会导致解密后得到错误结果。解决这需要更深入的逆向分析。通过静态分析找到负责“读取”和“写入”金币的函数理解其完整的变换逻辑。修改时可能需要模拟这个逻辑或者直接修改变换后的值并确保相关校验也能通过。这通常涉及算法逆向是更高级的挑战。问题5修改.wat后wat2wasm回编译失败提示语法错误。可能原因手动编辑.wat时引入了语法错误如括号不匹配、指令格式错误、或使用了错误的数值表示法。解决.wat是严格的 S-表达式格式。仔细检查你修改区域的括号是否成对。确保数字常量格式正确十进制、十六进制0x...。可以使用文本编辑器的括号高亮功能辅助检查。一个稳妥的方法是只修改指令中的立即数如i32.const后面的值不要改动指令结构和内存布局。这个过程就像一场数字侦探游戏需要耐心、细心和对底层原理的理解。从模糊的二进制到清晰的逻辑每一步破解都带来巨大的成就感。记住工具只是辅助最重要的还是你对程序运行逻辑的推理和假设验证能力。