逆向瑞数加密:海关数据平台动态JS补环境实战解析

📅 2026/7/27 5:12:54
逆向瑞数加密:海关数据平台动态JS补环境实战解析
1. 项目概述当海关数据遇上瑞数加密最近在分析一个公开的海关公示平台时遇到了一个老朋友——瑞数动态安全技术。这几乎是国内中大型政企、金融网站标配的防护方案尤其是在数据敏感、访问控制严格的场景下。海关数据因其商业价值和监管属性采用瑞数加密来保护查询接口、防止数据被批量爬取是再合理不过的操作。这个实战案例对于想深入理解现代Web前端反爬机制特别是动态混淆和代码执行流保护的朋友来说非常有嚼头。简单来说我们的目标是通过技术手段模拟浏览器行为成功调用该平台的公示数据查询接口。难点在于瑞数会生成一个动态变化的、名为RM4hZBv0dDon443M或类似格式的Cookie通常称为RM4或瑞数Cookie以及请求体中携带的动态令牌。服务器会校验这些值如果缺失或无效直接返回412状态码或者一段包含动态JS的HTML让你无法获取真实数据。这不像简单的参数加密你无法通过搜索某个固定关键词找到加密函数因为核心的代码逻辑和生成算法每次请求都可能被动态混淆和变换。所以这次“逆向”的核心不是去硬啃那坨面目全非的混淆代码而是理解瑞数的防护逻辑并找到一种可靠的方式让我们的程序能像真实浏览器一样计算出正确的动态凭证。下面我就把这次实战的完整思路、关键步骤和踩过的坑系统地梳理一遍。2. 瑞数加密核心原理与应对策略拆解在动手之前我们必须先搞清楚对手是怎么工作的。盲目硬刚混淆代码效率极低且容易迷失方向。2.1 瑞数动态安全的基本逻辑瑞数的核心思想是“动态混淆”和“行为验证”。它不是一个简单的加密算法而是一套在浏览器端运行的、不断变化的防御体系。首次访问与种子下发当你第一次用浏览器访问受保护的页面时服务器返回的HTML中会包含一段高度混淆的JavaScript代码我们常说的script标签内一大坨密文。这段代码的作用是初始化一个动态的执行环境并生成一个“种子”。这个种子可能来源于浏览器指纹如User-Agent、Canvas、WebGL等、当前时间戳、甚至是鼠标移动的初始轨迹。代码自执行与环境检测这段JS会在浏览器中自执行。它的一个重要任务是检测当前环境是否为真实的浏览器。它会检查一系列navigator、window、document对象的属性和方法检查是否有常见的自动化工具如Puppeteer、Selenium的痕迹或者是否存在window.outerWidth与window.innerWidth不符等异常。动态凭证生成通过一系列复杂的、动态生成的算法结合“种子”和检测到的环境信息计算出两个关键值一个是设置在Cookie里的RM4hZBv0dDon443M名称可能变体另一个是作为请求参数或请求体一部分的动态令牌可能叫v、token或别的名字。这两个值是后续请求的“通行证”。代码流混淆与反调试生成这些凭证的代码逻辑被深度混淆控制流扁平化、字符串加密、函数名随机化都是基本操作。更重要的是它内置了强大的反调试机制比如检测DevTools是否打开、在关键循环中插入debugger语句、或者通过Proxy/defineProperty劫持关键API一旦发现调试行为可能导致计算逻辑改变或直接进入死循环。2.2 我们的逆向策略选择面对这样的防御通常有几种思路纯算法还原Hard模式手动或借助工具去解混淆那一大坨JS理清其算法逻辑然后用Python/Node.js等语言重新实现。这对于最新版本的瑞数来说难度极高耗时极长且对方一次小更新就可能让你的努力白费。不推荐作为首选。补环境主流且稳健既然瑞数依赖浏览器环境来执行代码并生成凭证那我们就用Node.js或PyExecJS等模拟出一个足够真实的浏览器环境。把网站返回的那段动态JS代码抓下来在我们模拟的环境里执行让它自然生成出正确的Cookie和令牌。这是我们本次实战采用的核心方法。自动化工具直通省心但受限使用Puppeteer、Playwright或Selenium等无头浏览器直接操作。这种方式最接近真人能绕过绝大多数环境检测。缺点是资源消耗大、速度慢不适合高并发爬取且可能被更高级的行为指纹检测识别。寻找接口规律或替代方案巧劲有些网站的瑞数防护可能只针对主查询页面而数据接口本身校验不严或者存在未受保护的移动端接口、历史版本接口等。这需要大量的探测和尝试。综合评估对于这个海关公示平台我选择了“补环境”作为主攻方向。它兼顾了效率与稳定性一旦环境补全后续请求可以快速生成凭证适合需要频繁查询的场景。3. 关键步骤实操从首次请求到令牌生成整个流程可以概括为获取动态JS - 构建模拟环境 - 执行JS获取凭证 - 携带凭证发起业务请求。下面我们一步步拆解。3.1 第一步捕获动态JavaScript种子代码这是所有工作的起点。你需要用任何能显示原始网络请求的工具如浏览器F12开发者工具的Network面板或Fiddler、Charles等抓包工具清空缓存后首次访问目标海关公示平台的主页或查询页面。关键观察点响应状态码很可能是412Precondition Failed或者200但内容不是正常网页。响应内容查看Response你会看到一段script标签包裹着的、看起来像乱码的JavaScript代码。它的开头可能类似var _0x...或者是一长串字符数组定义。把这整段JS代码完整地复制保存下来我们称之为seed.js或动态代码片段。关键Cookie在本次请求的响应头Set-Cookie里你应该会看到一个以RM4开头的Cookie被设置下来但请注意这个首次设置的Cookie值通常不是最终可用的那个它只是一个引子。注意务必在无痕模式或清空浏览器所有该站点Cookie后操作确保捕获的是最原始的“首次挑战”响应。有时网站会有缓存如果直接返回了正常页面可以尝试在URL后加随机参数或等待一段时间再试。3.2 第二步构建Node.js模拟执行环境我们不能在浏览器里执行这段代码因为我们需要程序化地获取结果。Node.js是我们的沙箱。但直接扔给Node执行肯定会报错因为这段代码预设在浏览器全局对象window、document下运行。我们需要“补环境”即创建一个对象拥有足够多的属性和方法让瑞数的检测代码认为它正在真实的浏览器中运行。一个基础的补环境框架如下使用Node.js// vm是Node.js的虚拟机模块可以创建一个隔离的上下文来运行代码 const vm require(vm); const axios require(axios); // 用于后续网络请求 // 1. 创建一个模拟的window对象作为全局对象 let 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, appVersion: 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, // 补充其他navigator常见属性 webdriver: undefined, // 关键必须为undefined否则会被识别为自动化工具 plugins: [], mimeTypes: [], }, document: { createElement: function(tag) { return { tagName: tag.toUpperCase(), style: {} }; }, getElementsByTagName: function() { return []; }, documentElement: { clientWidth: 1920, clientHeight: 1080, }, // 根据目标JS可能调用的方法进行补充 addEventListener: function() {}, removeEventListener: function() {}, cookie: , // 用于读写Cookie // 非常重要瑞数常会取document的一些特性值 documentMode: undefined, compatMode: CSS1Compat, }, location: { href: https://目标海关网站域名.com, protocol: https:, hostname: 目标海关网站域名.com, }, screen: { width: 1920, height: 1080, availWidth: 1920, availHeight: 1040, colorDepth: 24, pixelDepth: 24, }, // 其他可能被检测的全局对象 PerformanceObserver: undefined, WebGLRenderingContext: undefined, // 函数 setTimeout: setTimeout, setInterval: setInterval, clearTimeout: clearTimeout, clearInterval: clearInterval, // 将window自身指向自己 window: this, }; // 2. 将window对象设置为全局对象的原型链上游 let sandbox Object.assign({}, global, window); sandbox.window sandbox; // 确保window指向沙箱自身 sandbox.self sandbox; // 确保self也指向沙箱自身 // 3. 创建VM上下文 const ctx vm.createContext(sandbox); // 4. 读取之前保存的动态JS代码 const dynamicJsCode ...; // 这里粘贴你捕获的那一大段seed.js代码 try { // 5. 在模拟环境中执行这段动态代码 vm.runInContext(dynamicJsCode, ctx); // 6. 执行后关键的输出可能挂在window的某个属性上或者直接修改了document.cookie // 我们需要从环境中提取出生成的Cookie和Token let generatedCookie ctx.document.cookie; // 如果代码通过document.cookie设置 // 或者有时会挂在 window._xxx 这样的变量里需要你根据代码行为观察 // let token ctx._token; console.log(生成的Cookie:, generatedCookie); // console.log(生成的Token:, token); } catch (error) { console.error(执行动态JS失败:, error); // 这里通常会报错因为我们的环境补得还不够全 }第一次运行这个脚本百分之百会报错。错误信息就是我们的“指路明灯”。3.3 第三步根据报错信息迭代补全环境瑞数的代码会尝试访问大量浏览器特有的对象、属性或方法。我们的模拟环境缺什么它就报什么错。逆向的过程就是一个不断根据报错信息“打补丁”的过程。常见的需要补充的对象和方法包括Canvas相关HTMLCanvasElement,CanvasRenderingContext2D(用于指纹生成)。WebGL相关WebGLRenderingContext,WebGLBuffer等用于更高级的指纹生成。DOM操作document.getElementById,document.querySelector,Element.prototype.addEventListener等。特殊对象Notification,localStorage,sessionStorage,indexedDB。函数劫持瑞数可能会用Object.defineProperty或Proxy来监听对document.cookie的访问。我们需要在模拟环境里也实现类似的劫持逻辑以便在代码试图设置或读取Cookie时捕获到值。这是一个迭代过程运行脚本 - 得到报错信息例如ReferenceError: HTMLCanvasElement is not defined。在模拟环境window对象上定义这个缺失的对象或函数。对于复杂对象如Canvas可以定义一个空构造函数或者从jsdom这类库中引入更真实的模拟。重新运行脚本处理下一个错误。循环往复直到动态JS代码能够顺利执行完毕且不再抛出阻碍执行的错误。实操心得不必追求100%还原浏览器所有API只需补全到目标JS代码能跑通并输出我们需要的凭证即可。有时一些不影响核心逻辑的API调用我们可以直接返回一个固定值或空函数。使用jsdom库可以极大地简化DOM和BOM环境的模拟但它比较重。对于瑞数有时需要更精细的控制手动补环境反而更灵活。3.4 第四步提取动态凭证并发起业务请求当动态JS成功执行后我们需要找到它生成的成果。提取Cookie (RM4hZBv0dDon443M)最常见的方式是代码会直接赋值给document.cookie。我们在补环境时可以劫持document.cookie的setter或者在执行后直接读取sandbox.document.cookie。提取出来的是一整个Cookie字符串你需要从中解析出RM4hZBv0dDon443Mxxxxxx;的部分。提取动态令牌这个令牌可能作为某个全局变量的值比如window._v、window.token。也可能是在代码执行过程中通过一个函数调用返回的。你需要仔细分析代码片段末尾的行为或者通过“打日志”的方式在模拟环境中重写console.log来输出中间变量。还有一种可能是令牌被直接拼接在了下一次请求的URL或请求体模板中。组装有效请求将解析出的RM4Cookie放入请求头Cookie: ...中。将提取出的动态令牌按照观察到的规律放入请求参数如?vxxx或请求体如{“token”: “xxx”}中。使用axios、request等库携带正确的User-Agent需与模拟环境中的一致和其他必要头信息向目标数据接口发起GET或POST请求。如果一切顺利你将收到包含真实海关公示数据的JSON响应而不是412错误或那段动态JS代码。4. 深度补环境技巧与反反调试实战补环境不是简单的定义空对象瑞数有诸多反制措施需要我们见招拆招。4.1 处理环境一致性检测瑞数会检查环境是否自洽。例如window.navigator与navigator是否指向同一对象。window.document与document是否指向同一对象。window.top和window.self在非iframe情况下应等于window。window.location和document.location应一致。我们的模拟环境必须保证这些引用关系的正确性。4.2 实现关键API的劫持与日志为了理解代码执行流和获取关键值劫持API非常有效。// 示例劫持document.cookie记录所有设置和读取操作 let cookieJar ; Object.defineProperty(sandbox.document, cookie, { get: function() { console.log([GET Cookie], cookieJar); // 瑞数代码可能会读取cookie我们需要返回当前存储的值 return cookieJar; }, set: function(value) { console.log([SET Cookie], value); // 当设置RM4开头的Cookie时特别关注 if (value.includes(RM4)) { console.log(!!! 捕获到关键RM4 Cookie: , value); } // 简单模拟追加或覆盖实际应做解析 cookieJar value; return true; } });同样可以劫持window.localStorage.setItem、window.sessionStorage等观察代码行为。4.3 绕过反调试陷阱动态JS里可能包含debugger;语句或基于时间差的反调试。在Node.js的vm环境中debugger;语句不会触发。但如果是基于Date.now()或performance.now()的计算循环我们需要确保这些时间API的行为是合理的有时可能需要轻微修改其返回值来绕过无限循环。一个常见技巧是在将动态JS代码放入VM执行前可以对其进行简单的字符串处理移除或注释掉明显的debugger;语句注意不要破坏代码逻辑。4.4 使用现成工具与模块加速手动补全所有环境非常繁琐。社区有一些优秀的工具可以帮助我们jsdom: 提供一个完整的、在Node.js中运行的Web标准环境。对于需要大量DOM操作的站点直接用jsdom生成一个window对象作为起点可以省去大量基础工作。const { JSDOM } require(jsdom); const dom new JSDOM(, { url: https://目标网站.com, pretendToBeVisual: true, // 模拟视觉相关API resources: usable, runScripts: dangerously // 允许执行脚本 }); let window dom.window; // 然后在此基础上继续补充jsdom可能缺失或需要定制的属性 delete window.navigator.webdriver; // 确保删除webdriver标志puppeteer-extra-plugin-stealth: 如果你最终选择无头浏览器方案这个插件能帮你隐藏大量的自动化特征对抗指纹检测。一些针对瑞数的开源项目GitHub上存在一些专门对抗瑞数vmp代码虚拟化或提供补环境思路的项目可以参考其实现但需注意时效性和法律风险。5. 常见问题排查与稳定性优化在实际操作中你肯定会遇到各种问题。这里记录一些典型的排查思路。5.1 动态JS执行成功但凭证无效或请求仍返回412这是最令人头疼的情况。可能的原因和排查方向问题现象可能原因排查步骤凭证生成但请求被拒1.环境指纹不唯一/不真实模拟环境中的Canvas、WebGL指纹过于简单或固定被服务器识别为同一机器批量请求。2.时间戳或随机数因子不一致瑞数算法可能融合了请求发起的时间戳精确到毫秒、鼠标移动事件等。我们的模拟环境缺少这些动态因子。3.Cookie与Token不匹配可能两者存在对应关系或与某个会话ID绑定单独替换无效。4.请求头不完整缺少Origin、Referer、Accept-Language等关键头或User-Agent与生成凭证时的环境不一致。1. 检查Canvas.toDataURL()的输出是否每次相同。尝试引入轻微随机性。2. 在生成凭证的函数中插入日志查看其输入的参数是否包含Date.now()、Math.random()等确保我们的环境提供了这些值。3. 同时捕获Cookie和Token确保在同一个“会话”中使用。检查网络请求看是否还有一个固定的会话ID如SESSIONID需要维持。4. 用浏览器正常访问一次完整记录下所有请求头与你的程序请求头进行逐一对比。凭证有时有效有时无效1.凭证过期瑞数生成的Cookie和Token可能有很短的有效期如几分钟。2.IP或会话频率限制即使凭证正确服务器端对同一IP或会话的请求频率做了限制。1. 记录凭证生成时间并在下次请求前检查是否超时。超时则需要重新执行一次动态JS获取新凭证。2. 增加请求间隔模拟人类操作节奏。考虑使用代理IP池。5.2 模拟环境执行时卡死或无响应原因大概率触发了瑞数的反调试无限循环。代码中可能包含基于debugger或时间检测的死循环。解决在运行vm.runInContext时设置超时。try { vm.runInContext(dynamicJsCode, ctx, { timeout: 5000 }); // 设置5秒超时 } catch (e) { if (e.message.includes(timeout)) { console.log(执行超时可能陷入反调试循环); } }对动态JS代码进行预处理搜索并替换或删除debugger;语句。更激进的方法是尝试定位反调试循环的代码块通过正则表达式或AST分析将其移除或修改条件判断。这需要一定的代码分析能力。5.3 如何维持会话与处理Token更新对于需要连续查询多个页面的场景不能每次请求都重新执行一遍动态JS效率太低。你需要弄清楚RM4Cookie的有效期通过观察浏览器看看这个Cookie的Expires或Max-Age是多久。在有效期内可以重复使用。动态令牌是否每次更新有些网站的令牌是一次性的每次数据请求都需要新的有些则可以复用几次。你需要测试。会话关联通常首次通过动态JS挑战后服务器会建立一个会话并可能设置一个普通的会话Cookie如JSESSIONID。后续请求需要同时携带这个会话Cookie和RM4Cookie。最佳实践编写一个TokenManager类它负责管理整个生命周期首次获取、定时刷新在过期前重新执行JS、失效重试。维护必要的会话Cookie。为每个业务请求提供最新的、有效的请求头包含Cookie和参数。6. 进阶自动化与工程化思考当核心逆向流程跑通后可以考虑如何让它更健壮、更易用。6.1 将补环境代码封装成独立服务你可以将Node.js补环境执行动态JS的逻辑封装成一个REST API服务或一个Python可调用的模块。这样其他爬虫程序可能是Python写的只需要调用这个服务传入动态JS代码就能获得凭证无需每个爬虫都维护一套复杂的Node.js环境。6.2 动态代码的缓存与更新目标网站的动态JS种子代码可能会不定期更新虽然频率不高。你的程序需要能检测到这种更新。一个简单的方法是在每次执行前计算一下抓取到的JS代码的哈希值如MD5与上一次成功的哈希值对比。如果不同则更新本地的缓存文件并可能需要调整补环境的代码如果API变化了。6.3 错误监控与降级策略在生产环境中必须有完善的错误处理。监控记录每次获取凭证的成功率、耗时。降级如果补环境服务连续失败可以有一个降级策略比如切换到无头浏览器模式Puppeteer作为备用方案虽然慢但更可靠。告警当失败率达到阈值时发送告警通知及时人工介入排查。6.4 法律与道德边界再强调海关公示数据是公开信息但大规模、高频次的爬取可能对对方服务器造成压力涉嫌违反网站的robots.txt协议或服务条款甚至可能触碰法律法规关于计算机系统安全的红线。务必控制速率在请求间添加合理延迟如3-5秒以上。尊重robots.txt检查目标网站是否有禁止爬虫的规则。明确用途确保数据用于合法的分析、研究或商业情报而非非法活动。咨询法律意见对于商业项目在启动前进行法律风险评估是必要的。逆向瑞数加密是一个不断博弈的过程它考验的不仅是技术深度更是耐心和系统性思维。从捕获代码、构建环境、迭代调试到最终稳定运行每一步都需要细致的观察和逻辑推理。这个海关平台的案例提供了一个非常典型的战场希望这次详细的复盘能为你下次遇到类似的高强度前端防护时提供一套清晰可循的方法论和实战工具箱。记住思路永远比硬刚代码更重要。