逆向瑞数6动态防护:药品查询平台sign参数生成与Node.js实现
1. 项目概述与核心挑战
最近在分析一个药品信息查询平台时,遇到了一个典型的“瑞数6”动态防护。这个平台的数据接口,每次请求都带有一个名为 sign 的参数,这个参数值每次都在变化,并且看起来毫无规律。如果你直接用浏览器访问,页面正常加载,数据也能出来;但一旦你试图用脚本或者抓包工具去模拟请求,立刻就会返回一堆乱码或者直接403。这就是瑞数动态安全技术(俗称“瑞数6”)的典型特征——它通过动态混淆的JavaScript代码,在客户端生成一个用于验证的令牌,服务端只有拿到正确的令牌才会放行。这个 sign 参数,就是本次逆向分析的核心目标。
对于从事数据采集、接口测试或者安全研究的同行来说,瑞数6是一个绕不开的“硬骨头”。它不像传统的验证码或者简单的Token,其核心逻辑被高度混淆和动态化,直接静态分析JS文件如同大海捞针。本次分析的目标,就是彻底拆解这个药品查询接口的 sign 生成逻辑,并实现一个稳定、可复用的生成方案。整个过程不仅涉及JavaScript逆向,还涉及到浏览器环境模拟、代码动态调试以及逻辑还原,是一次非常综合的实战演练。无论你是想学习瑞数6的对抗思路,还是急需解决某个具体网站的采集难题,相信这篇详细的记录都能给你提供清晰的路径和实用的技巧。
2. 逆向环境搭建与初步探测
2.1 工具选型与配置
工欲善其事,必先利其器。面对瑞数6,选择合适的工具链是成功的第一步。我的核心工具组合是 Chrome DevTools + Node.js + 一个轻量级HTTP拦截代理 。
- Chrome DevTools :这是主战场。务必使用无痕模式,并禁用缓存(Network面板勾选“Disable cache”),避免旧JS文件干扰。在Sources面板中,学会使用“Pretty-print”格式化混淆代码,这是阅读天书的第一步。
- Node.js :用于最终将逆向出来的JavaScript逻辑独立运行。我们需要一个能模拟浏览器部分环境(如
window,document对象)但不启动完整图形界面的环境。我选择puppeteer或playwright的核心CDP协议能力,或者更轻量的jsdom来补环境。 - HTTP拦截代理 :推荐使用
mitmproxy或Fiddler。它们不仅能抓包,更重要的是可以断点修改请求/响应。在初步探测阶段,用于确认哪些请求被瑞数保护(观察是否有Set-Cookie包含类似__jsluid_s,__jsl_clearance_s的字段),以及sign参数出现在哪个请求里。
注意 :不要一上来就用自动化脚本狂发请求,这很容易触发IP频率限制甚至封禁。初期所有操作尽量在浏览器内手动进行,摸清规律后再上脚本。
2.2 请求流程分析与关键点定位
首先,在浏览器中正常访问一次药品查询页面,例如搜索“阿司匹林”。打开DevTools的Network面板,仔细查看请求瀑布流。
- 首次请求 :通常第一个HTML文档请求会返回一个状态码为
200但内容是一段高度混淆的JavaScript代码的响应。这段代码就是瑞数的“挑战”代码,它包含了生成第一个验证令牌(jsluid和jslclearance)的逻辑。 - Cookie设置 :执行完上述JS后,浏览器会自动重发请求,并携带新生成的Cookie(
__jsluid_s和__jsl_clearance_s)。服务端验证通过后,才会返回真正的页面HTML。 - 数据接口请求 :页面加载后,当你进行查询操作时,会发起XHR或Fetch请求到数据接口。 此时,
sign参数出现了 。它通常作为查询参数(如?keyword=阿司匹林&sign=xxxxxx)或请求体的一部分。关键是要找到生成这个请求的JavaScript源。 - 定位入口 :在Network面板中找到携带
sign的接口请求,右键选择“Copy -> Copy as cURL”或“Copy -> Copy as Node.js fetch”。然后,在Sources面板的“Search”功能中,搜索sign这个关键词,或者搜索该接口的URL路径。通常能定位到一处或几处相关的JavaScript文件。这些文件往往也被混淆过,文件名可能是一串无意义的哈希值。
通过以上步骤,我们确定了几个关键点:瑞数6的防护是分阶段的(先过Cookie关,再过业务接口关); sign 的生成逻辑藏在某个被请求的JS文件中;这个JS文件是动态的,每次可能不同,但核心算法可能被抽取在某个固定模块中。
3. 核心JS逻辑逆向与动态调试
3.1 代码格式化与关键函数追踪
找到疑似生成 sign 的JS文件后,第一件事就是点击左下角的 {} 按钮进行代码美化。面对混淆后的代码,不要试图从头到尾理解。我们的策略是“顺藤摸瓜”。
- 搜索关键字符串 :在美化后的代码中,搜索
sign、encrypt、encode、param等关键词。很可能你会发现类似sign: s或params.sign = c这样的赋值语句。 - 设置断点 :在找到的赋值语句所在行设置行断点。然后,在网页上再次执行一次查询操作。浏览器会暂停在断点处。
- 调用栈分析 :这是最关键的一步!当断点触发时,立即查看右侧的“Call Stack”(调用栈)。调用栈显示了当前函数是被谁一层层调用的。从栈顶往下看,找到第一个不属于大型库(如jQuery、Vue)的、文件名看起来是项目本身的函数,点击跳转进去。
- 回溯与定位 :通过反复使用调用栈回溯,我们最终的目标是找到那个最原始的、接收原始参数(如查询关键词、时间戳等)并计算出
sign值的函数。我们把这个函数称为generateSign或calcSign。
3.2 环境依赖分析与补环境策略
在动态调试时,你可能会发现 generateSign 函数内部依赖了一些浏览器特有的对象或API,例如 window.atob , document.createElement , navigator.userAgent ,甚至是 WebAssembly 模块。这就是瑞数增加逆向难度的常用手段——将核心逻辑与浏览器环境深度绑定。
这时,需要记录下所有依赖的外部对象和函数。例如:
// 伪代码示例
function generateSign(params) {
var a = window.btoa(params.keyword);
var b = new Uint8Array(someArray);
var c = window.crypto.subtle.digest('SHA-256', b); // 异步API
// ... 更多逻辑
}
对于 btoa 、 Uint8Array 这类标准API,Node.js 环境或 jsdom 通常原生支持或容易模拟。但对于 window.crypto.subtle 这类较新或浏览器特有的API,就需要手动“补环境”。
补环境的核心思路 :在Node.js中,创建一个模拟的 window 和 document 对象,将缺失的属性或函数按需实现。对于复杂的加密操作,有时瑞数会将自己的算法打包在一个立即执行的函数表达式里,我们只需要把这个函数整体提取出来,并在一个模拟了必要环境(如 window 、 document 对象存在)的上下文中执行它。
实操心得 :不必追求100%完美模拟整个浏览器环境。我们的目标是让
generateSign函数能运行并输出正确结果。因此,采用“缺啥补啥”的策略最高效。用typeof window.xxx === 'undefined'来判断是否需要补,补的时候尽量用简单的实现,只要不报错且不影响核心计算逻辑即可。例如,document.createElement可以补一个返回空对象{}的函数。
3.3 算法还原与参数提取
在能够单步调试 generateSign 函数后,下一步就是厘清它的算法。
- 输入参数 :观察函数接收哪些参数。通常是业务参数(如
{ keyword: “阿司匹林”, page: 1 })和一个可能固定的salt(盐值)或secret(密钥)。这个secret可能硬编码在JS中,也可能从之前某个接口响应中获取。 - 处理流程 :
- 参数排序 :常见做法是将所有参数按键名ASCII码升序排序,拼接成
key1=value1&key2=value2的字符串。 - 字符串拼接 :将排序后的参数字符串,加上一个
secret(或时间戳、随机数等),组合成一个新的字符串。 - 哈希/加密 :对这个新字符串进行某种哈希运算(如MD5、SHA-1、SHA-256)或对称加密(如AES)。瑞数6比较喜欢用
SHA-256或自定义的位运算。通过调试,观察中间变量值,可以确定具体算法。例如,调试时看到crypto.subtle.digest(‘SHA-256’, data)这样的调用,就能确定是SHA-256。 - 编码输出 :将哈希/加密后的二进制结果,进行Base64或Hex编码,最终生成
sign字符串。
- 参数排序 :常见做法是将所有参数按键名ASCII码升序排序,拼接成
- 验证算法 :在浏览器调试器中,手动修改输入参数,执行
generateSign函数,将输出的sign与真实网络请求中的sign进行对比。一致,则算法还原成功。
4. 独立签名生成器的实现
4.1 Node.js 环境下的代码移植
算法搞清楚后,就要把它从浏览器环境“移植”到Node.js中,形成一个独立的签名生成模块。
- 提取核心函数 :将调试定位到的
generateSign函数及其所有直接依赖的内部函数,从混淆的JS文件中完整地提取出来。可以使用DevTools中的“Copy function definition”功能,但要注意它可能不会复制闭包内的依赖函数,需要手动一并提取。 - 创建补环境脚本 :新建一个Node.js文件(如
sign_generator.js)。开头部分,创建模拟的全局对象。// 补环境 if (typeof global !== 'undefined') { global.window = global; global.document = { createElement: function() { return {}; }, // ... 其他可能用到的属性 }; // 补全特定的加密API,例如使用Node.js原生crypto模块模拟 const crypto = require('crypto'); global.window.crypto = { subtle: { digest: function(algorithm, data) { // 将算法名映射到Node.js crypto const hash = crypto.createHash(algorithm.replace('-', '').toLowerCase()); hash.update(data); return Promise.resolve(hash.digest()); } } }; global.btoa = (str) => Buffer.from(str).toString('base64'); global.atob = (b64Encoded) => Buffer.from(b64Encoded, 'base64').toString(); } - 注入算法 :将提取出的核心函数代码(通常是几个相互嵌套或调用的函数)粘贴到补环境代码的后面。
- 封装调用接口 :最后,导出一个简洁的调用函数。
// 假设核心函数叫 `window.generateSign` module.exports.generateSign = function(queryParams, secretKey) { // 这里可能需要对参数做一些预处理,以匹配浏览器中的调用方式 return window.generateSign(queryParams, secretKey); };
4.2 稳定性测试与异常处理
生成器写好后,不能立刻投入生产环境,需要进行充分的测试。
- 多参数测试 :使用不同的查询关键词、页码、时间戳等进行测试,生成
sign,并与浏览器在同一时刻发起的请求中的sign进行比对,确保完全一致。 - 长时间运行测试 :连续运行生成器几个小时,模拟批量查询场景,观察是否有内存泄漏或偶然的签名错误。瑞数的JS可能依赖某些具有“状态”的全局变量,需要确保在Node.js中每次调用时环境是干净且一致的。
- 异常处理 :
- 参数缺失 :检查必要参数是否传入。
- 环境缺失 :捕获因环境模拟不全导致的
ReferenceError,并给出友好提示。 - 网络依赖 :如果
secretKey需要从网络接口定时获取,需要增加获取失败的重试机制和缓存逻辑。
- 性能考量 :如果
generateSign函数涉及复杂的循环或WebAssembly调用,可能会成为性能瓶颈。在Node.js中,可以考虑将计算密集部分放入工作线程,或者对固定参数的签名结果进行短期缓存(注意sign通常具有时效性)。
5. 集成到数据采集流程与风控应对
5.1 完整请求链路的构建
有了可靠的 sign 生成器,就可以构建完整的自动化请求链路了。流程如下:
- 初始化会话 :使用
puppeteer或playwright启动一个真实浏览器实例,访问目标网站首页,让浏览器自动完成瑞数第一阶段的Cookie验证(获取__jsluid_s和__jsl_clearance_s)。这一步虽然重,但最稳妥,能拿到合法的会话基础。 - 提取关键信息 :从成功加载的页面中,通过
page.evaluate()执行JS,提取出生成sign所需的secretKey(如果它是动态注入到页面中的)。同时,将必要的Cookie提取出来。 - 签名与请求 :在Node.js主逻辑中,调用本地化的
sign生成器,为每一个查询请求生成实时sign。 - 发送请求 :使用
axios或node-fetch等HTTP库,携带之前获取的Cookie和刚生成的sign,向数据接口发送请求。 此时应使用与浏览器一致的请求头 ,特别是User-Agent、Referer、Accept-Language等。 - 数据处理 :解析接口返回的JSON数据。
5.2 常见风控特征与规避策略
即使 sign 正确,请求仍可能被拦截。需要关注以下风控点:
| 风控特征 | 可能表现 | 规避策略 |
|---|---|---|
| IP频率 | 请求过快导致IP被临时封禁,返回403或验证码。 | 在请求间添加随机延迟(如 2~5秒)。使用高质量代理IP池,并轮换使用。 |
| 请求头指纹 | 缺少或存在不合理的请求头。 | 完整复制浏览器请求的所有Headers,特别是 Accept , Accept-Encoding , Connection 。 |
| TLS指纹 | 服务器检测到请求来自非浏览器客户端(如 curl , requests 库)。 |
使用能修改TLS指纹的库,如 tls-client (Python)或 curl 的特定版本。Node.js的 axios 默认指纹较易识别,可尝试使用 puppeteer 的 page.setRequestInterception 来代理请求,直接利用浏览器的网络栈。 |
| 行为模式 | 查询模式过于规律(如每秒一次,关键词顺序固定)。 | 模拟人类操作:随机化查询间隔,在查询中穿插翻页、点击等无关操作(如果使用浏览器自动化)。 |
| Cookie失效 | __jsl_clearance_s 有过期时间(通常较短)。 |
监控请求响应,一旦出现签名错误或重定向到挑战页面,立即触发重新获取Cookie的流程。 |
重要提示 :最稳健但效率较低的方式,是全程使用浏览器自动化工具(如
puppeteer)来执行操作和获取数据。将sign生成器本地化,是为了在必要时绕过浏览器执行JS的性能开销,实现更高效率的纯HTTP请求。在实际项目中,往往采用“ 浏览器维护会话 + 本地签名发起请求 ”的混合模式,在稳定性和效率之间取得平衡。
6. 疑难问题排查与深度优化
6.1 签名突然失效的排查步骤
这是最常遇到的问题。如果之前正常的签名突然开始被服务器拒绝,请按以下顺序排查:
- 检查Cookie :首先确认用于请求的
__jsluid_s和__jsl_clearance_s是否仍然有效。尝试在浏览器中打开新页面,看是否需要重新验证。Cookie失效是首要怀疑对象。 - 验证算法输入 :确认生成
sign的输入参数是否发生了变化。特别是secretKey,它可能每小时或每天从服务器端更新一次。你需要重新从页面中提取这个值。 - 对比浏览器请求 :在签名失效的时间点,手动在浏览器中进行一次相同的查询操作。抓取这次成功请求的
sign值,以及请求的所有参数(包括URL参数和请求体)。与你本地生成器使用的参数进行逐字段比对。 - 检查时间戳 :很多
sign算法会加入当前时间戳(到秒或毫秒)来防止重放攻击。确保你的服务器时间与目标网站服务器时间基本同步(误差在几分钟内)。如果算法使用时间戳,你生成签名的时间必须和发起请求的时间非常接近。 - 查看JS文件是否更新 :瑞数可能会不定期更新前端JS的混淆方式或算法细节。重新访问网站,查看生成
sign的核心JS文件内容是否发生了变更(对比文件哈希或关键函数结构)。
6.2 性能优化与代码维护
当你的签名生成器需要处理海量请求时,性能和维护性就变得很重要。
- 缓存策略 :
- 静态资源缓存 :从页面中提取的
secretKey和核心JS代码,如果在一定时间内不变,可以缓存在内存或Redis中,避免每次请求都去访问页面。 - 签名结果缓存 :对于相同的输入参数,其
sign在有效期内是固定的。可以建立一个短期缓存(例如5秒),键为参数排序后的字符串,值为sign。这能极大减少计算量。 但务必注意缓存的过期时间必须短于sign的实际有效期 。
- 静态资源缓存 :从页面中提取的
- 代码模块化与监控 :将补环境、算法核心、请求逻辑拆分成独立模块。为签名生成函数添加详细的日志,记录输入、输出、耗时。当签名失败时,能快速定位是哪个环节出了问题。
- 降级方案 :始终准备一个降级方案。当本地签名生成器因网站更新而大面积失效时,应能自动切换回使用完整的浏览器自动化(
puppeteer)来执行查询,虽然慢但能保证业务不中断,为你修复签名生成器争取时间。
整个逆向瑞数6并实现 sign 生成的过程,是一场与防护方案设计者的智力博弈。它没有一成不变的解决方案,需要你具备耐心细致的调试能力、对Web技术的深刻理解以及灵活的应变思维。每一次成功的逆向,不仅解决了一个具体的数据获取问题,更是对自身技术栈的一次强力升级。记住,尊重网站的 robots.txt 协议,控制请求频率,将数据用于合法合规的用途,是每一位技术从业者应尽的义务。
更多推荐
所有评论(0)