1. 项目概述当“瑞数”遇上“宝武资源”最近在分析宝武资源有限公司的官网时遇到了一个典型的“伪瑞数”防护。对于做数据采集或者安全测试的朋友来说“瑞数”这个名字往往意味着一个难啃的硬骨头它以其动态混淆和强环境检测闻名。但“伪瑞数”又是什么简单说就是网站采用了与瑞数类似的前端反爬思路——通过检测浏览器环境、注入动态代码来干扰自动化脚本但其实现可能并非瑞数官方方案或是某个定制化、简化版本。宝武资源有限公司作为大型国企的线上门户其数据具有一定的行业参考价值因此突破其前端防护实现稳定、可靠的数据获取就成了一个很有实践意义的挑战。这个案例的核心在于“面向对象补环境”。传统的补环境脚本往往是一堆零散的函数和变量定义维护起来如同面对一团乱麻。而面向对象Object-Oriented Programming, OOP的思想则为我们提供了一种更优雅、更健壮的解决方案。我们将浏览器环境如window、navigator、document等抽象成一个个类通过继承、封装和多态来模拟一个完整的、可动态调整的浏览器上下文。这不仅让代码结构清晰更重要的是它能更精准地应对目标网站对特定环境属性的深度检测。接下来我将详细拆解如何从零开始构建一个面向对象的补环境框架并成功应用于宝武资源官网的逆向案例中。2. 逆向目标分析与防护机制拆解2.1 目标网站特征与初步探测宝武资源有限公司官网的前端在关键数据请求如产品列表、价格信息查询前会加载一段经过混淆的JavaScript代码。使用浏览器开发者工具的网络面板Network进行抓包可以清晰地看到在发起真正的XHR或Fetch请求之前会有一个对特定JS文件的请求其响应内容是一大串高度混淆、难以直接阅读的代码。通过“全局搜索”一些关键字符串如setTimeout、document.createElement、Object.defineProperty或使用AST抽象语法树解析工具进行初步反混淆可以发现其核心逻辑代码执行后会生成一个或多个动态的token或加密参数并附加到后续请求的Header或Query String中。服务器端会校验这些参数如果校验失败则返回空数据或错误码。这就是典型的环境依赖型反爬——你的请求是否合法取决于你的JS执行环境是否“像”一个真实的浏览器。2.2 “伪瑞数”防护的核心逻辑虽然并非官方瑞数但其防护思想一脉相承主要包含以下几个层面浏览器指纹收集脚本会尝试读取大量浏览器和系统环境属性。这远不止是常见的userAgent还包括navigator对象plugins,mimeTypes,hardwareConcurrency,deviceMemory,platform。screen对象width,height,colorDepth,pixelDepth。window对象outerWidth,outerHeight,innerWidth,innerHeight。PerformanceAPI用于计算页面加载和执行时间。Canvas和WebGL指纹通过绘制图形并计算哈希值生成几乎唯一的设备标识。DOM与BOM行为监控代码会检测对document、window对象上某些方法和属性的访问。例如它可能重写了document.createElement在你创建特定元素如div,canvas时注入检测逻辑或者通过Object.defineProperty在window对象上设置getter当你的脚本读取某个属性时触发检测。异步逻辑与时间差攻击防护脚本中充满了setTimeout、setInterval、Promise等异步操作。它可能通过计算多个异步回调之间的时间差来判断执行环境是真实的浏览器事件循环还是模拟器如Node.js的单一线程。如果时间差不符合浏览器行为则判定为异常。代码流混淆与动态执行核心检测代码被严重混淆变量名替换、控制流平坦化、字符串加密并且可能采用eval、Function构造函数或script标签注入的方式动态执行增加静态分析的难度。注意在逆向初期切忌直接使用eval执行目标网站的混淆代码。这可能导致你的本地环境被意外修改甚至触发一些未知行为。正确的做法是在一个干净的、隔离的沙箱环境如Node.js的vm模块或浏览器开发者工具的Sources面板中的代码片段中逐步调试和分析。2.3 为什么选择“面向对象补环境”面对如此繁杂的检测点传统的“打补丁”式补环境即缺什么补什么在全局对象上直接挂载属性或函数存在明显弊端难以维护补丁代码散落在各处随着检测点增多代码迅速膨胀且混乱。容易遗漏检测可能是链式的例如先检测navigator.plugins再检测plugins的某个特定项零散的补丁难以构建完整的对象链。缺乏动态性某些属性是动态的或具有getter/setter简单的赋值无法模拟其行为。上下文污染直接在全局对象上修改可能影响其他依赖库的正常运行。面向对象的方式则能很好地解决这些问题封装将window、navigator、document等分别封装成类。类的内部维护自身的状态和属性。继承可以基于一个基础的“环境类”进行扩展复用通用逻辑。多态可以重写父类的方法以模拟特定的浏览器行为如重写document.getElementById返回一个模拟的DOM元素。结构清晰每个环境组件职责单一代码易于阅读、调试和扩展。3. 面向对象补环境框架设计与实现3.1 基础架构设计我们设计一个核心的BrowserEnv类作为总入口它负责组装和协调各个子环境模块。整体架构如下// 基础环境模拟类 class BaseEnvironment { constructor() { this._properties new Map(); // 用于存储动态属性 this._overrides new Map(); // 用于存储被重写的方法 } // 通用方法用于定义getter/setter属性 defineProperty(name, descriptor) { Object.defineProperty(this, name, descriptor); } // 通用方法用于重写原型方法 overrideMethod(className, methodName, newMethod) { // ... 实现逻辑 } } // 模拟Window类 class MockWindow extends BaseEnvironment { constructor() { super(); this.navigator new MockNavigator(); this.screen new MockScreen(); this.document new MockDocument(); this.location new MockLocation(); // 初始化window自身属性 this.defineProperty(innerWidth, { get: () 1920 }); this.defineProperty(innerHeight, { get: () 1080 }); // 模拟异步API this.setTimeout (fn, delay) { /* 模拟实现 */ }; this.setInterval (fn, delay) { /* 模拟实现 */ }; } } // 模拟Navigator类 class MockNavigator extends BaseEnvironment { constructor() { super(); this.userAgent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...; this.platform Win32; this.hardwareConcurrency 8; this.deviceMemory 8; this.plugins new MockPluginArray(); // 这是一个复杂的数组对象 this.mimeTypes new MockMimeTypeArray(); } } // 模拟Document类 class MockDocument extends BaseEnvironment { constructor() { super(); this.documentElement { clientWidth: 1903, clientHeight: 969 }; this.createElement (tagName) { // 返回一个模拟的DOM元素对象 const element new MockElement(tagName); // 特别处理canvas因为可能用于指纹生成 if (tagName.toLowerCase() canvas) { // 为canvas注入getContext方法返回模拟的2d上下文 element.getContext (contextType) { if (contextType 2d) { return new MockCanvasRenderingContext2D(); } // 可能还需要模拟webgl return null; }; } return element; }; this.getElementById (id) { /* 返回模拟元素或null */ }; } } // 主环境类 class BrowserEnv { constructor() { this.window new MockWindow(); // 将window作为全局对象 const global typeof globalThis ! undefined ? globalThis : (typeof window ! undefined ? window : global); // 将模拟的属性/方法注入到指定对象通常是vm创建的上下文 this.injectTo (target) { for (let key in this.window) { if (this.window.hasOwnProperty(key)) { target[key] this.window[key]; } } // 确保window和self指向自身 target.window target; target.self target; }; } }3.2 关键难点复杂对象的深度模拟宝武资源官网的检测脚本对navigator.plugins和navigator.mimeTypes的检测非常深入。这两个属性不是简单的数组而是PluginArray和MimeTypeArray对象它们具有特定的方法和属性遍历行为。MockPluginArray的实现示例class MockPluginArray extends BaseEnvironment { constructor() { super(); // 模拟常见的浏览器插件如Flash、PDF Viewer等 this._plugins [ { name: Chrome PDF Viewer, filename: internal-pdf-viewer, description: Portable Document Format }, { name: Chrome PDF Viewer, filename: mhjfbmdgcfjbbpaeojofohoefgiehjai, description: Portable Document Format }, { name: Native Client, filename: internal-nacl-plugin, description: } ]; // 关键模拟length属性 this.defineProperty(length, { get: () this._plugins.length }); } // 模拟 item 方法 item(index) { return this._plugins[index] || null; } // 模拟 namedItem 方法 (通过name查找) namedItem(name) { return this._plugins.find(p p.name name) || null; } // 非常重要的细节PluginArray 是可迭代的且直接通过下标访问会调用item方法。 // 我们需要通过Proxy或重写getter来模拟这种行为。 // 这里使用Proxy是更优雅的方式 static createProxy() { const instance new MockPluginArray(); return new Proxy(instance, { get(target, prop) { // 如果是数字索引则调用item方法 const index Number(prop); if (!isNaN(index) index 0 index target.length) { return target.item(index); } // 否则返回自身的属性或方法 return Reflect.get(target, prop); } }); } }在MockNavigator的构造函数中就需要这样使用this.plugins MockPluginArray.createProxy(); this.mimeTypes MockMimeTypeArray.createProxy(); // 类似实现Canvas指纹的模拟Canvas指纹是通过调用canvas.getContext(2d)后执行一系列绘图操作最后调用toDataURL()或getImageData()计算哈希得到的。我们必须模拟整个链条。class MockCanvasRenderingContext2D { constructor() { this._actions []; // 记录所有绘图调用用于调试 } fillRect() { this._actions.push(fillRect); // 什么都不做但记录行为 } fillText(text, x, y) { this._actions.push(fillText:${text}); // 记录文本内容某些检测会使用固定文本来校验 } // 模拟 getImageData。返回一个固定值的数据对象。 getImageData(sx, sy, sw, sh) { this._actions.push(getImageData); // 返回一个模拟的ImageData对象其data数组是一个固定的、符合预期的值。 // 这个固定值需要从真实浏览器中捕获一次。 const fixedData new Uint8ClampedArray([255, 255, 255, 255, 0, 0, 0, 255, /* ... 更多固定像素值 */]); return { data: fixedData, width: sw, height: sh }; } // toDataURL 也需返回固定字符串 toDataURL() { return data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg; } }实操心得Canvas指纹的固定值必须从目标网站在你本地真实浏览器中运行一次获取。方法是在浏览器控制台于检测代码执行后、生成token前下断点或插入代码将canvas.toDataURL()的结果打印出来并记录下来作为模拟环境的返回值。不同网站、甚至不同版本的检测这个值都可能不同。3.3 环境注入与代码执行有了完整的环境模拟类下一步就是将其注入到一个隔离的JavaScript执行环境中并运行目标网站的混淆代码。在Node.js中我们使用vm模块创建沙箱const vm require(vm); const { BrowserEnv } require(./browser_env); // 导入我们写的类 function runDetectCode(obfuscatedCode) { // 1. 创建模拟环境实例 const browserEnv new BrowserEnv(); // 2. 创建沙箱上下文 const sandbox { console, // 可以选择性注入console用于调试 // 其他可能需要的基础对象... }; // 将模拟的浏览器环境注入沙箱 browserEnv.injectTo(sandbox); // 3. 创建VM脚本 const script new vm.Script(obfuscatedCode); // 4. 在沙箱中运行代码 try { script.runInNewContext(sandbox, { timeout: 5000 }); // 5. 运行后从沙箱的特定变量中获取生成的token // 通常检测代码会将结果赋值给window的某个属性或一个全局变量。 const generatedToken sandbox.window._rsaToken || sandbox._token; return generatedToken; } catch (error) { console.error(代码执行失败:, error); // 这里通常是环境补得不全被检测到了。需要根据错误信息进一步调试。 return null; } }4. 针对宝武资源官网的专项补环境与调试4.1 动态分析与断点调试理论框架搭建好后真正的战斗在于细节调试。你需要让目标代码在你模拟的环境中和在真实浏览器中“行为一致”。并排调试打开两个窗口一个是真实的浏览器开发者工具Sources面板另一个是你的Node.js调试终端或使用VS Code调试。关键断点在真实浏览器中找到生成最终token或加密参数的那几行代码通常位于一个巨大的匿名函数末尾打上断点。记录行为在断点处记录所有关键变量的值、函数调用栈、以及环境属性的访问顺序。特别关注那些Object.defineProperty定义的getter被触发的情况。对比执行在你的Node.js脚本中尝试执行到相同逻辑位置。对比两边navigator.plugins[0].name、canvas.getContext(2d).getImageData(0,0,1,1).data[0]等关键属性的值是否一致。查漏补缺当Node.js环境报错或生成的值不同时就是你的补环境有遗漏。根据错误信息或差异点回头完善对应的模拟类。4.2 常见检测点与应对策略根据对宝武资源官网的分析以下是一些需要特别注意的检测点及应对方法检测点表现形式应对策略PluginArray迭代使用for...of或Array.from遍历navigator.plugins确保MockPluginArray实现了[Symbol.iterator]方法使其可迭代。Function构造函数使用new Function(return this)()来获取全局对象在你的沙箱中确保Function构造函数返回的this指向的是你模拟的window对象。可能需要重写Function原型。原型链检查检查document.createElement(div).toString()确保MockElement的原型链正确其toStringTag为[object HTMLDivElement]。性能API时间戳使用performance.now()计算代码执行间隔模拟performance对象让now()方法返回一个合理递增的时间戳而不是固定的或Node的高精度时间。异步时序连续多个setTimeout(fn, 0)检查执行顺序和间隔模拟浏览器的事件循环微任务队列是一个复杂问题。简易方案是使用setImmediate和process.nextTick来近似模拟但更可靠的方案是引入一个简单的异步调度器。4.3 代码流反混淆与动态加载处理宝武资源的混淆代码可能包含动态加载的代码块如通过eval或创建script标签。你的补环境框架需要能处理这种情况。处理eval和new Function在注入环境时可以重写沙箱中的eval和Function使其在正确的模拟上下文中执行传入的代码字符串。sandbox.eval (code) { const script new vm.Script(code); return script.runInContext(sandbox); }; const OriginalFunction sandbox.Function; sandbox.Function function(...args) { const body args.pop(); const params args; const func new OriginalFunction(...params, body); // 绑定正确的this上下文 return func.bind(sandbox); };处理document.createElement(script)当模拟的document创建了一个script元素并被添加到DOM中时你需要拦截其src加载或textContent执行。这通常意味着你需要一个简单的“DOM操作模拟器”能够解析script内容并动态执行。5. 完整流程集成与请求实战5.1 生成Token并构造请求假设通过上述补环境我们成功在沙箱中运行了检测代码并得到了一个名为_rsaToken的变量。// 在runDetectCode函数成功执行后 const token runDetectCode(obfuscatedJsCode); if (token) { // 构造请求头或参数 const headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, // 将生成的token放入约定的位置可能是自定义Header X-Token: token, // 也可能是Cookie或Query Param具体看网站要求 // Cookie: rsa_token${token} }; const requestParams { // ... 其他业务参数 rsaToken: token }; // 使用axios、request等库发起请求 const axios require(axios); axios.get(https://www.baowuresources.com/api/data, { headers, params: requestParams }) .then(response { console.log(数据获取成功:, response.data); }) .catch(error { console.error(请求失败:, error.response?.data || error.message); }); }5.2 错误处理与重试机制环境模拟不可能100%完美网络也可能波动因此健壮的程序需要包含错误处理和重试。Token失效处理如果服务器返回403、412或特定的错误码如token invalid说明环境被识别或token过期。这时不应立即重试而应检查本次生成token的环境模拟是否有警告或异常。考虑更新环境参数如微调userAgent、screen尺寸等。重新运行检测代码生成新token。指数退避重试对于网络错误采用指数退避策略进行重试。async function fetchWithRetry(url, options, maxRetries 3) { let lastError; for (let i 0; i maxRetries; i) { try { return await axios(url, options); } catch (error) { lastError error; if (error.response error.response.status 500) { // 服务器错误可以重试 const delay Math.pow(2, i) * 1000 Math.random() * 1000; // 指数退避加随机抖动 console.log(请求失败${delay}ms后重试第${i1}次); await new Promise(resolve setTimeout(resolve, delay)); } else { // 客户端错误如400403重试通常无效直接跳出 break; } } } throw lastError; }5.3 性能优化与代码维护面向对象的补环境框架虽然清晰但初始化一整套环境可能较慢。对于高频请求需要优化。环境实例复用不要每次请求都创建全新的BrowserEnv实例。可以创建一个“环境池”初始化多个实例备用或者对无状态的模拟部分进行复用。懒加载属性对于某些复杂的模拟对象如完整的PluginArray可以在其属性第一次被访问时才进行完整初始化而不是在构造函数中全部完成。代码模块化将MockWindow、MockNavigator等拆分成独立文件。针对不同网站可以像搭积木一样组合不同的模块。例如网站A只检测navigator.userAgent网站B检测全套Canvas那么我们可以准备basic_env.js和full_env.js两种配置。版本管理目标网站的反爬策略可能会升级。你的补环境代码应该有版本记录并与获取到的混淆JS文件的哈希值或版本号关联。当检测到JS文件变化时触发告警提示需要重新分析。6. 常见问题排查与进阶技巧6.1 问题速查表现象可能原因排查步骤生成的token无效请求被拒1. 关键环境属性模拟不准确。2. 异步执行时序被识别。3. Token生成逻辑有分支未覆盖。1. 对比真实浏览器和模拟环境中的关键属性值plugins, canvas指纹等。2. 在生成token的代码前后打日志对比两边代码执行路径是否完全一致。3. 检查是否有基于Date.now()或performance.timing的动态变量你的模拟值可能太“整齐”。Node.js沙箱执行报错如xxx未定义补环境缺失了某个全局对象、构造函数或API。1. 查看错误堆栈定位到混淆代码的具体行可尝试局部反混淆。2. 在真实浏览器中查看该行代码访问了什么对象然后在你的模拟环境中补全。代码执行陷入死循环或超时1. 模拟的while/for循环条件无法退出。2. 模拟的异步回调未被触发。1. 检查模拟的Math.random、Date.now等是否返回了预期范围的值可能循环条件依赖它们。2. 检查setTimeout/setInterval的模拟实现确保回调能被正确调度和执行。内存泄漏或性能低下1. 每次请求都创建大量复杂对象。2. Proxy使用不当导致递归。1. 实施环境实例复用和属性懒加载。2. 检查Proxy的get/set陷阱避免在其中进行不必要的复杂操作或创建新对象。6.2 进阶调试技巧使用Proxy进行访问追踪在你无法确定检测代码访问了哪些属性时可以在基础对象如最初的window上包裹一个全局的Proxy记录所有的get和set操作这能给你一份完整的“检测清单”。const accessLog new Set(); const tracedWindow new Proxy(sandbox.window, { get(target, prop, receiver) { accessLog.add(GET window.${prop.toString()}); return Reflect.get(target, prop, receiver); }, set(target, prop, value, receiver) { accessLog.add(SET window.${prop.toString()} ${value}); return Reflect.set(target, prop, value, receiver); } }); sandbox.window tracedWindow; // 执行代码后打印accessLogAST辅助分析对于极度混淆的代码使用babel/parser等工具将其解析为AST然后寻找对特定属性如navigator.userAgent的访问节点可以系统性地了解需要补哪些环境。补环境框架的单元测试为你的MockNavigator、MockCanvas等类编写单元测试确保它们的行为与真实浏览器一致。这能极大提高后续维护的效率。6.3 面向对象补环境的局限性尽管面向对象的方法大大提升了可维护性但前端反爬技术也在进化。一些更高级的防护会检测对象的内存地址或内部属性即使你的模拟对象行为再像它在JS引擎内部的表示可能与真实浏览器对象不同。原生函数的toString()结果document.createElement.toString()在真实浏览器中返回function createElement() { [native code] }而你的模拟函数返回的是你的源代码。这可以通过重写函数的toString方法来返回固定字符串解决。WebDriver或自动化工具特征如navigator.webdriver属性。在模拟环境中必须将其显式设置为false或undefined。没有任何一种补环境方案是银弹。面向对象补环境是一种优秀的工程实践它让这场“猫鼠游戏”的防守方我们的代码更加健壮、易于迭代。其核心价值在于当对方更新检测点时我们能快速定位到需要修改的类和方法而不是在数千行散乱的补丁代码中大海捞针。对于宝武资源有限公司这类“伪瑞数”案例这套方法已经足够应对绝大多数检测成功获取到所需数据。