1. 项目概述:当爬虫遇到Rabbit加密
在数据采集和逆向分析这个行当里,和加密算法打交道是家常便饭。最近几年,我在处理一些金融、社交和内容平台的爬虫项目时,发现一个叫“Rabbit”的加密算法出现的频率越来越高。它不像AES、DES那样广为人知,但在一些特定的、尤其是对实时性要求高的Web应用和移动端API里,却成了反爬体系里的“常客”。很多新手朋友在F12开发者工具里,看到网络请求中那一长串不知所云的密文,或者是在逆向JavaScript时遇到一个没见过的CryptoJS.Rabbit调用,往往就有点懵,不知道从何下手。
其实,Rabbit是一种流密码,属于对称加密算法家族。对称加密的意思很简单,就是加密和解密用的是同一把钥匙。这把钥匙,我们通常称为密钥。Rabbit的设计目标就是快,非常快,特别适合对大量数据进行实时加密解密,这正好契合了现代Web应用高频交互的需求。所以,当你发现目标网站的数据包不是常见的JSON明文,而是一堆乱码,并且其加密函数调用里带着“Rabbit”字样时,别慌,你很可能已经摸到了它的加密大门。
这篇文章,我就结合自己踩过的坑和实战经验,来详细拆解Rabbit加密算法。我会从它的原理、特点讲起,然后手把手带你用Python和JavaScript这两种爬虫逆向中最常用的语言,实现它的加密和解密过程。更重要的是,我会分享在真实爬虫逆向场景中,如何定位、识别并最终破解Rabbit加密的逻辑,包括密钥从哪里找、IV(初始化向量)如何获取这些核心问题。无论你是刚开始接触JS逆向的新手,还是想丰富自己加密算法工具箱的老手,相信都能从中找到实用的东西。
2. Rabbit加密算法核心原理与特点拆解
要逆向一个加密,首先得理解它。一知半解就去硬啃代码,往往事倍功半。
2.1 Rabbit是什么?流密码的核心思想
Rabbit算法是在2003年的FSE(快速软件加密)会议上被提出的。它被设计为一种高性能的流密码。这里需要先理解“流密码”和“分组密码”的区别,这对后续的逆向分析思路有直接影响。
你可以把分组密码(如AES)想象成一个粉碎机,它每次固定吃进去一块数据(比如128位),经过复杂的内部搅拌(置换和混淆),吐出一块同样大小的密文。数据如果不够一块,还得先填充。而流密码,更像是一个密码本生成器。它先根据密钥和IV,内部运转起来,生成一个近乎随机的、长长的“密钥流”。加密时,不是对数据本身做复杂变换,而是简单地将这个密钥流和你的原始数据(明文)进行按位异或(XOR)操作,得到密文。解密呢?完全一样,用相同的密钥和IV生成完全相同的密钥流,再和密文做一次XOR,就变回了明文。因为XOR运算有个美妙的特性:A XOR B XOR B = A。
所以,Rabbit的核心不是直接加密你的数据,而是生成那个关键的“密钥流”。它的内部有一个状态机,基于密钥和IV进行初始化,然后通过一个非线性函数不断迭代,更新内部状态,并从中提取出密钥流字节。这种机制使得它加密速度极快,因为主要的计算开销在密钥流生成阶段,而XOR操作是计算机底层的廉价操作。
2.2 算法特点与在爬虫中的典型应用
理解了流密码,Rabbit的这几个特点就很好懂了:
- 极高的速度:这是它最大的卖点。在软件实现上,Rabbit通常比AES等算法快得多,特别适合加密连续的数据流,如网络传输、实时通信。
- 密钥和IV:Rabbit需要一个128位(16字节)的密钥。同时,它还有一个64位(8字节)的IV(初始化向量)。IV的作用是确保即使相同的密钥加密相同的信息,只要IV不同,产生的密文也不同,这增加了安全性。在爬虫场景中,密钥往往是硬编码在JavaScript文件或APP源码中的,而IV则可能是一个固定值,或者由时间戳、随机数等动态生成,这需要逆向时仔细分析。
- 输出:Rabbit每次迭代可以产生128位的密钥流。在实际的API调用中,我们看到的密文通常是Base64编码后的字符串,或者直接是十六进制字符串。
在爬虫逆向中,你会在哪里遇到它?
- Web端:在网站的JavaScript代码中,你可能会发现类似
CryptoJS.Rabbit.encrypt(message, key, { iv: iv })的调用。CryptoJS是一个常用的前端加密库,它提供了Rabbit的实现。 - 移动端APP:在Android或iOS的逆向中,可能会发现使用Rabbit算法对请求体或响应进行加密的Native代码(C/C++)或Java/Kotlin、Objective-C/Swift的封装。
- 传输数据:登录的
password字段、查询请求的body、甚至是返回的列表数据,都可能被整体或部分用Rabbit加密。
注意:很多开发者会选择Rabbit,看中的就是它的轻量和快速,同时认为它比一些常见算法(如RC4)更“小众”,能增加逆向难度。但实际上,只要算法是公开的、对称的,并且密钥或生成逻辑可被获取,它就是可逆的。
2.3 与常见对称加密算法的对比
为了更清晰地定位问题,我们简单对比一下Rabbit和其他你可能更熟悉的算法:
| 特性 | Rabbit (流密码) | AES (分组密码) | DES/3DES (分组密码) | RC4 (流密码) |
|---|---|---|---|---|
| 密钥长度 | 128位 | 128, 192, 256位 | 56位 (DES), 112/168位(3DES) | 可变 (通常40-256位) |
| IV长度 | 64位 | 通常128位 (CBC等模式需要) | 64位 | 通常无 (或作为密钥一部分) |
| 加密模式 | 流加密 (CTR模式类似) | ECB, CBC, CFB, OFB, CTR等 | ECB, CBC等 | 流加密 |
| 速度 | 非常快 | 较快 | 慢 (DES) / 较慢(3DES) | 快 (但已被认为不安全) |
| 安全性 | 目前未发现严重漏洞 | 安全,行业标准 | DES已不安全,3DES逐渐淘汰 | 存在严重漏洞,已不安全 |
| 爬虫常见度 | 中等,特定场景 | 极高,非常普遍 | 较低,老旧系统 | 较低 (因不安全) |
这个对比能帮你快速排除选项。如果你在代码里看到固定128位密钥和64位IV,并且加密函数名包含“Rabbit”,或者性能要求很高,那基本就是它了。
3. 逆向实战:定位与识别Rabbit加密
理论说再多,不如动手干。现在,我们模拟一个最常见的场景:一个网页的登录或数据请求参数被加密了,你需要找到加密逻辑并复现它。
3.1 第一步:从网络请求入手,寻找加密痕迹
打开Chrome开发者工具(F12),切换到Network(网络)面板,勾选Preserve log。然后进行触发加密请求的操作,比如点击登录、搜索等。
- 观察请求负载:重点关注
XHR或Fetch请求。查看Request Payload或Form Data。如果里面不是清晰的username=xxx&password=123这样的键值对,而是一个像data=zqL8kF2a...==这样的字段,或者整个Payload是一串毫无规律的字符,那么它很可能被加密了。 - 查看响应内容:同样,如果服务器返回的
Response不是JSON或HTML,也是一堆乱码,那响应也可能被加密了。 - 搜索关键词:在开发者工具的
Sources(源代码)面板,按Ctrl+Shift+F进行全局搜索。关键词可以尝试:RabbitencryptCryptoJS(如果用了这个库)cipher- 你请求中那个加密字段的键名,比如
data - 可能存在的密钥的硬编码片段(虽然不常见,但可以试试如
key,secret,iv等)。
3.2 第二步:逆向JavaScript加密逻辑
假设我们在一个JS文件里搜索到了Rabbit。接下来就是仔细分析这段代码。
场景A:使用CryptoJS库这是最友好的情况。你可能会看到类似下面的代码:
// 引入CryptoJS(可能被混淆,但方法名通常保留) var CryptoJS = require("crypto-js"); // 或者直接使用全局的CryptoJS对象 function encryptData(data, keyStr, ivStr) { var key = CryptoJS.enc.Utf8.parse(keyStr); // 将UTF8字符串密钥转换成WordArray var iv = CryptoJS.enc.Utf8.parse(ivStr); // 同上,处理IV var encrypted = CryptoJS.Rabbit.encrypt(data, key, { iv: iv }); // 通常输出Base64字符串 return encrypted.toString(); } function decryptData(ciphertext, keyStr, ivStr) { var key = CryptoJS.enc.Utf8.parse(keyStr); var iv = CryptoJS.enc.Utf8.parse(ivStr); var decrypted = CryptoJS.Rabbit.decrypt(ciphertext, key, { iv: iv }); return decrypted.toString(CryptoJS.enc.Utf8); } // 调用示例 var myKey = "my-16byte-key!!"; // 注意:必须是16字节(128位)!这里长度刚好16字符(UTF8) var myIV = "8bytesIV"; // 必须是8字节(64位) var plainText = "Hello, World!"; var encrypted = encryptData(plainText, myKey, myIV); console.log("Encrypted:", encrypted);逆向要点:
- 找到密钥和IV:关键就是找到
myKey和myIV的值。它们可能是硬编码的字符串,也可能是通过某个函数动态生成的(比如从服务器获取,或由时间戳、用户ID等计算得出)。你需要顺着调用栈往上找,看这两个参数是怎么来的。 - 注意编码:CryptoJS通常要求密钥和IV是
CryptoJS.lib.WordArray类型。代码里常用CryptoJS.enc.Utf8.parse()将字符串转换过来。所以,你最终需要的往往是两个普通的字符串。 - 输出格式:加密后调用
.toString()默认输出Base64,也可能是.toString(CryptoJS.enc.Hex)输出十六进制。观察网络请求中的密文格式,与之匹配。
场景B:自定义实现或混淆严重的代码如果网站没有使用CryptoJS,或者代码被严重混淆(变量名变成a,b,c,逻辑被打乱),难度就大了。
- 跟栈:在Network面板中,找到那个加密的请求,右键点击,选择
Initiator(发起者)标签页,这里会显示调用栈。你可以一层层点击,跳转到发起这个请求的JavaScript代码处。加密逻辑通常就在这附近。 - 下断点:在可能包含加密操作的代码行设置断点,然后重新触发请求。当断点命中时,观察调用堆栈(Call Stack)、作用域(Scope)里的变量值。特别是那些被传入类似
encrypt、encode函数的参数。 - 搜索特征常量:Rabbit算法内部有一些固定的常量。如果代码是自定义实现,可能会包含这些常量的数值。你可以尝试搜索一些算法描述中提到的常量(虽然成功率不如直接搜“Rabbit”高)。
- Hook关键函数:如果代码动态加载或过于复杂,可以使用浏览器插件或Fiddler/Charles等抓包工具的自定义脚本功能,Hook
JSON.stringify、XMLHttpRequest.send或fetch函数,在数据发出前将其打印出来,直接看到加密前的原始对象。这是非常高效的一招。
3.3 第三步:验证猜想,确定算法
找到疑似密钥和加密函数后,需要验证。最直接的方法就是“抄作业”。
- 提取关键参数:从JS代码中提取出(或通过断点调试观察到)密钥字符串、IV字符串、待加密的明文。
- 本地复现:用Python或Node.js,按照你看到的逻辑(比如使用CryptoJS的Rabbit),用同样的密钥、IV和明文进行加密。
- 对比结果:将你本地加密的结果,与浏览器网络请求中发送的密文进行对比。如果完全一致,恭喜你,成功破译!如果不一致,检查以下几点:
- 密钥/IV的编码是否正确?(是不是多了一个换行符?)
- 明文是否完全一致?(可能JSON被压缩了,或者参数顺序不同)
- 加密后的输出格式是否一致?(Base64 vs Hex)
- 是不是还有其他的变换步骤?(比如加密后又进行了一次自定义的编码)
4. 核心环节实现:Python与JavaScript的Rabbit加解密
一旦逆向出密钥和逻辑,下一步就是在我们的爬虫程序中复现这个加解密过程。这里分别给出Python和JavaScript(Node.js环境)的实现方案。
4.1 Python环境下的Rabbit加解密实现
Python中没有一个像pycryptodome之于AES那样“标准”的Rabbit库。但我们可以用Crypto库(来自pycryptodome)中的一个较冷门的实现。
首先安装库:pip install pycryptodome
from Crypto.Cipher import Rabbit from Crypto.Util.Padding import pad, unpad import base64 def rabbit_encrypt(plaintext: str, key: bytes, iv: bytes) -> str: """ 使用Rabbit算法加密文本,返回Base64编码的密文。 注意:Rabbit是流密码,不需要对明文进行填充。 """ # 确保key是16字节,iv是8字节 if len(key) != 16: raise ValueError(f"Key must be 16 bytes long, got {len(key)}") if len(iv) != 8: raise ValueError(f"IV must be 8 bytes long, got {len(iv)}") # 创建Rabbit cipher对象 # Rabbit的MODE_CFB模式是常见的使用方式,但本质上它利用流密码特性。 # 实际上,pycryptodome的Rabbit通常直接用于流模式。 # 更常见的用法是将其作为流密码,直接加密字节流。 cipher = Rabbit.new(key=key, iv=iv) # 将明文转换为字节 plaintext_bytes = plaintext.encode('utf-8') # 加密。对于流密码,encrypt方法直接处理任意长度数据。 ciphertext_bytes = cipher.encrypt(plaintext_bytes) # 转换为Base64方便传输 ciphertext_b64 = base64.b64encode(ciphertext_bytes).decode('utf-8') return ciphertext_b64 def rabbit_decrypt(ciphertext_b64: str, key: bytes, iv: bytes) -> str: """解密Base64编码的Rabbit密文。""" if len(key) != 16: raise ValueError(f"Key must be 16 bytes long, got {len(key)}") if len(iv) != 8: raise ValueError(f"IV must be 8 bytes long, got {len(iv)}") # 解码Base64得到密文字节 ciphertext_bytes = base64.b64decode(ciphertext_b64) # 创建解密cipher对象(对称加密,加解密对象相同) cipher = Rabbit.new(key=key, iv=iv) # 解密 plaintext_bytes = cipher.decrypt(ciphertext_bytes) # 解码为字符串 plaintext = plaintext_bytes.decode('utf-8') return plaintext # ==================== 实战示例 ==================== # 假设我们从逆向中得到的密钥和IV是字符串 key_str = "my-16byte-key!!" # 16个字符的UTF-8字符串 iv_str = "8bytesIV" # 8个字符的UTF-8字符串 # 转换为字节。注意:必须确保字符串的UTF-8编码正好是16和8字节。 # 中文字符等会占用多个字节,需要特别注意。 key_bytes = key_str.encode('utf-8') iv_bytes = iv_str.encode('utf-8') print(f"Key bytes length: {len(key_bytes)}") # 应为16 print(f"IV bytes length: {len(iv_bytes)}") # 应为8 plain_text = "这是需要加密的敏感数据,比如password=123&username=admin" # 加密 encrypted = rabbit_encrypt(plain_text, key_bytes, iv_bytes) print(f"加密后的Base64: {encrypted}") # 解密 decrypted = rabbit_decrypt(encrypted, key_bytes, iv_bytes) print(f"解密后的明文: {decrypted}") print(f"加解密是否一致: {plain_text == decrypted}")Python实现的关键注意事项:
- 字节长度是硬性要求:Rabbit算法严格要求密钥为16字节(128位),IV为8字节(64位)。
pycryptodome的Rabbit.new()会检查这一点。如果你的密钥字符串用UTF-8编码后不是正好16字节,就需要处理,比如用空格补齐,或者用MD5等哈希函数将一个长密钥摘要成16字节(但这需要和前端逻辑完全一致)。 - 无填充模式:流密码不需要填充。所以不要对明文使用
pad函数。 - MODE问题:
pycryptodome的Rabbit实现可能没有像AES那样明确的MODE_ECB,MODE_CBC等。它通常以流密码模式工作。Rabbit.new(key, iv)是最常用的方式。如果遇到问题,可以查看库的官方文档。 - 编码一致性:这是爬虫逆向中最常见的坑。前端JavaScript的
CryptoJS.enc.Utf8.parse()和Python的str.encode('utf-8')必须确保对同一个字符串产生完全相同的字节序列。一个空格、一个不可见字符的差异都会导致加密结果天差地别。
4.2 Node.js环境下的Rabbit加解密实现
如果你习惯用Node.js写爬虫,或者需要完全复现前端逻辑,Node.js环境是更好的选择。我们可以直接用crypto-js这个和前端同源的库。
首先安装:npm install crypto-js
// rabbit_node.js const CryptoJS = require("crypto-js"); /** * 使用Rabbit加密(模拟前端CryptoJS行为) * @param {string} plaintext - 明文 * @param {string} keyStr - 密钥字符串(UTF8,长度需满足16字节) * @param {string} ivStr - IV字符串(UTF8,长度需满足8字节) * @returns {string} Base64编码的密文 */ function encryptWithRabbit(plaintext, keyStr, ivStr) { // 将字符串密钥和IV转换为CryptoJS需要的WordArray格式 const key = CryptoJS.enc.Utf8.parse(keyStr); const iv = CryptoJS.enc.Utf8.parse(ivStr); // 执行Rabbit加密 const encrypted = CryptoJS.Rabbit.encrypt(plaintext, key, { iv: iv }); // 将加密后的CipherParams对象转换为Base64字符串 return encrypted.toString(); } /** * 使用Rabbit解密 * @param {string} ciphertextBase64 - Base64编码的密文 * @param {string} keyStr - 密钥字符串 * @param {string} ivStr - IV字符串 * @returns {string} 解密后的明文 */ function decryptWithRabbit(ciphertextBase64, keyStr, ivStr) { const key = CryptoJS.enc.Utf8.parse(keyStr); const iv = CryptoJS.enc.Utf8.parse(ivStr); // 解密 const decrypted = CryptoJS.Rabbit.decrypt(ciphertextBase64, key, { iv: iv }); // 将解密后的WordArray转换为UTF8字符串 return decrypted.toString(CryptoJS.enc.Utf8); } // ==================== 测试 ==================== const key = "my-16byte-key!!"; // 16字符 const iv = "8bytesIV"; // 8字符 const originalText = "这是需要加密的敏感数据,比如password=123&username=admin"; console.log(`原始明文: ${originalText}`); console.log(`密钥字符串: ${key} (UTF8字节长度: ${Buffer.from(key, 'utf-8').length})`); console.log(`IV字符串: ${iv} (UTF8字节长度: ${Buffer.from(iv, 'utf-8').length})`); // 加密 const encryptedBase64 = encryptWithRabbit(originalText, key, iv); console.log(`\n加密结果(Base64): ${encryptedBase64}`); // 解密 const decryptedText = decryptWithRabbit(encryptedBase64, key, iv); console.log(`解密结果: ${decryptedText}`); console.log(`加解密是否一致: ${originalText === decryptedText}`); // 额外:如果需要输出十六进制格式 // const encryptedHex = CryptoJS.Rabbit.encrypt(originalText, key, { iv: iv }).toString(CryptoJS.enc.Hex); // console.log(`加密结果(Hex): ${encryptedHex}`);Node.js实现的关键注意事项:
- 库的一致性:确保使用的
crypto-js版本与目标网站可能使用的版本没有重大API变更。通常核心API很稳定。 - 参数格式:
CryptoJS.Rabbit.encrypt的第一个参数可以是字符串或CryptoJS.lib.WordArray。如果前端加密的是一个对象(如JSON),你需要先将其序列化成字符串(JSON.stringify),并且要确保序列化的结果(如空格、键序)完全一致。 - 输出格式:
.toString()默认输出Base64。如果网站用的是十六进制,就需要用.toString(CryptoJS.enc.Hex)。一定要和抓包看到的格式匹配。 - 调试利器:在Node.js中,你可以非常方便地逐行调试,并与浏览器端的加密结果进行比对,这是验证逆向逻辑是否正确的最可靠方法。
4.3 密钥与IV的获取与处理技巧
在实战中,密钥和IV很少会像示例中那样是简单的固定字符串。下面分享几种常见的处理情况:
硬编码在JS中:最简单的情况。在格式化、解混淆后的JS代码中直接搜索
key、secret、encryptKey等变量名,或者搜索类似"abcdefghijklmnop"的16位字符串。有时密钥会被拆分成几部分,然后用+拼接起来。由其他参数计算得出:
- 哈希衍生:密钥可能是某个固定字符串的MD5或SHA256哈希值的前16字节。例如
key = MD5("fixed_salt" + userId).substr(0, 16)。你需要找到这个固定盐值和用户ID的来源。 - 时间戳衍生:IV有时是当前时间戳(或经过某种截断、运算后的时间戳)。你需要在前端代码中找到获取时间戳的函数(如
Date.now()),并复现同样的计算逻辑。
- 哈希衍生:密钥可能是某个固定字符串的MD5或SHA256哈希值的前16字节。例如
从服务器端动态获取:这是比较棘手的一种。网站可能先发起一个
GET请求,从服务器获取一个“会话密钥”或“临时token”,然后用这个token作为后续请求加密的密钥。你需要分析第一个请求的响应,并找到后续请求是如何使用这个响应的。编码陷阱:
- Base64编码的密钥:有时你找到的密钥是一串Base64字符串(如
dGhpcyBpcyBhIDE2Ynl0ZSBrZXk=)。你需要先将其base64.decode()成字节,再作为密钥使用。在Python中可能是base64.b64decode(key_b64),在JS中可能是CryptoJS.enc.Base64.parse(key_b64)。 - 十六进制编码的密钥:类似地,
"6162636465666768696a6b6c6d6e6f70"这样的字符串,需要先将其从Hex解码为字节。
- Base64编码的密钥:有时你找到的密钥是一串Base64字符串(如
实操心得:遇到动态密钥时,不要只盯着加密函数本身。要像侦探一样,追踪密钥这个“变量”的生命周期。从它被定义、被赋值、被传递、被使用,一步步看下来。浏览器的“Sources”面板和断点调试是你最好的朋友。对于时间戳相关的IV,要特别注意时区和服务端时间同步的问题,有时需要加上或减去一个固定的时间差。
5. 常见问题排查与实战避坑指南
即使原理和代码都懂了,在实际逆向和复现过程中,还是会遇到各种稀奇古怪的问题。这里我整理了一个常见问题排查表,以及一些宝贵的避坑经验。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| 本地加密结果与浏览器不一致 | 1. 密钥/IV不一致(编码、空格、不可见字符)。 2. 明文不一致(JSON格式、参数顺序、空格)。 3. 加密模式或填充方式不对。 4. 输出编码不一致(Base64 vs Hex)。 | 1.逐字节对比:将浏览器中用于加密的密钥、IV、明文的字节形式(而不仅是字符串)打印出来,与本地生成的进行严格对比。在JS中用CryptoJS.enc.Utf8.parse(str).toString()看内部表示,在Python中用list(key_bytes)查看。2.最小化测试:用一个最简单的字符串(如 "test")进行加密测试,排除复杂明文的影响。3.Hook大法:在浏览器端Hook加密函数,直接打印其输入参数和输出结果,确保你看到的和你复现的输入完全一致。 |
解密时报错:ValueError: Incorrect IV length或类似 | IV长度不是8字节。 | 检查IV字符串的UTF-8编码长度是否为8。中文字符会导致长度超标。如果IV是动态生成的(如时间戳),确认其转换后的字节长度。可能需要截断或填充。 |
| 解密后是乱码 | 1. 密钥错误。 2. IV错误。 3. 密文在传输或处理中被修改(如URL编码/解码问题)。 4. 加密和解密使用的算法不是同一个(比如前端是Rabbit,你本地用了AES)。 | 1. 确认密钥和IV百分百正确。 2. 检查密文:Base64字符串在作为URL参数时,其中的 +和/可能被编码,需要先urldecode。确保传递给解密函数的是原始的、未损坏的密文。3. 用已知的明文-密文对进行验证,这是最有效的方法。 |
| 找不到Rabbit加密函数 | 1. 代码被严重混淆,函数名被改。 2. 使用了自定义实现的Rabbit,没有用CryptoJS。 3. 根本不是Rabbit加密。 | 1. 尝试搜索加密后密文的特征(如固定前缀),或搜索可能的关键词encrypt、encode、cipher。2. 使用“跟栈”和“下断点”的方法,定位到发起网络请求前的最后一步数据处理函数。 3. 分析密文长度与明文长度的关系。流密码的密文长度通常等于明文长度(无填充)。如果发现密文比明文长且是固定倍数,可能是分组密码(如AES-CBC)。 |
| 移动端APP的Rabbit加密 | 加密逻辑在Native层(so库或dex/jar中)。 | 1. 逆向难度大增。需要反编译APK,分析Java/Kotlin代码中调用Native方法的部分。 2. 使用Frida等动态插桩工具,Hook Native层的加密函数,直接打印参数和结果。 3. 如果密钥逻辑在Java层,相对容易,可以用Jadx等工具静态分析。 |
5.2 独家避坑技巧与心得
“字节级”思维:爬虫逆向加密,本质是让我们的程序能精确复现前端程序的字节级操作。任何一点差异——一个多余的空格、不同的换行符(
\nvs\r\n)、JSON字符串化时键的顺序——都会导致加密结果不同。养成用十六进制查看器或打印字节数组的习惯。从结果反推:如果正面强攻(找密钥)困难,可以尝试从结果反推。比如,如果你能控制一部分明文(比如用户名),可以尝试输入不同的值,观察密文的变化。流密码的特性是,相同的密钥流下,明文改变一位,密文对应位也会改变。这有时能帮你验证算法是否是流密码。
利用已知明文攻击(在合法范围内):如果你能通过正常操作,让前端加密一个你完全知道的内容(比如,在登录前,你知道它一定会加密
{"username":"test"}),那么你就得到了一个“已知明文-密文对”。你可以用这个对来暴力测试你猜测的密钥或算法(虽然不现实),或者更重要的,用来验证你逆向出来的逻辑是否正确。环境一致性:有些网站的加密逻辑会依赖浏览器环境,比如
navigator.userAgent中的某个值作为盐。这时,你的爬虫代码中的User-Agent头需要和调试时的浏览器保持一致。不要忽视IV:很多人找到密钥就以为万事大吉,结果栽在IV上。IV可能是固定的,也可能是变化的。如果是变化的(如时间戳),你必须在前端代码中找到生成IV的逻辑,并在爬虫中完全复现,包括可能存在的取整、除以1000等操作。
善用控制台:在浏览器开发者工具的Console中,你可以直接执行
CryptoJS的函数(如果网站加载了该库)。这是一个强大的验证工具。你可以把断点中抓到的密钥、IV、明文,直接在Console里调用CryptoJS.Rabbit.encrypt,看结果是否和网络请求中的一致。这能快速排除是密钥问题还是你的复现代码问题。
逆向Rabbit加密,就像解开一个设计精巧的锁。你需要耐心、细心和对细节的偏执。每一次成功破解,不仅意味着数据获取通道的打通,更是对你技术洞察力的一次提升。记住,核心永远在于理解前端代码的完整执行逻辑,并做到字节级别的精确复现。当你把浏览器的加密过程,像录像一样一帧一帧在本地重放出来时,就没有什么密文是不可解的了。