简介:这份资源围绕 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是 getter,location不可枚举。检测代码可能会用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。这些细节看起来不起眼,但往往就是跑不通的原因。
希望帮到你。
本文还有配套的精品资源,点击获取