瑞数6vmp逆向实战:从虚拟机入口到算法工程化落地 📅 发布时间:2026/9/16 20:56:10 👁 浏览次数: 说起瑞数这个反爬体系搞过JS逆向的同行应该都不陌生。早些年的瑞数5还相对友好虽然一样有动态令牌、cookie校验但至少代码结构还能靠AST和正则梳理出个七七八八。到了6代整套体系升级成 VMP 虚拟机保护方案业内俗称“瑞数6vmp”我印象里第一次接触是在某大厂的风控页面上打开Sources直接看到一段几千行的动态JS所有函数名全是短横线加数字运算过程全部落到一个巨大的字节码分发器里那一刻我才意识到之前那套“硬啃JS逻辑”的思路在6代面前基本行不通了。这篇文章不打算写成那种“复制粘贴即可跑”的教程倒卖文。我想从自己实际逆向国家企业征信系统页面时遇到的瑞数6vmp开始讲清楚这套虚拟机的入口怎么找、opcode怎么还原、控制流怎么脱混淆以及最后如何把算法从浏览器里抽出并工程化落地。无论你是刚接触瑞数的新手还是已经能过5代、正准备碰6代的进阶选手这篇都有些参考价值。1. 瑞数6vmp到底防的是什么加密链路的全貌1.1 动态令牌不是普通的cookie很多人以为瑞数就是生成一个cookie那么简单的反爬机制实际上它的核心逻辑远比“生成一个token”复杂。瑞数是一套动态安全防护方案它会先在你的浏览器里注入一段JS这段JS会采集浏览器环境信息UA、WebGL图形渲染结果、Canvas绘制内容、字体列表、时间戳偏移、鼠标轨迹特征等作为种子再经过大量运算生成一个动态令牌。服务器端会验证这个令牌的合法性包括令牌是否由真实浏览器生成、是否被篡改、生成时间是否合理、携带的指纹是否和当前请求环境一致。所以当你试图用requests直接请求目标页面时除非你手动带上曾经通过浏览器生成的cookie否则瑞数会在服务端检测到令牌缺失或异常直接给你返回一段用于生成新cookie的JS而不是你想要的数据。这个“动态”体现在两个层面每次会话生成的cookie值都不同而且同一会话内多次请求也可能刷新cookie生成cookie的JS代码本身也在变化每次请求返回的混淆代码几乎都不一样。这两个特性叠加导致网上很多“固定正则提取参数”的脚本在瑞数面前不堪一击。我见过不少初学逆向的同行拿着瑞数5时代的老脚本去跑瑞数6结果发现连生成cookie的入口函数名字都找不到了。原因很简单6vmp把核心算法用虚拟机保护了起来代码里真正执行的动作通过一串自定义字节码驱动而不是直接写在明文JS里。1.2 6代相比5代到底变化了什么如果你以前逆向过瑞数5你会有一个比较直观的印象代码虽然混淆得很厉害但你还是能找到一些有规律的结构比如生成长度固定的大数组、某个字符串拼接的算法模块、以及一串处理cookie赋值的逻辑。瑞数6代则直接把那段“看得见摸得着”的算法逻辑收进了一个自定义虚拟机体系里。我根据自己的分析经验把瑞数5代和6代的关键差异列了一个表对比维度瑞数5代瑞数6vmp核心算法形态普通JavaScript代码 字符串数组混淆字节码 虚拟机解释器控制流混淆数组乱序、函数分裂、花指令控制流平坦化 opcode分发反调试强度有但相对有限更强的环境检测检测到异常直接生成错误结果cookie更新频率相对稳定高频率动态变化依赖复杂状态逆向难度中等熟悉AST后较易处理高需要逆向虚拟机指令集更直白地说5代你面对的是“一段复杂但仍然是JS的算法”6代你面对的是“一个自定义的指令集解释器”。前者可以用语法分析工具强行梳理后者你得先搞清楚这个虚拟机的指令集长什么样然后才能谈还原算法。这也是为什么很多人都说“瑞数6vmp是个分水岭”一旦过了这道坎再去看其他家的VMP方案思路基本是通用的。1.3 分析前的准备环境在正式开搞之前环境准备这部分值得多说两句。我用的是 Node.js Puppeteer 来控制无头浏览器加载目标页面同时用 Chrome DevTools ProtocolCDP来断点调试和hook脚本。之所以选择 Puppeteer 而不是直接用本地 Chrome 手动调试是因为瑞数对“是否在真实浏览器环境运行”非常敏感。手动打开浏览器去页面调试当然可以但如果每次都要用手指去点断点人肉效率太低。通过 CDP 可以远程控制浏览器在关键代码位置自动下断点效率会高很多。另外我建议在你的调试环境里提前安装好以下工具Node.js 环境版本建议16以上配合Puppeteer使用Puppeteer最新版本Chrome DevTools Protocol相关的基础库puppeteer-core即可不需要额外引其他CDP库一个顺手的数据抓包代理工具比如Fiddler或者Charles方便比对请求和响应差异这些准备不是必要条件但能显著减少你“实际动手时才发现环境不对”的返工时间。2. 定位虚拟机入口绕过外围混淆的几条有效路径2.1 从cookie赋值点逆推是最稳的路径瑞数6vmp代码虽然极度混淆但它终究要在一个时间点把生成好的cookie写入document.cookie或者通过fetch/XHR的请求头把cookie带出去。因此定位cookie写入点是我个人认为最可靠的突破口。具体操作上我一般先在Chrome开发者工具里打开目标页面让页面刷新加载瑞数脚本然后执行以下几步在控制台输入document.cookie观察页面加载前后cookie的变化识别出瑞数生成的cookie名常见的有_xc、_xcd之类的命名在Sources面板中对cookie名进行全局搜索按CtrlShiftF找到所有对该cookie字符串的引用在所有引用位置下断点刷新页面观察哪一个位置实际触发了cookie赋值。只要断点命中你就是站在了vm入口的边缘。接下来要做的就是从当前调用栈向上回溯一层层脱离外围混淆壳最终找到那个真正开始处理字节码的函数。这个函数通常是整个vm的启动器在上一层的调用环境中你会看到类似runVm(vd, key, args)这种形态的调用当然变量名不会是明文这里是我按理解抽象出来的名字。2.2 hook关键APIs收集调用上下文直接下断点观察是一次性的如果刷新几次代码变了断点位置就会失效。更稳的做法是在关键API层面进行hook。瑞数6vmp的代码无论怎么变它最终必然要访问document.cookie、localStorage、canvas.toDataURL、WebGL的读取结果、以及各类存活时间相关API。你可以通过CDP的Page.addScriptToEvaluateOnNewDocument在页面加载最早期注入hook代码把这些最关键API的调用记录和返回值全部打印或者缓存起来。我在实际调试瑞数页面时最优先hook的是以下几个APIdocument.cookie的setter和getterlocalStorage和sessionStorage的存取接口canvas.toDataURL和canvas.getContext(2d)的调用结果navigator.userAgent等环境属性的读取Date.now()和performance.now()的时间戳结果为什么优先hook这些因为瑞数6vmp的cookie参数来源于环境指纹和时间戳而这些指纹提取一定建立在调用上述API之上。你只要把这些API的返回值和当前调用栈记录下来就能知道vm在生成算法时“吃了哪些输入”。有了输入和输出后续做纯算法还原时你就有了一张明确的对应表哪一段字节码对应读取了canvas指纹哪一段对应了UA特征拼接等等。值得注意的是hook代码要尽量隐蔽不要改变原始API的返回值和行为。我见过一些人在hook时图方便直接修改了返回值结果瑞数的环境检测瞬间就察觉到异常返回了错误数据——这种错误数据会让你误判自己的分析方向浪费大量时间。2.3 第一次靠近vm从大数组到dispatcher当你通过hook拿到调用栈后把断点设置到实际进入vm的那一层再单步进去你会看到vm内部的典型结构一串超长数组、一个指令指针、若干个寄存器/参数栈以及一个巨大的分发循环。大数组就是瑞数6vmp的“指令表”或者说“opcode存储区”。有些4、5代瑞数的数组可能只有几百个元素但6代的数组动辄几千个元素、每个元素有复杂的规律。6代的数组还有一个明显特征数组的内容在每次加载后都可能发生变化这意味着你无法写死某一组固定的opcode序列必须在运行时动态读取并解析。我自己的经验是不要在vs code或者文本编辑器里硬啃数组本身直接把数组dump下来交给脚本去分析其中的规律。比如统计每个opcode出现的频次、梳理opcode之间的跳转关系、找出哪些数组元素是数据而哪些是真正的指令。这一步听起来简单但在6vmp里是决定效率的关键。我在第一次逆向6代时就是因为过于相信自己的眼睛试图人工从几千个数字里找规律浪费了整整两天后来改成脚本统计后才真正开了窍。初次进入vm后你看不到任何有意义的函数名只会看到类似这样一个循环while (1) { opcode bytecode[ip]; switch (opcode) { case 1: // 入栈 stack.push(bytecode[ip]); break; case 2: // 出栈 a stack.pop(); b stack.pop(); stack.push(a b); break; // 大量case分支... } }当然真实代码里不会有这些case 1: 入栈的注释opcode的值也通常并不是直接从bytecode[ip]取出来就行可能会带上一些变长参数、可能带立即数混淆、可能通过一个间接跳转表来分发。但整体框架就是这样一个“取指令→解码→执行→跳回分发循环”的模式。只要你能识别出这个框架就已经成功了一大半。3. 虚拟机机制拆解opcode、指令处理和状态流转3.1 从数据流理解vm而不是从代码流理解不少人在逆向vm时第一反应是逐条读懂opcode的含义然后试图把那一段字节码的算法还原成能够阅读的代码。这个思路在指令集比较小的时候是可行的但6vmp的指令集通常非常庞大而且很多opcode的功能看起来高度相似实际上只是处理不同数据类型或不同上下文。我后来用了一个更高效的思路从数据流的角度去理解vm。具体来说vm在执行过程中会持续消费数组中的数据同时也可能会修改数组内部的部分元素作为临时变量。我先标记所有读和写的位置画出数据从哪里来、到哪里去然后反过来推测某段opcode的功能。这种做法比逐条翻译更快因为它符合一个基本事实瑞数生成cookie的核心算法并不复杂复杂的是它被千奇百怪的指令拆分成了无数个小块。你只要沿着数据流把小块串起来核心算法就会浮出水面。比如6vmp生成cookie时几乎必然存在一个“从环境中读取某些指纹数据→拼接原始数据→执行哈希或加密运算→生成最终cookie”的过程。你去vm字节码里直接找“加密算法”大概率什么也找不到但你按数据流梳理先找到指纹数据的来源点再追踪它经过哪些操作后变成最终输出整个过程就会非常清晰。3.2 dispatcher的还原核心操作是“找出opcode表的规律”在8.x时代还有不少逆向者试图直接抠出整个dispatcher代码放本地跑到了6vmp这个思路基本没戏。因为dispatcher的代码本身也是动态变化的你这次看到的分发循环下次加载可能完全不同。关键是找到opcode表的规律而不是copy某一份具体的实现。我在实际分析中总结了一套相对通用的“dispatcher识别流程”在cookie赋值断点处停止回溯到vm分配数组的位置设定条件断点在dispatcher循环的入口打印当前ip值和opcode值连续单步执行几十次记录下每次ip的变化轨迹观察ip的变化是否有特定模式是线性递增还是频繁跳转到某些固定位置统计opcode的取值分布尝试对opcode进行分类比如“数据搬移类”“算术运算类”“控制流类”“环境交互类”。做完这五步你对odopcode表的认识基本就成型了。接着可以做一件事把每条指令的参数长度也统计出来因为在可变长指令集里参数长度决定了你能否正确解码整个字节码流。如果这里解码错误后面的一切分析都白搭。这里有一个很重要的细节6vmp的odopcode表本身可能被“加盐”处理。也就是说实际使用的opcode值不是简单的1、2、3而可能是一个很大的数或者经过某个初始密钥异或后的值。你要在dispatcher里找到它是如何从原始字节码计算出最终opcode值的然后把这个“解密”逻辑同步到你的还原脚本里。3.3 控制流平坦化让还原后的代码变成可阅读的代码vm解密后你会发现虚拟机的字节码其实已经是对控制流平坦化的一种极端实现。你从cookie赋值点出发顺藤摸瓜找到一个又一个基本块但这些基本块之间的连接关系非常混乱if判断、跳转表、自增循环、异常跳转等等。想让代码变得可读需要做一次“控制流重整”。我常用的方法是先记录所有基本块然后分析每块结尾的跳转指令把真实目标恢复出来最后用工具画出一个简化版的控制流图。这个步骤会得到一个非常冗长的图但至少逻辑是清晰的。接下来你就可以做“指令折叠”了——把一组功能单一的连续指令合并成一个高层次的语义动作。比如连续几行都在做栈顶数据的加减运算那你就可以把它折叠成一个“累加器操作”。折叠得足够多之后原本几百步的字节码就变成了几十个操作。这几十个操作组合起来就是生成某个cookie子参数的算法。我说说实际逆向中的感受这一步是最枯燥但也是最出成果的。当那些本来像是天书一样的字节码折叠成类似hash.update(...); hash.digest()这样语义清晰的操作时整个vm的黑盒感会瞬间消失。后面的事就从逆向问题变成了普通的算法重写问题。4. 实战中的坑与完整排查链路4.1 环境检测UA、canvas、音频指纹一个都不能少如果说还原vm算法是“学走路”那过环境检测就是“学跑步”。瑞数6vmp不会只验证cookie本身它还会在生成cookie的过程里收取大量环境指纹并且在服务端进行交叉验证。也就是说就算你把vm算法原样还原生成出来的cookie看起来格式完全正确只要你的实际请求环境指纹和cookie里携带的指纹不符服务端一样会拒绝。我在实际调试中碰到过一个非常典型的案例我本机浏览器是Chrome 122UA里有一个特定版本的Sec-CH-UA头但我的调试脚本却误用了Puppeteer默认给的旧UA导致生成的cookie里携带的字段和实际请求头不一致。表面看cookie生成成功了发请求也返回了200但下一页就立刻拿到一个“安全验证不通过”的拦截页。后来我把UA统一后问题就消失了。所以在整个逆向过程中以下环境要素必须和实际请求严格保持一致navigator.userAgentnavigator.platformnavigator.languageWebGL渲染器的vendor和renderer字符串Canvas指纹包括字体渲染差异音频指纹AudioContext输出数据屏幕分辨率、色深、设备内存等硬件相关信息浏览器是否处于自动化控制状态尤其在最后一点上瑞数对webdriver、CDP连接、多线程调试等自动化特征有专门的检测。如果你用Puppeteer这类工具尽量加上--disable-blink-featuresAutomationControlled参数并且主动覆盖掉navigator.webdriver属性。4.2 补环境的陷阱为什么知道“该补什么”比“补什么”更难很多新手在过瑞数时喜欢“补环境”——也就是在Node.js里模拟出一整套浏览器API把vm跑起来。这种思路本身没有问题但难点在于你根本不知道哪些环境是瑞数真正会读取的。聪明的方案不是一股脑把所有浏览器API都实现一遍而是先通过hook环境API确定瑞数在运行期间到底消费了哪些值然后只针对性地提供这些值。我犯过的一个低级错误是我以为瑞数一定会读取canvas指纹于是优先补了canvas相关接口。但实际跑起来才发现瑞数在该版本的代码里根本没有读取canvas反而先去读了WebGL和字体列表。后来我再做瑞数项目时第一件事永远是先hook所有常见环境API观察并记录哪些被调用了、调用了多少次、参数是什么。基于这份记录来补环境效率会高得多。这背后其实也暴露了一个6vmp的设计思路它通过动态变化指令集让你无法用固定的“环境清单”来应对。今天它是依赖canvas指纹明天可能就换了另一种方式取指纹。所以真正有效的方案不是“补齐某套环境”而是“感知瑞数需要什么再去提供什么”。4.3 正则误判与超大数组泄漏问题还有一个我在瑞数6vmp实战中花费不少时间的坑就是正则匹配的误判。6代的代码里有一个很夸张的现象一个数组元素可能长达几千个字节里面既有看起来像指令的数字段也有大段十六进制编码的数据。如果你用粗糙的正则去提取数组很容易把多个元素切错或者把非数组内容当成数组最终导致dump下来的数据错误。我的解决方案是通过断点拿到数组对象的真实引用然后直接在浏览器控制台里调用JSON.stringify(arr)把数组完整序列化输出。这样拿到的数组是绝对可靠的。从浏览器控制台拿数据之后再保存在本地文件里后续所有脚本都基于这份dump来跑。每次页面刷新后数组可能变化所以每次分析时都要重新dump并保存好对应的上下文环境信息UA、页面URL、时间等方便后续排查问题。有一次我偷懒觉得自己已经分析清楚了直接用上一份dump去验证新生成的cookie结果两者不匹配。后来才发现那次是数组发生了变化而不是我的算法还原有误。所以在瑞数6vmp项目里保持“分析环境、dump数据、生成结果”三者的一致性是特别重要的工作习惯。5. 从算法到工程落地三种方案的选型对比5.1 纯算法还原高可控但维护成本高在把vm逻辑还原到一定程度后很多人会想把它转写成Node.js版本的纯算法模块。这个方案的最大好处是生成速度和稳定性都是最好的——完全脱离浏览器没有页面加载开销也没有自动化检测的风险。但缺点也很明显维护成本高。瑞数一旦更新算法或者改变指令集你就要重新分析一次。如果你打算走纯算法路线我的建议是模块化设计把环境指纹的采集、cookie各字段的生成、最终token的组装拆分成独立模块参数外部化把可能变化的部分比如UA、时间戳偏移、种子数组设计成外部可配置项写好自测用例每次改动后用同一组输入跑验证脚本确认输出和浏览器一致。我自己的项目里纯算法方案大概占了20%的代码量但它服务的是最核心的业务需求所以我愿意花时间维护。5.2 自动化浏览器方案最省心的“过检测”利器如果你不想纠结于彻底还原算法自动化浏览器方案是最直接的选择。热词里有“playwright过瑞数”其实就是把playwright或puppeteer当成浏览器环境让页面自己加载、生成cookie然后你从浏览器里把cookie取出来再交给其他业务逻辑使用。这种方式避开了对vm的逐条逆向是很多业务量不大的爬虫项目最常用的方案。这里有一种很成熟的操作方式用浏览器打开目标网站等瑞数完成cookie设置后你通过page.cookies()获取cookie然后交给requests或axios去请求实际接口。这样你既能拿到瑞数校验通过的cookie又不需要自己实现算法。关键是控制好页面加载和cookie生成之间的时间差太早获取cookie会导致生成不完整。playwright在过瑞数方面的优势在于它的自动化特征较少比puppeteer更容易通过瑞数的基础检测。但还是那句话瑞数6vmp的环境检测是动态的不能说某个浏览器自动化框架一定能100%过检测。做好UA、webdriver隐藏等基本操作仍然很有必要。5.3 RPC混合方案算法还原与浏览器执行结合的折中RPC方案是我在实际项目中用得最多的方案它的思路是用浏览器去执行瑞数的JS生成cookie但业务脚本不关注浏览器内部实现而是通过本地RPC接口获取最新cookie。这种方式既保留了纯算法方案的高性能又兼容了自动化浏览器方案的易维护性特别适合在分布式环境下使用。我用RPC方案时有过一个很深刻的教训在没有缓冲cookie的情况下每次请求都去实时调用RPC生成新cookie导致页面加载量大到被服务端限流。后来我加了一层缓存池预生成多个cookie放到队列里并设置合理的过期时间整体稳定性明显提升。方案开发效率性能维护成本风险点纯算法还原低高高算法更新时需重新逆向自动化浏览器高中低自动化检测可能拦截RPC混合方案中中高中需要维护浏览器的稳定运行选型时没有绝对优劣关键看你的业务体量和团队投入。如果只是几千条数据的小任务自动化浏览器方案完全够用如果是抖音、电商这类大流量采集那就值得投入做纯算法或RPC混合方案。5.4 工程落地中的常见排查流程最后分享一个我在工程化阶段反复使用的排查链路碰到了cookie失效、请求被拦截等问题时按这个顺序查基本都能定位到原因先确认cookie生成是否成功输出抓取到的cookie比对格式是否和浏览器完全一致确认cookie生成时间和请求时间差如果时间差过大可能触发时间窗口校验确认请求头环境UA、Accept、Accept-Language、Sec-CH-UA等头是否和cookie生成时一致确认IP的来源是否漂移如果cookie是在A IP下生成的请求却从B IP发出部分严格的服务端会拒绝确认自动化特征检查webdriver、CDP连接、控制台日志等是否暴露了自动化痕迹。这五步走下来80%的瑞数6vmp接入问题都能找到原因。剩下的一小部分往往是因为目标网站做了额外的风控策略比如行为频控、链路追踪等那已经不是单纯逆向能解决的范围了。我在实际跟进这些项目时最深的一个体会是瑞数6vmp的逆向不是“一次性知识”而是一套持续迭代的方法论。你每次遇到新版本都要重新走一遍“定位入口→分析odopcode→还原算法→落地工程”的流程。只有在第一次把这套流程完整走通之后遇到再大的加密变化也能快速适应。如果只是一遍遍地搜网上的现成脚本那永远只能跟在版本更新后面跑很被动。