ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JS逆向实战分析:瑞数加密动态Cookie的原理与补环境方案

JS逆向实战分析:瑞数加密动态Cookie的原理与补环境方案 1. 项目背景一次“加密对抗”引发的技术复盘“JS逆向实战五某海关公示平台分析瑞数加密”这个标题最初来自我对一批公开数据接口的稳定性测试。海关公示类平台的数据本身是面向社会公开的正常情况下不需要任何破解就能访问。但在实际抓取过程中我发现请求返回的cookie里多了一段非常长的加密参数请求头里也有明显的动态指纹痕迹这就是典型的高度混淆防护。于是我把这一期实战记录了下来取名为“实战五”——因为在它之前我已经在自建靶场里完整复现过四类不同的前端防护场景。先说清楚一个原则我这套内容里的“逆向”指的不是去恶意绕过商业平台的收费接口或授权边界而是研究前端加密机制本身的原理——尤其当你需要在自己开发的网站里部署同类防护或者需要为自有系统做安全评估时这种分析能力会比黑盒测试更有价值。瑞数加密River Security这几年在企业级网站上出现频率很高尤其在政府公示、民航、银行业务、电商价格监控这类场景里几乎成了标配。它解决的核心痛点是一键模拟请求的自动化脚本太泛滥了传统验证码容易被识别、被绕过而瑞数这类动态防护技术可以在前端执行环境里做“人机识别”——通过JS代码混淆、浏览器指纹采集、动态cookie生成、请求行为校验等多层手段把非浏览器环境拦在门外。这篇文章面向三类人第一类是Web前端工程师想了解下一代前端防护到底做了什么第二类是安全测试人员需要在不触碰红线的条件下验证防护强度第三类是对JS引擎执行机制感兴趣的进阶开发者想弄明白V8里那些“变态级”的混淆代码是怎么跑起来的。我会从原理解读、方案选型到实操录屏级的步骤拆解把整个分析过程写成可直接复盘的技术笔记。2. 瑞数加密的整体架构与核心设计思路2.1 瑞数防护的本质动态执行环境下的身份验证很多人一听“瑞数加密”第一反应是“这是个加密算法”。实际不是。瑞数不是单纯给某个参数做AES或RSA加密它更像是一套完整的动态防护体系核心由三个环节组成动态Token生成每次会话会通过前端JS生成一组加密的cookie值常见的是RM4hZBv0和FSSBBIl1UgzbN7N85T等名称这组值不是简单的“随机数”而是由当前客户端环境信息、时间戳、浏览器行为特征综合计算出来的签名结果。请求行为校验服务端不仅校验cookie是否存在还会校验cookie与后续请求是否“同源”——也就是同一套浏览器指纹、同一套鼠标轨迹特征、同一套执行环境参数。如果你拿一个静态cookie到处复用很快会被风控识别。个性化混淆方案瑞数每次下发的前端JS代码都是动态拼接的变量名、函数名、控制流结构每次都在变化也就是说你就算这次把代码逻辑分析清楚了下次它又变了。我们需要在公示平台场景里去判断它到底启用了哪几个环节。从实测来看当时遇到的这个站点启用了完整的环节一和环节二——每次页面加载都会生成新的动态cookie且在后续翻页请求时服务端会校验这个cookie里附带的行为特征。这直接决定了后面所有分析工作的方向。2.2 分析前面的方案选型浏览器自动化还是纯Node模拟在开始处理之前有一个关键问题必须先解决**用什么方式去“复现”这个加密过程**这里有三条路线我分别评估过第一用Selenium或Playwright整页加载。这条路最省事因为浏览器环境本身是真实的瑞数的JS能在里面正常执行cookie自动生成。但缺点也很明显瑞数专门检测自动化特征如navigator.webdriver标记、Chrome DevTools Protocol连接特征等。实测下来裸Selenium基本秒挂用Playwright加隐藏参数稳定性大概在60%到70%之间用来做小规模测试行但跑大规模采集会频繁触发风控。第二在Node.js里构造完整浏览器环境。这条路不需要真实浏览器窗口通过jsdom或者在Node全局对象里模拟window/document/navigator等API再执行瑞数的JS。优点是并发能力极强缺点是环境模拟的“真实性”很难做高——瑞数本身就对执行环境做了一大批检测包括但不限于document.cookie的可写入性、localStorage是否存在、Canvas指纹是否能正常获取等。这些在纯Node环境里几乎不可能完全模拟出来。第三分析cookie的生成逻辑后手动改写。也就是把瑞数那套JS的核心函数提取出来用Hook特殊函数的方式让它能在Node里跑出相同结果。这条路最难但一旦做通了效率是最高的——不再需要任何浏览器内核。我当时的选择是“先二后三”先用Node模拟环境做试跑判断JS执行到哪个环节报错再把报错点逐个攻破最终在纯Node环境里跑通。这也是整个实战五里最耗时间的部分后面会详细展开。2.3 关键技术难点TSOBject与加密参数生成器在瑞数防护体系里有两个名词几乎每一篇分析文章都会提到TSOBject和TT Object。实际上它们是同一类东西——一个由特定算法生成的、带签名信息的对象结构存储了浏览器环境的关键指纹以及时间戳数据。瑞数的JS代码在执行时会先采集上百项环境参数UA、屏幕分辨率、语言、时区、canvas指纹、字体列表、WebGL信息等把这些参数编排进一个JSON-like结构里再通过自实现的编码算法进行序列化最后作为cookie的一部分写入浏览器。这里有个容易误判的点很多新手以为拿到cookie就能靠“复制粘贴”来复用于是只用浏览器开着页面把cookie手动塞进Python脚本里。结果往往是一两分钟内有效之后请求就报“访问频率异常”或“非法请求”。原因在于cookie中编码的信息里包含了生成时间戳服务端校验时会对比当前时间和cookie里的时间戳超过一个很小的窗口通常在几十秒级别就判定失效。所以光拿到cookie没用必须理解它的生成逻辑让程序能在每次请求前“即时”生成新鲜的值。从这个角度看瑞数加密实际上是把“动态token”和“环境校验”两个概念融合在了一起。它的核心优点是可以完全不需要服务端维持会话状态每次请求都带上签名服务端只要验签不需要额外存储Session。3. 核心细节解析加密流程、动态Cookie与JS代码特征3.1 初次请求链路如何触发瑞数的动态防护对公示平台的第一次访问是这样的请求首页URL服务端返回一个状态码为200的HTML页面。这个页面看起来是完整的但其实只包含基本框架和一大堆内联JS核心数据全部由后续异步接口加载。在返回的HTML中夹杂着好几段极长的script标签内容有的甚至占据整个HTML体积的70%以上。浏览器在解析这些JS时会在内部自动执行生成动态cookie并写入document.cookie。只有带上这个新cookie发起后续的XHR异步请求服务端才会正常返回JSON数据否则会返回一段用于“刷新”的JS脚本触发新一轮执行。也就是说整个防护流程里有一个“先执行、再获取、后请求”的强顺序关系。我们在自己写爬虫逻辑时必须严格按照这个顺序来模拟——先拿“种子页面”再执行种子页面里的JS取出生成的cookie然后再带着cookie去请求真正的数据接口。3.2 动态Cookie的组成结构拆解以这个平台实际生成的cookie为例它在首次执行后生成了两组关键的cookie项一个名称类似RM4hZBv0瑞数的经典命名之一值非常长通常在500到1000个字符之间另一个名称类似FSSBBIl1UgzbN7N8值是cookie值变了。这里我特意把真实站点名隐去只保留技术特征。这两组cookie之间的关系可以这样理解第一组是“携带环境特征信息的凭证”第二组是“凭证是否有效的状态标记”。当第二组被置为cookie值变了时表示凭证处于活跃状态。后续请求时服务端会校验第二组的状态如果不存在则说明凭证已被标记为失效或过期服务端直接返回校验失败的JS。拆开第一组cookie的核心编码内容后发现它包含的信息可以粗略分为四段环境指纹段包括navigator.userAgent、screen.width、screen.height、navigator.language、navigator.platform等常规信息。时间戳段从JS执行时刻提取的Date.now()值做过一定运算处理并非明文。行为计数段用户在当前页面上的操作次数、页面加载次数、鼠标移动采样等。完整性校验段前六项信息按某种不可逆哈希算法生成的摘要值用来保证内容没有被篡改。这四个段以一种自定义的编码方式拼接最终生成一长串看似随机的字母和数字。解析它的时候不能只看表面需要通过debug命令跟踪JS从构建原始对象到执行编码函数的完整过程。3.3 JS代码的混淆特征变量名随机、控制流平坦化打开瑞数下发的JS代码第一眼的感觉是“乱”。大量使用下划线前缀的短变量名比如_0x2a4b9c、_0x3f8d21、_0x5fae74等这些是通过十六进制编码生成的随机标识符函数名同样如此很多看起来完全没有任何语义信息。这类属于标识符混淆是混淆工具的基操对分析者来说更像是一种“心理战”其实看得多了不会构成真正的门槛。更值得注意的是控制流平坦化Control Flow Flattening。正常JS代码是顺序执行的分析者看一遍就能明白逻辑先取A再算B然后判断最后返回。但经过控制流平坦化之后的代码会变成这样一个巨大的switch分支结构包住所有逻辑外层有个状态机变量_0x135a2e不断被赋值根据这个变量的值跳到不同的case分支里。也就是说原有代码的“顺序感”被彻底打散同一个函数内的多条语句会被打乱到switch的不同case中执行顺序由状态机变量来驱动完全无法用肉眼看懂。我做过一次实验用一个一万行级别的混淆JS文件如果直接肉眼去读大概需要几天时间但要是先跑一遍“调试日志插桩”让代码执行时自动打印每个函数名和关键参数值可能只需要两三个小时就能理出完整脉络。所以面对瑞数这类混淆核心对抗思路不是“读代码”而是“看它跑”。3.4 Hook技术如何在JS执行过程中拦截加密函数对JS执行细节的分析最常用的工具是Hook——通过改写或包装某个对象方法在它执行前或执行后插入自己的日志代码。瑞数环境里最经典的几个Hook点document.cookie的读写在window.__defineGetter__、window.__defineSetter__上做手脚或者在Document.prototype上直接改写cookie的getter/setter从而记录cookie写入的先后顺序。Function.prototype.constructor很多混淆JS会调用函数构造器来动态生成新函数Hook这个点能捕获所有动态函数体代码对还原逻辑帮助巨大。String.prototype.fromCharCode、String.fromCharCode编码操作的高频出口记录它的调用参数可以追溯出被隐藏的明文内容。Object.prototype.toString、Array.prototype.map等原型方法这一类Web API在瑞数的环境检测代码里被大量调用Hook这些位置能看到操作者的真正意图。在实操中我写了一个简单的hook_utils.js脚本放在页面加载前注入。专门用于监控document.cookie的赋值操作。每次cookie值发生变化都会自动打印出完整的新旧值和调用栈。这个脚本等同于给加密过程加上了“监控摄像头”能实时看到它每次往cookie里塞了什么。const cookieSetter Object.getOwnPropertyDescriptor(Document.prototype, cookie).set; Object.defineProperty(document, cookie, { get() { return cookieGetter.call(document); }, set(val) { console.log([Cookie Set], val); debugger; return cookieSetter.call(this, val); } });这段代码单独写在一个脚本里用Chrome开发者工具的Snippets功能保存访问目标页面时手动执行一遍就能在前面加载瑞数JS时捕获到cookie的写入时间点。这里的debugger会强制断下帮助我们观察当前调用栈里是哪一段代码触发了写入。4. 实操过程与核心环节实现从补环境到纯Node跑通4.1 环境搭建Node版本、依赖与工具清单在开始之前需要准备一个干净的Node.js环境。我使用的是Node 16 LTS版本这个版本的V8引擎与Chrome 90的JS行为已经很接近能提高执行兼容性。还需要几个关键依赖npm init -y npm install jsdom --save npm install canvas --save npm install vm2 --save这里的三个依赖各有分工jsdom负责提供基础的DOM和浏览器环境Mockcanvas提供CanvasRenderingContext2D相关的指纹API实现vm2则用来在Node里创建一个隔离的VM环境专门执行瑞数的混淆JS代码。需要注意vm2在2023年之后官方不再建议用于生产环境因为发现了多处沙箱逃逸漏洞但在这类单机调试场景下用完即弃问题不大也可以用Node自带的vm模块替代。依赖装好后再用fs模块将首次请求拿到的HTML保存到本地方便反复调试避免每次调试都重新请求线上页面触发风控。4.2 补环境第一阶段浏览器的GlobalWindow基础补全瑞数JS在浏览器中运行的前提是存在window对象而我们直接用Node执行时window是undefined。所以第一步是构造一个最小可用的全局windowconst { JSDOM } require(jsdom); const dom new JSDOM(!DOCTYPE htmlhtmlbody/body/html, { url: http://example.com/, referrer: http://example.com/, contentType: text/html, runScripts: outside-only }); global.window dom.window; global.document dom.window.document; global.navigator dom.window.navigator; global.screen dom.window.screen; global.localStorage dom.window.localStorage; global.sessionStorage dom.window.sessionStorage; global.document.cookie ;这一步只是把“有和无”的问题解决了。真正跑起来之后会不断遇到各种API缺失导致的报错比如window.chrome未定义、document.visibilityState读取失败、window.outerWidth不存在等。每遇到一个就在全局对象上补一个极简实现。这个过程在我们圈里叫“补环境”本质就是给JS伪造一个“看起来像浏览器”的执行上下文。4.3 补环境第二阶段特殊API的实现与坑点补环境这个阶段最典型的坑有三个第一个坑是document.createElement(canvas)返回的Context。瑞数的环境检测会调用canvas.getContext(2d)然后读取像素数据来计算Canvas指纹。jsdom默认不支持Canvas渲染getContext方法直接返回null。最简单的处理是接一个canvas依赖在Node里真实渲染一个相同尺寸的canvas再把toDataURL的结果喂给JS。const { createCanvas } require(canvas); const canvas createCanvas(200, 50); const ctx canvas.getContext(2d); ctx.font 14px Arial; ctx.fillText(Hitest, 100, 25); const fingerprint canvas.toDataURL(); global.document.createElement function(tagName) { if (tagName canvas) { return { getContext: () ctx, toDataURL: () fingerprint }; } };这个补法只适用于获取固定指纹的场景。如果瑞数在检测时动态改变canvas绘制内容或者连续调用toDataURL多次就必须每次重新绘制而不能复用同一个结果。这里我遇到过一次报错排查了半天才发现是demo页面里canvas的尺寸不同导致toDataURL的结果里包含了不同的空白区域签名校验不一致。第二个坑是navigator.webdriver属性。正常浏览器里这个值是false而jsdom和Node环境里默认不会定义这个属性或者定义成undefined。很多动态防护脚本会先检测这个属性是否存在存在且为true就判定为自动化环境。补充时直接定义就可以Object.defineProperty(navigator, webdriver, { value: false });第三个坑是window.chrome对象是否存在。正常Chrome浏览器里window.chrome对象包含loadTimes、csi等方法动态防护会检查这些方法是否真实。jsdom没有实现它所以需要手动补一个“皮皮虾版本”。因为瑞数实际上只是检查方法和属性是否存在并不深入验证方法返回值的具体计算逻辑所以用空函数去填充就行。4.4 排除干扰项把底噪信息从加密结果中剔除在实际生成cookie的调试过程中我一度以为自己的补环境完全成功了因为脚本能完整跑完并且cookie也确实生成了。拿生成的cookie去请求数据接口时服务端却多回了一段403页面里写着“当前操作被阻止”之类的提示。当时比较头疼排查了很久才发现问题出在环境指纹不一致上我的Node环境里navigator.userAgent被jsdom设置成了Node.js的UA而服务端在验签时发现UA和cookie中的UA不一致直接判定为非法请求。我补的screen.width和screen.height是0因为jsdom在默认情况下不模拟屏幕尺寸而瑞数环境检测里明确要求这两个值必须大于0。document.hidden属性和document.visibilityState的值没有设置瑞数判断当前页面是隐藏状态怀疑是在后台无头环境运行于是标记异常。把这三处修正后重新执行完整流程cookie生成一次通过数据接口正常返回了JSON内容。这个小插曲给了一个启发瑞数的环境检测并不过分依赖某种“高精尖”技术它靠的完全是大规模字段的综合交叉校验——单个值错了可能没事十几个值如果都偏离正常范围判定结果就会非常清晰。4.5 执行瑞数JS的封装代码段把整个流程封装成一个可复用的函数让每次请求前都能自动执行function generateDynamicCookie(mainHtml) { const scriptMatches mainHtml.match(/script[^]*data-version[^]*([\s\S]*?)\/script/g); const allScripts mainHtml.match(/script[^]*([\s\S]*?)\/script/g) || []; const vm require(vm); const context createMockBrowserContext(); allScripts.forEach(script { const content script.replace(/script[^]*/, ).replace(/\/script/, ); if (content.includes(eval) || content.length 10000) { vm.runInNewContext(content, context, { timeout: 5000 }); } }); return { RM4hZBv0: context.document.cookie.match(/RM4hZBv0([^;])/)?.[1] || , FSSBBIl1UgzbN7N8: context.document.cookie.match(/FSSBBIl1UgzbN7N8([^;])/)?.[1] || }; }这里有个关键点瑞数返回的JS不一定只在script标签里有时也会在script typetext/javascript里加上了自定义属性。匹配正则时一定要覆盖script[^]*这种方式不能只写script。另外执行时最好设置超时选项防止某段JS陷入死循环导致Node进程挂掉——这是我在调试中掉过的一个坑跑着跑着整个脚本没响应了只能任务管理器强制杀进程。5. 常见问题与排查技巧从实战中踩出来的经验5.1 排查维度Cookie生成成功但接口仍旧403这个问题出现过好几次整理成速查表方便对照排查可能原因判断方法解决方式UA不一致比对请求头里的UA与cookie内嵌UA将Node的默认UA改成与浏览器一致执行时间过长在生成cookie后延迟再发请求改为一生成cookie立即发送请求cookie过期观察cookie生成时间与请求时间差增加缓存策略重新生成cookie行为特征缺失请求头里缺少Referer或Origin补上完整的标准请求头JS执行不完整日志里出现未捕获的异常排查执行过程中的报错点有一点需要注意瑞数的服务端校验并不只是“检查cookie是否存在”它还会结合HTTP请求里的User-Agent、Accept-Language等头部去和cookie里的内容做一致性比对。只要有一处对不上很可能返回的不是403而是200页面里夹带一段“你被拦截了”的隐藏JS。所以遇到响应体是HTML但里面没有目标JSON的情况要优先怀疑“请求被安全策略拦了”而不是“接口地址不对”。5.2 调试技巧如何高效定位瑞数JS中的关键加密函数如果完全靠肉眼去阅读混淆代码非常痛苦且低效。更聪明的做法是给代码执行过程加日志。推荐一个组合先用--trace-function-calls启动Node记录所有被调用的函数再用vm2的timeout设置短一点的执行时间观察它卡在哪个API调用上最后在document.cookie的setter上打断点定位最后一次写入的调用链。调试时打开Chrome开发者工具在Sources面板里给cookie的setter设置条件断点条件写成typeof val string val.length 50这样可以精准捕获到目标cookie把前面几百次无关的合法写入全部过滤掉命中率大幅提升。相比之下更稳妥的方案是直接在关键编码函数入口处打日志但因为瑞数的函数名每次都会动态变化所以需要用“按调用特征匹配”的方式——例如在String.prototype.charCodeAt的调用点统一插桩把输入输出记录下来再根据这些数据的格式反推它是被用来做字符编码还是数据切割。有一个比较古老的技巧挺实用在js里给Function函数打补丁让它创建时保存自身的源码字符串和创建时间const origFunction Function; Function function(...args) { const f origFunction(...args); console.log([Function Created], args); return f; };很多混淆代码会通过new Function()或Function(return ...)()来创建子代码块这个方法能快速定位到那些“内嵌的子加密逻辑”比单纯的“全文件替换变量名”要省力得多。5.3 反检测升级More anti-detection ideas for repeatable testing实战中瑞数会不定时更新其检测方式。几个已经验证有效的注意事项执行JS的容器里不要放真实的生产业务代码所有伪造信息都应当在测试容器里避免任何交叉污染。在Node环境与原浏览器环境之间切换时必须重新生成全部环境参数不能沿用上一套cookie、UA、canvas指纹否则很可能因为“指纹漂移”被判定异常。每做一次完整调试循环后清理一次缓存目录和临时文件避免上次调试的cookie数据和本次混合。所有代理、负载均衡类工具尽量关闭只保留最普通的HTTP请求能力。瑞数的防检测方向一直在演进从早期只检查navigator.webdriver到后来开始检查chrome.runtime是否存在再到检查window.Notification的属性类型。本质上是把所有“浏览器有但JsDom没有”的API全部采集一遍再与真实环境里的统计分布做交叉验证。所以我们在补环境时宁可多补一些“用不到”的属性也不要让它检测到undefined然后直接报错。5.4 在合规范围内的应用场景建议最后再强调一下这个内容的适用范围。我对瑞数做这种拆解的最终目的不是为了批量抓取哪个受限平台的数据而是为了在自己开发的Web应用里理解这类防护的效果边界。比如要评估自家网站是否需要引入动态防护可以先在小范围环境里验证规则是否能识别“无环境执行”的模拟客户端再比如做产品安全设计时需要知道JS混淆并不能做到100%防破解它只是提高了攻击成本服务端的关键业务校验永远不能完全依赖前端签名。在我的实际项目中这套分析方法还帮我在自家站点上发现过一个严重的风险点由于登录接口没有独立的服务端风控即使前端做了完美的JS签名攻击者用浏览器或插件的内存抓取技术就能直接取得明文数据。这让我坚定了一个理念前端JS逆向分析是安全防护体系的“探照灯”而不是“盾牌”真正的安全永远要落在服务端。6. 实操总结与这类项目的扩展思路写到这里“实战五”这个项目的主体工作已经说完了。总结一下我对一个未知前端防护系统的分析路径大体分为四步先看网络请求和页面加载结构判断防护节点的位置再加载JS执行并拦截关键变量的变化定位动态cookie的生成时机接着补全缺失的浏览器环境最后回归HTTP协议本身确认所有请求头、请求体的完整性。结合这次实战我有个体会瑞数这类动态防护真正让人头疼的不是某一个加密算法有多强而是它把“环境校验、代码混淆、行为检测”三件事深度融合成了一套完整的闭环。单独看任何一个环节都能拆掉但只要它们协同工作就会让自动化工作变得异常脆弱——这里报个错那里缺个值整个流程就断了。这也是为什么我在前面反复强调“补环境”的重要性因为这套机制的大部分判定权重都压在了环境的一致性上。如果后续还想深化这个方向可以做几个扩展一是将这套补环境逻辑迁到Puppeteer里以隐身模式运行利用真实浏览器内核降低环境模拟的复杂度二是研究一下瑞数的WebSocket通道加密特征看它与普通HTTP请求的防护判定差异三是尝试在合法授权站点上做一次“模拟真实攻击者”的渗透测试完整地记录下从访问页面到提取数据全流程的攻击时间曲线。这些内容会比单纯“跑通cookie”更有价值因为它在真正回答“什么程度的自动化访问会触发风险”。给打算入坑这个方向的朋友一个建议先别着急去啃大站的深度混淆可以先在本地搭一个简单的Node服务人为给接口加上几层动态cookie校验自己练习“从JS里找出生成逻辑”这个过程。等把自己搭的靶场跑通了再回到真实业务场景里做合法授权测试效率会高很多。这条路是我走了不少弯路才摸索出来的希望对你们有用。
返回列表