JS混淆+WebAssembly双重防护怎么破?Python高级逆向全流程实战 📅 2026/6/21 8:00:02 做工业数据采集和接口逆向的朋友近两年应该能明显感觉到前端反爬的门槛正在快速拉高。前几年遇上加密接口大多是纯JS实现搜个encrypt、sign就能定位逻辑顶多绕一层变量混淆现在倒好先是控制流平坦化、字符串加密把JS代码搅成一锅粥等你好不容易扒开混淆层发现核心签名逻辑直接塞进了WebAssembly里——抓包只能看到一个.wasm二进制文件导出函数全是无意义的编号传统的JS逆向思路直接失效。很多人遇上Wasm就直接放弃或者退回去用浏览器渲染兜底。但其实只要摸透了它的运行机制再配合成熟的工具链绝大多数Wasm加密都能找到低成本的破解方案。今天这篇文章我把JS混淆与WebAssembly逆向的完整方法论讲透从底层原理到分步实操再到Python工程化落地以及那些踩过的坑一次性整理清楚。一、先看透本质两类防护的底层逻辑很多人逆向上来就对着代码硬读这是典型的战术勤奋。先搞清楚防护的架构和设计目标才能选对成本最低的破解方案。1.1 JS混淆从代码丑化到逻辑迷宫JS混淆的核心目标是提升代码的阅读成本而不是做到绝对不可破解。它的防护强度分三个层级初级混淆变量名替换、代码压缩、去除空格注释本质只是“丑化”格式化后就能读中级混淆字符串加密、死代码注入、函数名扁平化需要先解密字符串、清理垃圾代码高级混淆控制流平坦化、虚拟机保护、反调试检测把线性逻辑拆成状态机调度阅读成本指数级上升目前主流站点用得最多的是javascript-obfuscator的中高强度配置配合域名锁定、反格式化等手段。但不管混淆多复杂它终究运行在JS环境里只要是JS能执行的逻辑我们就能Hook、就能拦截。1.2 WebAssembly浏览器端的二进制黑盒WebAssembly简称Wasm是一种低级二进制指令格式由C/C/Rust编译而来运行在浏览器的沙箱环境中执行效率接近原生代码。站点把核心加密、签名、风控逻辑编译成WasmJS只负责传参和调用相当于把核心逻辑放进了黑盒里。和纯JS防护相比它有三个显著特点没有可读源码只有二进制字节码反编译后也是汇编级指令运行性能高适合做复杂的加密计算和风控检测内存独立和JS通过线性内存交互参数传递有固定套路防护架构对比混淆JSWasm防护架构写入线性内存结果写回内存业务参数混淆JS调度层WebAssembly核心模块密文/签名接口请求纯JS加密架构业务参数JS加密函数密文/签名接口请求简单来说混淆JS是“让你看不懂代码”Wasm是“干脆不给你代码”。两者叠加就是当前前端防护的顶配组合。二、逆向核心方法论先分层再选型很多人遇上混合防护就乱了阵脚一会儿抠混淆代码一会儿反编译Wasm忙活半天没进展。逆向不是比谁更能啃硬骨头而是找投入产出比最高的路径。通用逆向工作流纯JS混淆Wasm实现低强度高强度逻辑简单逻辑复杂抓包分析加密特征核心逻辑在哪?定位加密入口函数定位导出函数与内存混淆强度?扣代码补Node环境运行Hook输入输出 黑盒调用逻辑复杂度?Python加载Wasm直接调用浏览器侧RPC远程调用Python工程化封装记住一个核心原则能黑盒调用就不还原逻辑能直接运行就不反编译。逆向的最终目标是稳定拿到正确的加密结果不是读懂每一行代码。花三天反编译Wasm重写算法和花半小时搭个RPC调用服务最终效果一样但成本天差地别。三、JS混淆逆向从硬读到高效Hook先从大家最熟悉的JS混淆说起。很多人面对混淆代码的第一反应是“还原它”但在实战中Hook永远比还原代码效率更高。3.1 快速定位加密入口定位入口是逆向第一步三个方法按效率排序关键词搜索法全局搜sign、encrypt、aes、rsa、CryptoJS以及请求参数的字段名80%的场景能直接定位到附近XHR断点回溯给目标接口打XHR/fetch断点触发后查看调用栈从请求发出的位置往前回溯加密逻辑一定在参数组装的链条上通用Hook拦截针对JSON.stringify、btoa、encodeURIComponent这类高频方法打Hook加密前的明文一定会经过这些方法断下后顺着调用栈往上找就是加密入口3.2 混淆代码的高效处理定位到入口后不要上来就硬读代码按这个步骤处理先格式化用DevTools的Pretty Print先把压缩代码展开这一步不花时间解密字符串绝大多数混淆代码都有一个统一的字符串解密函数找到后直接把所有加密字符串替换成明文可读性立刻提升一个量级清理死代码删除永远不会执行的分支、无意义的变量赋值精简代码体积控制流还原如果遇到控制流平坦化优先用AST工具做自动化还原比如基于Babel写插件还原调度逻辑通用工具还原不了的再考虑手动梳理核心分支这里有个很重要的心态不需要还原全部代码。我们只需要搞清楚加密函数的入参、出参和依赖关系能让它在我们的环境里跑起来就行。无关的业务逻辑、垃圾代码完全可以跳过。3.3 Python侧调用方案把JS加密逻辑抽出来之后Python侧有两种常用的调用方式轻量场景用PyExecJS或者Node.js子进程执行适合调用频率不高的场景高性能场景把加密逻辑封装成HTTP服务用Python发请求调用进程常驻避免重复初始化的开销importsubprocessimportjsondefcall_js_encrypt(plaintext):通过Node子进程调用JS加密逻辑js_codef const encrypt require(./encrypt.js); console.log(encrypt({plaintext})); resultsubprocess.run([node,-e,js_code],capture_outputTrue,textTrue)returnresult.stdout.strip()四、WebAssembly逆向从黑盒到可控Wasm是很多人的知识盲区但只要搞懂了它和JS的交互规则大部分场景都能快速搞定。4.1 第一步定位Wasm模块与导出函数首先在浏览器Network面板找到.wasm文件下载到本地。然后在Sources面板的WebAssembly分类下能看到加载的模块点开后里面有所有导出函数Exported Functions。怎么确认哪个是目标加密函数两个实用技巧搜JS源码里的WebAssembly.instantiate或WebAssembly.Instance找到实例化后赋值的对象看它的方法调用给可疑的导出函数打断点触发一次加密请求哪个函数被命中哪个就是目标很多站点的导出函数没有名字只有数字编号比如func_12没关系我们只需要知道它的调用方式和参数规则。4.2 第二步搞懂参数传递规则Wasm不能直接传递字符串、对象这类复杂类型所有数据都通过**线性内存Linear Memory**交互这是Wasm逆向最核心的知识点。标准交互流程是JS侧把字符串转成Uint8Array写入Wasm内存的某个地址JS把内存地址指针、数据长度传给Wasm导出函数Wasm函数内部计算把结果写到内存的另一块地址Wasm返回结果的内存指针JS从对应地址读取字节并转成字符串所以调试Wasm的时候不用纠结内部汇编指令重点盯三件事入参指针、入参长度、返回指针。只要能对应上输入输出的内存位置就能当黑盒用。4.3 第三步Python直接加载运行Wasm如果只是要复现加密结果最省事的方案就是直接在Python里加载Wasm模块和浏览器侧一样调用。推荐用wasmtime库性能稳定兼容性好。核心实现代码importwasmtimeclassWasmEncryptor:def__init__(self,wasm_path):self.enginewasmtime.Engine()self.storewasmtime.Store(self.engine)self.modulewasmtime.Module.from_file(self.engine,wasm_path)self.instancewasmtime.Instance(self.store,self.module,[])# 获取导出函数和内存对象self._encryptself.instance.exports(self.store)[encrypt]self._mallocself.instance.exports(self.store)[malloc]self.memoryself.instance.exports(self.store)[memory]def_write_str(self,s:str)-tuple[int,int]:将字符串写入Wasm内存返回指针和长度datas.encode(utf-8)ptrself._malloc(self.store,len(data))bufself.memory.data_ptr(self.store)buf[ptr:ptrlen(data)]datareturnptr,len(data)def_read_str(self,ptr:int,max_len256)-str:从Wasm内存读取字符串到00结束符bufself.memory.data_ptr(self.store)endptrwhileendptrmax_lenandbuf[end]!0:end1returnbytes(buf[ptr:end]).decode(utf-8)defencrypt(self,plaintext:str)-str:ptr,lengthself._write_str(plaintext)result_ptrself._encrypt(self.store,ptr,length)returnself._read_str(result_ptr)这套方案的优势是性能高、不依赖浏览器、可以并发调用适合大规模采集场景。绝大多数标准加密算法实现的Wasm都可以用这种方式直接跑起来。4.4 兜底方案浏览器RPC调用如果遇到Wasm逻辑特别复杂、有环境检测、或者有动态生成的Wasm直接加载跑不通就用兜底方案用Playwright启动一个浏览器页面加载原始站点的JS和Wasm在页面注入Hook代码封装加密函数成全局方法Python侧通过页面evaluate调用加密函数拿到结果这种方案本质是借浏览器的环境跑原始代码兼容性拉满再复杂的防护都能绕过缺点是性能比原生调用低一些。适合逆向成本极高、但调用量不大的场景。五、踩坑实录这些坑我都替你踩过了Wasm和混淆JS的逆向细节坑特别多很多时候逻辑都对但结果就是不对问题都出在细节上。坑1字符串编码不匹配这是最高频的错误。JS侧默认用UTF-16Wasm里大多是UTF-8中文场景很容易出现明文一致、密文不同的情况。一定要确认编码方式两边统一用UTF-8字节流交互。坑2内存越界与地址冲突自己随便选个内存地址写数据很容易覆盖Wasm正在使用的内存导致结果异常甚至直接崩溃。正确做法是调用Wasm导出的malloc函数分配内存用完后free释放不要硬编码地址。坑3环境检测与反调试很多混淆JS和Wasm都会做环境检测比如检测process对象判断是不是Node环境检测navigator.webdriver判断是不是自动化浏览器。本地运行结果不对的时候优先排查环境检测补全缺失的浏览器对象。坑4Wasm动态生成有些站点不直接加载.wasm文件而是用JS拼接字节数组再动态实例化Wasm。这种情况不要去扒拼接逻辑直接HookWebAssembly.instantiate在实例化的时候把模块dump下来就行。坑5多轮加密与链式调用不要想当然认为只有一次加密。很多站点是Wasm算中间值JS再做二次处理或者JS混淆层做一次编码Wasm做一次加密。一定要从请求参数往前完整回溯确保没漏掉任何一步处理。六、写在最后聊到最后想说说对逆向这件事的理解。不管是JS混淆还是WebAssembly本质上都是成本博弈。站点花成本做防护提升逆向门槛我们花成本做破解权衡时间和收益。没有绝对破解不了的防护只有性价比不够高的方案。所以做逆向最忌讳钻牛角尖——为了还原一个算法死磕一周明明用RPC调用半天就能搞定。真正高效的逆向永远是先评估方案成本选最快落地的那条路先跑通业务再按需优化性能。技术是工具解决问题才是目的。合规提示本文所述技术仅用于合法合规的技术研究与公开数据分析场景请严格遵守目标站点的服务条款与robots协议禁止用于任何非法用途。