瑞数(RuiShu / RS)是目前国内政企、医药、招标、征信类站点最常见的「动态 Bot 防御」之一。它不像普通 JS 加密只算一个 sign,而是 HTML 下发种子 + 动态 VMP JS 采集指纹 + 双 Cookie 令牌 + 行为链校验 的组合拳。下面按「识别 → 请求流 → 内核结构 → 逆向路线(补环境/RPC/纯算)→ 实战踩坑」给你一套可落地的详解。
1. 先确认:目标是不是瑞数
满足以下特征基本就是瑞数(4/5/5.5/6/6.5 代都适用):
GET 返回 412(缺 Cookie),带正确 Cookie 后变 200
HTML 里第二个 <script> 有 $_ts = {...} ,且 $_ts.cd 是一段超长密文(>1000 字符)
页面还会引一个 文件名随机、每次刷新都变 的外链 JS(如 fjtvkgf7LVI2.a670748.js)
成功后写入 一对 Cookie,名字前 12 位相同、末位不同:
UA1L1zGonajvO(服务端下发,~90 字符,首字符是版本号 5/6)
UA1L1zGonajvP(客户端 VMP 生成,~400 字符)
或老版本 RM4hZBv0dDon443M、__jsl_clearance_s、mz+m 等
2. 瑞数 6 代标准请求时序(必须懂)
1. GET /index.html
← Set-Cookie: UA1L1zGonajvO=60uvxhWj... (服务端种 Cookie1)
← HTML 内含 $_ts.cd + 动态 JS 路径 /static/<rand>.js
2. 浏览器/Node 加载 <rand>.js(VMP 虚拟机)
- 读 $_ts
- 采集 canvas/webgl/font/plugins/screen/timing
- 算版本标识+时间戳+指纹+校验位
- document.cookie 写入 UA1L1zGonajvP=0VrEWqFt... (Cookie2)
3. 后续 AJAX:
Cookie: UA1L1zGonajvO=...; UA1L1zGonajvP=...
→ 200 数据
任一缺失/过期 → 412 → 重走流程
核心认知:Cookie1 是门票,Cookie2 才是令牌;Cookie2 有效期极短(分钟级),且绑 UA/IP/时间戳。
3. 瑞数 JS 的内核结构(VMP 视角)
瑞数不是普通 OB 混淆,是自研 VM:
$_ts 种子块:版本配置、字节码、运算数组、偏移规则、cp[] 自检和
外层静态 JS:eval($_ts.cd 解出的字节码) → 启动 VM
VM 内部:
控制流平坦化(巨型 switch + dispatcher)
字符串/函数名全部运行时位运算解密
内置多层 debugger、反 Hook、调用栈深度检测
出口动作:拼一个 128 位/176 位/144 位数组 → 定制 Base64(非标准表)→ 写 document.cookie
以瑞数 5.5 为例,Cookie 值内部可拆为:
版本标识(固定)
真/假时间戳运算
128 位主数组(20 个有效位,由 $_ts 属性映射 + 指纹填充)
32 位校验数组
定制 Base64 编码
猿人学 14 题(V5 风格)的 m Cookie 更直观:
m = md5(gee(aa,bb,c,d,e,b64_zw)) | 毫秒时间戳 | 时间戳*8 | 计数器
mz = base64(navigator/screen/location 拼接)
签名在 Cookie 里,不在 URL 里。
4. 三条主流逆向路线(按性价比排序)
路线 A:浏览器 RPC(最稳、工程)
不还原算法,让浏览器自己算。
Puppeteer/Playwright 打开首页 → 等 Cookie2 落地
暴露 page.evaluate 拿 document.cookie
或注入 hook:XMLHttpRequest/open、fetch、setRequestHeader 截获动态 sign
Python 侧用 subprocess 或 WebSocket 调 Node 桥,拿到 Cookie 塞进 requests
优点:瑞数每次换 JS 也不怕,行为链天然合法。
缺点:单实例 QPS 低(几十/分),需池化;过 navigator.webdriver 检测要改 launch 参数。
路线 B:Node 补环境(纯协议、主流教程方案)
把动态 JS 下载下来,在 Node 里造一个「假浏览器」跑 VM。
文件拆分:
01_env.js // 补 window/document/navigator/canvas/chrome 对象
02_ts.js // 每次从首页抽 $_ts 块注入
03_source.js // 每次从首页抽到的随机外链 JS
04_main.js // 按顺序 require,跑完读 document.cookie
补环境重点清单(瑞数 6 必补):
window.chrome.loadTimes / csi 返回结构必须像 Chrome
canvas.getContext('2d').fillText/measureText/toDataURL 返回稳定值
Function.prototype.toString 返回 [native code]
navigator.webdriver = false,UA 与请求头一致
performance.now 单调递增
对象属性枚举顺序、数组长度、正则 lastIndex 都不能露馅
优点:跑通后一条 Node 命令出 Cookie,不依赖 Chromium,QPS 比 RPC 高 5–10 倍。
缺点:瑞数升代(5→6→6.5)或每日换混淆时,环境检测点会变,需持续维护。
路线 C:纯算法还原(硬扣,适合写文章/通杀研究)
Hook eval / Function 抓 VM 明文
栈追踪到 (772,1) 之类 call 入口
AST 解一层平坦化,插桩打印 96→117→144→176 位数组演化
把 gee / m5 / sp 等核心函数抠出来,Node 里直跑
代价极大(瑞数 6 全扣通常 2 周起步),但可以得到「不依赖原 JS」的纯函数。一般生产不推荐,除非要做通用 SDK。
5. 实战断点定位速查
抓 VM 代码:Hook window.eval
const _eval = window.eval;
window.eval = function(c){ if(c&&c.length>500) console.log(c.slice(0,200)); return _eval(c); };
抓 Cookie 写入:Hook document.cookie setter,断下后「Call Stack 往上跟 3-5 帧」即到组装点。
过 debugger 轰炸:
const _F = Function;
Function = function(){ if(arguments[0]&&arguments[0].includes('debugger')) return function(){}; return _F.apply(this,arguments); };
XHR 断点:在 Network 复制数据接口 URL → Sources → XHR/fetch breakpoint,触发时看栈底有没有 _0x... 的 sign 拼接。
6. 常见 412 / 验证码回退原因
Cookie2 超时(>2~5min 或跨整点)
Node 补环境里 canvas.toDataURL 每次不一样 → 指纹校验败
UA 与 navigator.userAgent 不一致
直接发请求没带 Cookie1(只带了 Cookie2)
行为链:瑞数 6.5 会看鼠标轨迹/请求间隔,纯协议无行为 → 低频没事,高频触发 493
IP 切换导致 Cookie 绑 IP 失效
7. 一个最小可跑的 Node 补环境骨架(伪代码)
// 04_main.js
const fs = require('fs');
const vm = require('vm');
const sandbox = {
window: global,
document: {
cookie: '',
createElement: () => ({
getContext: () => ({
fillText(){}, measureText(){return {width:10}},
getImageData(){return {data:[]}}
}),
toDataURL(){ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...' }
})
},
navigator: { userAgent: 'Mozilla/5.0 ... Chrome/124', webdriver: false },
performance: { now: () => Date.now() },
setTimeout: (f)=>0, setInterval: ()=>0,
console
};
sandbox.window = sandbox;
vm.createContext(sandbox);
vm.runInContext(fs.readFileSync('./01_env.js','utf8'), sandbox);
vm.runInContext(fs.readFileSync('./02_ts.js','utf8'), sandbox);
vm.runInContext(fs.readFileSync('./03_source.js','utf8'), sandbox);
console.log('COOKIE2=', sandbox.document.cookie);
跑通标志:输出的 UA1L1zGonajvP= 长度 ~400,拿去 Python 请求返回 200。
8. 路线选择建议
临时采数据 / 站点少 → Puppeteer RPC,半天接完
长期采 / 多站 / 要 QPS → Node 补环境,一次投入持续用
写技术分享 / 做通杀库 → 再考虑纯算还原
瑞数本质上防的不是「算不出」,而是「环境不像人 + 行为不像人」。所以补环境里最值钱的工作不是还原 MD5/AES,而是把 chrome.loadTimes、canvas 像素分布、performance 抖动做成「像一台真实 Win+Chrome 机器」。
如果你愿意,我可以下一步直接给你:
瑞数 6 的 01_env.js 完整补环境模板(含 chrome/canvas 检测点注释)
用 Playwright 做 RPC 令牌中转的 Python+Node 骨架
针对某个具体站点(药监局/招标网/NMPA)的 $_ts 字段拆解