☰
Webpack混淆逆向实战:从XHR断点到加密参数还原,手把手抠出签名算法
2026/10/29 11:10:46 网站建设 项目流程

做JS逆向这几年,碰到最多的硬骨头就是Webpack打包的站点。代码拆成几百个模块,变量名全压缩成单字母,控制流做了平坦化,加密函数藏在几十层调用里。新手刚上手很容易懵:搜sign搜出几十个结果,个个看着都像,挨个看看到头晕,折腾一天也找不到真正的加密入口。

我刚碰Webpack的时候也踩过一模一样的坑:拿着混淆代码硬啃,逐行捋控制流,两天下来眼睛都花了,连加密算法是MD5还是SHA都没搞清楚。后来摸透了Webpack的模块加载机制,学会了Hook__webpack_require__拦截模块,效率直接提了十倍——不用挨个翻代码,哪个模块做了加密、传了什么参数、返回什么结果,清清楚楚打印出来。

今天就把完整的逆向流程分享出来,从识别Webpack特征、定位加密入口、Hook模块加载,到最终还原签名算法,一步步带你抠出加密参数。照着流程走,绝大多数Webpack打包的站点都能拿下。

一、先认特征:三秒判断是不是Webpack打包

不用上来就啃源码,先判断站点是不是Webpack构建的,找对方向再动手。三个最典型的特征,中一个基本就能确认。

1.1 三个标志性特征

  1. 模块加载函数__webpack_require__
    这是最核心的标识。Webpack把所有代码拆成模块,统一靠这个函数加载执行。打开控制台输入__webpack_require__,能返回函数体,百分百是Webpack。
    混淆程度高的站点会把这个函数改名,比如变成__webpack_require__的缩写、或者随机函数名,但结构是固定的:先查缓存,缓存没有就执行模块工厂函数,存缓存再返回。

  2. 模块表__webpack_modules__
    所有模块都存在这个对象里,键是数字ID,值是模块的工厂函数。控制台打印出来能看到一长串数字ID对应的函数,就是模块表没跑了。

  3. webpackJsonp数组
    老版本Webpack用全局的webpackJsonp数组做异步模块加载,新版本叫webpackChunkxxx。全局搜一下能找到,也是典型特征。

1.2 Webpack的运行原理

不用研究得太深,记住核心逻辑就行:

  • 所有业务代码、工具库、第三方依赖,打包后全部拆成一个个模块,每个模块对应一个数字ID
  • __webpack_require__(id)就是加载指定ID的模块,返回模块的exports
  • 模块只会执行一次,执行完结果存在缓存里,下次调用直接读缓存

搞懂这个,逆向的思路就通了:我们不用在几百个模块里瞎找,只要Hook住加载函数,哪个模块做了加密、什么时候调用的,一目了然。

二、第一步:XHR断点追调用栈,找到加密入口

很多人一上来就全局搜encrypt、sign、md5,这在混淆代码里基本是白费功夫——变量名全被压缩了,函数名早就没了,搜出来的全是无关的临时变量。

最高效的方法永远是从请求入手,顺着调用栈往上追。加密函数最终是为了生成请求参数,只要请求发出去,就一定能顺着链路追回去。

2.1 断在请求发出的瞬间

  1. 打开DevTools的Network面板,找到目标接口,复制接口路径的关键字
  2. 切到Sources面板,点开「XHR/fetch 断点」,把关键字加进去
  3. 触发一次请求,代码会自动断在请求发出的位置
  4. 看右侧的Call Stack调用栈,从内往外逐层回溯,跳过明显是框架、请求库的层,找到业务代码层

2.2 识别Webpack模块调用

追着追着你会看到类似这样的代码:

vart=n(42);varsign=t.default(params);

这里的n就是混淆后的__webpack_require__,42是模块ID,t.default就是模块导出的加密函数。

把模块ID记下来,比如这里是42,接下来我们就可以针对这个模块做分析。

踩坑提醒:不要在请求库的封装层打转。很多站点封装了统一的request方法,断住后第一层都是请求拦截器的代码,往上多追几层,到业务逻辑层才是真正生成签名的地方。

三、核心技巧:Hook模块加载器,拦截所有加密调用

找到了单个模块还不够,很多站点的加密是多层调用的:参数预处理在A模块,核心加密在B模块,密钥生成在C模块。只看一个模块很容易漏掉关键逻辑。

最实用的方法就是重写__webpack_require__,给所有模块加载加上拦截。哪个模块被调用了、传了什么参数、返回了什么结果,全部打印出来,加密逻辑再复杂也藏不住。

3.1 通用Hook代码

直接在控制台Sources面板的Snippets里新建一个片段,粘贴进去运行即可:

(function(){// 保存原始require函数constoriginalRequire=__webpack_require__;// 重写加载函数__webpack_require__=function(moduleId){// 调用原始函数拿到结果constresult=originalRequire(moduleId);// 按需过滤,只打印你关心的模块ID// 比如我们之前追到的42号模块if(moduleId===42){console.log(`[模块加载] ID:${moduleId}`);console.log('导出内容:',result);// 打印调用栈,方便看是谁调用了这个模块console.trace();}returnresult;};// 把模块表也挂出来方便看window.__all_modules__=__webpack_modules__;console.log('Hook成功,模块表已挂载到 __all_modules__');})();

运行之后,再触发一次请求,控制台就会打印出对应模块的加载信息,还有完整调用栈。你能清楚看到:这个模块导出了什么方法、是谁调用的、调用的时候传了什么参数。

3.2 进阶:批量扫描加密模块

不知道哪个模块是加密的?直接遍历所有模块源码,按特征批量筛选,几分钟就能定位候选模块:

(function(){constkeywords=['hmac','sha256','md5','encrypt','sign','crypto','AES'];constresult=[];for(constmoduleIdin__webpack_modules__){constcode=__webpack_modules__[moduleId].toString();for(constkwofkeywords){if(code.toLowerCase().includes(kw)){result.push(moduleId);break;}}}console.log('疑似加密模块ID:',result);})();

跑一下就能列出所有包含加密关键词的模块ID,再逐个Hook验证,比挨个翻代码快得多。

踩坑提醒:不要上来就Hook所有模块打印,几百个模块会把控制台刷爆。先通过调用栈定位到大致的模块范围,再针对性Hook,效率最高。

四、拆解加密逻辑:不用全读懂,盯紧输入输出就行

找到加密模块后,很多人就开始逐行啃混淆代码,对着控制流平坦化的switch-case头晕。其实完全没必要,逆向不是做代码还原,只要搞清楚三个问题就够了:
加密的明文是什么?用的什么算法?密钥是什么?

4.1 第一步:打断点看真实输入输出

在加密函数入口和出口各打一个断点,触发一次请求,直接看真实的入参和返回值。

  • 入参是什么格式?是字符串还是对象?有哪些字段?
  • 返回值是什么格式?多少位?是十六进制还是base64?

比如返回值是32位小写十六进制,大概率是MD5;64位大概率是SHA256;固定长度的base64字符串,大概率是AES或RSA。

4.2 第二步:判断加密算法类型

不用逐行抠逻辑,靠特征快速判断:

  1. MD5 / HMAC-MD5:输出32位十六进制,常见于简单签名。如果有密钥参与,就是HMAC-MD5。
  2. SHA系列:SHA1是40位,SHA256是64位,SHA512是128位。带密钥就是HMAC-SHA系列,电商、资讯站最常用。
  3. AES对称加密:输出是base64或十六进制,长度随明文变化但有对齐规律。一般会有固定的密钥、偏移量、填充模式。
  4. RSA非对称加密:输出长度固定,比如1024位加密后是128字节转base64,公钥一般是硬编码的长字符串。

4.3 第三步:还原明文拼接规则

这是最容易出错的一步,也是最关键的一步。重点看几个细节:

  • 参数排序:是不是按键名字典序排序?是升序还是降序?大小写敏感吗?
  • 拼接格式:是key=value&key2=value2,还是直接拼字符串?前后有没有加盐?
  • 固定参数:有没有时间戳、随机串、设备ID?盐值是硬编码的,还是从Cookie/接口取的?

最稳妥的方法:断点处拿到真实的明文和密文,自己按猜测的规则算一遍,和真实结果对得上,说明规则就找对了。

实战经验:90%的接口签名,本质都是「参数排序 + 拼接固定盐 + MD5/HMAC」。先往这个方向猜,命中率最高,不行再考虑其他算法。

五、两种落地方案:快速验证 vs 生产复现

搞清楚算法和规则之后,就可以落地成自己的代码了。分两种场景选方案。

方案一:直接扣代码,Node.js运行

适合混淆严重、算法复杂的场景,不用逐行还原,拿过来直接用,最快。

  1. 把加密模块的代码完整扣出来,补全依赖的其他模块
  2. 封装成一个导出函数,接收参数返回签名
  3. 用Node.js直接跑,或者用PyExecJS调用

优点是快,复杂混淆也不怕;缺点是依赖JS环境,性能一般,适合验证和小规模使用。

方案二:Python原生复现

适合生产环境大规模使用,性能好,易维护。
把搞清楚的算法规则用Python原生实现,比如HMAC-SHA256:

importhmacimporthashlibimporttimeimportrandomimportstringdefgenerate_sign(params,app_key):# 1. 参数按ASCII字典序排序sorted_items=sorted(params.items(),key=lambdax:x[0])# 2. 拼接成key=value格式param_str='&'.join([f"{k}={v}"fork,vinsorted_items])# 3. 前后加盐,HMAC-SHA256签名sign=hmac.new(app_key.encode('utf-8'),param_str.encode('utf-8'),digestmod=hashlib.sha256).hexdigest()returnsign

Python原生实现的性能比调用JS高得多,也更容易做并发和工程化,正式项目优先选这个。

六、Webpack逆向最容易踩的六个坑

坑1:模块ID是固定的?错

很多人扣完代码,把模块ID写死,结果站点一更新就失效。Webpack每次打包,模块ID都可能重新生成,不要依赖固定ID,要用特征动态定位。

坑2:死磕控制流平坦化

很多站点做了控制流平坦化混淆,整个函数全是switch-case和状态机,逐行读能把人绕晕。
解决:不要读流程,直接打断点看输入输出。黑盒测试比代码还原高效得多,知道输入什么输出什么就行,不用搞懂中间每一步。

坑3:密钥一定在JS里?错

很多加密的密钥、盐值,不是硬编码在JS里的,可能在Cookie里、在页面meta标签里、或者是接口下发的。死磕JS代码找密钥,找一天也找不到。
解决:看加密函数的所有入参,往上追来源,是从哪传进来的,顺着找总能找到。

坑4:主包里找不到,在异步chunk里

有些站点把加密逻辑拆成了异步加载的JS文件,主包里没有,页面加载完一段时间才会加载进来。
解决:触发一次加密操作,等异步JS加载完再Hook;或者在Network面板里看所有加载的JS文件,逐个排查。

坑5:字符串加密,肉眼看不到关键词

有些站点把所有字符串都做了加密,运行时才解密,所以搜sign、encrypt全搜不到。
解决:不要依赖字符串搜索,从调用栈+Hook模块入手,不管字符串怎么加密,函数调用关系是藏不住的。

坑6:非要还原全部代码

新手最容易犯的毛病:非要把整个混淆的函数还原成可读代码,花大量时间做反混淆。
解决:逆向的目标是复现加密结果,不是还原源码。能黑盒调用就黑盒调用,能复现算法就直接复现,不要做无用功。

总结

Webpack混淆的逆向,说难也难,说简单也简单。
难在代码拆得散、混淆程度高,瞎搜瞎看如同大海捞针;简单在它的模块加载机制是固定的,只要Hook住加载器,加密逻辑再深也能定位到。

核心思路永远是:从请求入手,用调用栈定位,用Hook做拦截,用断点看输入输出。不用把所有混淆代码都读懂,抓准输入、算法、密钥三个关键点,就能还原出完整的加密逻辑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询