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

定位步骤:

  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 对象)

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

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

方法二:按需提取(找到目标加密函数所在模块)

  1. 在 DevTools Sources 面板中搜索加密函数关键词
  2. 定位到所在模块的函数体
  3. 记录该模块的 ID
  4. 扣出该模块及其所有依赖模块
// 找到目标模块后,记录依赖链
// 例如目标模块 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 中搜索加密函数

步骤:

  1. 在 DevTools Sources 面板中,使用 Ctrl+Shift+F 全局搜索(所有文件)
  2. 常用搜索关键词:
    • 加密算法关键词:CryptoJS, SHA256, AES, HMAC, md5
    • 逻辑关键词:sign, token, encrypt, secret, key
    • 参数名:timestamp, nonce, appid
  3. 找到可疑代码后,在该行打断点
  4. 触发请求,观察断点处的调用栈和变量
// 常用:在 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__) {
            // 懒加载模块
        }
    }
]);

处理方法:

  1. 在 Network 面板过滤 .js,触发操作,观察新加载的 chunk 文件
  2. 在 chunk 推送函数处打断点,捕获动态加载的模块
  3. 将所有 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'));

如何补充 windowdocument 等浏览器全局变量

常见的补全方式:

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

更完整的补环境方案参见 补环境jsdom完全指南

处理模块间依赖的方法

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

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

// 在浏览器断点处,递归收集依赖
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 方式在浏览器中调用,避免扣代码。


参见

补环境
混淆还原
RPC远程调用
jsdom完全指南
Babel AST入门

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog