资讯中心

逆向瑞数6代动态安全:Node.js补环境核心原理与实战

📅 2026/8/15 7:13:03
逆向瑞数6代动态安全:Node.js补环境核心原理与实战
1. 从一次“卡壳”的自动化任务说起那天下午我盯着屏幕上那个熟悉的“请完成安全验证”的提示框心里一阵烦躁。这是一个需要定期从某监管机构网站抓取公开数据的自动化任务脚本已经稳定运行了小半年突然就“罢工”了。页面能正常加载但一到关键的数据查询接口返回的就是一堆乱码或者直接跳转到验证页面。经验告诉我这大概率是网站的防护策略升级了而“瑞数”这个名字几乎是所有做数据采集的开发者绕不开的“老朋友”。这次遇到的是传闻中更棘手的第六代。瑞数动态安全Botgate的防护核心在于其“动态令牌”和“行为验证”机制。它不像传统的验证码那样给你一张图片而是在前端JavaScript层面通过一套复杂的、实时生成的代码我们常说的JSVMP即JavaScript Virtual Machine Protection来验证访问环境的“真实性”。简单说它要判断你是不是一个“真人”在用“真浏览器”访问。我们常用的requests、httpx等库发起的请求缺乏一个完整的浏览器环境如window、document、navigator等对象在瑞数看来就是“非真人”行为请求自然会被拦截。“补环境”就是为了应对这种检测。它的核心思路不是去逆向那套复杂的、每次都可能变化的JSVMP加密逻辑那是一条艰难且不稳定的路而是“欺骗”它在非浏览器环境中如Node.js模拟构建出一个足够“真实”的浏览器上下文对象让瑞数用于检测的JavaScript代码运行在我们提供的环境里并顺利计算出正确的令牌参数。playwright这类现代浏览器自动化工具的出现提供了一种新思路直接启动一个真实的、无头浏览器来执行所有操作用“真人”行为来过验证。但playwright方案资源消耗大、速度相对慢而“补环境”方案如果构建得当则能以接近原生HTTP请求的速度和效率运行这对于需要高并发、低延迟的采集场景至关重要。本文将深入拆解针对“瑞数6代”的补环境核心要点与实战流程。2. 瑞数6代环境检测的核心维度与应对策略与之前版本相比瑞数6代通常指rs6或rs_c等后缀的JS文件的环境检测更为立体和隐蔽。它不再仅仅满足于检查几个明显的对象是否存在而是会深入探测对象的属性、方法的行为是否符合真实浏览器的表现甚至包括一些时序特征。我们的补环境工作就是一场精心策划的“伪装”。2.1 关键对象与属性的深度模拟首先我们需要建立一个最基础的浏览器全局对象window并确保其原型链正确。// 在Node.js中使用vm2模块创建一个相对安全的沙箱环境来运行不可信代码 const { VM } require(vm2); const vm new VM({ sandbox: {} }); // 构建最基础的window对象 let window { document: {}, navigator: {}, location: {}, // ... 其他属性 }; window.self window; window.window window; // 关键让window.window指向自身 // 将window设置为vm沙箱的全局对象 vm.freeze(window, window);但这仅仅是开始。瑞数6代会检查许多细节navigator对象这是重灾区。不能只提供userAgent。window.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, languages: [zh-CN, zh], hardwareConcurrency: 8, // 逻辑CPU核心数 deviceMemory: 8, // 内存大小GB maxTouchPoints: 0, // 注意一些插件相关属性如plugins, mimeTypes在无头环境下通常是空数组但结构要对 plugins: [], mimeTypes: [], webdriver: undefined, // 必须为undefined这是识别自动化工具的关键标志。 // 新增检测点connection, permissions 等 connection: { downlink: 10, effectiveType: 4g, rtt: 50 } };注意webdriver属性至关重要。在真实的Chrome浏览器中这个属性是undefined。而通过puppeteer或playwright控制的浏览器即使做了隐藏有时在深度检测下仍可能暴露。纯补环境方案必须确保其为undefined。document对象及其子属性document不是一个静态对象它拥有大量动态方法和属性。window.document { charset: UTF-8, characterSet: UTF-8, cookie: , // 初始为空后续由服务端Set-Cookie或JS赋值 referrer: , // 一些方法需要模拟瑞数可能会调用 createElement: function(tag) { return { tagName: tag.toUpperCase(), style: {} }; }, getElementsByTagName: function() { return []; }, documentElement: { clientWidth: 1920, clientHeight: 1080 }, // 新增document.mode, compatMode 等 compatMode: CSS1Compat, documentMode: undefined, // 关于Cookie的读写 __defineGetter__(cookie, function() { return this._cookie || ; }), __defineSetter__(cookie, function(val) { // 简单的cookie累加真实浏览器行为更复杂 this._cookie (this._cookie ? this._cookie ; : ) val.split(;)[0]; }) };函数toString与原型链瑞数会通过Function.prototype.toString.call(someFunction)来检查一个函数是否是原生函数。我们模拟的函数返回的字符串通常是function () { [native code] }。但直接返回这个固定字符串可能被检测出一致性过高。更稳妥的做法是重写Function.prototype.toString根据调用者返回不同的、符合预期的字符串。const nativeToString Function.prototype.toString; Function.prototype.toString function() { // 判断this是否是我们自己模拟的函数 if (this.__isFakeFunction) { // 返回一个类似原生函数的字符串可以加入随机空格或注释增加差异性 return function ${this.name || }() { [native code] }; } // 如果是环境自带的函数则调用原始的toString方法 return nativeToString.call(this); }; // 在我们自己模拟的函数上打标记 window.document.createElement.__isFakeFunction true;2.2 应对新增的“行为指纹”检测瑞数6代加强了对“行为”的检测而不仅仅是静态属性。异步定时器Timer的篡改检测瑞数可能会检查setTimeout、setInterval、Date.now()等函数和对象的准确性。它们会注入代码来检测这些函数的“延迟”是否被篡改例如一些自动化工具会加速定时器。在补环境中我们不能直接替换全局的setTimeout但可以在沙箱环境中提供与原生日志一致的行为。// 在vm2沙箱中我们可以提供接近原生的定时器实现 vm.setGlobal(setTimeout, (fn, delay) { // 这里实现一个简单的、基于事件循环的模拟确保delay参数被接收 // 实际上对于计算令牌可能不需要真正等待但函数必须被正确调用 // 更复杂的模拟需要实现一个微任务队列 if (typeof fn function) { // 立即执行或使用setImmediate模拟不真正等待delay setImmediate(fn); } return 123; // 返回一个模拟的timer ID }); // 确保Date.now()返回一个合理的时间戳 const startTime Date.now() - Math.random() * 1000; // 模拟一个过去的随机起始点 vm.setGlobal(Date, class extends Date { static now() { return startTime (performance?.now() || 0); } constructor(...args) { if (args.length 0) { super(Date.now()); // 调用重写后的now } else { super(...args); } } });Canvas与WebGL指纹虽然核心令牌计算可能不直接依赖Canvas但检测代码可能会尝试获取Canvas指纹来验证环境一致性。我们需要模拟HTMLCanvasElement及其上下文。class MockCanvasRenderingContext2D { fillText() {} measureText() { return { width: 40 }; } // ... 模拟其他必要方法 } window.HTMLCanvasElement class { getContext(contextType) { if (contextType 2d) { return new MockCanvasRenderingContext2D(); } return null; } toDataURL() { return data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg; } }; // 将Canvas构造函数挂载到document上因为检测代码可能通过document.createElement(canvas)创建 window.document.defaultView window; // 建立关联错误监控与事件监听瑞数可能会监听未处理的错误或特定事件来检测环境异常。我们需要确保模拟的环境不会抛出预期外的错误并可能需要在window上模拟addEventListener等方法安静地“吞掉”一些事件监听请求。3. 逆向分析定位动态令牌的生成入口补环境的前提是知道要把哪些JS代码放到我们模拟的环境里执行。这就需要找到瑞数生成动态令牌通常是一个名为cookie或header中的特定参数如RM4hZBv0dDon443M的关键代码入口。3.1 网络请求分析与关键JS定位首次访问与拦截用浏览器开发者工具打开Preserve log首次访问目标网站。通常会收到一个状态码202、412或503的响应响应体是一段混淆的JavaScript代码或者页面直接重定向到一个包含大量JS的验证页。这个JS就是瑞数的“引导器”。搜索关键标识在Network面板查看被拦截的XHR/Fetch请求其请求参数中往往包含一长串看似随机的字符串这就是动态令牌。记下它的参数名比如x-cookie-rs。在Sources面板对所有加载的JS文件进行全局搜索CtrlShiftF。搜索关键词可以是令牌参数名x-cookie-rs瑞数特征rs6,rs_c,rs_,$_ts,$_z可能的方法名getCookie,setCookie,encrypt,sign可疑的常量$dbsm_开长字符串。下断点与逻辑跟踪在疑似入口函数如包含$_ts赋值或调用了document.cookie设置的操作处下断点。刷新页面程序会断住。然后通过Call Stack调用栈向上回溯找到最外层的、一次性执行的匿名函数或入口函数。这个函数往往接受几个关键参数如$_ts内部包含了完整的检测和令牌生成逻辑。我们的目标就是将这个入口函数及其所有依赖提取出来。3.2 代码提取与初步净化找到入口函数后将其代码通常是一个巨大的自执行匿名函数保存到本地。它通常是高度混淆的变量名替换、控制流平坦化、字符串加密。我们不需要完全反混淆但需要做初步处理提取依赖确保提取的代码片段是完整的。如果它内部引用了其他外部变量或函数这些可能定义在更早的script标签里需要一并提取。补全浏览器API在代码开头手动声明一些它可能直接访问但未定义的顶级变量防止在Node.js中直接运行时报ReferenceError。// 在提取的瑞数代码之前添加 if (typeof window undefined) { global.window global; global.document { cookie: , documentElement: { clientWidth: 1920, clientHeight: 1080 } // ... 其他必要属性 }; global.navigator { userAgent: ... }; global.location { href: https://目标网站.com }; global.screen { width: 1920, height: 1080 }; // 注意这是一个非常简陋的补全仅用于通过语法检查。真正的补环境在vm沙箱中进行。 }定位输出点在提取的代码中搜索document.cookie的赋值语句或者查找最终生成的令牌被赋值给了哪个全局变量。这将是我们的“收获”点。我们需要修改代码让它把计算出的关键结果令牌暴露给我们而不是直接设置到document.cookie这在Node.js中无效。// 假设原代码最后有这样一句 // document.cookie RM4hZBv0dDon443M generatedToken ; path/; // 我们可以将其修改为同时赋值给一个我们定义的全局变量 // window._myRsToken generatedToken; // document.cookie RM4hZBv0dDon443M generatedToken ; path/; // 保留原语句避免代码逻辑因缺失而报错4. 构建完整的Node.js补环境执行流程现在我们将前面模拟的环境和提取的瑞数代码结合起来构建一个可执行的Node.js脚本。4.1 使用vm2沙箱执行不可信代码vm2模块比Node.js原生的vm模块更安全能更好地隔离环境防止补环境代码“逃逸”并影响主进程。const { VM } require(vm2); const fs require(fs); // 1. 读取提取并初步净化后的瑞数JS代码 const rsCode fs.readFileSync(./rs6_extracted.js, utf-8); // 2. 构建一个尽可能真实的沙箱环境 const sandbox { window: {}, document: {}, navigator: {}, location: {}, screen: {}, // ... 其他可能需要的全局对象如Object, Array, Math等vm2会默认注入一部分 }; // 初始化关键对象这里调用前面章节定义的模拟函数来完善sandbox initializeBrowserEnv(sandbox); // 假设这是一个函数将2.1和2.2中的模拟对象赋值给sandbox // 3. 创建一个VM实例 const vm new VM({ sandbox: sandbox, // 允许require某些内置模块如果瑞数代码用到了通常不会 // require: (id) { if (id crypto) return require(crypto); } eval: false, // 禁用eval更安全 wasm: false, // 禁用WebAssembly }); // 4. 暴露一个钩子函数用于获取令牌 sandbox.window._getRsResult function() { // 这个函数将在瑞数代码执行后由我们调用 // 瑞数代码需要把结果存到某个地方比如 window._rsToken return this._rsToken || this._myRsToken || (this.document this.document.cookie); }; // 5. 执行瑞数代码 try { vm.run(rsCode); console.log(瑞数代码执行完毕。); // 6. 获取生成的令牌 const result vm.run(window._getRsResult()); console.log(生成的令牌/关键数据:, result); // 7. 使用这个令牌构造请求头或Cookie发起真正的数据请求 const headers { User-Agent: sandbox.navigator.userAgent, Cookie: RM4hZBv0dDon443M${result}; other_cookies..., // 将令牌填入 // ... 其他必要头部 }; // 使用axios、got等库发起请求 // const data await axios.get(https://目标网站.com/api/data, {headers}); } catch (error) { console.error(执行瑞数代码时出错:, error); // 错误信息是调试的最重要依据可能提示缺少某个属性或函数行为不对 }4.2 调试与迭代处理环境差异报错第一次运行几乎肯定会失败。错误信息是我们的指路明灯。TypeError: Cannot read property xxx of undefined说明某个对象不存在。根据错误栈找到瑞数代码中访问该属性的位置然后在sandbox中补全这个对象链。例如错误是Cannot read property clientWidth of undefined指向document.documentElement那么就需要确保sandbox.document.documentElement对象存在且拥有clientWidth属性。TypeError: xxx is not a function说明某个被调用的属性不是函数。需要检查我们模拟的对象方法是否正确。例如document.createElement被调用但我们模拟成了一个普通对象就需要将其更正为一个函数。生成的令牌不正确或请求依然被拦截环境不完整还有某个关键的检测点没模拟到。可以用浏览器执行一遍瑞数代码在关键位置用console.log输出所有访问到的对象路径与我们的沙箱环境对比。行为不一致比如Date.now()的返回值序列被检测出异常或者Math.random()的随机数序列在沙箱中是固定的。我们需要让这些函数的行为更“自然”。代码依赖外部变量可能还有某个全局变量是在另一个script中定义的我们没提取完整。需要检查网络请求找到所有在瑞数代码之前加载的JS文件。令牌有时效性瑞数令牌可能是一次性的或者与当前会话、时间戳强相关。需要确保每次请求都重新执行一次完整的瑞数代码生成新令牌而不是复用旧的。5. 高级技巧与稳定性优化当基础补环境能跑通后下一步是提升其稳定性和兼容性以应对网站更新和更复杂的检测。5.1 使用Proxy进行动态属性拦截手动补全每一个可能的属性是不现实的。ES6的Proxy可以创建一个对象的代理拦截对该对象的所有访问get、赋值set等操作。我们可以用Proxy来包装window、document等核心对象当访问不存在的属性时动态返回一个合理的模拟值而不是undefined。function createDeepProxy(target, handler) { // 递归地为所有对象属性创建Proxy if (typeof target object target ! null) { for (const key in target) { if (typeof target[key] object) { target[key] createDeepProxy(target[key], handler); } } return new Proxy(target, handler); } return target; } const windowHandler { get: function(obj, prop) { // 如果属性存在直接返回 if (prop in obj) { const value obj[prop]; // 如果返回值是对象也确保它被Proxy包装 if (typeof value object value ! null !(value instanceof RegExp) !(value instanceof Date)) { return createDeepProxy(value, windowHandler); } return value; } // 如果属性不存在根据属性名返回一个合理的默认值避免报错 console.warn([Proxy] 访问了不存在的window属性: ${String(prop)}返回空对象模拟。); // 返回一个空函数的Proxy以应对函数调用 if (typeof prop string (prop.startsWith(on) || prop.endsWith(EventListener))) { // 对于事件监听器返回一个空函数 const fakeFunc () {}; fakeFunc.__isFakeFunction true; return fakeFunc; } // 否则返回一个空的Proxy对象允许链式访问 const fakeObj {}; return createDeepProxy(fakeObj, windowHandler); }, set: function(obj, prop, value) { obj[prop] value; return true; } }; const proxiedWindow createDeepProxy({}, windowHandler); proxiedWindow.self proxiedWindow; proxiedWindow.window proxiedWindow; // 然后将proxiedWindow作为沙箱的window注意过度使用Proxy可能会影响性能并且如果瑞数代码通过Object.getOwnPropertyDescriptor等方式深度检测可能会发现属性是动态生成的而非真实存在。因此Proxy更适合作为兜底方案核心常用属性仍需手动精确模拟。5.2 对抗反调试与代码完整性校验瑞数可能会检测代码是否被修改、是否在调试状态下运行。检测debugger关键字代码中可能包含debugger;语句在开发者工具打开时会不断断点。在提取代码后可以简单地将所有debugger;语句删除或注释掉。检测代码长度或哈希某些代码会计算自身的哈希值进行校验。直接删除代码行可能会破坏校验。更稳妥的方法是不修改原始代码逻辑而是通过“猴子补丁”Monkey Patch在运行时覆盖检测函数。例如找到计算哈希的函数让它直接返回一个固定的、正确的值。// 假设在瑞数代码中有一个函数$_cs做校验 // 我们在执行瑞数代码前先覆盖它 const overrideCode if (typeof $_cs ! undefined) { const original_cs $_cs; $_cs function() { // 直接返回预期的正确值绕过校验 return expected_hash_value_here; }; } ; vm.run(overrideCode rsCode);时间差检测代码可能测量两个函数执行点之间的时间差如果时间过长因为下了断点则判定为调试。在补环境执行时这种检测通常不影响结果但需要注意。5.3 集成与自动化将补环境脚本集成到爬虫框架中令牌管理将生成令牌的逻辑封装成一个异步函数。在发起数据请求前调用该函数获取新鲜令牌。可以考虑加入简单的缓存机制缓存几秒钟避免同一会话内重复计算但要注意令牌的时效性。错误重试与降级如果补环境请求失败应有重试机制。重试时可以尝试完全重新初始化环境再执行一次。作为降级方案可以准备一个playwright的备用方案当补环境失效时自动切换到无头浏览器模式虽然慢但能保证可用性。环境更新监控定期如每天用测试用例跑一下补环境脚本确保生成的令牌依然有效。如果失效需要重新分析网站提取最新的瑞数代码。补环境过瑞数6代是一个精细且需要耐心的过程它考验的是对前端JavaScript运行环境细节的深刻理解。没有一劳永逸的“通杀代码”因为瑞数本身的代码和检测点也在不断变化。核心思路始终是通过逆向找到入口通过模拟创造一个足以乱真的执行环境让防护代码自己计算出正确的结果。这个过程充满了调试和迭代每一个错误信息都是通往更完美模拟的阶梯。保持耐心细致分析你就能让机器在规则的边界内优雅地获取所需的数据。