webpack打包代码逆向实战:从anti-content参数学习补环境与模块还原
2026/9/7 3:55:55 网站建设 项目流程

简介:面向JavaScript逆向学习者,以webpack补环境为核心,研究拼多多anti_content机制,适合有一定前端基础、关注JS混淆与模块还原的开发者。压缩包仅40KB,共2个文件:1个Python脚本与1个JavaScript脚本,可配合完成请求模拟与浏览器环境补全;已有431人学习下载,是典型的小型逆向案例。围绕anti_content环境补丁,代码展示了webpack加载器的调试思路,包括定位关键函数、补齐window与document对象,并通过Python脚本验证参数。读者可借此掌握常用补环境技巧,并能直接复现学习,快速对照自身项目的同类问题。该示例虽小,却足以体会从捕获到解码再到环境模拟的整体流程,适合补环境入门者参考。 前阵子我在复盘 webpack 相关的 JS 逆向,正好用拼多多 web 端的 anti-content 参数做了一次完整的练手。这个参数名看着不起眼,背后却是一整套 webpack 打包后的模块调用关系,把它当成学习样本,可以一次性把“定位、扣模块、补环境”这三个逆向基本功全部串起来。这篇文章会把我的完整思路和实际操作过程写清楚,包括怎么从浏览器里找到 anti-content 的生成逻辑,怎么把 webpack 模块搬到 Node.js 里跑,以及补环境时最容易踩的坑。适合懂一点 JavaScript、想入门 JS 逆向,或者想从逆向角度重新理解前端构建工具的读者。

1. 项目整体思路与目标拆解

1.1 先搞清楚 webpack 打包后的文件长什么样

webpack 打包出来的 JS 文件,不管业务代码多复杂,最终都会变成一个立即执行函数,函数内部维护一个模块表和模块加载器。模块表可以是一个数组,也可以是一个对象,里面每个元素就是一个模块函数。模块加载器一般叫__webpack_require__,它的作用就是根据模块 ID 找到对应模块,执行后返回 exports。

简化后的结构大概是这个样子:

(function(modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = (installedModules[moduleId] = { exports: {} }); modules[moduleId](module, module.exports, __webpack_require__); return module.exports; } return __webpack_require__(0); })({ 0: function(module, exports, require) { var getSign = require(1); module.exports = { sign: getSign }; }, 1: function(module, exports, require) { module.exports = function() { return "hello-sign"; }; } });

这个结构对逆向的意义非常大。它意味着所有业务代码都被拆散成独立模块,任何加密函数都不是单独存在的,而是存在于某个模块里,并且通过require引用其他模块。比如一个签名算法可能依赖时间戳函数、md5 工具、甚至是某个环境检测模块。所以我们实际要做的,是把相关模块连成一张依赖图,然后整体搬出来。

1.2 anti-content 是什么

anti-content 是拼多多 web 端请求里的一个签名参数,通常会出现在部分敏感接口的请求头或者请求体里。它的作用是做风控校验,防止请求被篡改,也在一定程度上防止数据被批量抓取。所以在浏览器里打开开发者工具,刷新页面,就能在 Network 面板里看到这个参数。

这里要强调一下,研究它并不是为了去突破别人的风控,而是把它当作一个典型的 webpack 场景来学习。真实工作中,前端大多会用到 webpack 打包,很多企业又在 webpack 之上做了一层代码混淆,导致我们看到的代码可读性极差。通过这种案例,可以学会如何在压缩混淆的代码里找到关键逻辑,如何还原模块依赖。这套能力对前端性能分析、安全测试、代码维护都有帮助。

1.3 学习路线拆解

我的习惯是不急着看代码,先画一条路线图,后面所有动作都围绕它走。对于 anti-content 这个参数,路线大概是这样的:

  1. 在 Network 面板里找到参数名。
  2. 通过全局搜索定位到参数字符串所在的代码位置。
  3. 向上回溯调用栈,找到生成参数的核心函数。
  4. 把核心函数所在的 webpack 模块以及它依赖的所有模块抠出来。
  5. 在 Node.js 里搭一个__webpack_require__加载器,尝试执行。
  6. 根据报错信息补环境,反复调试,直到输出结果与浏览器一致。

这个路线其实是 JS 逆向的通用方法论,anti-content 只是载体。学会这套流程之后,再遇到其他 webpack 项目,基本都能套用。

2. 环境准备与定位技巧

2.1 需要准备的开发环境

工具不需要多复杂,我实际使用的就三样:Node.js(14 以上版本)、Chrome 浏览器、VS Code。Chrome DevTools 是主力,Node.js 用来跑抠出来的模块代码,VS Code 用来写调试脚本。不需要额外安装任何抓包工具,因为浏览器自带的 Network 面板和调试器已经足够定位参数生成逻辑了。

有一个小技巧我特别推荐:进入开发者工具后,先打开 Sources 面板,在左侧找到页面引用的 JS 文件,如果代码是压缩过的,点击左下角的“格式化”按钮(花括号图标),代码就会展开成可读的格式。拼多多的代码量很大,格式化之后搜索速度会慢一点,但可读性提高很多。

2.2 在网页里定位 webpack 模块

定位的第一步,是先在 Network 面板里找到包含 anti-content 的那个请求。右键这个请求,选择“Break on”里面的“Fetch/XHR”,然后刷新页面或者重新触发请求,就会自动断在发起请求的地方。这个办法比手动下断点快很多,因为请求发起的地方往往离参数生成的位置很近。

断下之后,在 Call Stack 调用栈面板里,能看到发起请求的函数链。每一层函数对应的代码文件可能都不同,但如果是 webpack 项目,很多调用关系都是通过模块 ID 关联的。这时候可以在当前作用域里找一找有没有__webpack_require__或者module这类关键字,如果有,就能顺藤摸瓜找到模块 ID。

接下来用全局搜索定位 anti-content 字符串。在格式化后的代码里按 Ctrl+Shift+F(Mac 是 Cmd+Shift+F),输入 anti-content,搜索结果一般会出现在某个模块的字符串常量里。点击进去,在那一行附近打上断点,再重新触发请求,就能看到这个参数被赋值的具体上下文。

2.3 找到参数生成调用链

定位到 anti-content 赋值语句后,通常能看到类似t.headers["anti-content"] = (0, s.getSign)()这样的代码。这里面的s.getSign就是从这个模块的 require 引用中取出的函数。接下来要做的,就是把s对应的模块 ID 记录下来。

我一般会在 Console 里临时执行require.resolve或者直接查看模块对象,但拼多多的代码里不一定能把require暴露到全局。所以更稳的办法是回到格式化代码里找s的来源,比如var s = __webpack_require__(123)。这一步会得到一个模块 ID,比如 123,然后去模块表里把 123 对应的函数内容复制出来。此时不要急着复制整个文件,只需要复制这个模块函数体,以及后续会依赖到的其他模块。

实际操作中,我习惯用递归收集的方式:先搜索这个模块里的所有require(数字),把数字记下来,再去模块表里找对应的函数,继续重复,直到所有依赖都找齐。这一步虽然繁琐,但非常必要,因为缺一个模块运行就会报错。

3. webpack 模块加载器与补环境原理

3.1webpack_require到底做了什么

很多人第一次接触 webpack,看到那一大段打包代码就蒙了,其实核心逻辑特别简单。__webpack_require__做了三件事:

  • 判断这个模块是不是已经加载过了,如果加载过,直接返回缓存里的 exports。
  • 如果没有,就新建一个module对象,里面挂一个空对象exports
  • 调用模块函数,把modulemodule.exports__webpack_require__传进去,最终返回module.exports

这个过程和 Node.js 的 CommonJS 加载机制很像。所以我们在 Node.js 里复刻一个__webpack_require__,本质上是模拟一个最小的模块加载器,让抠出来的模块代码能正常互相引用。网上能找到很多现成模板,但最好还是自己手写一遍,理解更深。我写过的简化版长这样:

const modules = { 0: function(module, exports, require) { const getSign = require(1); module.exports = { sign: getSign }; }, 1: function(module, exports, require) { module.exports = function() { return "sign-value"; }; } }; const cache = {}; function __webpack_require__(id) { if (cache[id]) return cache[id].exports; const module = (cache[id] = { exports: {} }); modules[id](module, module.exports, __webpack_require__); return module.exports; } const result = __webpack_require__(0).sign(); console.log(result);

3.2 为什么需要补环境

如果直接把扣出来的 webpack 代码丢到 Node.js 里跑,第一反应基本就是报错。原因很简单,浏览器里的 JS 运行环境有windowdocumentnavigatorlocation这些全局对象,而 Node.js 的全局对象是global,两者不是一回事。那些加密函数在浏览器里正常访问window.navigator.userAgent,到了 Node.js 里连window都没有,自然直接抛异常。

补环境的本质,就是在 Node.js 里把浏览器环境模拟出来,让代码以为它还跑在浏览器里。很多人一听到“补环境”就觉得高级,其实没那么玄乎,就是缺什么补什么。真正麻烦的地方在于,代码检测环境的方式非常多样,有的直接读属性,有的判断属性是否存在,有的是判断原型的toString标签,有的是访问某个属性时触发了 getter。这些都需要我们逐个去满足。

3.3 原型链补环境

最粗浅的补环境方式是直接把所有全局属性挂在global上:

global.window = global; global.navigator = { userAgent: "Mozilla/5.0" };

这种写法在一些简单场景下能用,但遇到稍微聪明一点的代码就会被识别出来。因为真实浏览器里navigator是一个内置对象,它的userAgent属性是挂在原型链上的,而不是一个普通对象上面的可枚举属性。如果代码里执行Object.keys(navigator),真实环境得到的可能是空数组,而上面这种粗暴写法会得到["userAgent"],一眼就能分辨出来。

所以更推荐用原型链的方式来补。先定义一个构造函数,把属性和方法挂到原型上,再创建实例赋值给全局变量:

function Navigator() {} Navigator.prototype.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."; Navigator.prototype.platform = "Win32"; Navigator.prototype.language = "zh-CN"; global.navigator = new Navigator();

用这种方式模拟出来的对象,属性访问正常,Object.keys又不会暴露原型上的键,和真实对象的行为更接近。这种思路在面试题里也经常出现,考察的就是对 JavaScript 原型链的理解深度。很多“webpack 面试题”里会问“如何手动实现一个模块加载器”,其实补环境里的原型链操作也是类似的思想。

3.4 补环境不是越多越好

我刚开始学补环境的时候,总想在开始之前把所有可能用到的浏览器对象全部声明一遍,结果发现这样反而更慢。因为每个项目访问的环境属性都不一样,你补齐了 A,它又报 B 缺失,永远补不完。正确的做法是“按需补环境”:跑一次,看报错,补一个;再跑,再看报错,再补一个。每轮补的时候,尽量定位到源代码里真正访问那个属性的一行,理解它为什么要读这个属性。

另外一个常见误区是把所有对象都补成同一个对象。比如有人图省事,直接把windowdocumentnavigator全部赋值为global,这会在代码检测window !== document时直接失败。补环境的时候,不同对象要有不同的形态和职责,不能图省事。

4. 实操:扣出 anti-content 生成逻辑并跑通

4.1 把模块代码搬出来

定位到核心模块之后,下一步就是把模块代码搬到本地。这里要注意,webpack 模块表里的每一个模块都只是一个函数,函数签名固定是function(module, exports, require),函数体内部才真正执行业务逻辑。

我一般会把模块表做成一个 JavaScript 文件,格式类似:

const modules = { 123: function(module, exports, require) { // 这里放从浏览器里复制出来的模块函数体 }, 456: function(module, exports, require) { // 另一个依赖模块 } };

然后写一个简单的模块加载器,运行入口模块。如果你复制的模块比较多,可以用脚本批量提取。我给一个简单思路:在格式化后的整个 JS 文件里,用正则匹配模块ID:function(module,exports,require){...},然后把函数体提取出来。不过正则容易出错,建议前期手动复制,熟悉之后再写自动化脚本。

在复制模块函数体时,一定要留意代码里是否有evalnew Function这类动态执行逻辑,有的话要特别小心,因为动态执行很容易拿到当前作用域外的变量,导致运行结果和浏览器不一致。

4.2 在 Node.js 里跑第一次

拿到模块表之后,写一个最小化的加载器并运行入口模块。这一步大概率会报错,而且报错信息五花八门。常见的报错类型有:

  • ReferenceError: window is not defined:缺少全局对象。
  • TypeError: Cannot read property 'userAgent' of undefined:缺少 navigator。
  • TypeError: document.createElement is not a function:缺少 document。
  • ReferenceError: location is not defined:缺少 location。

我的习惯是,在补环境之前,先全局搜索一下代码里到底用了多少个环境变量。可以用 VS Code 的全局搜索,把window.document.navigator.location.都搜一遍,做个初步统计,这样补起来更有方向。

4.3 环境补全实录

下面我记录一个简化版的补环境过程,不是完全还原拼多多的代码,而是展示常见的操作路径。

第一次运行,报了window is not defined。我直接在最上面加:

global.window = global;

但紧接着又报了Cannot read property 'location' of undefined。因为代码里访问了window.location.href。所以我补充 location 对象:

global.location = { href: "https://example.com/path", host: "example.com", hostname: "example.com", protocol: "https:", pathname: "/path", search: "" };

继续运行,又遇到 navigator 缺失。这里我用原型链的方式补:

function Navigator() {} Navigator.prototype.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."; Navigator.prototype.platform = "Win32"; Navigator.prototype.language = "zh-CN"; Navigator.prototype.plugins = []; global.navigator = new Navigator();

这个时候程序能继续跑了,但输出的结果和浏览器里的值对不上。我怀疑代码里读了一个我没补充的属性,于是采用 Proxy 拦截的方式打印所有属性访问:

global.navigator = new Proxy(new Navigator(), { get(target, prop) { console.log("navigator get:", prop); return target[prop]; } });

通过打印,我发现代码读取了navigator.webdriver,这是一个很典型的自动化检测点。于是我显式设置:

Navigator.prototype.webdriver = false;

再执行,最终得到的 anti-content 值和浏览器里的一致。整个过程大约用了十几轮“报错、定位、补充、再跑”的循环。只要思路清晰,其实是体力活。

4.4 验证结果而不是只看不报错

很多人以为程序不报错就说明成功了,其实并不一定。由于环境属性缺失会被当成undefined,有时候代码并不会报错,但计算结果已经错了。比如某些函数内部对一个 undefined 做了拼接,得到的就是"undefined"字符串,看起来能跑,值却完全不对。

我建议在浏览器的 Console 里先手动执行一次,拿到真实的 anti-content 值,然后再跑 Node.js 脚本,两个值做对比。如果对不上,就回到上一步,继续用 Proxy 打印属性访问。还有一个技巧是,在执行结果前插入一些调试日志,把参与计算的中间变量打印出来,和浏览器里手动计算的中间值做对比,这样能快速定位是哪一步环境没补对。

5. 常见问题与排查技巧

5.1 变量值为 undefined 但不报错

这个问题上面提到过,最典型的表现就是程序正常跑完,但结果的哈希值不对。排查思路很简单:先确认参与计算的每个变量都有值,尤其是时间戳、随机数、userAgent 这类容易变化的值。可以把整个环境对象都用 Proxy 包一层,记录所有 get 操作,然后逐个和浏览器里的执行结果对比。

5.2 模块缺失导致的死循环或报错

如果报错信息是Cannot find module 123,说明还有依赖模块没抠出来。可以在当前模块函数体里搜索所有的require(123)这类调用,然后去 webpack 模块表里补上对应 ID 的模块。有人会问,能不能直接用正则批量把整个模块表都抠出来?可以,但要注意模块表很大,里面可能包含大量无关代码,运行效率会很低,而且可能会因为某个无关模块引用了更复杂的浏览器 API,给补环境增加负担。所以尽量只保留依赖链路上的模块。

5.3 检测 window 与 document 的一致性

有些代码会做window.document === document这样的判断。如果你把window直接指向global,而document单独定义,那么这个判断可能为 false。我一般会统一维护一个环境对象,让window.documentglobal.document指向同一个对象,保持一致性。还有window.top === windowwindow.self === window这类判断,也需要把引用关系打通。

下面整理了一个问题速查表,方便对照排查:

典型现象可能原因解决办法
window is not defined缺少全局 window 对象global.window = global,同时补 location 等子对象
navigator读取为 undefined缺少 navigator 对象用构造函数加原型的方式补完整属性
document.createElement is not a functiondocument 对象太简陋补充 document 核心方法,并让 createElement 返回带 style 等属性的对象
程序不报错但结果不对某个环境变量被当成 undefined 参与运算用 Proxy 拦截打印属性访问,检查中间变量
Object.prototype.toString.call(navigator)不符合预期缺少 Symbol.toStringTag给原型的 Symbol.toStringTag 赋值为 "Navigator"
模块运行时出现死循环或 CPU 飙升依赖模块缺失或加载器缓存异常检查模块表与加载器的缓存逻辑,缩小模块范围

5.4 关于 Symbol.toStringTag 的细节

浏览器内置对象通常都会有类型标签,比如Object.prototype.toString.call(navigator)返回的是[object Navigator]。我们手动补出来的对象,默认返回[object Object],如果代码刚好做了这个判断,就会出问题。解决办法是在原型上定义 Symbol.toStringTag:

Object.defineProperty(Navigator.prototype, Symbol.toStringTag, { value: "Navigator", configurable: true });

这种细节很冷门,但遇到就会卡住。平时做补环境练习时,多积累这些“冷知识”,后面遇到类似问题就能快速反应。

6. 一点学习心得

做这个案例的过程中,我比较强烈地感觉到,理解构建工具比单纯写业务代码更有长尾价值。webpack 的打包优化配置,本质上就是对模块加载策略的理解;而 JS 逆向要求你反着理解这个加载策略,两者相互印证,学起来会很快。现在我再看 webpack 和 vite,思路会更清晰:webpack 打包后是一个自执行函数加模块表,vite 则依赖浏览器原生 ESM,模块暴露得更加直接。这也是为什么有的同学说 vite 项目里逆向更“容易”,因为代码结构和源码差异更小。

如果你是想通过这个案例入门 JS 逆向,我的建议是不要只盯着 anti-content 这三个字母,而是把重点放在“如何从打包代码里还原模块依赖”和“如何模拟浏览器对象”这两件事上。把这两个基本功练扎实,从拼多多练到其他站点,从 webpack 练到其他打包工具,思路都是通透的。

最后说一句,技术本身是中性的,拿来做正向学习、安全研究、性能分析都没问题,但别用它去爬取他人数据或者绕过别人的风控系统。希望这篇学习笔记能帮你少走一些弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询