瑞数加密逆向实战:从动态令牌到浏览器自动化的完整破译流程
1. 先说结论这题为什么值得写看到标题里的“瑞数加密”四个字有过JS逆向经验的朋友应该都会心一笑。瑞数在业内以“动态安全”出名它的核心思路不是让加密算法本身多难破而是让你的调试过程寸步难行——代码被重度混淆、运行时反复自校验、页面频繁跳动态令牌、关键逻辑散落在几千行密集的生成函数里。正因如此只要啃过一个瑞数站后面再遇到同类体系基本就是复用经验的问题了。这次拿某海关公示平台做实战对象本质上是一个典型的信息查询类网站输入条件、发起查询、后端返回数据、前端渲染列表。表面看平淡无奇但因为它挂了一层瑞数的防护导致直接请求接口会被甩脸拒绝——要么状态码直接200却返回一段空白数据要么拿不到合法动态令牌就被拦截。这让很多刚开始接触JS逆向的朋友一上来就卡死因为问题压根不在接口参数而在你还没到发请求那一步就被服务器识破“这是脚本”。我用了几种不同方案去试纯静态分析、AST还原大法、断点调试配合补环境、最后再用浏览器自动化做兜底。整套流程走下来感受最深的一点是瑞数加密的难点从来不在某一处单独的函数而在它的整体防御节奏。你要是跟着它的节奏走会被带偏你得反过来先摸清它防御的骨架再找下手点。这篇文章就把我完整踩过的路、用过的工具、掉进去的坑全部梳理出来给正在啃瑞数或者打算碰同类站点的朋友做个参考。先明确一下范围整个过程完全从技术研究和学习角度出发用于理解前端防护机制的实现逻辑所有目标站点信息均已做脱敏处理抓包和调试只针对公开接口的加密方式不涉及任何未授权数据抓取。下面进入正题。2. 瑞数加密的机理先搞清楚对手是谁2.1 瑞数核心防护机制的构成瑞数这套防护体系的官方名称里有“动态安全”这个词它跟传统WAF最大的区别是传统WAF是在服务端拦截特征瑞数则把战场摆到了浏览器端。它会在页面里注入一段庞大的JS脚本这个脚本负责在你的浏览器环境里生成一个动态令牌每次请求都必须携带这个令牌服务端验证通过后才会放行。我把它的核心构成拆成四块理解这四块对后续找下手点特别重要第一块是代码混淆体系。瑞数的JS经过重度的变量名混淆、字符串加密、控制流平坦化关键逻辑被拆到几百个子函数里每个函数的逻辑都被搅碎。用浏览器直接看几乎等同看天书。第二块是环境指纹采集。脚本会读取你浏览器的一大堆环境属性包括UA、字体列表、Canvas指纹、WebGL信息、屏幕参数、时间偏移等等把这些信息编码后参与令牌生成。不同环境生成的令牌不一样你如果在Node里模拟环境一不匹配令牌就是错的。第三块是动态令牌机制。这个令牌有时放在Cookie里有时放在请求头里核心特点是“一次性、有时效、与环境和请求参数联动”。你手动刷新一次页面脚本就会重新生成一个。直接复制上一次的来用没过多久就失效。第四块是反调试与自校验。瑞数脚本里藏了多个定时器周期性检测你是否打开了DevTools、是否修改了函数原型、是否在关键位置下断点。一旦发现异常脚本会立即清除当前生成的令牌并重新生成导致你刚调试到一半目标上下文全变了。所以瑞数厉害的地方在于它不是靠一个“很难的算法”挡住你而是靠一整条链路的“动态化环境绑定反调试”组合让几乎所有静态分析和半自动脚本都很难稳定复现。2.2 为什么常规直接请求接口会失败很多人第一次处理这种站点时会犯一个惯性错误打开DevTools的Network面板看到接口返回data就以为可以直接拿到URL去模拟请求。但瑞数站点对这种操作有很明确的防御响应第一次直接请求接口地址返回的状态码可能是200但响应体里是一段空JSON或者一段跳转JS代码。服务端识别到请求里没有合法令牌直接把你当成“非浏览器客户端”处理了。第二次你尝试复制Cookie再请求会发现Cookie里那个关键的动态字段值有效期极短通常只有几十秒到几分钟过期后再发就变了。第三次你想从页面里直接找生成令牌的函数名结果发现整个JS里的函数名全是__0x和下划线加数字的组合靠肉眼根本没法定位。这种情况下强行绕过只会浪费时间。正确的思路是先通过浏览器正常访问一次观察网络层面的完整报文流找出令牌出现在哪里再由令牌这个锚点反向定位到生成它的JS代码位置。这也是我接下来实际操作的主线。3. 目标分析与前期准备动手前必须做的事3.1 环境补齐与工具选型正式开始之前我花了一些时间把环境准备好。瑞数对标准浏览器环境的依赖很强所以自动化方案里我优先选择的不是纯Requests模拟而是真实浏览器内核加调试能力。我本地环境的核心组件如下工具用途版本建议Node.js跑JS还原脚本、补环境脚本16Python 3.8抓包脚本、接口测试脚本3.8以上即可Chrome / Edge目标站点实际调试最新稳定版ProxyPin / CharlesHTTPS抓包查看双向流量任意一款即可AST工具包变量名还原、字符串解密辅助babel/core 7.x这里补一句HTTPS抓包工具很多人忽略但瑞数这类站点经常在请求头里藏动态字段光靠浏览器Network面板看不够全面有时候一些跳转和重试逻辑需要靠代理抓包才能看清全貌。我用的是ProxyPin优点是轻量、支持移动端抓包但对于纯Web调试Charles或者Fiddler都完全够用。3.2 从页面加载开始梳理完整请求链路准备工作做完后我没有直接去看那个巨大的JS文件而是先从Network面板的请求顺序入手把目标站点的访问链路完整记录下来。这一步看似简单但往往决定了后面定位的效率。打开目标公示平台的首页完整观察到的请求顺序通常是这样先请求HTML页面返回内容中包含一个外链script标签指向瑞数的核心JS文件浏览器加载该JS文件这是最大的一块耗时JS执行完毕页面会发一个后台验证请求这个请求里可以看到首次生成的动态令牌后续所有业务接口的请求头或Cookie里都会带上这个令牌。我做的事情是把每个请求的URL、请求头、Cookie字段、响应状态码都记录下来整理成一张对照表。比如某一次请求中我发现响应头里有set-cookieCookie里包含一个叫FSSBBIl1UgzbN7N80T之类的动态字段具体名称每个站点会不同有的叫FSSBBIl1UgzbN7N80T有的叫FSSBBIl1UgzbN7N8T以实际抓包为准。这个字段值就是瑞数动态令牌之一。把请求链路摸清之后真正需要核心分析的JS文件就被锁定为一个特定URL。接下来进入最关键的代码定位环节。4. 核心破译实操从动态令牌反查JS逻辑4.1 用搜索结果找到令牌生成锚点拿到那个巨大的JS文件后第一步不是读代码而是搜索。在格式化后的JS文本里直接搜索之前抓到的那串Cookie名称比如FSSBBIl1UgzbN7N80T。这个搜索操作通常能精确定位到一段设置Cookie的代码附近。我这次搜索到的情况是代码中有一段类似document.cookie的赋值语句值是几个变量通过加号拼接而成。拼接的结果就是动态令牌的完整内容。顺着这个赋值语句往回走就能找到每个参与拼接的变量的来源。这里要特别提醒一句不懂瑞数的人看到document.cookie就直接改它这是误区。你改了Cookie的生成结果服务端验证时发现格式不对、环境指纹不匹配照样拒绝你。正确做法是找到这个Cookie的“上游生成函数”把生成流程整体还原出来而不是在产出物上动手脚。瑞数的代码通常会把令牌拆成多个片段分别存放在不同的变量中执行到某个节点再统一拼接。所以定位到赋值语句后还要做几件事找到赋值的右值表达式标记所有参与的变量逐个变量反向追踪其来源找到生成函数分析生成函数内部是否依赖环境指纹、时间戳、随机数等输入。4.2 混淆对抗与AST还原实操瑞数的JS变量名通常是_0x开头的十六进制字符串函数名也类似直接阅读几乎不可能。为了降低分析难度我先用Babel写了一个简单的还原脚本做两步处理第一步把代码解析成AST第二步将选项提取并替换关键变量名。以下是我实际用过的简化版本思路是把函数调用顺序分离出来先把每个函数体的范围打上标记const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const code fs.readFileSync(rs.js, utf-8); const ast parser.parse(code); traverse(ast, { FunctionDeclaration(path) { if (path.node.id path.node.id.name.startsWith(_0x)) { // 记录函数名、参数、函数体行数范围 console.log(函数名: ${path.node.id.name}, 起始行: ${path.node.loc.start.line}, 结束行: ${path.node.loc.end.line}); } } });这段代码的作用是生成一份“函数地图”把几千行混淆代码里所有函数的位置列出来。有了函数地图再结合上一步找到的Cookie生成位置就能快速缩小范围知道是哪几个函数参与了令牌拼接。AST还原整体建议分几步走解析并格式化JS文本去掉压缩后的一行长代码恢复缩进用脚本标记所有_0x变量和函数生成名称映射表对字符串解密函数做插桩把密文还原成明文定位核心生成函数逐步替换混淆变量名为可读名称。这里必须说明瑞数的字符串解密通常使用一个自执行的解密函数传入一个索引值返回一串解密后的字符串。如果你在代码里看到了大量形如_0x3f2e(0x1a)的调用那基本就是一个解密函数在起作用。把这个函数摘出来在Node里单独执行就能得到一份索引与明文的对照字典。把字典替换回原代中阅读难度会下降一个量级。4.3 从混淆代码里找到时间戳和指纹取用点为了验证我找到的函数确实是令牌生成核心我做了两个检查第一在函数内部搜索Date.now()、getTime()、Math.random()这类动态值调用。瑞数生成的令牌肯定依赖当前时间所以这个调用点必须存在。第二搜索环境指纹相关属性比如userAgent、language、screen.width、canvas.toDataURL等。这些属性值的拼接结果会直接参与令牌计算。只要两个检查都通过基本可以确认核心算法已经锁定。接下来处理的环境问题反而是最大的坎。5. 补环境与半自动方案从分析到可复现5.1 为什么不能只靠Node环境直接跑有人会想既然核心函数找到了那把整个JS里涉及到生成令牌的部分抽出来放到Node里跑一下不就直接得到合法令牌了吗理论上行但实际操作时会发现瑞数的脚本对环境属性的检查极其严格。它在生成令牌的过程中会调用浏览器特有的DOM API比如document.getElementById、navigator.userAgent、window.localStorage、甚至document.createElement(canvas)并读取其指纹。这些在Node的原生环境里全部不存在一旦调用就会报错脚本就中断了。所以“补环境”是所有非浏览器抓取方案里最麻烦的一环。你要在Node的全局对象上手工模拟出足够多的浏览器属性伪装成浏览器环境。5.2 我实际使用的补环境思路补环境没有万能银弹但有一个实用的基本盘用jsdom做基础DOM模拟再手动补充window、navigator、document、canvas等关键属性。我搭了一个简化版的骨架const { JSDOM } require(jsdom); const dom new JSDOM(!DOCTYPE htmlhtmlhead/headbody/body/html, { url: targetUrl, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., pretendToBeVisual: true, }); global.window dom.window; global.document dom.window.document; global.navigator dom.window.navigator; global.localStorage dom.window.localStorage; global.sessionStorage dom.window.sessionStorage; // 补充canvas指纹接口 HTMLCanvasElement.prototype.toDataURL function() { return data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg; };这里有两个细节第一url必须填真实的目标页面地址因为脚本内部可能通过window.location.href校验来源页面 第二userAgent必须与抓包时浏览器使用的保持一致否则指纹校验失败。补环境的方法非常依赖实践调试经常跑完一遍发现报某个属性未定义然后手动补一个再跑再报错反复迭代。瑞数脚本里到底调用了多少属性不同站点不完全一样所以这个过程的调试成本可能非常高。很多时候补环境调一天还卡在某个奇怪属性上并不稀奇。5.3 用浏览器自动化做兜底方案如果补环境太耗时或者你只想要快速跑通业务接口还有一个性价比更高的路线直接用selenium或playwright控制真实浏览器让页面自己生成合法令牌再从浏览器上下文里把令牌取出来给业务脚本用。这种半自动方案在工程上非常常见。具体流程是用Playwright打开目标页面等待页面加载完执行一段JS读取document.cookie或者某个全局对象里的令牌值把令牌传回Python或Node的业务脚本用于后续接口请求。这等于让瑞数自己给自己生成通行证完全绕过了环境指纹校验的问题。缺点是打开浏览器的开销比单纯请求大但在合规研究和低频访问场景下稳定性远高于纯Node补环境。我这次的实际落地就采用了这个方案配合抓包得到的接口参数格式成功拿到了目标公示平台的查询数据。整个过程没有直接破解或篡改任何服务端逻辑所有操作基于前端公开代码和浏览器自身行为。6. 实战过程记录从打开页面到拿到数据6.1 第一步完整抓包并整理请求头第一次用Chrom正常访问目标公示平台首页Network面板里能看到以下几类关键请求首页HTML文档瑞数核心JS文件文件体积通常在200KB以上名字一般是数字加字母的组合非常不起眼一个初始化验证请求响应里返回了一个动态Cookie后续业务查询接口。我整理了一下查询接口实际发送时的请求头关键部分脱敏后大致是这样POST /api/queryList HTTP/1.1 Host: example.com Content-Type: application/json User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... Cookie: FSSBBIl1UgzbN7N80Tabc123; 其他业务Cookiexxx;那个FSSBBIl1UgzbN7N80T就是动态令牌之一。它的生成源头就在瑞数JS内部。有了这个锚点后面所有分析就有了方向。6.2 第二步格式化JS并定位核心函数把核心JS文件下载到本地后我用Prettier先做格式化把原本压缩成几行的代码变成可阅读的多行代码。随后用Babel脚本跑了一遍“函数地图”得到了一份函数名与行号范围的清单。通过搜索Cookie名称FSSBBIl1UgzbN7N80T我定位到了一段document.cookie赋值代码位置大致在生成一个拼接字符串的语句处参与拼接的变量有三个。这三个变量中一个来自时间戳相关函数一个来自环境指纹编码结果另一个是固定前缀字符串。顺着这三个变量的出处继续回溯就找到了核心生成函数。整个定位过程说到底就是“搜索引擎 手动追变量”的组合。瑞数代码再乱也是由一个一个函数串起来的只要锚点找得准回溯链路是能走通的。6.3 第三步用Playwright落地完整采集流程定位完算法以后我决定直接用浏览器自动化做最终落地原因是这套方案最稳、最快、不用和环境指纹死磕。实际代码核心逻辑是这样的from playwright.sync_api import sync_playwright def get_token_and_query(query_data): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) ..., viewport{width: 1920, height: 1080} ) page context.new_page() # 先访问首页触发瑞数脚本执行并生成令牌 page.goto(https://example.com/, wait_untilnetworkidle) # 提取cookie或全局令牌 token page.evaluate(() document.cookie) # 带着令牌直接请求业务接口 resp page.request.post( https://example.com/api/queryList, dataquery_data, headers{Content-Type: application/json} ) print(resp.text()) browser.close() get_token_and_query({keyword: 测试企业})这段代码的核心点在于用同一个浏览器上下文访问页面和请求接口保证Cookie和Token始终处于同一完整状态中。很多人在自动化时遇到间歇性失败多半是因为页面上下文和接口请求上下文没有共享Cookie。6.4 第四步常见异常与排查记录实际运行过程中我碰到了两个高频异常这里做一个速查记录异常现象可能原因排查方法接口返回{code:403,msg:forbidden}动态令牌缺失或失效查看请求头是否带上了Cookie确认页面刷新后是否重新取Token接口返回空数据但状态码200令牌与请求参数未绑定或环境指纹不匹配检查查询参数是否被动态加密重新走一遍页面渲染流程页面加载极慢偶尔白屏瑞数脚本执行中出现异常更新浏览器的UA、关闭无头模式再试必要时等待特定元素出现6.5 对应对策与心得一开始我使用的是headlessTrue结果发现某些情况下瑞数识别无头浏览器直接拒绝生成合法令牌。后来改成headlessFalse且设置真实UA问题就消失了。这说明瑞数不仅校验UA还可能会检查浏览器自动化的特征。另外一个经验是不要试图用Requests直接复用浏览器里拿到的Cookie。因为瑞数的令牌跟用户会话和页面状态绑定你单独用Requests发请求时请求头里如果缺少一些由浏览器自动添加的字段仍然可能被拦截。最稳的方式是让Playwright发请求或者把全部请求头完整复制并定时刷新Cookie。7. 常见问题与排查技巧实录7.1 瑞数日常调试中最容易踩的坑我整理了一份这段时间调试中真正踩过的坑每条都对应一个具体的调试细节混淆后的JS文件巨大直接打开容易卡死。建议先本地保存格式化不要在浏览器里直接看压缩后的文本。字符串解密函数必须在初始化之后执行提前单独调用会返回undefined。所以抽取解密函数时要把它的前置初始化代码一起带上。时间戳函数可能被封装成某个随机函数调用的参数不能直接搜Date.now还要搜getTime、new Date等变体。部分站点会同时存在多个动态Cookie必须全部带上。我见过有的站点在同一个页面里生成了两个互相校验的动态字段少一个都会失败。直接在DevTools里看变量值有时会被反调试机制干扰建议勾选“Deactivate breakpoints”或者直接脚本化打开目标页面再挂载调试器。7.2 一份可以直接用的排查顺序如果你也卡在某个瑞数保护的站点上我建议按下面的顺序排查避免无效劳动检查是否拿到了所有动态Cookie和请求头字段确认业务接口的请求参数本身有没有被加密确认页面是否正常完成所有JS执行没有因为异常中断排查请求头里是否缺少浏览器自动携带的标准头如Accept、Sec-Fetch-*等如果以上全没问题但依然失败回到JS代码里重新定位令牌生成逻辑看看是否有二次校验。这条排查顺序我每次都会走一遍80%的问题在第一步和第二步就能解决。剩下20%才是真的需要深入JS代码去分析的疑难杂症。8. 写在最后的实操心得这次实战走下来最大的体会是瑞数加密的护城河不是算法复杂度而是调试耐心。它的代码量巨大、混淆手段密集、环境校验苛刻如果你抱着“几分钟搞定”的心态一定会在某个环节被磨到怀疑人生。反过来只要你把它当成一个普通的大型前端项目来分析按部就班定位锚点、回溯函数、搭配合适的自动化方案整个过程其实是有规律可循的。从工程角度看如果你只是想稳定获取数据我强烈建议优先考虑浏览器自动化全家桶而不是死磕Node补环境。补环境的成本太高而且不同站点之间差异大调试体验非常痛苦。真正值得投入时间做的是把整个流程封装成一个可配置的采集模块以后遇到同类站点可以直接复用。最后再分享一个经验处理这类防护站点一定要保留一份完整的调试笔记把每次抓到的Cookie字段名、JS函数定位、异常现象都记录下来。因为瑞数体系内的站点虽然长相各异但核心逻辑高度相似。你调通了一个后面再遇到就是套模板的事。这次我把目标站点的调试链路完整走了一遍最大的价值是以后再看到类似防护心里有底了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →