ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

瑞数6代JSVMP动态Cookie逆向:从412到200的攻防拆解

瑞数6代JSVMP动态Cookie逆向:从412到200的攻防拆解 1. 项目拆解从412到200到底在做什么1.1 412和200一场浏览器与服务端的握手先说结论这个项目要解决的事情就是让一个被风控拦截的请求从返回412的状态变成正常返回200的状态。中间最关键的一环是搞清楚瑞数6代通过JSVMP生成的动态cookie是怎么来的、服务端又是怎么验证它的。412这个状态码全称是Precondition Failed翻译过来叫前置条件失败。放在瑞数6代的场景里含义非常直白服务端不认识你。它觉得这个请求不是来自一个正常的、可信的浏览器环境所以不给数据而是扔回一段动态JS要求客户端先把这段代码跑完生成一个带加密信息的cookie然后再带着这个cookie重新发起请求。只有服务端验签通过才愿意返回200。所以整个从412到200的过程本质上就是一次动态cookie的生成与重放。它不是简单地改个请求头就能解决的需要完整理解瑞数6代的验证链路。这篇博文我就按自己实操的路径把cookie生成、JSVMP执行、服务端校验、以及踩坑修复的完整过程一步步拆开来讲。1.2 瑞数6代和JSVMP难啃的骨头瑞数6代是瑞数信息的动态安全产品市面上很多网站都在用。它和传统WAF不太一样传统WAF靠规则库做拦截瑞数靠动态两个字做防护。它每次下发到浏览器的JS代码可能都不一样cookie的字段名、加密算法、执行流程都可能动态变化。这意味着你今天分析出来的结果明天可能就失效了。JSVMP全称是JavaScript Virtual Machine Protection中文可以理解成基于JavaScript虚拟机混淆的保护方案。它把原本能看懂的JS代码转换成一套自定义的字节码指令然后再通过一个解释器去逐条执行这些指令。你直接在源码里看到的不是逻辑清晰的业务代码而是一个个指令分发循环、虚拟栈、寄存器数组。这种方案对静态分析极度不友好你打断点也断不到真正的逻辑位置因为你看到的执行过程是解释器在跑而不是业务代码在跑。瑞数6代把JSVMP和动态cookie结合得很紧密cookie不是简单加密生成的而是在一个动态变化的虚拟机环境里通过几千条甚至上万条指令算出来的。想靠肉眼逆向基本不可能。1.3 这个项目的最终目标与适合人群我做这个项目时的目标是不依赖任何人写的现成工具从零开始把瑞数6代的cookie生成与验证流程拆到自己能讲清楚原理、能复现、能排查问题的程度。最终也确实从一上来就吃412做到了带cookie请求稳定返回200。如果你是对前端安全、风控对抗、反爬策略感兴趣的前端开发者或安全测试同学这篇文章应该能帮你省不少时间。另外如果你自己在做网站安全防护想理解像瑞数这类动态防护产品的实现思路这篇文章同样值得一看。前提是你得会用浏览器开发者工具、会抓包、知道cookie的基本机制。如果这些还不熟建议先把下一节的内容看一遍我特意把cookie的基础知识放在了前面。2. 先把cookie的几个基础知识点补齐2.1 从cookie中文聊起它到底存的是什么网上经常有人搜cookie中文这个词其实cookie没有特别高深的中文翻译它最初源于魔饼那个典故现在大家习惯直接叫cookie或者叫小型文本文件。本质上它是服务端下发到浏览器、由浏览器保存的一小段数据。每次浏览器向同一个域名发起请求时会自动带上这些数据。cookie最常见的用途是维持登录态。你登录一个网站服务端验证账号密码通过后会在响应头里通过Set-Cookie下发一段标识浏览器存下来下次请求自动带过去服务端一看这个标识就知道这个人已经登录过。这就是cookie登录的基本原理。结合这个项目来说瑞数6代的动态cookie做的其实是类似的事它不发账号密码而是把浏览器环境信息、时间戳、加密签名打包进cookie用来证明我是一个正常浏览器不是脚本。2.2 cookie和session的区别客户端状态与服务端状态的协作cookie和session的区别也是被问烂的问题。一句话总结cookie存在客户端session存在服务端。cookie是浏览器帮忙保管和携带的数据session是服务端为某个会话临时保存的内存数据或缓存数据。两者经常配合使用。服务端首次收到请求时创建一条session记录同时下发一个session id写到cookie里。浏览器后续请求带上这个session id服务端据此找到对应的session数据。但如果服务端做了多机部署、没有共享session存储A机器创建的session在B机器上找不到就会出现明明登录了换个节点就掉线的问题。所以大型系统一般会把session挪到Redis这类公共存储里。回到瑞数6代它有一个很特殊的地方服务端几乎不依赖传统session来验证这一步。它把验证信息直接编码在cookie里服务端拿到cookie后自己解密、验签。这样对服务端来说是无状态的压力小也不怕多机部署。理解这一点你才能明白为什么复现一个动态cookie就等价于通过了瑞数的验证。2.3 HttpOnly、SameSite与新版Chrome的cookie策略这块在调试里特别容易踩坑建议多花两分钟看一下。先说HttpOnly。这个是cookie的一个属性设置了HttpOnly之后浏览器里的JavaScript就无法通过document.cookie读取到这个cookie。很多网站会把登录态的cookie设为HttpOnly防止XSS脚本偷走。但对分析瑞数6代的人来说这意味着有些cookie你只能在开发者工具的网络面板里看到在Console里执行document.cookie是看不见的。如果你分析的时候只盯着document.cookie可能漏掉关键信息。再说SameSite。Chrome从80版本开始默认把没有设置SameSite属性的cookie当作Lax处理限制第三方上下文的携带。到了Chrome 90对第三方cookie的限制越来越严很多人搜索chrome98 无法携带cookie说的就是这类变化。实际表现是如果你在iframe里或者跨域场景下调试cookie可能不会按预期携带明明登录了请求却是未登录状态。这个坑不一定是瑞数造成的很可能是浏览器策略变了。另外设置cookie时还有Domain、Path、Expires、Secure这些属性每一项都会影响cookie在什么时候、什么场景下能被携带。分析动态cookie时务必在开发者工具的Application面板里查看完整属性而不是只看值。3. 瑞数6代cookie生成与验证链路拆解3.1 生成链路从首次请求到cookie落地先说生成链路我把它拆成五个步骤来理解。第一步客户端向目标站点的页面或接口发起首次请求。这个请求通常没有带瑞数要求的动态cookie或者带了但服务端不认。第二步服务端返回412状态码同时在响应内容里注入一段动态JS。这段JS不是简单的普通脚本而是经过JSVMP保护的、会在浏览器里执行的虚拟指令集合。有时候它是一段内联的script有时候是一个独立JS文件具体形式看目标站点的配置。第三步浏览器加载并执行这段JS。执行过程中JSVMP解释器会逐条解释指令期间会调用浏览器环境里的一堆API读取UA、屏幕分辨率、时区、Canvas指纹、WebGL信息、浏览器插件列表甚至检测当前是否处于调试状态、是否被Hook、是否存在自动化工具标记。这些信息都会被收集起来作为后续生成cookie的输入。第四步解释器执行到关键逻辑时生成一个加密字符串最终通过document.cookie写入到浏览器。写入的cookie名和cookie值通常都是一长串看不出含义的乱码。但注意有些cookie可能由服务端响应头直接设置有些由JS动态写入还有可能两个来源叠加分析时要分别处理。第五步页面加载完成页面里的后续请求会自动携带这个cookie。服务端验签通过后就不再返回412而是正常返回200。从开发者的角度看从第一次请求到最终200中间就是浏览器自动帮忙执行了一段代码整个过程通常发生在几百毫秒到一两秒内。肉眼很难感知但抓包时能看到清晰的412、JS加载、第二次带cookie请求、200这条链路。3.2 验证链路服务端到底在验什么服务端拿到带动态cookie的请求后不是简单查一下cookie在不在而是会做一串校验。校验内容我从实操经验里总结为三层。第一层是签名有效性。cookie值里通常包含加密签名段服务端用自己的密钥去解密验签如果篡改过cookie验签直接失败返回412。第二层是时间戳新鲜度。瑞数生成的cookie几乎都有有效期短的可能只有几十秒长的也不过几十分钟。超时后就算签名对也会拒绝。这也是很多人调试时发现cookie一会儿就失效的根本原因。第三层是环境指纹一致性。cookie里编码了UA、IP、浏览器特性等环境信息服务端拿到请求后会对比当前请求的实际环境信息和cookie里携带的环境信息是否一致。如果你生成cookie时用的UA是A真正发起请求时UA变成了B服务端比对不上照样拦截。除了这三层很多站点还会叠加行为验证。比如请求频率异常、同一个cookie短时间内大量并发、缺少正常的页面浏览路径都可能触发二次验证最常见的表现就是弹滑块或验证码。所以项目中控制频率和保持请求行为合理同样重要。3.3 JSVMP为什么让复现变得困难如果你之前逆向过普通JS混淆可能会觉得无非就是字符串加密加控制流平坦化慢慢抠总能抠出来。但JSVMP完全不是这个思路。普通混淆的代码最终还是要放到浏览器引擎里以原生方式执行你的断点、调用栈至少还能看。JSVMP则是把代码变成了自定义指令浏览器不会直接执行这些指令而是由一个虚拟机解释器去跑。真正的业务逻辑藏在一堆opcode、虚拟栈操作里你看到的指令流和实际逻辑之间隔了一层翻译层。更麻烦的是JSVMP通常会做这些事把字符串加密运行时才动态还原用while switch结构做指令分发整个文件就是一个巨大的循环检测你是否打开了开发者工具检测执行环境是非浏览器环境检测关键函数是否被Hook比如Function.prototype.constructor、document.getElementById等指令集本身可能动态变化每次请求拿到的脚本都不一样。这就导致抠代码出来本地跑这条路非常难走。你很难确定自己抠出来的逻辑是全的更难确定环境检测会不会在某个角落把你识别成非浏览器。这也是为什么网上一谈到瑞数很多人的建议都是别硬抠做环境复用。4. 实操全记录一步步从412拿到2004.1 抓包与初始确认第一步永远是抓包确认现象而不是上来就看JS。我用浏览器开发者工具打开目标站点的页面并在Network面板勾选Preserve log避免页面跳转或重放时把初始请求冲掉。刷新页面后观察到的现象是第一个请求返回412响应内容是一个HTML或者一段JS里面有一大段压缩混淆的脚本紧接着自动出现了第二个请求这个请求带上了新生成的动态cookie返回200。这说明浏览器已经把生成cookie并重放的过程自动执行完了我们要做的就是把浏览器自动执行的这段原理彻底搞明白。把这个初始响应完整保存下来命名为step1.html留作对照。后面分析脚本、对比cookie生成逻辑都会反复用到这个样本。建议顺手把响应头也保存下来因为服务端可能会在响应头里植入一些额外的标记或动态参数。4.2 定位cookie生成入口确认现象之后下一步是找到cookie到底在哪段JS里被生成。我用了两种办法。第一种办法在Sources面板里给document.cookie设置访问断点。具体做法是切到Sources找到右侧的Event Listener Breakpoints或直接在代码里搜索document.cookie赋值处不过JSVMP代码里直接看到document.cookie的概率不高所以更建议用第二种。第二种办法在Console里手动Hook写入操作注入一个监控函数把document.cookie的每次赋值都记录下来。大致思路是用Object.defineProperty重写cookie的setter在设置时打印调用栈和赋值内容。这样当动态脚本写入cookie时控制台会打印出写入的值以及触发写入的调用栈顺着调用栈就能定位到生成cookie的核心函数。实测下来第二种办法效率更高因为你能直接看到哪个函数、在哪个脚本、通过哪一串调用链写入了这个cookie。有了这个入口再往上游找参数来源往下游找加密算法就清晰多了。注意有些瑞数版本会检测document.cookie是否被改写一旦发现就会走反调试逻辑。如果Hook后请求出现问题可以先用无痕窗口、关掉部分插件再试或者改用条件断点观察。4.3 分析JSVMP指令流与关键调用定位到入口后真正的硬活才开始。打开核心JS文件你会看到一段结构非常拧巴的代码一个巨大的分发循环、一堆数组索引操作、大量连续赋值和位运算。直接通读完全不现实我一般按下面几步处理。第一步先找到解释器的主分发结构。JSVMP不管怎么变核心都是一个读取指令 - 根据opcode跳转 - 执行对应操作的循环。找到这个主循环和操作码列表就相当于拿到了一本指令字典。后面再看到某个opcode就知道它在执行什么类型的操作。第二步找到外部API的调用点。指令再虚拟最后总要落到底层浏览器API上比如设置cookie、读取navigator、操作canvas。这些调用点就是虚拟指令和真实浏览器之间的桥梁。我会在关键API处打断点或Hook观察虚拟指令执行到这一步时传入了哪些参数、产生了什么返回值。这比逐条去读指令流高效得多。第三步对比多个样本。我保存了不同时间段、不同UA条件下请求到的JS样本用diff工具做对比。如果只是指令排列顺序变了说明是动态生成的指令集如果某个环境检测代码块出现了说明这次下发更严格。通过对比能快速识别哪些逻辑是核心加密逻辑哪些是防调试干扰逻辑。这个过程会比较磨人我实际用了大约两个晚上才把主流程理透。中间无数次想直接放弃去网上找现成库但坚持下来之后对JSVMP的理解确实提升了一大截。4.4 在本地复现cookie生成理清逻辑之后就要在本地跑起来。我同时试过两条路线简单对比一下。路线A是用浏览器自动化工具直接加载目标页面等它自动生成cookie然后从浏览器上下文里取出cookie。这条线路最稳因为本质上你仍然让真实的浏览器执行了全部逻辑只是你不需要手工读取那段JS。我用这种方法很快拿到了有效cookie带cookie重放请求直接200。路线B是把JS抠出来在Node.js里用jsdom等库补环境。这条路线的难点在于你需要mock掉window、document、navigator、location、canvas、webgl等一系列浏览器API而且每个API的返回值都要和真实浏览器尽量一致。Canvas和WebGL指纹尤其严格差一个像素值或者一个错误码最终生成的cookie里编码的指纹字符串就不一样服务端验证直接失败。实操下来我的建议是如果只是想让请求正常返回200优先用路线A省时间、成功率也高如果是为了搞懂机制、做深度研究可以走路线B但一定要做好多次调试的心理准备。我自己是先用路线A打通整个流程再逐步过渡到更自动化的执行方式。4.5 验证请求结果并控制频率拿到cookie之后验证方法很简单把cookie拼到请求头里重放之前返回412的请求看状态码是不是变成200。我第一次验证的时候就翻车了。直接用curl带着cookie去请求结果还是412。排查后发现那个动态cookie不只是值对就行可能还要求特定的请求头顺序、特定的UA、甚至特定的TLS指纹。后来我换成和浏览器完全一致的请求头集合才稳定拿到200。另外一个血泪教训是频率控制。刚拿到200那会儿我写了个循环连续请求了十几次没过多久就触发了滑块。这说明即使cookie有效异常频率依然会被识别。实际项目中一定要在两次请求之间加上合理延时控制并发数量最好让请求节奏贴近真实用户操作。频率控制不是技术难点但做不好前面全白费。5. 高频问题与排查技巧实录5.1 生成cookie后仍返回412这是最容易让人崩溃的问题明明浏览器里能200我把同样的cookie拿过来却还是412。排查方向按优先级排列先确认UA能不能对得上。cookie里编码了UA信息你换一个UA发请求本质上跟在浏览器里改UA访问差不多服务端一比对就露馅。再确认IP有没有变动。cookie里可能绑定了IP或者绑定了IP的粗粒度信息跨IP使用会被判定异常。检查请求头。浏览器会自动带Accept、Accept-Language、Sec-Fetch-*等一大堆请求头这些头也参与了行为分析。手动请求时只带Cookie远远不够。最后确认时间。本地机器时间如果和真实时间偏差较大会导致cookie里的时间戳过期或看起来来自未来验签失败。排查时最有效的办法是抓一次浏览器内成功请求的完整header和顺序然后尽量原样复制。5.2 滑块或验证码突然介入本来请求得好好的突然返回一个包含滑块页面或者验证码页面的HTML说明风控已经注意到你了。触发验证码的原因很多请求频率过高、IP被标记、无头浏览器特征太明显、WebDriver检测被触发、cookie刚生成就去请求高价值接口等等。我的经验是先降频把并发降到几乎串行的程度再观察是否能恢复。如果仍然触发那就得检查执行环境有没有自动化标记。很多浏览器自动化工具都会暴露运行时特征比如navigator.webdriver为true或者Chrome启动参数里带了--headless。这些特征本身就是风控最关注的信号。需要特别说明的是验证码的定位是区分真人和脚本遇到验证码就停止高频请求这是合规的底线不建议去研究什么绕过验证码方向歪了风险也高。5.3 瑞数脚本更新后原方案失效瑞数6代是动态更新的可能你在本地调试好的逻辑第二天再跑就失效了。最典型的表现是以前能生成正确cookie今天生成的cookie带过去返回412或者cookie名都变了。遇到这种情况先不要慌。重新抓一份最新的JS样本和旧样本做对比。重点看三处差异指令集结构有没有调整、环境检测逻辑有没有新增、cookie生成算法有没有变化。如果是指令集调整可能需要更新你本地的执行流程如果只是cookie名变化那只需要同步一下配置。不要把整个流程写得太死。我的做法是把动态获取JS - 分析关键逻辑 - 执行生成cookie拆成独立模块哪个环节失效就单独替换而不是每次全部重写。5.4 本地和线上环境不一致本地跑得好好的一到服务器就412这是部署阶段最常见的坑。原因通常是服务器的IP段和本地不一样而目标站点对IDC机房IP的容忍度很低服务器时间没同步和真实时间差了几分钟甚至更多导致cookie时间戳校验不过服务器没有安装浏览器依赖库无头浏览器缺少字体、Canvas库、GPU支持生成的环境指纹就和真实浏览器不一致浏览器版本不一样。新版Chrome、老版Chrome对Cookie策略、对WebGL渲染、对同源策略的实现都有差异。还记得前面提到的Chrome新版cookie携带限制吗换成新版本部署后cookie行为可能会变。我的建议是部署前先在目标服务器上跑一遍浏览器环境的自检脚本逐项确认UA、时区、字体、Canvas指纹是不是符合预期。环境一致性是整个项目成败的关键这一节省不了。6. 复盘与扩展6.1 环境一致性是整个项目成败的关键整轮做下来我最深的感受是这个项目的难点不在逆向解密本身而在于让本地环境和目标环境完全对齐。你可以花很多时间读懂JSVMP指令但如果环境指纹对不上cookie就是无效的。反过来哪怕你对指令流理解得浅一些只要能让一个真实的浏览器自动化执行完整流程也一样能拿到200。所以做这类项目先把统一UA、统一浏览器版本、统一时区、统一IP出口这些基础配置做好收益远大于硬啃指令流。很多时候问题出在配置而不是算法。6.2 从分析视角看防守方的设计亮点作为一个常年做技术分析的人我也习惯从攻防视角去理解产品设计。瑞数6代让我印象最深的是动态二字。它不仅仅是把代码混淆一下而是把整个验证链路都做成动态的cookie名可以变、指令集可以变、编码方式可以变。这种设计让静态分析很难规模化复用也让写通用工具的成本极高。对做安全防护的团队来说这个设计思路值得参考。与其堆一堆静态规则不如把校验逻辑做成动态下发、一次一密。只要攻击方每次面对的都是不同形态的验证逻辑自动化攻击的成本就会成倍上升。6.3 后续还能往哪些方向延伸这次做完之后其实还有几个方向可以继续挖。一个是深入分析瑞数cookie里的字段结构搞清楚每一段字符分别编码了什么信息这对理解整个指纹体系很有帮助。另一个是研究服务端校验策略的切换逻辑比如什么情况下会从412升级到验证码这些阈值和策略往往比加密算法本身更能反映产品设计思路。还有一点这套分析方法不只对瑞数有效。现在很多动态防护产品的思路都在向客户端环境信任评估靠拢核心都是先采集环境信息、再动态生成令牌、最后服务端验签。把这个流程吃透了换一个品牌的产品你也能很快找到切入点。回头再看看最初那个从412到200的目标其实真正收获的不只是那个200状态码而是把一整条动态安全验证链路从头到尾摸清了。这比拿到一个能用的cookie值有价值得多。
返回列表