Frida与WT-JS实战:动态Hook与静态还原破解App混淆JS代码

📅 2026/6/30 9:02:50
Frida与WT-JS实战:动态Hook与静态还原破解App混淆JS代码
1. 项目概述与核心目标最近在分析一个名为“X嘟牛”的App时遇到了一个典型的场景它的核心业务逻辑被一层JavaScript代码包裹并且做了混淆和加密。直接静态分析JS文件看到的是一堆面目全非的变量名和难以理解的逻辑。这种时候动态分析就成了破局的关键。我的目标很明确就是要把这个运行在App里的、被混淆的JS代码给“掏”出来还原成可读、可分析的原始形态。这不仅仅是“抓个包”那么简单它涉及到对App运行时的深度介入。整个流程会围绕两个核心工具展开Frida和WT-JS。Frida负责在App运行时进行动态插桩和监听像手术刀一样精准地切入到JS引擎执行的关键节点而WT-JS则负责对抓取到的、经过压缩和混淆的JS代码进行格式化、解混淆和初步还原让我们能看清代码的真实结构。这个过程充满了挑战比如如何绕过App可能存在的反调试、反Frida检测如何准确定位到JS代码加载和执行的生命周期函数以及如何处理WT-JS还原后可能依然存在的语义混淆。接下来我会以一个实战者的角度详细拆解每一步的操作、背后的原理以及我踩过的那些坑。2. 环境准备与工具链搭建工欲善其事必先利其器。一个稳定、隔离的分析环境是逆向工作的基础能避免污染主力机也方便随时回滚。2.1 核心工具安装与配置首先解决Frida的环境问题。很多人卡在第一步比如命令行输入frida提示“不是内部或外部命令”。这通常是因为Python环境或PATH配置问题。Frida安装推荐使用Python虚拟环境我强烈建议为逆向工作创建一个独立的Python虚拟环境。这能避免与系统或其他项目的Python包冲突。# 创建并激活一个名为‘re_env’的虚拟环境以Windows为例 python -m venv re_env re_env\Scripts\activate # 激活后命令行提示符前会出现 (re_env) # 在虚拟环境中安装frida-tools它包含了frida和frida-ps等命令行工具 pip install frida-tools安装完成后在激活的虚拟环境中输入frida --version应该能正常显示版本号。如果还报错检查你的Python和pip是否指向了虚拟环境内的路径。WT-JS工具获取WT-JS是一个专门用于还原微信小程序或类似V8引擎混淆JS的工具包它通常以Python脚本集合的形式存在。你需要从可靠的开发者社区或仓库获取。拿到手后它是一个文件夹里面包含main.py和各种解混淆模块。我通常把它放在一个固定的工作目录下比如D:\Reverse\Tools\wt-js。使用前确保你的Python环境就是上面装Frida的那个虚拟环境已经安装了必要的依赖通常WT-JS的requirements.txt会列出用pip install -r requirements.txt安装即可。Android测试环境我选择使用雷电模拟器作为测试设备。原因有几个一是它支持方便地Root这对Frida工作几乎是必须的二是它的网络桥接模式稳定方便主机和模拟器通信三是快照功能可以快速保存和恢复干净的App状态。安装并启动雷电模拟器例如9.0版本对应Android 9。进入模拟器设置开启Root权限。在模拟器的“属性设置”中将网络连接模式设置为“桥接模式”并选择一个你的主机正在使用的网卡如WLAN网卡。这样模拟器会从你的路由器获取一个和主机在同一局域网的IP地址例如192.168.1.105。记下这个IP后面Frida连接要用。2.2 Frida Server部署与连接测试Frida的工作模式是“客户端-服务器端”。我们的PC是客户端运行着Frida Python脚本Android设备模拟器是服务器端需要运行一个frida-server守护进程。步骤获取frida-server去Frida的官方GitHub Release页面根据你模拟器的架构雷电模拟器9通常是x86_64和已安装的Frida客户端版本下载对应的frida-server-xx.x.x-android-x86_64.xz文件。版本号必须与PC端frida --version显示的版本一致否则会出现Error: version mismatch等连接问题。推送与运行解压下载的.xz文件得到frida-server可执行文件。# 将frida-server推送到模拟器的临时目录 adb push frida-server /data/local/tmp/ # 进入模拟器的shell adb shell # 切换到推送目录 cd /data/local/tmp # 赋予可执行权限 chmod 755 frida-server # 以后台方式启动frida-server ./frida-server 连接测试保持模拟器shell或者新开一个命令行在PC端的虚拟环境命令行中测试连接。# 使用模拟器的IP地址进行连接端口默认27042 frida-ps -H 192.168.1.105如果一切正常这条命令会列出模拟器上运行的所有进程。看到进程列表就意味着Frida环境打通了。注意有些加固或风控较严的App会检测Frida。常见的检测点包括检测frida-server进程名、检测端口27042、检测特征文件或内存映射。在实战中可能需要对frida-server进行重命名、修改默认端口或者使用一些对抗脚本。这是一个猫鼠游戏需要根据目标App的具体情况调整。3. 目标App分析与Hook点定位环境准备好后就要开始对付“X嘟牛”这个目标了。逆向就像侦探破案需要先观察再假设最后验证。3.1 静态初探与动态观察首先我们需要一个APK文件。可以通过模拟器内的应用商店安装然后用adb pull拉取或者从其他渠道获取。拿到APK后用常规的反编译工具如Jadx-GUI打开进行静态分析。我们的核心目标是找到JavaScript代码加载和执行的地方。在Android中WebView或类似V8/JSCore引擎加载JS通常通过以下类和方法WebView.loadUrl(“javascript:...”)WebView.evaluateJavascript()对于更底层的引擎如Cocos2d-js、一些自研框架可能会调用com.example.bridge.JavaBridge之类的类中的callJS或evaluateScript方法。在Jadx中可以全局搜索关键词如“evaluateJavascript”、“loadUrl”、“js”、“javascript”、“bridge”。同时关注那些名字里带“Web”、“JS”、“Bridge”、“Core”的类。在“X嘟牛”的案例中我通过搜索发现了一个名为com.xxx.core.JSEngineHelper的类里面有一个关键方法public String executeJSScript(String script)。这很可能就是我们的目标。3.2 使用Frida进行动态验证与监听静态分析只是猜测动态Hook才是验证。我们要写一个Frida脚本去监听这个疑似的方法。编写第一个Hook脚本hook_js.jsJava.perform(function () { // 定位到我们怀疑的类 var JSEngineHelper Java.use(com.xxx.core.JSEngineHelper); // Hook executeJSScript 方法 JSEngineHelper.executeJSScript.implementation function (script) { // 打印入参也就是即将执行的JS代码可能是混淆后的 console.log(\n[] JSEngineHelper.executeJSScript called!); console.log([] Script length: script.length); // 如果脚本不太长直接打印出来看看 if (script.length 5000) { console.log([] Script content:\n script); } else { console.log([] Script too long, saving to file...); // 这里可以调用Native函数将长脚本写入文件后续会讲 } // 调用原方法获取返回值 var result this.executeJSScript(script); // 打印返回值 console.log([] Return value: result); // 返回原方法的返回值避免影响App正常运行 return result; }; console.log([*] Hook for JSEngineHelper.executeJSScript installed.); });这个脚本做了几件事拦截executeJSScript方法打印传入的JS脚本内容参数script和方法的返回值。运行Hook脚本在模拟器上启动“X嘟牛”App。在PC上使用Frida附加到该进程。首先用frida-ps -H 192.168.1.105找到App的进程名比如com.xduniu.app。运行脚本frida -H 192.168.1.105 -n com.xduniu.app -l hook_js.js-H指定设备-n指定进程名-l加载脚本。分析输出在App内进行一些操作比如点击某个触发JS计算的功能观察Frida控制台的输出。如果我们的Hook点正确你会看到一大段被压缩成一行、变量名类似a,b,c,0x1234的混淆JS代码被打印出来。同时可能还会看到这个方法返回了一些数据可能是JSON字符串。实操心得第一次Hook可能不成功。如果没输出检查1. 类名和方法名是否完全正确大小写、包名2. App是否在子进程执行JS需要Hook多进程3. 方法是否被混淆或动态加载。这时需要扩大搜索范围或者Hook更底层的方法如android.webkit.WebView.evaluateJavascript。4. 高效抓取与保存混淆JS代码当Hook成功后我们会面临一个问题打印到控制台的JS代码可能非常长几万甚至几十万行控制台会卡死并且复制不全。而且我们可能需要捕获App启动时或不同场景下加载的多个JS脚本。4.1 优化Hook脚本进行文件存储我们需要修改脚本将捕获到的JS脚本自动保存到手机存储中。这里利用Frida的NativeFunction调用Linux的C库函数来写文件。增强版Hook脚本hook_js_save.jsJava.perform(function () { var JSEngineHelper Java.use(com.xxx.core.JSEngineHelper); // 定义Native函数用于写入文件到设备 var fopen new NativeFunction(Module.findExportByName(null, fopen), pointer, [pointer, pointer]); var fputs new NativeFunction(Module.findExportByName(null, fputs), int, [pointer, pointer]); var fclose new NativeFunction(Module.findExportByName(null, fclose), int, [pointer]); var scriptCounter 0; // 用于给文件编号 JSEngineHelper.executeJSScript.implementation function (script) { scriptCounter; var filename /data/local/tmp/script_${scriptCounter}.js; // 将JavaScript字符串转换为C字符串 var cPath Memory.allocUtf8String(filename); var cMode Memory.allocUtf8String(w); var file fopen(cPath, cMode); if (file.isNull()) { console.log([-] Failed to open file: ${filename}); } else { var cScript Memory.allocUtf8String(script); fputs(cScript, file); fclose(file); console.log([] Script saved to: ${filename}, length: ${script.length}); } // 同时也打印一个简短的哈希或片段到控制台方便确认 var snippet script.substring(0, Math.min(100, script.length)); console.log([] Snippet: ${snippet}...); var result this.executeJSScript(script); // 如果需要也可以保存返回值 // var resultFilename /data/local/tmp/result_${scriptCounter}.txt; // ... 保存result的代码类似 return result; }; console.log([*] Enhanced hook installed. Scripts will be saved to /data/local/tmp/); });这个脚本会在每次executeJSScript被调用时将传入的script字符串保存到模拟器的/data/local/tmp/目录下文件名为script_1.js,script_2.js等。4.2 批量拉取与整理运行App尽可能多地触发各种功能让不同的JS脚本被执行和保存。然后将这些文件从模拟器拉取到电脑上进行分析。# 将保存的JS脚本全部拉取到本地当前目录的scripts文件夹 adb pull /data/local/tmp/script_*.js ./scripts/现在你本地就有一堆原始的、混淆的JS文件了。用文本编辑器打开一个看看应该是一行代码几乎没有空格和换行变量名都是无意义的短字符。5. 使用WT-JS进行代码还原与解混淆拿到了“矿石”混淆代码下一步就是用“熔炉”WT-JS来提炼。WT-JS的工作原理通常包括解析抽象语法树AST、识别特定的混淆模式如字符串数组化、控制流平坦化、标识符混淆、然后应用相应的还原规则。5.1 WT-JS基础还原操作假设WT-JS工具包的主入口是main.py它通常支持多种还原模式。基本还原命令# 切换到WT-JS工具目录 cd D:\Reverse\Tools\wt-js # 使用虚拟环境Python运行对一个混淆JS文件进行还原 python main.py -i ../scripts/script_1.js -o ../scripts/script_1_restored.js-i指定输入文件-o指定输出文件。运行后WT-JS会尝试进行格式化将单行代码美化添加缩进和换行。常量还原如果代码里用了[str1,str2][index]这种数组方式来隐藏字符串它会尝试计算并替换回原始字符串。简单的变量名重命名有时效果有限因为语义信息已丢失。打开script_1_restored.js你会发现代码已经变得“好看”多了有了结构。但很可能核心的函数名、变量名依然是a,b,c并且可能还存在一些复杂的控制流混淆比如大量的switch-case嵌套即控制流平坦化。5.2 处理高级混淆与手动分析WT-JS可能无法一键解决所有问题。对于控制流平坦化这种强混淆需要更专门的插件或手动分析。识别控制流平坦化还原后的代码中如果你看到一个函数体开头有一个while(1)或for(;;)循环里面是一个大的switch(state)state变量在case块中被改变以跳转到下一个代码块这就是典型的控制流平坦化。它把原本线性的执行流程打散成一个个碎片用状态机来调度极大地增加了阅读难度。应对策略寻找WT-JS的扩展插件有些WT-JS的变种或社区版本提供了针对特定混淆器如obfuscator.io某个版本的平坦化还原插件。查看WT-JS的文档或plugins文件夹。手动分析与简化如果代码量不是特别大可以尝试手动分析。目标是理解这个状态机的逻辑。通常真实的代码逻辑就藏在各个case块里。你可以尝试将case块中的代码按原始顺序根据state的变化顺序提取出来重新组合。这是一个枯燥但有效的方法。关注核心函数我们的最终目的往往不是理解全部代码而是找到关键的加密函数、参数生成函数或通信函数。即使有平坦化你也可以通过搜索关键词如encrypt、sign、MD5、AES、JSON.stringify、XMLHttpRequest等来定位关键代码段。一旦定位到可以只针对这一小段代码进行深入的手动还原。示例定位签名函数在还原后的代码中全局搜索sign或加密等关键词。你可能会找到类似这样的代码块function getSign(t) { var e Object.keys(t).sort().map(function(n) { return n t[n] }).join(); e e keyxxxxxxxx; // 这里可能是固定的密钥 return md5(e); // 假设这里调用了md5函数 }即使这个函数外面包裹着平坦化混淆但函数内部的逻辑是清晰的。我们可以直接把这个函数提取出来放到Node.js环境里进行测试验证。6. 验证还原结果与构建测试环境还原出来的代码是否正确需要用实际数据来验证。6.1 使用Node.js模拟执行将你认为关键的JS函数如上面的getSign以及它依赖的其他函数如md5函数一起复制出来保存为一个单独的.js文件比如sign_logic.js。然后安装Node.js创建一个测试脚本test_sign.js// 引入我们提取的逻辑 var { getSign } require(./sign_logic.js); // 模拟App中构造的请求参数 var testParams { userId: 123456, timestamp: 1687854321, data: hello }; // 计算签名 var calculatedSign getSign(testParams); console.log(Calculated Sign:, calculatedSign); // 与从Frida Hook到的真实网络请求中的签名进行对比 var realSignFromCapture f7a9a8f8c7b...; // 这里填你从抓包或Hook中看到的真实签名 if (calculatedSign realSignFromCapture) { console.log(✅ 签名验证成功还原的代码逻辑正确。); } else { console.log(❌ 签名验证失败。需要检查还原是否完整或是否有隐藏的盐值、随机数。); }运行node test_sign.js观察输出。如果签名一致那恭喜你核心逻辑还原成功如果不一致就需要回头检查是否漏掉了某个全局变量密钥 (key) 是否是动态获取的是否有环境依赖如window对象下的某些属性6.2 处理环境依赖与补环境很多前端JS代码依赖于浏览器或App注入的环境比如window、document、location或者App自定义的JSBridge对象。在Node.js中直接运行会报错ReferenceError: window is not defined。解决方案补环境。创建一个env.js文件模拟这些全局对象// env.js - 模拟浏览器/App环境 global.window global; global.document { createElement: function() { return {}; }, // ... 其他可能用到的属性 }; global.location { href: https://app.xduniu.com }; // 如果代码中使用了App注入的Native对象如 AppBridge global.AppBridge { getDeviceId: function() { return simulated_device_id; }, encryptData: function(data) { /* 模拟加密后期可替换为真实Hook到的函数 */ return data; } }; // 将md5等工具函数挂载到global global.md5 require(./md5.js); // 假设你有一个md5.js然后在你的测试文件开头引入这个环境文件require(./env.js)。注意事项补环境是一个迭代过程。运行测试看报什么错缺什么就补什么。有时候一些环境检测代码比如检查navigator.userAgent可能本身就是反调试的一部分你需要模拟一个合理的值来绕过检测。7. 常见问题排查与实战技巧在这一整套流程中你会遇到各种各样的问题。这里记录一些典型的坑和解决思路。7.1 Frida相关错误与解决Error: version mismatch: 这是最常见的问题。确保PC上frida、frida-tools的版本与推送到设备上的frida-server版本完全一致。去Frida官网下载对应版本。Error: unable to connect to remote frida-server: 连接失败。检查1. 模拟器IP是否正确2. 防火墙是否阻止了27042端口3.frida-server是否在设备上正常运行ps | grep frida4. 是否使用了-H参数指定IP。TypeError: cannot read property implementation of undefined: Hook脚本报错。说明没找到指定的类或方法。可能原因1. 类名/方法名写错2. 类尚未被加载需要在Java.choose或等待类加载3. 方法被混淆需要更模糊的匹配或枚举方法。App闪退或检测到Frida这是高级对抗。尝试1. 重命名frida-server二进制文件并修改端口启动时加-l 0.0.0.0:80802. 使用对抗脚本在Frida脚本开头清除常见特征如环境变量、端口检测3. 尝试在非Root环境下使用frida-gadget注入但难度更高。7.2 WT-JS还原问题还原后代码语法错误混淆器可能使用了非常规的JS语法或极端压缩导致WT-JS的解析器出错。尝试1. 使用WT-JS的不同模式如--mode aggressive2. 先用别的在线JS美化工具格式化一下再喂给WT-JS3. 手动修复明显的语法错误如不匹配的括号。还原不彻底变量名仍是a,b,cWT-JS主要解决语法层混淆对于语义混淆变量名替换无能为力因为原始名称信息已丢失。这时需要你结合上下文、数据流进行分析手动给关键变量和函数起有意义的名字。浏览器的调试器“Sources”面板可以辅助将还原后的代码格式化后贴进去利用调试器单步跟踪观察变量值的变化来推断其含义。无法处理特定的混淆模式WT-JS可能针对的是某一类混淆器。如果遇到新型或自定义混淆需要你研究其模式甚至自己编写AST处理脚本。这是一个更深的领域需要熟悉esprima、estraverse、escodegen等JS AST操作库。7.3 模拟执行与环境问题Node.js中函数执行结果与真实环境不一致除了补环境还要注意时区、随机数种子、设备特定信息如IMEI等。确保你模拟的环境参数与真实App运行时一致。最可靠的方式是用Frida Hook到这些动态值如AppBridge.getDeviceId()的返回值直接在你的测试脚本里硬编码这些值进行测试。代码中存在大量未定义函数这说明你提取的函数片段不完整遗漏了依赖的函数或工具库。你需要回溯还原后的完整JS文件找到这些依赖函数的定义一并提取出来。有时候这些工具库是单独的一个大模块被整体注入。整个逆向过程从Frida动态监听到WT-JS静态还原再到Node.js环境验证是一个“动态-静态-动态”的循环。每一步都可能需要反复调整和回溯。耐心和细致的观察是关键。当你成功还原出核心算法并能在独立环境中复现时那种成就感是无可替代的。这不仅仅是技术上的胜利更是对程序逻辑和开发者思路的一次深刻理解。