1. 项目概述:当瑞数6遇上逆向,一场攻防的硬仗
最近在安全研究和数据爬取圈子里,瑞数动态安全(特别是其6代版本,常被称为6vmp)成了一个绕不开的“硬骨头”。无论是做业务安全测试,还是需要自动化处理一些前端数据,你大概率会碰到它。简单来说,瑞数6vmp是一套部署在浏览器端的前端安全解决方案,它的核心目标就两个:第一,让你没法轻易调试它的JavaScript代码(反调试);第二,把你关心的业务数据(比如表单提交内容、API接口参数)用一套动态变化的算法加密得面目全非(数据加密)。你看到的可能是一堆乱码或者一个长得离谱的token,而服务器却能正确解密。我们的目标,就是逆向这套机制,搞清楚它“加锁”和“藏钥匙”的方法。
这活儿为什么有挑战性?因为它不是简单的Base64编码或者固定AES密钥。瑞数6vmp的JavaScript代码通常被高度混淆、压缩,并且夹杂了大量用于检测调试环境、计算环境指纹的“陷阱”代码。一个不小心,你的调试器就可能被检测到,导致页面卡死、代码逻辑跳转或者直接抛出错误。更棘手的是,其加密算法(如生成cookie或请求参数中的xxx-um、xxx-cp等字段)的核心逻辑往往与浏览器环境特征(如navigator属性、canvas指纹、WebGL渲染结果)动态绑定,每次刷新页面,密钥或盐值都可能变化。
所以,这个“实战”不仅仅是写几行Python还原算法。它是一场系统的工程,涉及浏览器行为分析、JavaScript动态调试技巧、代码还原与理解,以及最终将浏览器中运行的逻辑“翻译”成独立可执行的代码(如Python或Node.js)。接下来,我会拆解整个逆向流程中的关键环节和实战技巧,这些经验大多来自真实的对抗场景,希望能帮你少走弯路。
2. 逆向环境搭建与核心工具选型
工欲善其事,必先利其器。对抗瑞数6vmp,你的武器库需要精心挑选。一个稳定、隐蔽且功能强大的调试环境是成功的基石。
2.1 浏览器与调试器:以Chrome DevTools为核心
主浏览器推荐使用Google Chrome的稳定版或开发者版。其内置的DevTools功能最为全面和稳定。有几个关键设置需要调整:
- 禁用缓存:在Network面板勾选“Disable cache”,确保每次都能加载最新的、未缓存的混淆代码。
- 停用Source Map:在Settings -> Preferences中,确保“Enable JavaScript source maps”是取消勾选状态。瑞数的代码不会提供Source Map,开启它可能干扰调试。
- Overrides功能:这是重中之重。在Sources面板,点击左侧的“Overrides”选项卡,选择一个本地文件夹作为覆盖目录。然后,在Page标签中找到瑞数的主JS文件(通常名字包含
vmp、security或是一长串哈希值),右键选择“Save for overrides”。之后,你对该文件所做的任何修改(如打日志、下断点、代码格式化)都会保存到本地,并且浏览器会优先加载你修改后的版本,即使刷新页面也有效。这避免了每次刷新都要重新定位代码和打补丁的麻烦。
注意:瑞数会检测常见的开发者工具打开行为。直接按F12可能会触发反调试。一个更隐蔽的方法是,先打开一个空白标签页,再打开DevTools,然后将地址栏导航到目标页面。或者,使用
chrome.exe --remote-debugging-port=9222命令行启动浏览器,然后通过独立的调试客户端(如VS Code)连接进行调试。
2.2 辅助调试与Hook工具:Frida与Tampermonkey
- Frida:这是一个动态插桩工具,可以注入脚本到浏览器进程,拦截和修改函数调用。在对抗反调试时非常有用。例如,你可以用Frida脚本来Hook
console.log的原生实现,防止瑞数通过检测console对象的变化来发现调试器;或者HookDate.now、performance.now来扰乱其对代码执行时间的检测。编写一个基础的Frida脚本(.js文件)来覆盖关键检测点,能极大降低调试难度。// 示例:Hook setTimeout,防止反调试通过延迟检测 setTimeout = function (func, delay) { // 如果发现是瑞数可能用于反调试的延迟函数,直接跳过或修改延迟 if (delay > 5000) { // 假设长延迟可能是反调试陷阱 console.warn('[Anti-Anti] Intercepted a long setTimeout:', delay); delay = 10; } return original_setTimeout(func, delay); }; - Tampermonkey(油猴脚本):用于在页面加载早期注入一些辅助代码。比如,你可以写一个脚本,在瑞数主JS加载之前,就重写
Object.defineProperty方法,防止它通过定义getter来检测document或window对象的属性访问(这是一种常见的反调试手段)。油猴脚本加载早,能抢占先机。
2.3 代码分析与还原工具
- AST(抽象语法树)解析库:对于高度混淆的代码,手动阅读是噩梦。使用像
Babel、Esprima这样的JavaScript解析器,可以将代码转换成AST。然后你可以编写脚本,对AST进行遍历和转换,比如:还原十六进制或Unicode编码的字符串;将复杂的三元表达式a?b:c展开成if-else;扁平化控制流(这是瑞数混淆的常见手段,将顺序执行的代码打乱成switch-case跳转)。这是一个进阶技能,但能从根本上理清代码逻辑。 - 本地Node.js环境:当你最终将加密算法还原后,需要在浏览器环境外执行。Node.js是最佳选择。你需要模拟出浏览器中特定的对象和行为,比如
window、navigator、document,甚至canvas的toDataURL方法。jsdom库可以帮助你构建一个虚拟的DOM环境。
3. 反调试机制的识别与绕过实战
瑞数6vmp的反调试手段多样且层层嵌套。我们需要系统性地识别并中和它们。
3.1 常见反调试技术盘点
调试器检测:
console.log输出劫持:检测console.log是否被重写或调用栈深度。debugger语句:在代码中插入无尽的debugger;语句,一旦DevTools打开就会不断断下。Function构造函数重写:通过重写Function构造函数的toString方法,检测函数代码是否被格式化(调试器常做格式化)。Performance API时间差:利用performance.now()计算两段代码执行的时间差,如果时间过长(因为打了断点),则判定为调试状态。
环境指纹差异检测:
navigator属性:检查navigator.webdriver、navigator.plugins.length、navigator.languages等。普通浏览器和自动化工具(如Selenium、Puppeteer)这些属性值不同。window属性:检查window.outerHeight、window.outerWidth。DevTools打开时,窗口尺寸会变化。Chrome特有属性:检测window.chrome对象下的某些属性是否存在或为特定值。
代码执行流混淆:
- 控制流扁平化:这是VMP(虚拟化保护)的核心之一。将原本顺序执行的代码块,封装成一个巨大的
switch语句,通过一个“分发器”来跳转执行。静态分析几乎无法看懂逻辑。 - 代码动态生成:使用
eval、Function构造函数或setTimeout/setInterval动态执行代码片段,增加静态分析的难度。
- 控制流扁平化:这是VMP(虚拟化保护)的核心之一。将原本顺序执行的代码块,封装成一个巨大的
3.2 针对性绕过策略与实操
面对上述技术,我们的策略是“先隐藏,后对抗”。
第一步:隐藏调试痕迹(被动防御)在打开目标页面前,通过Frida或油猴脚本预先注入代码,覆盖关键检测点。
- 处理
debugger语句:重写Function.prototype.constructor,在生成函数时过滤掉debugger关键字。// 油猴脚本示例 const originalFunction = Function.prototype.constructor; Function.prototype.constructor = function(...args) { const body = args[args.length - 1]; if (typeof body === 'string' && body.includes('debugger')) { console.log('[Anti-Anti] Removed debugger statement from Function constructor.'); args[args.length - 1] = body.replace(/debugger;?/g, ';'); } return originalFunction.apply(this, args); }; - 固定环境指纹:在页面脚本执行前,锁定
navigator.webdriver为false,并固定window.outerHeight/Width的值。注意,有些属性是只读的,需要使用Object.defineProperty在对象创建早期就进行定义。// 使用Object.defineProperty定义只读属性为所需值 Object.defineProperty(navigator, 'webdriver', { get: () => false });
第二步:主动断点与代码追踪(主动分析)当基础反调试被抑制后,就可以开始下断点了。关键断点位置:
- 网络请求发起处:在
XMLHttpRequest.prototype.send和fetch上设置断点。当加密后的数据被发送时,调用栈会引导你找到加密函数。 - 加密函数入口:通常,加密函数名也是混淆的,但可以通过搜索特征字符串来定位。例如,在代码中全局搜索
JSON.stringify、encodeURIComponent、CryptoJS(如果用了)或者加密后数据的前几个字符(如/、i等Base64常见开头)。找到后,在该函数入口打上条件断点。 - 时间戳/随机数生成处:加密密钥常与时间戳或随机数有关。在
Date.now、Math.random上设置断点,观察哪些调用是加密流程的一部分。
第三步:处理控制流扁平化这是最耗时的部分。当你跟入一个加密函数,发现里面是一个巨大的switch块,变量名都是_0x1a2b3c这种格式时,就需要用到AST工具了。
- 将这段代码保存为本地文件。
- 使用
Babel编写一个还原脚本。核心思路是:- 识别出控制流“分发器”变量(通常是一个自增或经过运算的索引)。
- 遍历
switch的所有case,将每个case块中的代码提取出来。 - 根据“分发器”变量的初始值和变化规律,模拟执行,得到
case块的真实执行顺序。 - 将按顺序排列的
case块代码拼接起来,替换掉原来的switch结构,得到可读性更高的线性代码。 这个过程可能需要多次迭代,因为混淆可能是多层的。一个实用的技巧是:不要试图一次性还原整个文件。先通过动态调试,精确锁定包含加密核心逻辑的那一小段代码(可能只有几十行),只对这部分进行AST还原,效率会高很多。
4. 数据加密算法逆向与还原
绕过反调试后,我们终于可以直面核心——加密算法。瑞数的加密不是固定的,但通常遵循一个模式:收集环境指纹 -> 生成动态密钥/盐 -> 使用标准或魔改的加密算法加密数据。
4.1 算法定位与参数捕获
假设我们找到一个提交登录请求的接口,其请求体里有一个data字段,值是eyJ...(类似JWT的长字符串)。我们的目标是找到生成这个data的函数。
- 堆栈追踪法:在
XMLHttpRequest.send或fetch处断住后,查看JavaScript调用栈(Call Stack)。从栈顶往下找,排除jquery.min.js、axios等库文件,找到第一个属于目标网站域名下的、文件名可疑的(如包含vmp、security)栈帧。点击跳转过去,通常就离加密函数很近了。 - 日志注入法:如果调用栈不明显,或者函数被多层封装。我们可以利用之前设置的“Overrides”,在疑似加密的函数体开头,手动添加
console.log语句,输出函数的参数、this对象以及关键中间变量。刷新页面,在Console中观察输出,就能清晰看到输入(明文)和输出(密文)的对应关系,以及中间的计算过程。// 在Overrides的文件中,找到疑似函数,添加日志 function _0xabcde(inputData) { console.log('[Encrypt] Input:', inputData); // 打印入参 console.log('[Encrypt] This:', this); // 查看上下文 var _0x12345 = someOperation(inputData); console.log('[Encrypt] Middle var:', _0x12345); // 打印中间变量 // ... 更多操作 var finalResult = _0x67890; console.log('[Encrypt] Output:', finalResult); // 打印结果 return finalResult; }
4.2 算法分析与模拟实现
通过日志,我们可能发现加密流程如下:
- 将输入对象
JSON.stringify。 - 获取当前时间戳
t和window的一些属性(如navigator.userAgent)拼接成一个字符串s。 - 对字符串
s进行MD5或SHA256哈希,取前16位作为密钥key。 - 使用
AES-CBC模式,以key为密钥,一个固定或动态生成的iv(初始化向量),对步骤1的字符串进行加密。 - 将加密后的二进制数据进行
Base64编码,可能还会进行URL安全的替换(+/变成-_)。
还原步骤:
- 提取关键函数:将日志中观察到的所有操作对应的函数代码块,从混淆的源文件中提取出来。注意函数的依赖关系,有些工具函数(如
MD5、AES的实现)可能在其他地方定义,需要一并提取。 - 构建独立执行环境:在Node.js中,创建一个新的
.js文件。将提取出的所有函数代码复制进去。由于这些代码可能依赖浏览器环境(window、document),我们需要在文件开头模拟它们:// simulate_browser_env.js global.window = global; global.navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...' }; global.document = {}; // 如果代码使用了`btoa`/`atob`,Node.js没有,需要polyfill global.btoa = (str) => Buffer.from(str).toString('base64'); global.atob = (b64) => Buffer.from(b64, 'base64').toString(); - 补全缺失逻辑:混淆代码可能会缺失一些标准算法的实现(如
CryptoJS),但它可能只使用了其中的一小部分。我们可以直接用Node.js内置的crypto模块来替换。例如,发现它调用了CryptoJS.AES.encrypt,我们就在还原代码中,用crypto.createCipheriv('aes-128-cbc', key, iv)来替代。 - 验证与调试:用一组已知的明文和密文(从浏览器日志中获取)来测试你的还原代码。如果输出不一致,使用
console.log逐步对比浏览器环境和Node.js环境中每一步的中间结果,找出差异点。常见的差异来源:字符串的编码(UTF-8 vs Latin1)、字节序、随机数iv的生成方式、时间戳的精度。
4.3 处理环境绑定的动态密钥
最复杂的情况是密钥与浏览器环境强绑定,例如密钥来源于canvas.toDataURL()的哈希值。这意味着每次浏览器实例、甚至每次页面加载生成的密钥都不同。
- 解决方案1:完全模拟环境。在Node.js中,使用
canvas库(如node-canvas)来绘制同样的图形,生成相同的dataURL。这需要你精确分析浏览器中绘制canvas的代码,包括绘制的文本、字体、渐变等所有细节,并完全复现。 - 解决方案2:Hook并固定输出。这是更取巧的办法。在浏览器调试时,找到生成环境指纹(如
canvas指纹)的函数,将其Hook,让它返回一个固定的值。这样,无论环境如何变化,密钥都固定了,便于我们分析和后续的稳定复现。在油猴脚本或Frida中实现这个Hook。 - 解决方案3:密钥外置。如果业务允许,可以将每次动态生成的密钥(或生成密钥的种子)作为请求的一部分(例如放在另一个请求头里)发送给服务器,服务器用同样的逻辑生成密钥来解密。逆向时,我们只需要关注密钥的生成逻辑,而不必在无头环境中完美复现
canvas渲染。
5. 常见问题排查与稳定性优化
即使算法还原成功,在长期运行中也会遇到各种问题。这里记录一些典型的“坑”和解决思路。
5.1 请求失败与签名过期
- 现象:模拟的请求偶尔成功,经常返回
403或签名错误。 - 排查:
- 时间戳同步:检查加密算法是否使用了服务器时间戳。你的本地时间可能与服务器有较大偏差。解决方案是从服务器响应头(如
Date)获取时间,或者直接请求一个时间戳接口,用服务器时间来计算加密参数。 - 随机数
nonce/iv:确认iv是每次请求随机生成,还是从某个环境值推导而来。如果是随机的,你的模拟代码也必须每次请求随机生成。确保随机数生成器的强度足够(使用crypto.randomBytes)。 - 请求参数顺序:有些签名算法要求对请求参数按字典序排序后再拼接。检查浏览器中实际发送的参数顺序和你模拟的是否一致。使用浏览器Network面板仔细查看
Payload或Query String Parameters的原始顺序。
- 时间戳同步:检查加密算法是否使用了服务器时间戳。你的本地时间可能与服务器有较大偏差。解决方案是从服务器响应头(如
5.2 环境检测升级导致脚本失效
- 现象:稳定运行了一段时间的脚本突然全部失效。
- 排查与应对:
- 对比代码快照:定期保存瑞数主JS文件的副本。失效时,用文本对比工具(如
Beyond Compare)对比新旧版本,查找检测点的新增或修改。常见的升级点:增加新的navigator属性检测(如navigator.mediaDevices)、使用新的Performance API(如PerformanceObserver)、修改控制流扁平化的模式。 - 更新Hook脚本:根据差异,更新你的Frida或油猴Hook脚本,覆盖新的检测点。
- 降低特征:考虑使用更底层的浏览器自动化工具,如
CDP(Chrome DevTools Protocol)直接驱动无头Chrome,并通过CDP命令在页面上下文中提前执行反检测脚本,这比Selenium等工具更隐蔽。
- 对比代码快照:定期保存瑞数主JS文件的副本。失效时,用文本对比工具(如
5.3 性能瓶颈与优化
- 现象:模拟加密过程太慢,尤其是需要计算复杂环境指纹(如
canvas)时。 - 优化:
- 缓存计算结果:对于在一定会话期内(如一个Cookie存活期内)不变的环境指纹,计算一次并缓存起来,后续请求直接使用缓存值。
- 算法移植:将JavaScript核心算法用
Python的C扩展或Go、Rust等高性能语言重写。对于纯计算的哈希、加密操作,性能提升可达数十倍。 - 并行处理:如果业务需要高并发,使用异步IO或线程池来并行执行加密计算,避免阻塞。
5.4 问题速查表
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
请求返回403 Forbidden | 1. 签名错误 2. 请求头缺失(如 Referer,User-Agent)3. Cookie失效或格式错误 | 1. 核对加密每一步的中间值 2. 对比浏览器请求的完整Headers 3. 检查Cookie获取和携带逻辑 |
无限debugger循环 | 反调试的debugger语句未被清除 | 检查并加强Function构造函数和eval的Hook |
| 算法还原后,结果前几位对,后面不对 | Base64编码/解码的填充(padding)或URL安全替换问题 | 对比浏览器中加密后字符串的原始形态和你的Base64结果,注意=填充和+/字符 |
Node.js环境中window未定义 | 还原的代码依赖浏览器全局对象 | 在文件开头模拟global.window = global;并补充缺失的API(如btoa) |
| 动态密钥每次不同,无法模拟 | 密钥绑定canvas等动态环境指纹 | 尝试Hook浏览器中的指纹生成函数,或使用node-canvas库精确复现绘制逻辑 |
逆向瑞数6vmp是一个持续对抗的过程,没有一劳永逸的方案。核心思路是动态分析为主,静态分析为辅。优先在真实的浏览器环境中,利用调试工具观察代码的运行时行为,而不是试图去阅读那堆面目全非的混淆代码。将大问题拆解成小问题:先解决反调试,再定位加密函数,最后还原算法。过程中保持耐心,细致地记录每一个变量、每一个步骤,就像在解一个复杂的动态谜题。当你成功构造出第一个能被服务器接受的合法请求时,那种成就感会让你觉得所有的折腾都是值得的。最后记住,所有的分析和还原工作应仅用于安全研究、授权测试和个人学习,务必遵守相关法律法规和服务条款。