Webpack 逆向还原
Webpack 打包产物的核心结构是一个立即执行函数(IIFE),将所有模块以对象或数组的形式传入: 快速识别关键词: Webpack 5 典型特征: 入口模块是 Webpack 启动时最先执行的模块,通常通过以下方式找到: 定位步骤: 1. 全局搜索 __webpack_require__.s 找到入口 ID 赋值处 2. 在 bundle 末尾找到 IIFE 的最后一行调用 3. 若有多个 chunk,在 runtime chunk 中查找 在浏览器控制台或断点处,将所有模块导出到全局变量: 全量 dump 脚本(在合适断点处注入): 1. 在 De
官方文档:https://webpack.js.org/concepts/
适用场景:分析 Webpack 打包产物,定位并提取加密模块的 require ID 和调用方式
Webpack bundle 结构识别
经典 Webpack bundle 特征
Webpack 打包产物的核心结构是一个立即执行函数(IIFE),将所有模块以对象或数组的形式传入:
// Webpack 4 经典结构(modules 为对象,key 为模块 ID)
(function(modules) {
// 模块加载器
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
var installedModules = {};
// ... 挂载辅助方法 ...
return __webpack_require__(__webpack_require__.s = 0); // 启动入口模块
})({
0: function(module, exports, __webpack_require__) {
// 入口模块
var utils = __webpack_require__(1);
},
1: function(module, exports, __webpack_require__) {
// 工具模块
exports.encrypt = function(data) { /* ... */ };
}
});
快速识别关键词:
__webpack_require__installedModules或__webpack_module_cache____webpack_require__.m(modules 对象)__webpack_require__.s(入口模块 ID)- 函数签名
function(module, exports, __webpack_require__)
Webpack 4 vs Webpack 5 的 bundle 格式差异
| 特征 | Webpack 4 | Webpack 5 |
|---|---|---|
| 模块缓存变量名 | installedModules |
__webpack_module_cache__ |
| modules 存储变量名 | modules(局部变量) |
__webpack_modules__(顶层变量) |
| 模块 ID 格式 | 数字 ID 为主 | hash 字符串 ID 为主 |
| chunk 加载 | webpackJsonp 数组推送 |
self["webpackChunk*"] 数组推送 |
| 运行时代码 | 内联在 bundle 中 | 可分离为独立 runtime chunk |
| Tree Shaking | 有限支持 | 更激进,模块可能被合并 |
Webpack 5 典型特征:
// Webpack 5:模块定义在顶层变量
var __webpack_modules__ = ({
"abc123": function(module, __webpack_exports__, __webpack_require__) {
// ESM 模块
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"encrypt": function() { return encrypt; }
});
function encrypt(data) { /* ... */ }
}
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = { exports: {} };
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
入口模块定位
入口模块是 Webpack 启动时最先执行的模块,通常通过以下方式找到:
// Webpack 4:直接调用
__webpack_require__(__webpack_require__.s = 0)
// 或
__webpack_require__(0)
// Webpack 5:
__webpack_require__("./src/index.js")
// 或通过 startup 函数
__webpack_require__.x = function() {
__webpack_require__("abc123");
};
定位步骤:
- 全局搜索
__webpack_require__.s找到入口 ID 赋值处 - 在 bundle 末尾找到 IIFE 的最后一行调用
- 若有多个 chunk,在 runtime chunk 中查找
模块 ID 格式
| 类型 | 示例 | 出现场景 |
|---|---|---|
| 数字 ID | 0, 1, 42 |
Webpack 4 默认,production 模式 |
| 字符串路径 ID | "./src/utils/crypto.js" |
Webpack development 模式 |
| hash ID | "a1b2c3d4" |
Webpack 5 默认,contenthash |
| 相对路径 ID | "../node_modules/axios/lib/axios.js" |
保留原始路径时 |
提取模块的方法
方法一:全量提取(dump 整个 modules 对象)
在浏览器控制台或断点处,将所有模块导出到全局变量:
// Webpack 4:在 IIFE 内部断点,找到 modules 局部变量
// 在 __webpack_require__ 函数可访问的作用域内执行:
window._modules = modules;
window._require = __webpack_require__;
// 之后可以在控制台调用任意模块
var cryptoModule = window._require(1);
cryptoModule.encrypt("test");
// Webpack 5:__webpack_modules__ 通常在顶层作用域可访问
window._modules = __webpack_modules__;
window._require = __webpack_require__;
全量 dump 脚本(在合适断点处注入):
(function() {
// 找到 __webpack_require__ 后,遍历所有已加载模块
var result = {};
Object.keys(__webpack_module_cache__).forEach(function(id) {
result[id] = __webpack_module_cache__[id].exports;
});
console.log(JSON.stringify(Object.keys(result)));
window._cache = __webpack_module_cache__;
window._req = __webpack_require__;
})();
方法二:按需提取(找到目标加密函数所在模块)
- 在 DevTools Sources 面板中搜索加密函数关键词
- 定位到所在模块的函数体
- 记录该模块的 ID
- 扣出该模块及其所有依赖模块
// 找到目标模块后,记录依赖链
// 例如目标模块 ID 为 42,依赖模块 7 和 15
var targetModule = __webpack_require__(42);
// 检查 targetModule 的导出
console.dir(targetModule);
__webpack_require__ 的模拟
在本地 Node.js 环境中复现 __webpack_require__:
// local_runner.js
const fs = require('fs');
// 从 bundle 中扣出的模块对象
const modules = {
42: function(module, exports, __webpack_require__) {
// 目标加密模块代码
var dep = __webpack_require__(7);
exports.sign = function(data) {
return dep.md5(data + 'secret');
};
},
7: function(module, exports, __webpack_require__) {
// MD5 依赖模块
exports.md5 = function(str) { /* ... */ };
}
};
const cache = {};
function __webpack_require__(moduleId) {
if (cache[moduleId]) {
return cache[moduleId].exports;
}
const module = cache[moduleId] = { exports: {} };
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
// 加载目标模块
const target = __webpack_require__(42);
console.log(target.sign('hello'));
模块缓存的利用
模块缓存(installedModules / __webpack_module_cache__)存储已执行模块的 exports,可以直接读取而不重新执行:
// 在页面中,利用缓存直接获取已加载模块的导出
// 无需知道模块内部逻辑,直接拿到导出对象
Object.entries(__webpack_module_cache__).forEach(([id, mod]) => {
const keys = Object.keys(mod.exports || {});
if (keys.some(k => /encrypt|sign|token|hash/.test(k))) {
console.log('找到可疑模块:', id, keys);
}
});
也可以污染缓存来替换模块行为(mock):
// 替换缓存中的模块,使后续 __webpack_require__(id) 返回自定义值
__webpack_module_cache__["abc123"] = {
exports: {
encrypt: function(data) {
console.log('[hook] encrypt called with:', data);
return originalEncrypt(data);
}
}
};
调试定位技巧
在 bundle 中搜索加密函数
步骤:
- 在 DevTools Sources 面板中,使用
Ctrl+Shift+F全局搜索(所有文件) - 常用搜索关键词:
- 加密算法关键词:
CryptoJS,SHA256,AES,HMAC,md5 - 逻辑关键词:
sign,token,encrypt,secret,key - 参数名:
timestamp,nonce,appid
- 加密算法关键词:
- 找到可疑代码后,在该行打断点
- 触发请求,观察断点处的调用栈和变量
// 常用:在 XMLHttpRequest.send 处打 hook,逆向追溯参数来源
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
console.trace('XHR send called');
console.log('body:', body);
return originalSend.apply(this, arguments);
};
Source Map 的使用
如果站点在生产环境留有 source map(.js.map 文件),可以还原为原始源码:
# 检查是否存在 source map
# bundle 文件末尾通常有注释
# //# sourceMappingURL=main.abc123.js.map
# 下载 source map
curl https://example.com/static/main.abc123.js.map -o main.js.map
# 使用 source-map 工具还原
npm install -g source-map-explorer
source-map-explorer main.abc123.js main.js.map
使用 sourcemap-toolkit 提取单个文件:
// 使用 node source-map 包还原指定位置的原始代码
const { SourceMapConsumer } = require('source-map');
const fs = require('fs');
async function resolvePosition(mapFile, line, column) {
const rawMap = JSON.parse(fs.readFileSync(mapFile, 'utf8'));
const consumer = await new SourceMapConsumer(rawMap);
const pos = consumer.originalPositionFor({ line, column });
console.log('原始文件:', pos.source);
console.log('原始行:', pos.line, '列:', pos.column);
consumer.destroy();
}
resolvePosition('./main.js.map', 1, 12345);
DevTools 中若检测到 source map,Sources 面板会自动展示原始文件结构,直接在原始文件打断点即可。
Webpack chunk 异步加载的处理
Webpack 的 import() 动态导入会生成独立的 chunk 文件,通过 JSONP 加载:
// Webpack 4 chunk 加载:bundle 中有 webpackJsonp 推送
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
[2], // chunk ID
{
10: function(module, exports, __webpack_require__) {
// 懒加载模块
}
}
]);
// Webpack 5 chunk 加载
(self["webpackChunkmy_app"] = self["webpackChunkmy_app"] || []).push([
["vendors-async"],
{
"abc123": function(module, exports, __webpack_require__) {
// 懒加载模块
}
}
]);
处理方法:
- 在 Network 面板过滤
.js,触发操作,观察新加载的 chunk 文件 - 在 chunk 推送函数处打断点,捕获动态加载的模块
- 将所有 chunk 文件下载下来,合并 modules 对象后本地运行
// 监听 chunk 加载,自动收集新模块
const originalPush = self["webpackChunkmyapp"].push.bind(self["webpackChunkmyapp"]);
self["webpackChunkmyapp"].push = function(chunk) {
const [, newModules] = chunk;
console.log('新 chunk 加载,模块 IDs:', Object.keys(newModules));
return originalPush(chunk);
};
模块导出的常见形式
| 导出形式 | 代码示例 | 说明 |
|---|---|---|
| CommonJS 默认导出 | module.exports = function() {} |
整个 exports 被替换 |
| CommonJS 具名导出 | exports.fn = function() {} |
挂载到 exports 对象 |
| ESM(经 Webpack 编译) | __webpack_require__.d(exports, { fn: () => fn }) |
Webpack 将 ESM 转为此形式 |
| 混合导出 | module.exports.default = ...; module.exports.fn = ... |
混合使用时 |
识别 ESM 模块:模块开头有 __webpack_require__.r(exports) 调用(标记为 ES module)。
本地还原模板
完整的本地运行模板(Node.js)
// webpack_runner.js
'use strict';
// 补充浏览器全局环境
global.window = global;
global.self = global;
global.navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' };
global.location = { href: 'https://example.com/', hostname: 'example.com', protocol: 'https:' };
global.document = {
cookie: '',
createElement: function() { return {}; },
getElementById: function() { return null; }
};
// 模块定义(从 bundle 中扣出)
const __webpack_modules__ = {
// 在此粘贴扣出的模块
42: function(module, exports, __webpack_require__) {
// 目标加密模块
},
7: function(module, exports, __webpack_require__) {
// 依赖模块
}
};
// 模拟 Webpack 运行时
const __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
const cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
const module = __webpack_module_cache__[moduleId] = { exports: {} };
__webpack_modules__[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
return module.exports;
}
// Webpack 辅助方法(按需补充)
__webpack_require__.d = function(exports, definition) {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
__webpack_require__.o = function(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
};
__webpack_require__.r = function(exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 加载并调用目标模块
const target = __webpack_require__(42);
console.log(target.sign('test_data'));
如何补充 window、document 等浏览器全局变量
常见的补全方式:
// 最小化补全,按报错逐步添加
global.window = global;
global.self = global;
// 如果代码访问 window.location
global.location = {
href: 'https://target-site.com/path',
host: 'target-site.com',
hostname: 'target-site.com',
pathname: '/path',
protocol: 'https:',
search: '',
hash: ''
};
// 如果代码访问 navigator
global.navigator = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
platform: 'Win32',
language: 'zh-CN',
cookieEnabled: true
};
// 如果代码访问 document.cookie
global.document = {
cookie: 'session=xxx; token=yyy',
referrer: '',
title: ''
};
// 如果代码使用 atob/btoa
const { Buffer } = require('buffer');
global.atob = (str) => Buffer.from(str, 'base64').toString('binary');
global.btoa = (str) => Buffer.from(str, 'binary').toString('base64');
// 如果代码使用 setTimeout/setInterval(通常 Node.js 已有,但有时需要同步化)
global.setTimeout = function(fn) { fn(); };
global.setInterval = function() {};
处理模块间依赖的方法
当目标模块依赖多个其他模块时,有两种处理策略:
策略一:递归扣出所有依赖
// 在浏览器断点处,递归收集依赖
function collectDeps(moduleId, visited = new Set()) {
if (visited.has(moduleId)) return;
visited.add(moduleId);
const src = __webpack_modules__[moduleId].toString();
const depPattern = /__webpack_require__\(([^)]+)\)/g;
let match;
while ((match = depPattern.exec(src)) !== null) {
const depId = match[1].trim().replace(/['"]/g, '');
console.log(`模块 ${moduleId} 依赖 ${depId}`);
collectDeps(depId, visited);
}
}
collectDeps(42);
策略二:直接从页面注入调用(RPC 方式)
不扣代码,直接在浏览器中运行,通过 RPC远程调用 将结果传回本地。
常见 bundler 变体
Browserify 特征
// Browserify 特征:require 使用数字 ID,整体结构不同
(function(){function r(e,n,t){/* ... */}
r({
1: [function(require, module, exports) {
// 模块代码
var dep = require(2);
}, {}],
2: [function(require, module, exports) {
// 依赖
}, {}]
}, {}, [1]);
})();
关键识别点:
- 模块数组格式为
[function(require, module, exports){}, {deps_map}] - 无
__webpack_require__,使用标准require - bundle 末尾有入口数组
[1]
Rollup 产物特征
// Rollup 打包产物:通常将所有模块内联,无模块加载器
// IIFE 格式
var MyLib = (function () {
'use strict';
// 模块 A 的代码(直接内联)
function utilFn(data) { /* ... */ }
// 模块 B 的代码(直接内联)
function encrypt(data) {
return utilFn(data);
}
return { encrypt };
}());
// ESM 格式(无 IIFE)
function utilFn(data) { /* ... */ }
function encrypt(data) { return utilFn(data); }
export { encrypt };
关键识别点:
- 无模块 ID 和加载器,代码直接内联
- 函数之间直接调用,无
require - 模块边界消失,Tree Shaking 后只保留用到的代码
- 变量名通常保留(未混淆时),更易读
esbuild 产物特征
// esbuild 打包产物:使用简短的变量名,但结构清晰
(() => {
// node_modules/some-lib/index.js
var import_lib = __toESM(require_lib());
// src/crypto.js
var __defProp = Object.defineProperty;
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
var __export = (target, all) => {
__markAsModule(target);
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// 模块内容
function encrypt(data) { /* ... */ }
})();
关键识别点:
- 通常有模块路径注释(
// src/crypto.js) - 使用
__toESM,__toCommonJS,__defProp等辅助函数 - 打包速度极快,产物体积小,混淆较少
- chunk 分割较少,常合并为单文件
踩坑与注意事项
模块 ID 在不同版本可能变化
Webpack 4 的数字 ID 是基于模块的顺序和依赖关系计算的,每次构建可能不同。Webpack 5 使用内容 hash 作为 ID,理论上内容不变则 ID 不变,但实际上添加新模块可能导致 hash 变化。不要硬编码模块 ID,应通过搜索导出内容来定位:
// 通过搜索导出内容定位模块,而不是依赖固定 ID
Object.entries(__webpack_module_cache__).find(([id, mod]) => {
return mod.exports && typeof mod.exports.encrypt === 'function';
});
动态导入需要特殊处理
import() 产生的 chunk 不会在页面初始加载时执行,需要手动触发(点击按钮、切换路由等)。扣代码时需要同时下载对应的 chunk 文件,合并 __webpack_modules__ 后才能本地运行。
Tree Shaking 后模块可能被合并
开启 Tree Shaking 和代码优化后,原本多个文件的代码可能被合并到同一个模块函数中,模块边界消失。此时按文件搜索可能找不到,需要直接搜索函数名或特征字符串。
this 上下文问题
模块函数中的 this 指向 module.exports(Webpack 调用时用 .call(module.exports, ...))。本地模拟时需要保持一致:
// 正确:使用 .call 保持 this 指向
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 错误:直接调用,this 为 global 或 undefined
modules[moduleId](module, module.exports, __webpack_require__);
Webpack 5 的 __webpack_require__.e(chunk 预加载)
Webpack 5 的 chunk 加载是异步的(返回 Promise),本地模拟时需要特殊处理:
// 若代码中调用了 __webpack_require__.e,可以 mock 为同步空操作
__webpack_require__.e = function() { return Promise.resolve(); };
最佳实践
在 Console 中挂载全局 require 函数:Webpack 4 中找到 __webpack_require__ 函数后,window.req = __webpack_require__ 将其挂到全局;之后在 Console 中用 req(模块ID) 直接调用任意模块的导出,无需修改代码。
搜索 __webpack_require__ 找 IIFE 入口:大多数 Webpack bundle 是一个立即执行函数,__webpack_require__ 是内部加载函数;搜索该关键字定位 bundle 的最外层函数,确认模块字典的结构。
Source Map 泄露时直接还原源码:检查 Network 请求中是否有 *.js.map 文件,或在 JS 文件末尾看 //# sourceMappingURL=;有 Source Map 则直接用 source-map-explorer 或 Chrome DevTools 查看原始源码,比逆向编译更准确。
模块 ID 可能是字符串或数字:Webpack 4 默认数字 ID,Webpack 5 默认基于路径的字符串 ID;按 ID 格式搜索对应的调用方式(__webpack_require__(42) vs __webpack_require__("./src/crypto.js"))。
找到模块后用 Node.js 独立运行:扣出目标模块(加密函数)后,模拟 module.exports 环境在 Node.js 中运行,比在浏览器 Console 更方便批量调用和自动化测试。
常见陷阱
陷阱:__webpack_require__ 在外部不可访问
现象: 打包后的代码是 IIFE,window.__webpack_require__ 是 undefined,无法从 Console 访问。
原因: Webpack 将 __webpack_require__ 定义在 IIFE 作用域内,不暴露到全局。
解决: 在 IIFE 内部打断点,暂停时在 Console 中 window.req = __webpack_require__;或在 Overrides 中修改 bundle 文件,在 IIFE 内部末尾加 window.req = __webpack_require__ 后保存并刷新。
陷阱:模块 ID 在每次构建后改变
现象: 昨天记录的模块 ID 42 今天失效,对应的是其他模块。
原因: Webpack 数字模块 ID 基于模块的注册顺序,任何模块增减都会导致后续 ID 改变。
解决: 不要硬编码模块 ID;用内容搜索(在 bundle 中搜索加密函数的特征字符串)重新定位模块 ID;或使用 Source Map 定位原始文件路径对应的模块 ID。
陷阱:扣出的模块在 Node.js 中无法运行
现象: 复制模块代码到 Node.js,缺少 __webpack_require__ 函数,报 ReferenceError。
原因: 模块内部使用 require 会被 Webpack 替换为 __webpack_require__,扣出单个模块时缺少这个内部函数。
解决: 同时扣出 __webpack_require__ 函数及目标模块的所有依赖模块,组成一个最小可运行的 bundle 子集;或用 RPC 方式在浏览器中调用,避免扣代码。