news 2026/9/11 15:15:03

瑞数加密逆向实战:Cookie生成链路与绕过方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
瑞数加密逆向实战:Cookie生成链路与绕过方案

从"页面能打开但数据全是空"到真正拿到完整JSON,这个排查过程整整耗了我一个周末。如果你也在逆向某个带瑞数加密的公示平台,大概率会遇到同样的体验:Request Headers 里躺着一个__jsl_clearance_s,页面正常访问没问题,脚本一跑就是 403,连静态HTML都拿不到。

先说清楚,这篇文章是实战记录,不是原理教科书。我会用某海关公示平台作为分析样本,把瑞数加密的识别、定位、断点调试、Cookie生成链路拆开讲,最后给出几种可落地的执行方案和各自的坑。内容偏JS逆向基础向,适合已经会F12、会看调用栈、但第一次正面撞上瑞数的朋友。

1. 瑞数加密的防护机制:为什么一个Cookie能卡住大批爬虫

瑞数和传统的WAF不一样,它不靠规则库封IP,也不靠简单的验证码拦截。它的核心逻辑是:服务器动态下发一段JS,浏览器执行这段JS后生成一个动态令牌,后续每次请求都要带上这个令牌,服务器端验证通过才返回真实数据。

1.1 动态令牌的生成链路

整个链路可以拆成四步。

第一步,浏览器首次请求页面时,服务器返回的不是真正的HTML,而是带有一段加密JS的壳页面。这段JS的URL后缀通常带随机数,比如wzws_js_xxx.js,实际执行时会动态生成新的JS代码。

第二步,浏览器执行这段动态JS,会进行一堆环境检测(Canvas指纹、字体列表、时间戳偏移、浏览器属性等),然后根据检测结果计算出Cookie值,也就是__jsl_clearance_s

第三步,设置了Cookie之后,页面自动刷新,带上这个Cookie重新请求。

第四步,服务器验证Cookie。验证通过,返回真实内容;验证失败,继续给你一段新的JS,让你再算一次。

这个设计很聪明,因为每次下发的JS都不同,生成的Cookie也不同,你就算把某一次的算法逆向出来,下一次它又变了。这也是瑞数被叫做"动态防护"的原因。

1.2 它到底在防护什么

很多人有个误解,觉得瑞数是用来防破解的,逆向它的人都是想破解它。其实不是,瑞数防的不是破解,是自动化。

它的核心目标是区分"真人操作"和"脚本请求"。真人操作会有鼠标轨迹、键盘事件、页面停留时间等行为特征,这些特征会在执行JS的过程中被采集并编码进Cookie。脚本请求没有这些特征,环境检测那一关就过不去,拿到的Cookie就是无效的。

理解了这一点,你就知道为什么纯Python模拟请求基本走不通了——因为Cookie的计算依赖浏览器环境。你要么用浏览器环境的JS引擎去执行,要么把浏览器环境本身模拟出来,没有第三条路。

2. 识别判断:怎么确认目标是不是瑞数加密

在动手之前,先学会确认目标。识别瑞数其实不难,有四个比较明显的特征。

特征一:页面会莫名刷新一次。

手动用浏览器访问目标站点,你会发现页面加载到一半,会自动刷新一次。第一次加载的HTML里没有真实数据,刷新之后才有。这个"刷新"就是瑞数在设置Cookie后自动发的。

特征二:Cookie里有特征字段。

打开开发者工具的Network面板,刷新页面,观察请求的Cookie。如果出现了__jsl_clearance_s,基本可以确定是瑞数,这个字段是瑞数最典型的标识。

特征三:JS文件路径带随机数。

看Network里的JS请求,如果某个JS文件的URL是以.js结尾但前面带一大串随机字符,或者动态生成、每次刷新都不一样,大概率是瑞数的壳。不同版本的瑞数文件名不一样,常见的有wzws_js_*.jschallenge_*.js等。

特征四:格式化工具基本废掉。

你把这堆JS拉到本地用Prettier格式化,会发现代码恢复得七七八八,但变量名完全是乱码,而且代码结构非常诡异——大量的数组索引跳转、字符串拼接、动态函数构造。这就是瑞数对JS代码做的混淆处理。

我当时分析的海关公示平台就是典型样本:首页HTML窗口期极短,想抓包得开Preserve log,刷新两次之后才能看到真实接口。如果你打开一个页面发现符合以上两条以上特征,就可以锁定瑞数了。

3. 断点定位:从一个Cookie反推到核心生成函数

识别只是开始,真正麻烦的是定位。瑞数把核心逻辑藏得比较深,上来就搜document.cookie大概率什么都搜不到。我一般有三个步骤。

3.1 从触发点反推调用链

第一步,在Network面板里找到那个带__jsl_clearance_s的Cookie的Set-Cookie请求,右键Copy as cURL,但这里不是让你用curl访问,而是要看这个Cookie是在哪个请求里被种下的。

接下来对Cookie的下发点下断。在Sources面板里搜__jsl_clearance_s,大概率能搜到一个赋值语句,类似:

document.cookie = "__jsl_clearance_s=" + t + ";Path=/;Max-age=31536000";

在那个t上右键 Add to watch,然后刷新页面,就能看到这个t是哪儿来的。继续往下追,你会发现它是某个函数调用的返回值。

这个方法有两个坑:一是搜__jsl_clearance_s可能搜出多个结果,需要手动排除;二是有些版本里字段名被动态拼接,光搜这个字符串搜不到。这种情况就换思路,搜"clearance"或者"cookie"关键字。

3.2 直接搜特征字符串锁定核心函数

第二个思路更直接——搜特征字符串。瑞数生成的Cookie值里有一段很明显的固定结构:一段明文+一段密文,明文里包含时间戳和随机数格式,密文是一长串十六进制,无意中会在JS里留下特征。

在Sources面板全局搜索(Mac 是Cmd+Shift+F,Windows 是Ctrl+Shift+F),搜Math.floorDate.noweval这些关键字,往往能快速定位到核心代码段。

我当时定位到的核心代码是一个自执行函数,里面用了一个二维数组来存储各种字符串子串,然后通过不断拼接、反转、base64解码生成真正的逻辑代码。这种混淆方式在瑞数里很常见。

3.3 借助调用栈回溯到发起位置

断点打在关键赋值处后,不用急着看代码内容,先看右边的Call Stack。从调用栈从上往下翻,能快速理清调用层级。我习惯从最顶层往下数第三到第五层,那里通常是核心入口函数。

这里有个技巧:瑞数的逻辑函数经常是动态生成的,你在Sources面板里看到的代码并不是原貌,而是执行到一半才被eval出来的。盯住eval这个词,只要断点停在eval上,就往下一层走,那才是真正的核心逻辑所在。

4. Cookie结构拆解:明文段与密文段到底在表达什么

拿到核心逻辑之后,先别急着去管代码怎么写的,把Cookie本身的结构拆开,能省下大量时间。

4.1 一个真实的__jsl_clearance_s长什么样

以我当时抓到的为例:

__jsl_clearance_s=1717149325.975|0|P%2Bt4GJZkQZvZ%2Bz0j8I0IoIuVz%2BO...|RkRnM2ZoUmpabn...

|分割后是两段半。第一段1717149325.975是生成Cookie时的时间戳,精确到毫秒。第二段是0,这个值在不同版本里含义略有不同,有的是版本号,有的是计数器。剩下两段长编码分别是明文加密结果和密文校验结果。

关键是服务器会校验时间戳。如果请求进来时时间戳和服务器时间偏差过大,或者Cookie生成时间过久(比如超过一定分钟数),服务器直接拒绝。这就是为什么临时生成一个Cookie去访问不可行,秒级时效是硬指标。

4.2 核心函数还原的关键步骤

拆完结构之后,主要工作就是还原那两段加密编码的生成逻辑。这里我给一个通用思路。

首先,观察动态JS的代码结构。瑞数对代码做了大量"数组化"处理——字符串都被拆成字符数组,再通过索引引用。逆向时第一件事是把这些数组还原为字符串常量。手工做太慢,用工具辅助,比如在线解码工具或写个简单的正则替换脚本,把_0x[0-9a-f]+这类索引映射到真实值。

其次,定位加密函数。搜索AESDESbase64不太靠谱,因为这些词在混淆后基本不存在。正确做法是找fencrypt等常见命名被混淆后的映射。我的经验是:找到一个函数,传入一个对象,输出一个字符串,且入参和出参都经过多轮XOR或位移运算,那基本就是加密函数。

最后,也是最关键的——环境检测校验。瑞数会采集浏览器指纹,把这些指纹的运算结果拼进Cookie。你在本地Node环境里执行JS,如果环境对不上,生成的Cookie就是无效的。具体来说,Canvas的toDataURLnavigator.userAgentscreen.widthdocument.fonts等属性都会被采集成字符串,然后做哈希运算拼进密文。

4.3 环境指纹的还原策略

这就是瑞数逆向里最耗精力的一环。每个版本的瑞数采集的指纹种类不一样,你在逆向时逐个对比即可。

举几个常见的:

  • window.navigator.userAgent
  • window.screen.width + window.screen.height
  • navigator.languages.join(",")
  • document.createElement("canvas").toDataURL()

其中Canvas指纹是最难搞的,因为不同环境光栅化结果不同。好在信息技术足够成熟:你可以在Node环境里用node-canvas补齐。实在补不齐的,就上浏览器环境模拟,让代码直接跑在真实的浏览器引擎里,这样环境指纹天然正确。

5. 执行方案的取舍:纯Python改写、Node补环境、浏览器RPC

到这一步,理论上你已经理解了瑞数的完整链路,接下来要选一种执行方式落地。我列一下我这边的测试对比,方便你按自己的情况选。

方案实现成本稳定性反检测强度适用场景
纯Python模拟执行JS高,需要改写全部算法低,环境指纹难对齐几乎不推荐
Node.js + jsdom 补环境中,本质是把混淆JS拉进Node执行中,依赖补环境完整度接口不多、个人学习
浏览器RPC(调用真实浏览器执行)高,指纹天然正确项目成形、稳定采集
无头浏览器直接访问最低高,但会拉高资源占用数据量小、重效率

5.1 纯Python模拟为什么不推荐

有的朋友习惯用Python的execjs库来执行任意JS,但瑞数的JS混淆程度太高,不是不能执行,而是每次下发的代码都不一样,你没法把代码写死。

就算用execjs动态执行新下发的JS,环境指纹那一关依然过不了。Node里还能靠补环境解决,Python里补起来更痛苦——你需要在Python里手动设置一堆全局变量、属性和方法,工作量不比重写一遍算法小哪去。

5.2 Node.js补环境的完整思路

这是纯技术流选手比较偏爱的方案。基本思路是:把瑞数下发的JS代码拿过来,放到Node.js环境里执行,同时把缺失的浏览器API一一补上。

我当时用jsdom做基础DOM模拟,然后手动补齐了这么几类东西:

// 用 Node 模拟 window 对象的常用属性 global.window = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://目标站点地址', referrer: 'https://目标站点地址/', userAgent: 'Mozilla/5.0 ...' }).window; // 补齐 canvas 指纹相关 const canvas = require('canvas'); global.document.createElement = function(tag) { if (tag === 'canvas') { return canvas.createCanvas(300, 150); } return new JSDOM().window.document.createElement(tag); };

这里有个坑特别提醒:canvas库的渲染结果和真实浏览器有差异,如果对方采集的是canvas.toDataURL(),你会发现补出来的结果是错的。我的解决办法是固定返回一段预先计算好的哈希值,因为目标站点最在意的是这个值,而不是真的图像内容。

jsdom本身也有坑,它并不完全支持window.Windowwindow.Document等属性。所以我补环境时做了一些改写:

if (!(global.window instanceof Object.getPrototypeOf(global.window).constructor)) { // 修正原型链 }

5.3 浏览器RPC方案:投入产出比最高的稳定方案

如果你做的是长期项目,而不是一次性分析,我建议直接上浏览器RPC。

核心思路:不逆向JS,直接控制一个真实的浏览器实例,让它完成Cookie生成和请求,然后通过WebSocket把Cookie回调出来。这样你既可以在脚本侧用Python写业务逻辑,又能让浏览器负责最麻烦的加密计算。

我当时的做法是写了一个Payload脚本,注入到浏览器页面里:

var ws = new WebSocket("ws://127.0.0.1:6789"); ws.onopen = function() { ws.send(JSON.stringify({ action: "get_cookie", value: document.cookie })); };

然后在Python侧开一个WebSocket服务端,等浏览器主动上报,拿到Cookie后放进后续请求的Header里。这个过程完全绕开了JS逆向,稳定性非常高。代价就是内存占用大、需要常驻浏览器进程。

6. 几个容易翻车的细节和合规线

这部分聊点实际的坑,我踩过之后才明白的。

6.1 最容易忽略的坑

坑一:Cookie有端口绑定。

有些版本在生成Cookie时会把当前源信息也算进去。你从本地调试环境拿到的Cookie,放到服务器上请求,可能直接失效。排查半天环境问题,结果是这个。

坑二:并发请求会导致Cookie失效。

我试过拿着同一个Cookie去并发请求多个接口,结果其中几个返回403。因为服务器端会校验请求频率和请求上下文,高频短时间内的多个请求,触发了风控。解决方案要么控制请求间隔(建议至少300毫秒),要么每个请求都获取新Cookie。

坑三:Cookie的失效时间比想象中短。

__jsl_clearance_s不是永久的,实测通常在几十秒到几分钟之间。你拿到Cookie之后别磨蹭,立刻发请求,否则等Cookie过期了又得重新生成一遍。

坑四:不要只补Cookie,忽略了Headers。

有时候你费劲搞定了Cookie,发现返回的还不是目标数据。那是因为瑞数会把请求头里的Accept-LanguageSec-Ch-UaUser-Agent等当成认证因子,如果和Cookie生成时的不一致,照样拒绝。

6.2 合规线

再说直白一点,做JS逆向是为了技术学习、接口调试和公开数据的合规采集。分析过程可以顺手,但别越界。

我当时选择分析海关公示平台,是因为公示数据本身是向社会公开的,做的是信息公开的抓取,不是绕过权限去窃取非公开数据。如果你的目标是登录后的用户数据、受保护的个人信息,或者打算批量爬取后用于商业变现,我建议在动手之前先确认目标是否合规,别最后惹上麻烦。

学会瑞数的识别和应对思路,最大的价值是理解了"动态防护"这一类系统的设计思路。当前端把逻辑藏在一层层混淆和动态执行背后的时候,你能不能顺着蛛丝马迹摸到源头,这才是值得练习的能力。最后再提一个小建议:分析任何站点之前,先把目标站点的 robots 协议和用户协议读一遍,这能帮你避免大量不必要的麻烦。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 15:14:55

银河麒麟V10服务器OpenSSH编译升级到9.8p1完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 15:14:52

ZLUDA 完整指南:在 AMD 显卡上运行 CUDA 应用

ZLUDA 完整指南&#xff1a;在 AMD 显卡上运行 CUDA 应用 【免费下载链接】ZLUDA CUDA on non-NVIDIA GPUs 项目地址: https://gitcode.com/GitHub_Trending/zl/ZLUDA ZLUDA 是一个 CUDA 兼容层&#xff0c;让你不改一行代码就能在 AMD 显卡上运行未修改的 CUDA 程序&am…

作者头像 李华
网站建设 2026/9/11 15:14:27

链表基础详解:从数组短板到C++/Python实现与高频考点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 15:13:41

SystemInformer:3 个文件定位 DLL 注入入口与内存监控链路

SystemInformer&#xff1a;3 个文件定位 DLL 注入入口与内存监控链路 【免费下载链接】systeminformer A free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Solu…

作者头像 李华
网站建设 2026/9/11 15:08:58

车载Android串口开发实战:UART/RS485底层适配与Modbus RTU通信

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华