Webpack混淆逆向实战:从XHR断点到加密参数还原,手把手抠出签名算法

本文系统讲解Webpack打包站点的JS逆向方法,聚焦于通过XHR断点定位加密入口、Hook模块加载器动态拦截调用、分析输入输出快速识别MD5/SHA/AES等加密类型,并还原参数拼接规则。强调黑盒分析优于代码还原,提供Node.js扣代码与Python原生复现两种落地方式,规避模块ID固化、控制流平坦化、密钥隐藏等六大典型陷阱。

做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模块调用

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

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 第二步:判断加密算法类型

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

  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:

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做拦截,用断点看输入输出。不用把所有混淆代码都读懂,抓准输入、算法、密钥三个关键点,就能还原出完整的加密逻辑。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员威哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值