JS逆向实战:Node.js补环境破解电商平台anti_content反爬参数

📅 2026/7/5 22:28:14
JS逆向实战:Node.js补环境破解电商平台anti_content反爬参数
1. 项目概述与核心挑战最近在分析某多多平台的商品数据接口时遇到了一个老朋友也是老对手anti_content参数。这个参数对于做过电商平台逆向的朋友来说应该都不陌生它本质上是一个用于反爬虫的动态令牌由前端JavaScript生成并随着请求一同发送给服务器进行校验。服务器会验证这个令牌的合法性如果校验失败直接返回错误或者空数据。这次遇到的版本其生成逻辑嵌套在一个经过混淆和加密的JavaScript文件中并且严重依赖对浏览器环境的检测。直接扣代码运行会报各种navigator、window、document未定义的错误这就是典型的“环境检测”对抗。因此本次逆向的核心从单纯的算法还原转变为了一个系统的“补环境”工程。所谓“补环境”就是在非浏览器环境如Node.js下模拟出一个足够“真实”的浏览器运行环境让那些依赖window、document、navigator等浏览器特有对象的JS代码能够正常执行从而计算出正确的anti_content值。这不仅仅是定义几个空对象那么简单它要求我们对浏览器环境有深入的理解知道目标代码检测了哪些属性、调用了哪些方法然后进行精准的、最小化的模拟。补得不够代码跑不起来补得太过又可能因为环境“太假”而被识别。这个过程业内也常被称为“过5秒盾”或“环境对抗”的核心环节因为很多反爬方案如Cloudflare 5秒盾的核心机制就是复杂的环境检测。本笔记将详细记录我从定位anti_content生成入口到分析其环境依赖再到在Node.js中一步步构建补环境方案最终稳定获取参数的全过程。其中会重点分享如何高效定位关键检测点、如何设计既轻量又够用的环境模拟对象以及调试过程中遇到的坑和解决方案。无论你是刚接触JS逆向的新手还是想深化补环境技巧的同行希望这篇详实的流程记录都能给你带来直接的参考价值。2. 逆向目标分析与入口定位2.1 接口与参数初步观察首先通过浏览器开发者工具F12的Network面板观察目标请求。在搜索商品或翻页时会发现请求参数中携带一个长长的、看似随机的anti_content字符串。它的格式通常类似于一堆编码后的字符每次请求都会变化。清除Cookie后首次访问往往拿不到数据直到anti_content参数出现数据才正常返回这印证了其作为反爬令牌的身份。关键一步是尝试删除或修改这个参数再重发请求结果通常是接口返回错误码或空列表。这说明服务端确实依赖此参数进行风控校验。因此我们的目标非常明确找到生成这个anti_content参数的JavaScript代码并能在本地复现其生成逻辑。2.2 关键代码定位策略面对一个大型单页应用SPA如某多多所有的业务逻辑都打包在少数几个混淆过的JS文件中。直接搜索anti_content字符串可能在源码中找不到因为它可能是由变量拼接或经过某种变换后生成的。更有效的方法是使用“Hook”技术或基于堆栈的定位。方法一XHR/Fetch Hook在Console中注入一段Hook脚本拦截所有网络请求并在请求发出前打印出当时的调用堆栈。这样当观察到携带anti_content的请求发出时通过堆栈信息就能反向追踪到生成并添加该参数的JavaScript代码位置。这是最精准的定位方法之一。方法二事件监听与关键字断点在Sources面板中对setRequestHeader方法或XMLHttpRequest.prototype.send设置断点。因为anti_content最终是要被设置到请求头或请求体中的当程序执行到这些方法时通过调用堆栈Call Stack面板同样可以向上追溯。此外还可以在混淆的JS文件中搜索一些可能相关的关键词如anti、content、getAntiContent等即使被混淆其字符串字面量可能仍在然后在其附近下断点。方法三基于行为的观察注意到anti_content的生成可能依赖于页面上的某些用户行为或时间戳。可以观察在触发请求前是否有其他JS文件被加载或执行。通过“Performance”面板录制一段时间内的操作观察JS执行时序有时也能发现端倪。经过一番追踪我最终在一个名为vendor.xxxxxx.js的大型混淆文件中找到了疑似入口。通过堆栈信息定位到一个函数调用其内部进行了复杂的字符串操作和加密运算最终赋值给了一个变量该变量随后被添加到了请求参数中。接下来就是深入这个函数进行逻辑分析。3. 核心JS逻辑分析与环境依赖梳理3.1 算法逻辑初步还原将找到的代码片段通常是一个巨大的、被压缩成一行的函数复制出来利用一些在线的JS美化工具进行格式化得到可读性稍好的代码。虽然变量名仍然是a、b、c、d这类无意义的字符但通过分析其控制流和数据流可以开始理解其逻辑。这个生成函数暂且称之为getAntiContent的大致流程如下收集环境信息函数内部会尝试访问大量浏览器环境对象如window.navigator下的userAgent、platform、language、hardwareConcurrencywindow.screen下的width、height、colorDepthwindow对象本身的一些属性document对象及其documentElement甚至包括performance.timing、WebGL渲染信息等。信息加工与序列化将收集到的信息按照特定规则进行排序、拼接、或转换为字符串。加密/编码使用某种加密算法常见的有MD5、SHA系列、或自定义的位运算对拼接后的字符串进行运算生成一个摘要或密文。在某多多的案例中它可能使用了类似TEA、AES或RC4的对称加密或者是一个自定义的哈希算法。输出格式化将加密结果进行Base64或Hex编码最终生成anti_content字符串。至此逆向的重点已经清晰算法的核心是确定的但执行算法的“环境”是动态检测的。如果我们不能提供一个让代码“感觉”正确的环境第一步“收集环境信息”就会抛出异常导致整个计算流程中断。3.2 环境依赖详细清单通过静态阅读代码和动态调试在浏览器中于关键位置下断点观察代码试图访问哪些属性我整理出了一份该getAntiContent函数所依赖的环境属性清单。这是补环境工作中最关键的一步决定了我们需要模拟对象的范围和深度。核心依赖对象与属性window对象:window.navigator这是重灾区。navigator.userAgent: 必须模拟且需与发起请求的Node.js环境的HTTP头中的UA保持一致否则可能前后端校验不一致。navigator.platform: 如Win32、Linux x86_64。navigator.language: 如zh-CN。navigator.hardwareConcurrency: CPU逻辑核心数。navigator.deviceMemory: 设备内存GB。navigator.maxTouchPoints: 最大触摸点数。navigator.webdriver:极其重要必须设置为undefined或false。在真实浏览器中此属性通常为undefined而在Selenium/Puppeteer等自动化工具中可能为true。很多反爬会直接检测此属性。navigator.plugins,navigator.mimeTypes: 可能需要模拟一个空数组或特定结构的对象。window.screen:screen.width,screen.height: 屏幕分辨率。screen.availWidth,screen.availHeight: 可用屏幕大小。screen.colorDepth: 颜色深度通常是24或30。window.location和document.location: 可能需要模拟href,hostname等属性与目标网站一致。window.performance和performance.timing: 用于获取页面性能计时数据可能需要模拟一系列时间戳。window.WebGLRenderingContext: 如果代码尝试获取WebGL信息来生成Canvas指纹则需要更复杂的模拟。document对象:document.documentElement: 通常需要模拟其clientWidth和clientHeight可能被读取。document.createElement,document.getElementById等: 如果代码尝试操作DOM来检测则需要模拟这些方法返回一个具有基本属性的对象。document.cookie: 可能需要读取或设置。document.charset,document.characterSet: 文档字符集。其他全局对象/函数:Date: 用于获取当前时间戳必须存在且行为正常。Math.random: 用于生成随机数必须模拟。注意有时为了得到确定性的结果需要“固定”随机数种子但这在对抗动态参数时可能不适用。Object,Array,String,Function等内置构造函数必须存在。setTimeout,setInterval,clearTimeout等如果代码中有异步逻辑可能需要模拟。注意这份清单是基于当前案例的不同的网站、甚至同一网站的不同版本检测点都可能不同。原则是“按需补全”即代码访问什么我们就补什么。过度补全会增加复杂度和被识别的风险如果你的模拟环境过于“完美”反而显得假。4. Node.js补环境方案设计与实现4.1 方案选型纯对象模拟 vs 无头浏览器在Node.js中执行浏览器端JS主要有两种思路无头浏览器如Puppeteer, Playwright直接启动一个真实的Chromium内核环境是100%真实的。优点是一劳永逸几乎无需关心环境检测。缺点是资源消耗大内存、CPU运行速度慢不适合高并发或集成到轻量级爬虫框架中。纯对象模拟补环境在Node.js的vm2沙箱或直接修改全局对象用JavaScript对象模拟出浏览器环境。优点是轻量、快速、资源占用小。缺点是需要精准分析工作量大且随着网站检测升级需要持续维护。对于anti_content这种通常在页面加载初期或请求前同步计算的参数其逻辑相对独立不涉及复杂的DOM交互和异步渲染。因此选择纯对象模拟方案是更优解它能带来更高的执行效率和更好的集成性。4.2 构建基础环境模拟层我们创建一个名为patch_env.js的模块专门用于构建补环境对象。// patch_env.js const crypto require(crypto); class EnvironmentPatcher { constructor(options {}) { this.options { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., platform: Win32, language: zh-CN, screenWidth: 1920, screenHeight: 1080, ...options }; this.initWindow(); this.initNavigator(); this.initScreen(); this.initDocument(); this.initPerformance(); // 其他需要补的对象... } initWindow() { // 创建一个模拟的window对象 this.window { navigator: null, // 稍后赋值 screen: null, document: null, location: { href: https://www.pdd.com, hostname: www.pdd.com, // ... 其他location属性 }, performance: null, // 必须将window自身指向自己因为代码中可能用 window.window 或 self window: this, // 关键让 window.window 和 window.self 都指向自己 self: this, Date: Date, // 暴露原生Date Math: Math, // 暴露原生Math但注意可能需处理Math.random Object: Object, Array: Array, String: String, Function: Function, setTimeout: setTimeout, setInterval: setInterval, clearTimeout: clearTimeout, clearInterval: clearInterval, // 如果目标代码使用了 console.log最好也模拟一下防止报错 console: console, }; } initNavigator() { this.window.navigator { userAgent: this.options.userAgent, platform: this.options.platform, language: this.options.language, languages: [this.options.language, en-US, en], hardwareConcurrency: 8, // 模拟8核CPU deviceMemory: 8, // 模拟8GB内存 maxTouchPoints: 0, webdriver: undefined, // 关键必须是undefined // plugins 和 mimeTypes 需要模拟成类数组对象 plugins: [], mimeTypes: [], // 为了通过 navigator.plugins.length 等检测 get plugins() { return this._plugins || []; }, set plugins(val) {}, // 类似地处理 mimeTypes get mimeTypes() { return this._mimeTypes || []; }, set mimeTypes(val) {}, // 一些只读属性的getter模拟 get cookieEnabled() { return true; }, get onLine() { return true; }, // 可能被调用的方法 javaEnabled: function() { return false; }, }; // 让 navigator 指向自己应对 navigator.navigator 的调用 this.window.navigator.navigator this.window.navigator; } initScreen() { this.window.screen { width: this.options.screenWidth, height: this.options.screenHeight, availWidth: this.options.screenWidth - 100, // 粗略模拟任务栏占用 availHeight: this.options.screenHeight - 100, colorDepth: 24, pixelDepth: 24, }; } initDocument() { this.window.document { documentElement: { clientWidth: this.options.screenWidth, clientHeight: this.options.screenHeight, }, characterSet: UTF-8, charset: UTF-8, cookie: , // 初始为空可根据需要设置 // 模拟常用方法返回简单对象避免报错 createElement: function(tagName) { return { tagName: tagName.toUpperCase(), getContext: function() { // 如果调用getContext(2d)返回一个模拟的Canvas上下文 return { fillText: function() {}, // ... 其他Canvas API }; }, toDataURL: function() { return data:image/png;base64,...; }, }; }, getElementById: function(id) { return null; }, getElementsByTagName: function(name) { return []; }, // location 通常指向 window.location get location() { return this.defaultView.location; }, set location(href) { /* 可能重定向 */ }, defaultView: this.window, // 关键document.defaultView 通常指向 window }; // 让 document 的 ownerDocument 指向自己 this.window.document.ownerDocument this.window.document; } initPerformance() { const now Date.now(); this.window.performance { timing: { navigationStart: now - 1000, // 假设1秒前开始导航 fetchStart: now - 900, domainLookupStart: now - 800, domainLookupEnd: now - 700, connectStart: now - 600, connectEnd: now - 500, requestStart: now - 400, responseStart: now - 300, responseEnd: now - 200, domLoading: now - 150, domInteractive: now - 100, domContentLoadedEventStart: now - 50, domContentLoadedEventEnd: now - 40, domComplete: now - 30, loadEventStart: now - 20, loadEventEnd: now - 10, }, now: function() { return Date.now() - (this.timing.navigationStart || 0); }.bind(this) }; } // 获取补全后的全局对象用于注入沙箱 getContext() { return this.window; } } module.exports EnvironmentPatcher;这个EnvironmentPatcher类提供了一个基础的、可配置的浏览器环境模拟。它创建了一个自包含的window对象其下的navigator、screen、document、performance等属性都按照分析得到的需求进行了模拟。4.3 使用vm2沙箱隔离与执行我们不能直接污染Node.js的全局对象因此使用vm2模块创建一个干净的沙箱Sandbox将我们模拟的环境对象注入进去然后在这个沙箱中执行目标JS代码。// vm2_sandbox.js const { VM } require(vm2); const EnvironmentPatcher require(./patch_env); function createAntiContent(jsCode, extraParams {}) { // 1. 实例化环境补丁 const patcher new EnvironmentPatcher({ userAgent: extraParams.userAgent || Mozilla/5.0 ..., // ... 其他可覆盖的配置 }); const sandboxContext patcher.getContext(); // 2. 创建VM沙箱并注入模拟环境 const vm new VM({ sandbox: sandboxContext, // 允许require某些Node.js模块如果目标JS代码依赖通常不依赖 // require: (moduleName) { if (moduleName crypto) return crypto; } }); // 3. 关键将目标JS代码包装在一个函数或立即执行函数表达式(IIFE)中 // 假设我们找到的生成函数叫 window.getAntiContent // 我们需要确保它在沙箱的上下文中被定义和执行。 const wrappedCode (function() { // 将原始的、混淆的生成函数代码放在这里 ${jsCode}; // 假设生成函数被赋值给了 window.xxx 或全局变量 global_anti_func // 这里调用它并返回结果 try { var antiResult window.getAntiContent(); // 或 global_anti_func(); return antiResult; } catch (error) { return { error: error.message, stack: error.stack }; } })(); ; // 4. 在沙箱中运行代码 try { const antiContent vm.run(wrappedCode); return antiContent; } catch (err) { console.error(VM执行错误:, err); return null; } } // 示例用法 const fs require(fs); const targetJSCode fs.readFileSync(./obfuscated_anti_content.js, utf-8); const result createAntiContent(targetJSCode); console.log(生成的 anti_content:, result);为什么用vm2而不是Node.js原生vmvm2提供了更强的沙箱隔离能有效防止目标代码逃逸沙箱并访问或危害主机环境安全性更高。对于来源不明的混淆代码这是一个重要的安全考量。5. 调试技巧与问题排查实录补环境的过程极少能一蹴而就几乎必然会在执行时报出各种undefined或xxx is not a function的错误。这时系统的调试方法至关重要。5.1 错误追踪与属性补全当在Node.js中执行上述代码报错时例如ReferenceError: navigator is not defined或TypeError: Cannot read property userAgent of undefined这明确告诉我们代码试图访问navigator对象但在当前执行上下文中不存在。我们的补环境代码可能没有正确注入或者目标代码访问的路径与我们模拟的不同例如它可能通过this.navigator或self.navigator访问。调试步骤捕获详细错误在vm.run的catch块中打印完整的error.stack。堆栈信息会指向混淆代码中的具体行号虽然行号对应的是混淆后的代码但结合源代码查看器可以定位到大概位置。在浏览器中动态调试回到浏览器开发者工具在报错对应的代码行或附近下断点。观察在浏览器真实环境中代码访问的完整路径是什么比如是window.navigator还是self.navigator.appName把完整的访问链记录下来。更新补环境对象根据观察到的访问链更新我们的patch_env.js。例如如果代码访问了self.navigator.appName我们需要确保self指向我们的模拟window并且navigator对象上有appName属性。迭代循环重复步骤1-3直到不再出现环境缺失的错误。这个过程可能需要进行十几次甚至几十次。5.2 处理特殊检测与“坑点”有些环境检测比较隐蔽或棘手函数toString()检测有些反爬会检查原生函数的toString()结果。例如document.createElement.toString()在真实浏览器中会返回function createElement() { [native code] }。而在我们的模拟中如果createElement是我们自己写的函数toString()返回的就是我们的函数源码。这可能会成为检测点。解决方法是为关键模拟函数设置特殊的toString方法document.createElement function(tagName) { // ... 实现 }; document.createElement.toString function() { return function createElement() { [native code] }; };属性描述符检测有些属性在浏览器中是只读的configurable: false, writable: false。如果我们的模拟对象这些属性是可写的也可能被检测。可以使用Object.defineProperty来定义属性模拟其描述符。Object.defineProperty(navigator, userAgent, { value: Mozilla/5.0..., writable: false, configurable: false, enumerable: true });原型链检测确保模拟对象的原型链正确。例如document.createElement(canvas)返回的对象其原型链应该是HTMLCanvasElement - HTMLElement - Element - Node - EventTarget - Object。简单模拟一个普通对象可能不够。对于深度检测可能需要构建更复杂的原型链。但在很多情况下如果代码只是调用canvas.getContext(2d)那么返回一个具有getContext方法的普通对象即可。异步与定时器如果生成anti_content的代码用到了setTimeout或Promise我们需要确保这些异步API在沙箱中能工作。vm2沙箱默认可以使用setTimeout等但要注意执行时序。5.3 验证与对比成功生成anti_content后必须进行验证格式对比将本地生成的anti_content与浏览器网络请求中抓到的anti_content进行对比看长度、字符集是否都是Base64字符是否大致相同。完全一致在动态参数下很难因为可能依赖精确的时间戳或随机数。功能验证将本地生成的anti_content填入请求参数可使用Python的requests库或Node.js的axios发送一次真实的网络请求看是否能成功获取数据。这是最终的验收标准。稳定性测试连续生成多个anti_content观察其变化规律是否合理如时间戳部分递增。并发测试多个生成实例确保环境隔离良好不会相互干扰。6. 优化、封装与集成建议6.1 性能与可维护性优化最初的补环境脚本可能随着发现的检测点增多而变得臃肿。为了优化模块化将navigator、document、screen等不同对象的模拟拆分成独立文件便于管理和更新。惰性初始化不是所有属性都在一开始就创建。可以为某些对象设置getter只有在代码真正访问时才计算并返回模拟值这能提升初始化速度。缓存与复用如果anti_content生成函数会被多次调用且环境对象在单次会话中不变可以只创建一次沙箱和环境然后多次调用其中的函数避免重复初始化开销。配置化将userAgent、屏幕分辨率等可变参数提取为外部配置方便适配不同的模拟设备。6.2 封装为即用模块最终我们可以将整个补环境和执行逻辑封装成一个干净的模块或类。// anti-content-generator.js const { VM } require(vm2); const EnvironmentPatcher require(./patch_env); const fs require(fs); class AntiContentGenerator { constructor(jsCodePath, options {}) { this.rawJSCode fs.readFileSync(jsCodePath, utf-8); this.options options; this.vm null; this.initVM(); } initVM() { const patcher new EnvironmentPatcher(this.options); const sandbox patcher.getContext(); this.vm new VM({ sandbox }); // 在沙箱中定义生成函数这里假设原代码将函数挂载到了 window 上 const initCode ${this.rawJSCode}; // 确保生成函数在沙箱全局可访问例如命名为 generateAntiContent if (typeof window.getAntiContent function) { global.generateAntiContent window.getAntiContent; } ; this.vm.run(initCode); } generate(additionalData {}) { // 如果生成函数需要参数可以通过这里传入 const callCode (function() { try { return global.generateAntiContent(${JSON.stringify(additionalData)}); } catch(e) { return { error: e.toString() }; } })(); ; return this.vm.run(callCode); } } module.exports AntiContentGenerator;使用方式就变得非常简洁const AntiContentGenerator require(./anti-content-generator); const generator new AntiContentGenerator(./vendor_obfuscated.js, { userAgent: your_ua_string }); setInterval(() { const antiContent generator.generate(); console.log([${new Date().toISOString()}] Generated:, antiContent); // 然后使用这个antiContent去发起请求... }, 5000); // 每5秒生成一次6.3 集成到爬虫框架将上述生成器集成到ScrapyPython或CrawleeNode.js等爬虫框架中。通常的做法是在爬虫启动时初始化一个或多个AntiContentGenerator实例。在构造请求Request之前调用生成器获取最新的anti_content参数。将该参数添加到请求的cookies、headers或formdata中具体位置需根据实际接口确定。发送请求并处理响应。注意事项并发安全如果爬虫是并发/分布式的确保每个爬虫实例或线程有自己的、独立的环境模拟避免共享状态导致参数冲突或被封。错误处理与重试网络请求可能因anti_content失效而失败。需要实现重试机制并在重试前重新生成anti_content有时可能需要刷新页面或获取新的令牌种子。更新与维护反爬策略会升级。需要定期检查anti_content是否仍然有效并关注JS代码是否有更新。当失效时重新进行逆向分析和环境补全。补环境是一场精细的攻防战需要耐心、细心和对浏览器环境的深刻理解。通过本次对某多多anti_content的逆向实践我们不仅获得了一个可用的参数生成方案更建立起一套应对类似环境检测反爬的系统方法。记住核心思路永远是在非浏览器环境中精准、最小化地模拟出目标代码所期望的浏览器环境。随着经验的积累你会逐渐形成自己的“环境补全库”面对新的挑战时也能更快地定位和解决问题。