Webpack打包JS逆向实战:anti-content参数补环境与调试 简介这份资源围绕 Webpack 打包场景下的拼多多 anti_content 安全机制展开面向具备一定 JavaScript 基础、希望入门 JS 逆向与补环境实战的开发者。内容聚焦如何通过 Webpack 配置分析与逆向调试思路理解 anti_content 参数的生成逻辑并借助补环境手段在本地复现调用过程适合爬虫研究、风控对抗学习等场景。压缩包共 2 个文件包含 1 个 py 脚本与 1 个 js 文件分别承担请求调用与核心逻辑还原的用途整体约 40KB体量轻便、便于快速阅读与调试。目前已有 433 人学习下载说明该方向具备一定关注度。读者可从中获取 anti_content 补环境的整体思路、关键代码片段与排错参考帮助建立从打包结构定位到环境补丁落地的完整认知为后续深入逆向分析打下基础。1. 从一次 anti-content 报错说起webpack 打包的 JS 逆向到底难在哪打开某电商页面F12 里搜anti-content你会看到请求头里挂着一长串密文刷新一次变一次。想复现它把代码抠出来单独跑控制台立刻甩你一脸ReferenceError: window is not defined或者某个undefined的属性读取失败。这不是你代码抄错了而是这套 JS 从设计之初就没打算让你在 Node 里跑——它依赖浏览器一整套环境而 webpack 又把这些依赖打散进了一个巨大的模块运行时里。这篇笔记讲的就是这条链路怎么从一个 webpack 打包产物里定位到 anti-content 的生成逻辑怎么用补环境的方式让它在 Node 里跑通以及中间那些让人抓狂的坑。适合已经会基本扣代码、但一遇到 webpack 模块化和环境检测就卡住的同学。读完你应该能独立完成一次从定位到补环境再到出参的完整流程而不是停留在“能搜到但跑不起来”的阶段。2. 先看懂 webpack 打包产物模块运行时与 anti-content 的藏身之处2.1 webpack 的模块加载器长什么样webpack 打包后的代码核心是一个自执行函数里面维护了一个模块数组或模块对象每个模块被一个函数包裹通过__webpack_require__来加载。典型结构长这样(function(modules) { var installedModules {}; 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; } return __webpack_require__(__webpack_require__.s 0); })([ function(module, exports, __webpack_require__) { // 入口模块 }, function(module, exports, __webpack_require__) { // 其他模块 } ]);这段代码的逻辑很清晰modules是一个数组每个元素是一个函数函数体就是原始模块的代码。__webpack_require__负责执行模块函数并缓存导出结果。参数说明上moduleId就是数组下标installedModules防止重复执行module.exports是模块对外暴露的对象。你要找的 anti-content 逻辑大概率藏在某个被__webpack_require__调用的模块里。常见做法是在__webpack_require__函数入口打一个日志把moduleId打出来然后观察哪些模块被加载了。但更高效的方式是直接在 Sources 面板里搜关键字比如anti-content、antiContent、sign找到赋值的那一行往上追调用栈。2.2 定位 anti-content 生成函数的三步法第一步在 Network 面板找到携带 anti-content 的请求右键 Copy as cURL确认这个头是在哪个 XHR 或 fetch 里设置的。第二步在 Sources 面板全局搜索anti-content注意大小写和连字符的变体。如果搜不到试试搜antiContent或者setRequestHeader。第三步在找到的位置打条件断点刷新页面断下来之后看调用栈一层层往上翻直到找到计算这个值的函数。这里有个血泪经验很多站点会把关键字符串拆开拼接比如anti -content或者用 Unicode 转义。搜不到的时候试试搜content或者直接搜anti范围放宽一点。另外webpack 打包后变量名会被混淆成a、b、c但字符串常量通常不会变所以搜字符串比搜函数名靠谱。找到生成函数后不要急着扣代码。先看它依赖了哪些外部变量。常见的有window、document、navigator、location还有一些自定义的全局对象。把这些依赖列出来就是后面补环境的清单。2.3 把目标模块从模块运行时里摘出来假设你定位到 anti-content 的逻辑在模块 ID 为 1234 的函数里最直接的做法是在__webpack_require__里加一行判断function __webpack_require__(moduleId) { if (moduleId 1234) { debugger; // 断下来方便观察 } // ... 原有逻辑 }断下来之后你可以把这个模块的函数体整个复制出来单独放到一个文件里。但注意它内部可能还调用了其他模块通过__webpack_require__(5678)这种方式。这时候你有两个选择一是把被依赖的模块也一起抠出来二是自己实现一个简易的__webpack_require__把需要的模块注册进去。我一般会先把整个modules数组保存下来然后在 Node 里重建一个最小的运行时const modules { 1234: function(module, exports, __webpack_require__) { // 从浏览器里抠出来的模块代码 }, // 其他被依赖的模块 }; 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 result __webpack_require__(1234); console.log(result);这段代码的关键是modules对象键是模块 ID值是模块函数。cache用来缓存已执行的模块避免重复执行。__webpack_require__的逻辑和浏览器里保持一致。跑起来之后如果报window is not defined就说明需要补环境了。3. 补环境的核心思路让 Node 里的 JS 以为自己在浏览器3.1 原型链补环境到底在补什么补环境不是简单地定义几个全局变量就完事。很多检测逻辑会检查window的原型链比如window.hasOwnProperty、Object.getOwnPropertyDescriptor(window, navigator)甚至检查window的toString结果。如果你直接用global.window {}一检测就露馅。原型链补环境的核心是让 Node 里的对象在原型链上看起来和浏览器一致。举个例子浏览器里window是Window的实例Window又继承自EventTarget。你可以在 Node 里模拟这条链class EventTarget {} class Window extends EventTarget {} const window new Window(); global.window window;但这样还不够因为浏览器里window的属性描述符descriptor有特定配置比如navigator是 getterlocation不可枚举。检测代码可能会用Object.getOwnPropertyDescriptor来验证。所以更稳妥的做法是用Object.defineProperty来定义Object.defineProperty(global, navigator, { get() { return { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, platform: Win32, language: zh-CN, languages: [zh-CN, zh], cookieEnabled: true, onLine: true, }; }, configurable: true, });参数说明get返回一个模拟的 navigator 对象configurable: true允许后续修改enumerable默认 false 更接近浏览器行为。userAgent要选一个常见的真实值不要用默认的 Node UA否则容易被识别。3.2 用 Proxy 拦截环境检测的常见套路有些检测代码会遍历window的属性或者检查某个属性是否存在。这时候可以用Proxy来做一个兜底const windowProxy new Proxy(global, { get(target, prop) { if (prop in target) { return target[prop]; } // 对于不存在的属性返回一个空对象或函数避免报错 console.log([补环境] 访问了未定义的属性: ${String(prop)}); return undefined; }, has(target, prop) { // 让 in 操作符总是返回 true骗过存在性检测 return true; }, }); global.window windowProxy;这段代码的逻辑是当代码访问window上不存在的属性时get会被触发打印日志方便你定位还缺什么。has拦截in操作符让xxx in window永远为 true。注意has返回 true 有风险如果检测代码用in来判断某个功能是否可用可能会走到错误的逻辑分支。所以更推荐的做法是先用get打日志把缺的属性一个个补上而不是一上来就用has兜底。3.3 补环境的验证怎么确认补对了补环境最怕的是补了一堆但检测代码走了另一条分支结果算出来的值不对。验证的方法是在浏览器里跑一遍原始代码记录输入和输出在 Node 里跑补环境后的代码用同样的输入对比输出。如果输出不一致就在关键分支打日志看是在哪里分叉的。常见做法是在补环境代码里加一个开关把访问过的属性都记录下来const accessedProps new Set(); const windowProxy new Proxy(global, { get(target, prop) { accessedProps.add(prop); return target[prop]; }, }); // 跑完之后打印 console.log([...accessedProps]);这样你能清楚地看到代码到底用了哪些环境属性而不是盲目地补。如果某个属性被访问了但你没定义target[prop]会返回undefined后续操作大概率报错这时候日志就能帮你定位。4. anti-content 参数生成的还原与调试从密文到可复现4.1 找到加密入口和入参anti-content 的生成通常是一个函数接收几个参数返回一个字符串。参数可能包括时间戳、随机数、页面 URL、cookie 里的某些值。你要做的是找到这个函数的定义看清楚它接收什么、返回什么。在浏览器里可以在调用处打一个断点看传入的实参。常见的形式是const antiContent window._antiContent({ timestamp: Date.now(), nonce: Math.random().toString(36).slice(2), url: location.href, });如果找不到调用处可以在生成函数内部打日志把arguments打印出来。注意webpack 打包后函数可能被包裹在多层闭包里arguments不一定直接可见。这时候可以在函数体第一行加debugger断下来后看 Scope 面板里的 Closure 和 Local 变量。4.2 在 Node 里跑通生成逻辑把生成函数和相关依赖抠出来之后在 Node 里跑。如果报错按报错信息补环境。常见的报错和对应补法window is not defined定义global.window。document is not defined定义global.document至少要有cookie、createElement、getElementsByTagName等。navigator is not defined定义global.navigator。location is not defined定义global.location包含href、protocol、host等。Cannot read property xxx of undefined说明某个对象没定义往上追是哪个全局变量缺失。一个比较完整的补环境骨架global.window global; global.document { cookie: , createElement: () ({ getContext: () null }), getElementsByTagName: () [], documentElement: { style: {} }, }; 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, }; global.location { href: https://example.com/, protocol: https:, host: example.com, hostname: example.com, pathname: /, search: , hash: , };注意global.window global这一行让window指向 Node 的全局对象这样window.xxx和global.xxx等价。但有些检测会检查window globalThis在 Node 里globalThis就是global所以这样写是安全的。4.3 用 AI 辅助定位和补环境现在用 AI 辅助 JS 逆向已经很常见了。我的做法是把混淆后的代码片段丢给 AI让它解释这段代码在干什么或者让它帮我写补环境的桩代码。但要注意AI 对混淆代码的理解经常出错尤其是变量名被替换成_0x开头的时候。所以 AI 的输出只能作为参考最终还是要靠自己在浏览器里验证。一个比较有效的用法是把报错信息和相关代码片段一起给 AI问它“这段代码在浏览器里依赖了哪些全局变量”。AI 通常能列出window、document、navigator这些明显的但一些自定义的全局对象它可能看不出来。这时候还是要靠Proxy打日志来补。另外AI 可以帮助解释加密算法的特征。比如你看到代码里有CryptoJS或者md5、sha256的字样可以让 AI 帮你确认是不是标准算法。如果是标准算法直接用 Node 的crypto模块替代比补环境跑原始代码更稳定。5. 避坑指南补环境时最容易翻车的五个地方5.1 现象补了 window 但代码还是报 undefined原因window定义了但代码访问的是window.xxx而xxx没定义。或者代码用了this在模块函数里this不等于window。解决用Proxy拦截get把访问的属性打出来。如果this有问题检查模块函数的调用方式确保this指向window。可以在模块函数开头加this.window this或者用.call(window)来调用。5.2 现象navigator.userAgent 补了但检测还是不过原因检测代码不只看userAgent还看navigator.plugins、navigator.mimeTypes、navigator.webdriver等。或者它用Object.getOwnPropertyDescriptor检查userAgent是不是 getter。解决把navigator定义成一个对象用Object.defineProperty定义userAgent为 getter。同时补上plugins和mimeTypes可以是空数组但要有length属性。webdriver要设为false或者undefined。5.3 现象代码在 Node 里跑通了但生成的 anti-content 和浏览器不一致原因入参不一致。比如时间戳精度不同、随机数生成方式不同、URL 带了不同的参数。或者代码内部用了Math.random而 Node 和浏览器的随机数序列不同。解决把浏览器里的入参完整记录下来在 Node 里用同样的入参。如果代码内部用了随机数找到随机数的生成位置看它是否影响最终结果。有些 anti-content 算法会把随机数也加密进去这时候需要把随机数也固定下来。5.4 现象补环境代码越补越多最后自己都乱了原因没有系统地记录哪些属性被访问了导致重复补、漏补。解决用Proxy的get和set拦截把所有访问和赋值的属性记录到一个Set里。每跑一次看新增了哪些属性针对性地补。补完之后把日志关掉保持代码干净。5.5 现象代码里有 debugger 反调试一打开 DevTools 就断住原因站点用了debugger语句或者Function.prototype.constructor来检测调试。解决在 Sources 面板右键选择 “Never pause here”或者用 Charles 之类的工具做本地替换把debugger替换成空语句。更彻底的做法是找到反调试代码把它注释掉。注意有些反调试是定时触发的要找到setInterval或者setTimeout里的debugger。6. 进阶把补环境做成可复用的模板补环境这件事做多了就会发现很多代码是重复的。我现在的习惯是维护一个browser-env.js模板把常用的window、document、navigator、location、history、screen都定义好用Proxy做兜底日志。每次遇到新站点先跑模板看日志缺什么再针对性补。模板的核心结构function createBrowserEnv(options {}) { const env {}; const accessed new Set(); const handler { get(target, prop) { accessed.add(prop); if (prop in target) return target[prop]; // 返回一个空函数避免直接报错 return function() {}; }, }; env.window new Proxy({ document: {}, navigator: {}, location: {}, }, handler); env.document new Proxy({ cookie: options.cookie || , createElement: () ({}), getElementsByTagName: () [], }, handler); env.navigator new Proxy({ userAgent: options.userAgent || Mozilla/5.0 ..., platform: Win32, language: zh-CN, }, handler); return { env, accessed }; }这个模板的好处是Proxy的get会记录所有访问过的属性跑完之后打印accessed你就知道还缺什么。get里返回空函数是为了避免undefined is not a function这种报错但要注意空函数被调用后返回undefined可能引发后续错误所以还是要根据日志补上真实的实现。验证补环境是否完整我一般会做两件事一是对比浏览器和 Node 的输出二是用JSON.stringify把生成过程中的中间变量打出来逐段对比。如果中间变量一致但最终结果不一致说明最后一步的编码或加密有问题重点查那部分。最后说一个我踩过的坑有些站点的 anti-content 算法会检测window.top和window.self是否相等在 Node 里这两个都是undefined一比较就露馅。补的时候要把window.top window、window.self window、window.parent window都设上。还有window.frames要设成空数组window.length设为 0。这些细节看起来不起眼但往往就是跑不通的原因。希望帮到你。本文还有配套的精品资源点击获取