瑞数6代Cookie逆向实战:动态令牌生成算法与自动化破解

发布时间:2026/7/27 7:38:40
瑞数6代Cookie逆向实战:动态令牌生成算法与自动化破解 1. 项目概述瑞数6代Cookie的攻防博弈在Web安全与数据爬取的战场上瑞数动态安全Botgate俗称“瑞数”一直是一块难啃的骨头。从早期的3代、4代到如今主流的6代其核心防护机制——动态令牌即标题中提到的173位Cookie通常指RM4hZBv0dDon443M这类长字符串——的生成逻辑始终是逆向工程师和爬虫开发者绕不开的挑战。这个Cookie并非服务器直接下发而是由前端JavaScript执行一系列复杂的加密和混淆算法后动态生成的每次访问都可能不同直接用于验证请求的“合法性”拦截机器流量。我最近在分析一个目标站点时再次正面遭遇了瑞数6代。和以往一样简单的请求会立刻被重定向到一个包含大量混淆JS的挑战页面返回经典的“重定向次数过多”错误。这背后的逻辑正是瑞数通过动态生成的Cookie在服务端验证请求是否来自“真正的浏览器”。我们的目标就是深入这片混淆代码的丛林找到生成那串173位实际长度可能略有浮动Cookie的算法核心并实现本地化动态生成从而让我们的自动化程序能够“持证上岗”。这个过程不仅仅是找到几个加密函数那么简单。它涉及到对庞大混淆代码的静态分析、动态调试、逻辑梳理以及最终将碎片化的算法还原成可独立运行的代码。接下来我将完整复盘这次实战从环境搭建、调试定位到算法还原与代码实现分享一路踩过的坑和总结出的有效技巧。2. 核心防护机制与逆向思路拆解在动手之前我们必须先理解瑞数6代防护的基本原理和我们的逆向目标。知己知彼才能制定有效的策略。2.1 瑞数6代Cookie的作用与生命周期瑞数的动态Cookie通常以RM4hZBv0dDon443M、FSSBBIl1UgzbN7N80T等为名称其值是一长串看似随机的字符。它的核心作用是一个会话凭证和行为指纹。首次访问与挑战当客户端无论是浏览器还是我们的脚本首次向受保护的站点发起请求时服务端会检测到请求缺乏有效的动态Cookie或Cookie已过期。此时它不会返回正常的页面内容而是返回一个302重定向或者一个包含特殊JavaScript代码的HTML页面状态码可能是200但内容是挑战脚本。执行挑战脚本浏览器会自动执行这段JS。这段代码高度混淆、复杂其核心任务之一就是收集客户端的环境信息如浏览器指纹、Canvas、WebGL、字体列表等并基于这些信息和一个来自服务端的“种子”或“密钥”通过一系列算法计算生成动态Cookie。提交Cookie并验证生成的Cookie会被自动设置到请求头中浏览器随后携带这个Cookie重新发起请求或自动重定向。服务端收到带有Cookie的请求后会用相同的逻辑进行验算。如果验证通过则返回正常数据否则继续重定向挑战或直接拒绝。我们的逆向目标就是模拟步骤2在无头环境中计算出这个合法的Cookie值。2.2 逆向分析的总体策略与工具选型面对高度混淆的代码盲目阅读是不可行的。我的策略是“动静结合以动为主”。静态分析辅使用代码格式化工具如Prettier和简单的重命名工具对核心的混淆JS文件进行初步美化了解代码整体结构寻找可能的入口点或特征字符串如Cookie名RM4hZBv0dDon443M。动态调试主这是最核心的手段。我们必须让代码在受控的环境中运行起来通过调试器设置断点、观察变量、跟踪调用栈亲眼看到Cookie是如何一步步被计算出来的。工具选择Chrome DevTools或基于Chromium的Edge DevTools是首选。它们对JavaScript的调试支持最完善。在某些复杂场景下Fiddler或Charles等抓包工具用于拦截和替换JS文件也很有用。为什么不用Node.js直接运行因为瑞数的JS严重依赖浏览器环境如window、document、navigator等BOM对象甚至可能检测调试器。直接扔进Node环境大概率会报错。因此我们需要一个“浏览器环境”来执行它。基于此我选择的实战路径是使用Chrome浏览器无头模式配合Puppeteer自动加载目标页面在关键点注入调试代码或利用DevTools Protocol进行动态拦截和观察。这样既能满足浏览器环境依赖又能实现自动化调试和数据提取。注意有些文章会提到补环境即用Node.js模拟一个浏览器环境对象。这对于瑞数6代来说工作量异常庞大且极易遗漏细节导致生成失败。动态调试虽然前期繁琐但一旦找到算法核心提取出的代码更加精准可靠。3. 完整动态调试流程与定位实战理论清晰后我们进入实战环节。我将以一次真实的调试过程为例展示如何一步步定位到Cookie的生成函数。3.1 环境准备与首次请求拦截首先我们需要搭建一个能够自动控制浏览器并允许我们调试的脚本环境。const puppeteer require(puppeteer); (async () { // 1. 启动浏览器开启调试端口并禁用无头模式方便肉眼观察 const browser await puppeteer.launch({ headless: false, // 首次调试设为false看到浏览器界面 devtools: true, // 自动打开开发者工具 args: [ --disable-blink-featuresAutomationControlled, // 禁用自动化控制特征 --no-sandbox ] }); // 2. 创建一个新页面 const page await browser.newPage(); // 3. 设置请求拦截重点捕获JS文件 await page.setRequestInterception(true); page.on(request, interceptedRequest { // 可以在这里拦截特定的JS文件用于本地替换后续会用到 if (interceptedRequest.url().includes(critical_rs6.js)) { // 拦截并响应本地修改后的文件 // interceptedRequest.respond({...}); } else { interceptedRequest.continue(); } }); // 4. 监听响应找到那个返回挑战代码的响应 page.on(response, async response { const url response.url(); const status response.status(); if (status 200 || status 302) { // 检查响应头或内容判断是否是瑞数挑战页 const headers response.headers(); if (headers[content-type] headers[content-type].includes(text/html)) { const text await response.text(); if (text.includes(RM4hZBv0dDon443M) || text.includes(动态安全)) { console.log([] 发现瑞数挑战页: ${url}); // 可以将页面内容保存下来分析 } } } }); // 5. 访问目标网站 await page.goto(https://你的目标网站.com, { waitUntil: networkidle2, timeout: 30000 }); // 不要立即关闭保持浏览器打开以便调试 // await browser.close(); })();运行这段代码浏览器会打开并访问目标网站。不出意外页面会卡住或反复刷新重定向挑战。此时打开DevToolsF12切换到Network网络面板勾选Preserve log保留日志然后刷新页面。3.2 关键JS文件定位与初步分析在网络请求列表中寻找返回内容类型为application/javascript且文件较大通常几百KB甚至上MB的请求。这些很可能就是包含核心逻辑的混淆JS。文件名可能带有随机字符串。找到它点击这个JS请求在Preview预览或Response响应标签页你会看到一团压缩的、变量名都是_0x123abc格式的代码。这就是我们的主战场。保存到本地右键点击请求链接选择Copy - Copy link address。然后可以用curl或wget下载到本地或者直接在DevTools的**Sources源代码**面板中找到这个文件右键Save as...。格式化代码用编辑器如VSCode打开或者使用在线工具、Prettier插件进行格式化。格式化后代码结构会清晰一些但变量名和函数名仍然是混淆的。3.3 动态断点调试与算法入口追踪格式化后的代码可读性依然很差我们需要通过动态执行来理解逻辑。搜索特征点在格式化后的JS文件中搜索目标Cookie的名称例如RM4hZBv0dDon443M。你可能会找到它被赋值的地方比如document.cookie ...或者作为某个函数的参数。找到这行代码记下其所在的行号或函数名如_0x485f21。在DevTools中设置断点回到浏览器在DevTools的Sources面板找到我们下载或网络加载的那个JS文件。使用Ctrl F搜索刚才找到的特征代码行。在行号左侧点击设置一个断点蓝色标记。触发断点刷新页面或让Puppeteer脚本继续执行。代码执行到设置Cookie的那一行时会自动暂停。调用栈分析此时查看Call Stack调用栈面板。这里显示了执行到当前断点所经过的所有函数调用链。我们的目标是向上回溯找到最顶层的、计算Cookie值的那个函数。点击调用栈中的上一层函数查看其代码。通常计算Cookie值的函数会返回一个字符串然后传递给设置Cookie的函数。在这个函数内部你可能会看到大量的位运算^,,|,,、数组操作、以及一些常量。这很可能就是加密/哈希算法的实现部分。关键函数提取在调用栈中找到一个看起来像是“主逻辑”的函数它可能接收一些参数如时间戳、浏览器指纹等然后经过一系列计算返回结果。将这个函数及其所有依赖的函数通过右键Store as global variable存储为全局变量或在Console中直接复制其定义的方式提取出来。实操心得瑞数的代码经常会有“代码扁平化”混淆即把大量逻辑拆分成无数个小函数然后通过一个巨大的数组或字符串来调度。在调用栈中你可能会看到一个函数不断调用其他小函数。这时需要耐心地一层层跟进理解每个小函数的作用可能是简单的加法、异或也可能是某个标准算法如SHA256的一部分。我们的目标是找到最终拼接或生成那173位字符串的核心代码块。3.4 依赖环境与外部参数的捕获Cookie的生成绝非仅靠静态算法它严重依赖运行环境和服务端下发的初始数据。环境参数算法中可能会读取window、navigator、screen、document等对象的属性。在调试时注意观察生成函数内部访问了哪些浏览器属性。这些需要在我们的最终生成代码里进行模拟补环境。动态种子服务端通常会在第一次挑战响应中通过HTML的script标签、Cookie本身如一个短的__jsluid_s或某个JS变量传递一个关键的“种子”值。这个值会作为加密算法的一个输入。务必在首次请求的响应体中搜索找到这个看似随机但至关重要的字符串。全局变量在JS文件的最外层可能会定义一些全局变量或数组这些是算法运行的“配置表”或“常量表”。提取核心函数时必须把这些依赖的全局变量一并提取。如何捕获在算法函数执行前设置断点然后在Console中记录所有用到的外部变量值。或者更高效的方法是在Puppeteer脚本中在页面执行完毕后通过page.evaluate()注入一段代码将我们关心的全局变量和生成Cookie的函数结果一并导出。// 在页面加载并执行完所有JS后注入代码获取关键数据 const envData await page.evaluate(() { // 这个函数在浏览器上下文执行可以访问所有页面变量 const data {}; // 1. 捕获可能的种子值假设它存储在 window.__seed 中具体名称需调试确定 data.seed window.__seed || ; // 2. 捕获生成Cookie的函数假设我们通过调试知道函数名是 window._$xy if (typeof window._$xy function) { // 尝试调用并捕获结果注意可能需要参数 data.cookieValue window._$xy(); } // 3. 捕获一些浏览器指纹 data.userAgent navigator.userAgent; data.platform navigator.platform; // ... 其他需要的环境信息 return data; }); console.log(捕获到的环境与结果:, envData);4. 算法还原与本地化代码实现通过动态调试我们已经将核心算法函数和相关依赖“挖”了出来。接下来就是将这些碎片化的、依赖浏览器环境的代码重构成可以独立运行的Node.js模块。4.1 核心加密逻辑梳理与提取假设我们通过调试最终定位到一个名为generateRM4Cookie的核心函数。在DevTools的Console中我们可以尝试执行toString()方法查看其源码并手动将其复制出来。// 这是从混淆代码中提取并经过人工整理后的伪代码示例 function generateRM4Cookie(seed, browserFp) { var _0x12ab [/* 一个巨大的常量数组 */]; var _0x34cd function(_0x56ef, _0x78gh) {/* 一些辅助函数 */}; // 步骤1: 将种子和浏览器指纹进行某种组合 var combined _0x34cd(seed, browserFp); // 步骤2: 经过多轮自定义的变换可能包含循环、查表、位运算 for (var i 0; i 10; i) { combined _0x12ab[combined % _0x12ab.length] ^ combined; combined (combined 5) | (combined 27); // 循环左移 } // 步骤3: 可能进行Base64编码或转换为十六进制字符串 var hexStr combined.toString(16); // 步骤4: 填充或格式化到特定长度如173位 while (hexStr.length 43) { // 43*4172位再加一位实际需调试确定 hexStr 0 hexStr; } // 可能还会加上前缀或进行二次哈希 return RM4hZBv0dDon443M hexStr ; }你的任务就是整理出这样一个清晰的逻辑流程。过程中需要明确输入种子从哪里来、浏览器指纹需要模拟哪些属性。过程经历了哪些关键运算函数。将这些函数一一提取并确保它们不依赖未定义的全局变量。输出最终的字符串格式。4.2 浏览器环境模拟补环境这是将浏览器代码移植到Node.js的关键一步。我们需要创建一个对象模拟算法所依赖的浏览器全局对象。// rs6_env.js - 环境模拟模块 const createBrowserEnv () { const env { window: {}, document: {}, navigator: {}, screen: {}, location: {} }; // 1. 模拟 navigator 对象 env.navigator { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., // 使用一个常见的UA platform: Win32, language: zh-CN, languages: [zh-CN, zh], hardwareConcurrency: 8, // 注意瑞数可能会检测更隐蔽的属性如 webdriver, plugins, mimeTypes webdriver: undefined, // 必须为undefined不能是false plugins: [], mimeTypes: [] }; // 2. 模拟 screen 对象 env.screen { width: 1920, height: 1080, colorDepth: 24, pixelDepth: 24 }; // 3. 模拟 document 对象及其常用属性 env.document { documentElement: { clientWidth: 1903, clientHeight: 969 }, charset: UTF-8, characterSet: UTF-8 }; // 4. 将 window 指向自身并挂载其他对象 env.window env; env.window.navigator env.navigator; env.window.document env.document; env.window.screen env.screen; env.window.location env.location; // 5. 可能需要的其他全局函数或对象 env.window.Date Date; env.window.Math Math; env.window.parseInt parseInt; env.window.parseFloat parseFloat; // 如果算法用了 atob/btoa也需要模拟 env.window.btoa (str) Buffer.from(str, binary).toString(base64); env.window.atob (b64) Buffer.from(b64, base64).toString(binary); return env; }; module.exports createBrowserEnv;然后在运行核心算法前我们需要将这段代码“注入”到一个模拟的全局作用域中。可以使用vm2这个安全的沙盒模块或者更简单地将算法函数改写成接受一个window对象作为参数的形式。// generate_cookie.js - 主生成逻辑 const createBrowserEnv require(./rs6_env); // 这是你从混淆代码中提取并清理后的核心算法函数 function extractedCoreAlgorithm(seed, fpData) { // ... 大量的位运算和逻辑 // 注意原函数内部可能直接使用了 window、navigator 等全局变量 // 我们需要将其修改为使用传入的 _window 对象 // 例如将 navigator.userAgent 改为 _window.navigator.userAgent } function generateCookie(seed) { // 1. 创建模拟环境 const _window createBrowserEnv(); // 2. 准备浏览器指纹数据可以从模拟环境中取也可以自定义 const fpData { ua: _window.navigator.userAgent, screen: ${_window.screen.width}x${_window.screen.height}, // ... 其他算法需要的指纹信息 }; // 3. 将模拟的全局对象作为算法函数的上下文 // 方法A使用call/apply绑定this如果算法用到了this // const cookieValue extractedCoreAlgorithm.call(_window, seed, fpData); // 方法B将_window作为参数传入更清晰 // 首先需要修改 extractedCoreAlgorithm 函数使其内部通过参数使用 _window const cookieValue extractedCoreAlgorithm(_window, seed, fpData); return cookieValue; } // 使用示例 const seed 从第一次请求中提取的种子值; const dynamicCookie generateCookie(seed); console.log(生成的Cookie:, dynamicCookie);4.3 种子值的获取与请求流程集成动态Cookie的生成离不开服务端下发的种子。这个种子通常隐藏在第一次挑战响应的HTML或JS变量中。提取种子用你的爬虫或Puppeteer发起首次请求分析响应内容。可能在script标签的某个变量里var __jsl_clearance ...或var seed abcdef123456;可能在某个Cookie里第一次响应可能会设置一个像__jsluid_sxxx的Cookie这个xxx可能就是种子的一部分。可能需要执行一段简单的JS代码才能得到。你可以用page.evaluate()执行那段JS来获取值。集成到完整请求链const axios require(axios); const generateCookie require(./generate_cookie); async function getProtectedData(url) { // 第一步发起初始请求获取种子和挑战JS const firstResp await axios.get(url, { headers: { User-Agent: 你的UA }, maxRedirects: 0, // 禁止自动重定向手动处理 validateStatus: null // 接受所有状态码 }); const seed extractSeedFromResponse(firstResp); // 实现这个提取函数 const challengeJS extractJSUrl(firstResp); // 获取挑战JS的URL如果需要 // 第二步动态生成Cookie const dynamicCookieValue generateCookie(seed); // 第三步携带生成的Cookie重新请求 const finalResp await axios.get(url, { headers: { User-Agent: 你的UA, Cookie: RM4hZBv0dDon443M${dynamicCookieValue}; 其他必要的Cookie } }); return finalResp.data; // 应该得到正常的数据了 }5. 调试与生成过程中的常见问题与解决方案即使按照流程操作你也一定会遇到各种问题。下面是我总结的一些典型坑点和解决思路。5.1 环境检测与反调试对抗瑞数的JS会尝试检测是否在真实浏览器中运行以及是否被调试。问题代码在浏览器中运行正常但提取到Node.js中生成的结果不对。排查检查环境属性在浏览器调试器中在算法执行时记录所有访问的navigator、window、document属性。对比你的模拟环境是否缺失或值不对。特别注意navigator.webdriver、navigator.plugins.length、document.documentElement.clientWidth/Height等。检查函数toString()结果有些检测会调用Function.prototype.toString()并检查内容。确保你的模拟函数返回的字符串格式和原浏览器环境一致。检测debugger语句混淆代码中可能包含debugger;语句或通过console检测。在Puppeteer中可以通过CDP协议禁用调试await page.evaluateOnNewDocument(() {window.console {};});但需谨慎可能影响代码运行。解决方案尽可能完整地模拟环境。使用Object.defineProperty来定义属性使其不可枚举、不可配置更像真实浏览器。Object.defineProperty(env.navigator, webdriver, { get: () undefined, configurable: false, enumerable: false });5.2 算法依赖未捕获的全局变量或函数问题运行提取的算法时报错xxx is not defined。排查仔细检查错误信息。这个xxx可能是一个你遗漏的全局变量如_0x12ab这个大数组也可能是一个浏览器内置但Node.js没有的函数如btoa、atob、escape等。解决方案回溯调试时的调用栈确保提取了所有被引用的上层变量和函数。在浏览器Console中在算法执行前输入Object.keys(window).filter(k k.includes(_0x))可以列出所有混淆的全局变量检查是否有遗漏。对于浏览器特有的函数在模拟环境中实现它们。5.3 生成的Cookie长度或格式不正确问题能生成一个字符串但长度不是173位左右或者服务端不认可。排查长度确认最终生成字符串的精确长度要求。可能是43个字符的十六进制172位加上等号或其他分隔符。在调试时打印出最终结果的length和原始值进行比对。编码确认最后一步是toString(16)十六进制还是toString(36)36进制或者是经过btoaBase64编码。仔细跟踪最后几步的转换。种子错误确认你提取的种子值是完全正确的并且没有经过额外的URL解码或处理。时间戳或随机数算法中可能混入了Date.now()或Math.random()。你需要确保在Node.js中生成的时间戳和随机数序列与浏览器执行时的逻辑一致。有时需要固定随机数种子或者将时间戳对齐到某个粒度如秒级。5.4 请求依然被拦截或重定向问题携带生成的Cookie请求仍然返回挑战页面。排查Cookie作用域确保Cookie的domain和path设置正确。在浏览器中观察成功请求的Cookie完整信息。请求头完整性除了Cookie检查其他请求头是否与浏览器一致如User-Agent、Accept、Accept-Language、Referer等。瑞数可能会校验User-Agent与生成Cookie时使用的指纹是否匹配。Cookie时效性动态Cookie可能有极短的过期时间如1-2秒。确保生成Cookie后立即发起请求避免延迟。多级Cookie瑞数有时会设置两个动态Cookie如一个RM4一个FSSB。你需要同时生成并发送这两个。算法版本/参数变化不同网站或同一网站不同时间部署的瑞数版本可能有细微差异。你逆向的算法可能只适用于特定时刻。需要重新抓取最新的JS进行分析。整个逆向过程就像一场精细的考古工作需要极大的耐心和细致的观察。没有一个通用的破解代码每个目标站点都需要独立的分析和适配。成功的关键在于动态调试的熟练度以及将碎片化逻辑还原成完整、独立算法的能力。最后请务必在符合法律法规和网站robots.txt协议的前提下进行技术研究。