> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Webpack 逆向还原
- URL: https://blog.vercanti.com/webpack-ni-xiang-huan-yuan/
- Published: 2026-08-28T14:35:08.000Z
- Updated: 2026-08-28T14:58:09.000Z
- Description: Webpack 打包产物的核心结构是一个立即执行函数（IIFE），将所有模块以对象或数组的形式传入： 快速识别关键词： Webpack 5 典型特征： 入口模块是 Webpack 启动时最先执行的模块，通常通过以下方式找到： 定位步骤： 1. 全局搜索 __webpack_require__.s 找到入口 ID 赋值处 2. 在 bundle 末尾找到 IIFE 的最后一行调用 3. 若有多个 chunk，在 runtime chunk 中查找 在浏览器控制台或断点处，将所有模块导出到全局变量： 全量 dump 脚本（在合适断点处注入）： 1. 在 De
- Author: yellowdog
- Tags: js逆向, 技巧步骤

> 官方文档：<https://webpack.js.org/concepts/>  
> 适用场景：分析 Webpack 打包产物，定位并提取加密模块的 require ID 和调用方式

## Webpack bundle 结构识别

### 经典 Webpack bundle 特征

Webpack 打包产物的核心结构是一个立即执行函数（IIFE），将所有模块以对象或数组的形式传入：

```javascript
// 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 典型特征：

```javascript
// 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 启动时最先执行的模块，通常通过以下方式找到：

```javascript
// 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");
};

```

定位步骤：

1. 全局搜索 `__webpack_require__.s` 找到入口 ID 赋值处
2. 在 bundle 末尾找到 IIFE 的最后一行调用
3. 若有多个 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 对象）

在浏览器控制台或断点处，将所有模块导出到全局变量：

```javascript
// Webpack 4：在 IIFE 内部断点，找到 modules 局部变量
// 在 __webpack_require__ 函数可访问的作用域内执行：
window._modules = modules;
window._require = __webpack_require__;

// 之后可以在控制台调用任意模块
var cryptoModule = window._require(1);
cryptoModule.encrypt("test");

```

```javascript
// Webpack 5：__webpack_modules__ 通常在顶层作用域可访问
window._modules = __webpack_modules__;
window._require = __webpack_require__;

```

全量 dump 脚本（在合适断点处注入）：

```javascript
(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__;
})();

```

### 方法二：按需提取（找到目标加密函数所在模块）

1. 在 DevTools Sources 面板中搜索加密函数关键词
2. 定位到所在模块的函数体
3. 记录该模块的 ID
4. 扣出该模块及其所有依赖模块

```javascript
// 找到目标模块后，记录依赖链
// 例如目标模块 ID 为 42，依赖模块 7 和 15
var targetModule = __webpack_require__(42);
// 检查 targetModule 的导出
console.dir(targetModule);

```

### `__webpack_require__` 的模拟

在本地 Node.js 环境中复现 `__webpack_require__`：

```javascript
// 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，可以直接读取而不重新执行：

```javascript
// 在页面中，利用缓存直接获取已加载模块的导出
// 无需知道模块内部逻辑，直接拿到导出对象
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）：

```javascript
// 替换缓存中的模块，使后续 __webpack_require__(id) 返回自定义值
__webpack_module_cache__["abc123"] = {
    exports: {
        encrypt: function(data) {
            console.log('[hook] encrypt called with:', data);
            return originalEncrypt(data);
        }
    }
};

```

---

## 调试定位技巧

### 在 bundle 中搜索加密函数

步骤：

1. 在 DevTools Sources 面板中，使用 `Ctrl+Shift+F` 全局搜索（所有文件）
2. 常用搜索关键词：  
  - 加密算法关键词：`CryptoJS`, `SHA256`, `AES`, `HMAC`, `md5`
  - 逻辑关键词：`sign`, `token`, `encrypt`, `secret`, `key`
  - 参数名：`timestamp`, `nonce`, `appid`
3. 找到可疑代码后，在该行打断点
4. 触发请求，观察断点处的调用栈和变量

```javascript
// 常用：在 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` 文件），可以还原为原始源码：

```bash
# 检查是否存在 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` 提取单个文件：

```javascript
// 使用 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 加载：

```javascript
// 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__) {
            // 懒加载模块
        }
    }
]);

```

处理方法：

1. 在 Network 面板过滤 `.js`，触发操作，观察新加载的 chunk 文件
2. 在 chunk 推送函数处打断点，捕获动态加载的模块
3. 将所有 chunk 文件下载下来，合并 modules 对象后本地运行

```javascript
// 监听 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）

```javascript
// 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` 等浏览器全局变量

常见的补全方式：

```javascript
// 最小化补全，按报错逐步添加
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() {};

```

更完整的补环境方案参见 [补环境](https://blog.vercanti.com/bu-huan-jing/) 和 [jsdom完全指南](https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/)。

### 处理模块间依赖的方法

当目标模块依赖多个其他模块时，有两种处理策略：

策略一：递归扣出所有依赖

```javascript
// 在浏览器断点处，递归收集依赖
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远程调用](https://blog.vercanti.com/rpc-yuan-cheng-diao-yong/) 将结果传回本地。

---

## 常见 bundler 变体

### Browserify 特征

```javascript
// 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 产物特征

```javascript
// 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 产物特征

```javascript
// 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，应通过搜索导出内容来定位：

```javascript
// 通过搜索导出内容定位模块，而不是依赖固定 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, ...)`）。本地模拟时需要保持一致：

```javascript
// 正确：使用 .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），本地模拟时需要特殊处理：

```javascript
// 若代码中调用了 __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 方式在浏览器中调用，避免扣代码。

---

## 参见

[补环境](https://blog.vercanti.com/bu-huan-jing/)  
[混淆还原](https://blog.vercanti.com/hun-yao-huan-yuan/)  
[RPC远程调用](https://blog.vercanti.com/rpc-yuan-cheng-diao-yong/)  
[jsdom完全指南](https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/)  
[Babel AST入门](https://blog.vercanti.com/babel-ast-ru-men/)