做JS逆向这几年,碰到最多的硬骨头就是Webpack打包的站点。代码拆成几百个模块,变量名全压缩成单字母,控制流做了平坦化,加密函数藏在几十层调用里。新手刚上手很容易懵:搜sign搜出几十个结果,个个看着都像,挨个看看到头晕,折腾一天也找不到真正的加密入口。
我刚碰Webpack的时候也踩过一模一样的坑:拿着混淆代码硬啃,逐行捋控制流,两天下来眼睛都花了,连加密算法是MD5还是SHA都没搞清楚。后来摸透了Webpack的模块加载机制,学会了Hook __webpack_require__ 拦截模块,效率直接提了十倍——不用挨个翻代码,哪个模块做了加密、传了什么参数、返回什么结果,清清楚楚打印出来。
今天就把完整的逆向流程分享出来,从识别Webpack特征、定位加密入口、Hook模块加载,到最终还原签名算法,一步步带你抠出加密参数。照着流程走,绝大多数Webpack打包的站点都能拿下。
一、先认特征:三秒判断是不是Webpack打包
不用上来就啃源码,先判断站点是不是Webpack构建的,找对方向再动手。三个最典型的特征,中一个基本就能确认。
1.1 三个标志性特征
-
模块加载函数
__webpack_require__
这是最核心的标识。Webpack把所有代码拆成模块,统一靠这个函数加载执行。打开控制台输入__webpack_require__,能返回函数体,百分百是Webpack。
混淆程度高的站点会把这个函数改名,比如变成__webpack_require__的缩写、或者随机函数名,但结构是固定的:先查缓存,缓存没有就执行模块工厂函数,存缓存再返回。 -
模块表
__webpack_modules__
所有模块都存在这个对象里,键是数字ID,值是模块的工厂函数。控制台打印出来能看到一长串数字ID对应的函数,就是模块表没跑了。 -
webpackJsonp数组
老版本Webpack用全局的webpackJsonp数组做异步模块加载,新版本叫webpackChunkxxx。全局搜一下能找到,也是典型特征。
1.2 Webpack的运行原理
不用研究得太深,记住核心逻辑就行:
- 所有业务代码、工具库、第三方依赖,打包后全部拆成一个个模块,每个模块对应一个数字ID
__webpack_require__(id)就是加载指定ID的模块,返回模块的exports- 模块只会执行一次,执行完结果存在缓存里,下次调用直接读缓存
搞懂这个,逆向的思路就通了:我们不用在几百个模块里瞎找,只要Hook住加载函数,哪个模块做了加密、什么时候调用的,一目了然。
二、第一步:XHR断点追调用栈,找到加密入口
很多人一上来就全局搜 encrypt、sign、md5,这在混淆代码里基本是白费功夫——变量名全被压缩了,函数名早就没了,搜出来的全是无关的临时变量。
最高效的方法永远是从请求入手,顺着调用栈往上追。加密函数最终是为了生成请求参数,只要请求发出去,就一定能顺着链路追回去。
2.1 断在请求发出的瞬间
- 打开DevTools的Network面板,找到目标接口,复制接口路径的关键字
- 切到Sources面板,点开「XHR/fetch 断点」,把关键字加进去
- 触发一次请求,代码会自动断在请求发出的位置
- 看右侧的Call Stack调用栈,从内往外逐层回溯,跳过明显是框架、请求库的层,找到业务代码层
2.2 识别Webpack模块调用
追着追着你会看到类似这样的代码:
var t = n(42);
var sign = 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函数
const originalRequire = __webpack_require__;
// 重写加载函数
__webpack_require__ = function(moduleId) {
// 调用原始函数拿到结果
const result = originalRequire(moduleId);
// 按需过滤,只打印你关心的模块ID
// 比如我们之前追到的42号模块
if (moduleId === 42) {
console.log(`[模块加载] ID: ${moduleId}`);
console.log('导出内容:', result);
// 打印调用栈,方便看是谁调用了这个模块
console.trace();
}
return result;
};
// 把模块表也挂出来方便看
window.__all_modules__ = __webpack_modules__;
console.log('Hook成功,模块表已挂载到 __all_modules__');
})();
运行之后,再触发一次请求,控制台就会打印出对应模块的加载信息,还有完整调用栈。你能清楚看到:这个模块导出了什么方法、是谁调用的、调用的时候传了什么参数。
3.2 进阶:批量扫描加密模块
不知道哪个模块是加密的?直接遍历所有模块源码,按特征批量筛选,几分钟就能定位候选模块:
(function() {
const keywords = ['hmac', 'sha256', 'md5', 'encrypt', 'sign', 'crypto', 'AES'];
const result = [];
for (const moduleId in __webpack_modules__) {
const code = __webpack_modules__[moduleId].toString();
for (const kw of keywords) {
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 第二步:判断加密算法类型
不用逐行抠逻辑,靠特征快速判断:
- MD5 / HMAC-MD5:输出32位十六进制,常见于简单签名。如果有密钥参与,就是HMAC-MD5。
- SHA系列:SHA1是40位,SHA256是64位,SHA512是128位。带密钥就是HMAC-SHA系列,电商、资讯站最常用。
- AES对称加密:输出是base64或十六进制,长度随明文变化但有对齐规律。一般会有固定的密钥、偏移量、填充模式。
- RSA非对称加密:输出长度固定,比如1024位加密后是128字节转base64,公钥一般是硬编码的长字符串。
4.3 第三步:还原明文拼接规则
这是最容易出错的一步,也是最关键的一步。重点看几个细节:
- 参数排序:是不是按键名字典序排序?是升序还是降序?大小写敏感吗?
- 拼接格式:是
key=value&key2=value2,还是直接拼字符串?前后有没有加盐? - 固定参数:有没有时间戳、随机串、设备ID?盐值是硬编码的,还是从Cookie/接口取的?
最稳妥的方法:断点处拿到真实的明文和密文,自己按猜测的规则算一遍,和真实结果对得上,说明规则就找对了。
实战经验:90%的接口签名,本质都是「参数排序 + 拼接固定盐 + MD5/HMAC」。先往这个方向猜,命中率最高,不行再考虑其他算法。
五、两种落地方案:快速验证 vs 生产复现
搞清楚算法和规则之后,就可以落地成自己的代码了。分两种场景选方案。
方案一:直接扣代码,Node.js运行
适合混淆严重、算法复杂的场景,不用逐行还原,拿过来直接用,最快。
- 把加密模块的代码完整扣出来,补全依赖的其他模块
- 封装成一个导出函数,接收参数返回签名
- 用Node.js直接跑,或者用PyExecJS调用
优点是快,复杂混淆也不怕;缺点是依赖JS环境,性能一般,适合验证和小规模使用。
方案二:Python原生复现
适合生产环境大规模使用,性能好,易维护。
把搞清楚的算法规则用Python原生实现,比如HMAC-SHA256:
import hmac
import hashlib
import time
import random
import string
def generate_sign(params, app_key):
# 1. 参数按ASCII字典序排序
sorted_items = sorted(params.items(), key=lambda x: x[0])
# 2. 拼接成key=value格式
param_str = '&'.join([f"{k}={v}" for k, v in sorted_items])
# 3. 前后加盐,HMAC-SHA256签名
sign = hmac.new(
app_key.encode('utf-8'),
param_str.encode('utf-8'),
digestmod=hashlib.sha256
).hexdigest()
return sign
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做拦截,用断点看输入输出。不用把所有混淆代码都读懂,抓准输入、算法、密钥三个关键点,就能还原出完整的加密逻辑。
本文系统讲解Webpack打包站点的JS逆向方法,聚焦于通过XHR断点定位加密入口、Hook模块加载器动态拦截调用、分析输入输出快速识别MD5/SHA/AES等加密类型,并还原参数拼接规则。强调黑盒分析优于代码还原,提供Node.js扣代码与Python原生复现两种落地方式,规避模块ID固化、控制流平坦化、密钥隐藏等六大典型陷阱。

386

被折叠的 条评论
为什么被折叠?



