声明:本文仅供安全技术学习研究之用,请勿用于任何违法违规用途。爬取数据前请遵守相关法律法规及网站的 robots.txt 协议。
一、背景知识
1.1 什么是瑞数(Ruishu)
瑞数是国内主流的反爬防护产品之一,其核心特点是:
- VMP(Virtual Machine Protection)虚拟化保护:将真实逻辑编译为私有字节码
- 动态参数生成:每次请求附带动态变化的校验参数
- 浏览器指纹检测:采集 Canvas、WebGL、字体等特征
1.2 什么是国密 SM4
SM4 是中国国家密码管理局发布的分组加密算法,特点:
- 分组长度:128 位
- 密钥长度:128 位
- 加密模式:ECB、CBC、CTR 等
- 标准 FK 常量:
0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dd
二、技术架构分析
2.1 多层防护体系
| 层级 | 类型 | 表现 | 难度 |
|---|---|---|---|
| 反爬层1 | 瑞数6代 VMP | 每次 AJAX 请求 URL 附带动态参数 cw2qEsfh |
★★★★☆ |
| 反爬层2 | 滑块验证码 | 响应 errorCode=501 时弹出滑块 |
★★☆☆☆ |
| 反爬层3 | SM4-ECB 响应加密 | 所有 API 响应均为加密 Base64 字符串 | ★★★☆☆ |
| 反爬层4 | 图片混淆字段 | 关键字段以 Base64 PNG 图片返回,需 OCR | ★★☆☆☆ |
| 反爬层5 | IP 频控封锁 | 高频访问触发 1-3 小时 IP 封锁 | ★★☆☆☆ |
2.2 请求流程示意
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 页面加载 │ ──→ │ JS 初始化 │ ──→ │ 动态参数 │
│ │ │ (VMP执行) │ │ 生成 │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌─────────────┐ ┌─────────────┐ ┌──────▼──────┐
│ 数据展示 │ ←── │ SM4 解密 │ ←── │ API 请求 │
│ │ │ (前端执行) │ │ (带Token) │
└─────────────┘ └─────────────┘ └─────────────┘
三、VMP 防护原理
3.1 代码虚拟化技术
VMP 将原始 JavaScript 逻辑编译为自定义字节码:
javascript
// 原始代码(开发者编写)
function calcToken() {
return hash(fingerprint + timestamp);
}
// VMP 保护后(外部可见)
if($_ts.cd){(function(_$ca,_$kn){var _$Jy=_$ca(/* 字节码数据 */)...}
3.2 动态 Hook 机制
VMP 运行时会对原生 API 进行 Hook:
javascript
// 示意:Hook XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
// 自动追加动态参数
const dynamicToken = vmRuntime.generateToken();
url = appendParam(url, 'token', dynamicToken);
return originalOpen.call(this, method, url, ...args);
};
3.3 为什么难以逆向
| 难点 | 说明 |
|---|---|
| 字节码私有 | 指令集不公开,静态分析无法还原 |
| 运行时解密 | 关键逻辑在内存中动态解密执行 |
| 环境绑定 | Token 与浏览器指纹强关联 |
| 频繁更新 | 脚本文件哈希和逻辑定期更换 |
四、SM4 加密分析
4.1 算法识别方法
通过查找标准常量确认算法身份:
javascript
// SM4 FK 常量(国密标准)
const FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dd];
4.2 密钥提取思路
混淆代码常用的字符串数组保护:
字符串数组洗牌混淆的工作原理:
- 所有关键字符串放入一个数组
- 程序启动时对数组执行若干次 shift/unshift 操作
- 直到某个校验函数返回
true,数组位置稳定 - 之后所有字符串通过索引引用,静态看不到明文
javascript
// 原始形式
var _0xabc = ['encrypt', 'decrypt', 'key123...'];
// 执行时动态重组
while (checksum !== targetValue) {
_0xabc.push(_0xabc.shift()); // 数组轮转
}
// 稳定后通过索引引用
window['decryptFunc'] = _0xabc[0xb7]; // 获取解密函数名
window['encryptFunc'] = _0xabc[0xd1]; // 获取加密函数名
4.3 响应结构
加密响应通常为 Base64 编码的密文:
原始响应:k/++tB6BUKnPxpG5ijoqgZHVsBc9JSk5ZCf7FaDVDLivFmOy80SE...
↓ Base64 解码
密文字节:[0xf5, 0x8a, 0x3f, 0xbe, ...]
↓ SM4-ECB 解密
明文 JSON:{"ok": true, "data": {...}}
五、方案演进历史(学习记录)
以下记录本人在学习过程中,从失败到理解原理的探索历程,供读者参考技术思路。
接口分析
目标网站:某企业进出口信用信息公示平台
目标接口
POST http://credit.customs.gov.cn/ccppserver/ccpp/queryList?cw2qEsfh=<动态token>
Content-Type: application/json
{ "manaType": "C", "apanage": "", "depCodeChg": "", "curPage": 1, "pageSize": 20, "checkCode": ""}
baseUrl 构造方式
从 common.js 逆向得出:
javascript
// webserver 页面 → ccppserver 接口
baseUrl = location.href.split('ccppwebserver')[0] + 'ccppserver/'
// 结果:http://credit.customs.gov.cn/ccppserver/
响应结构
响应体为 SM4-ECB 加密后的 Base64 字符串,解密后得到:
json
{
"ok": true,
"data": {
"totalCount": 1341,
"totalPages": 1341,
"curPage": 1,
"copInfoList": [
{
"socialCreditCode": "b@se&64:<base64图片>",
"fullName": "b@se&64:<base64图片>",
"creditLvl": "C",
"customsCodeName": "b@se&64:<base64图片>",
"creditLvlModfTime": "b@se&64:<base64图片>"
}
]
}
}
注意 :
totalPages字段值实为总记录数(totalCount),实际总页数需自行计算:
实际总页数 = ceil(totalCount / pageSize)
5.1 方案一:纯 HTTP 请求(理解局限)
尝试思路:
- 直接构造 POST 请求,手动设置 headers 和 cookies
- 期望通过静态参数获取数据
遇到的问题:
- ❌ 无法生成合法的动态 Token(VMP 实时计算)
- ❌ 缺少服务端验证所需的 Cookie
学习收获 :
理解到 VMP 防护的核心在于"动态"和"环境绑定",静态参数无法复用。
5.2 方案二:自动化浏览器工具(被检测)
尝试思路:
- 使用 Playwright/Selenium 驱动 Chromium
- 拦截页面响应,在 Python 端进行 SM4 解密
python
# Playwright 方式(片段)
from playwright.async_api import async_playwright
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=False)
page = await browser.new_page()
page.on('response', on_response) # 拦截响应
await page.goto(PAGE_URL, wait_until='networkidle')
await page.evaluate('window.queryLostcredit()')
遇到的问题:
- 被反爬检测 :瑞数检测到自动化工具特征
navigator.webdriver === true- CDP 注入留下的标记
- 解密失败 :Python 端 SM4 解密出现字节错误(首字节非预期值)
- 可能原因:密钥与浏览器会话绑定
- 可能原因:响应存在额外编码层
- IP 封锁:触发防护规则,临时限制访问
错误提示示例:
出错啦!该操作已触发系统访问防护规则,请于1-3小时后重试。
学习收获:
- 理解浏览器自动化检测的常见特征
- 认识到客户端与服务端解密环境的差异
- 了解 IP 频控策略的存在
5.3 方案三:更换自动化工具(部分进展)
尝试思路:
- 改用基于原生 CDP 协议的工具(如 DrissionPage)
- 使用本地 Chrome,减少自动化特征注入
python
from DrissionPage import ChromiumPage, ChromiumOptions
opts = ChromiumOptions()
opts.set_argument('--disable-blink-features=AutomationControlled')
page = ChromiumPage(addr_or_opts=opts)
page.listen.start('ccpp/queryList', method='POST') # 监听接口
page.get(PAGE_URL)
page.run_js('window.queryLostcredit()')
packet = page.listen.wait(timeout=25)
body = packet.response.body # 获取响应体
取得进展:
- ✅ 页面正常加载,绕过瑞数检测
- ✅ 成功获取加密响应(Base64 格式)
仍有问题:
- ❌ Python 端 SM4 解密依然失败
- 密文解密后首字节不符合预期,说明存在环境绑定
学习收获:
- 不同自动化工具的检测风险差异
- 加密数据与执行环境的强关联性
SM4解密
用真实浏览器访问,接口正常返回,但 Body 是:
k/++tB6BUKnPxpG5ijoqgZHVsBc9JSk5ZCf7FaDVDLivFmOy80SE...
75736 个字符的 Base64 字符串。不是 JSON,不是任何可读格式。
浏览器里的 JS 会解密这段数据,然后渲染到页面。分析其形成原因,该部分数据通过压缩到一行的 SM4 实现,带有字符串数组洗牌混淆。
模拟这段逻辑,用 Node.js 跑:
javascript
// 模拟洗牌过程
var arr = [/* 原始字符串数组 */];
while (true) {
var checksum = computeChecksum(arr);
if (checksum === 0xa65f4) break; // 目标校验值
arr.push(arr.shift()); // 每次移动一个元素
}
// 共执行 122 次,数组稳定
稳定后,从数组中提取关键信息:
| 索引 | 内容 |
|---|---|
0xb7 |
解密函数名 MuData_KXC |
0xd1 |
加密函数名 CaData_KXC |
0x121 |
SM4 密钥:11HDESaAhiHHugDz |
Python 端解密尝试(失败)
python
from gmssl.sm4 import CryptSM4, SM4_DECRYPT
import base64
SM4_KEY = b'11HDESaAhiHHugDz'
def sm4_decrypt(raw: str) -> str:
cipher_bytes = base64.b64decode(raw.strip())
sm4 = CryptSM4()
sm4.set_key(SM4_KEY, SM4_DECRYPT)
plain = sm4.crypt_ecb(cipher_bytes)
pad_len = plain[-1]
if 1 <= pad_len <= 16:
plain = plain[:-pad_len]
return plain.decode('utf-8') # ← 这里报错
运行报错:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf5 in position 0
解密后第一个字节是 0xf5,不是合法 JSON 的起始字符 {。
说明 Python 拿到的响应体,和浏览器 JS 拿到的,不是同一份内容。可能存在额外编码层,或者密钥存在会话动态变化。
5.4 方案四:JS Hook 思路(理解原理)
核心思路转变 :
不再尝试"绕过"或"破解",而是:
"让浏览器自身完成加解密,通过合法方式观察结果"
原理示意图:
┌─────────────────────────────────────────┐
│ 浏览器环境 │
│ ┌─────────┐ ┌─────────┐ │
│ │ VMP │───→│ 生成 │ │
│ │ 运行时 │ │ Token │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ └──────────────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ API 请求 │ │
│ │ (自动带Token)│ │
│ └──────┬──────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ SM4加密 │ │
│ │ 响应数据 │ │
│ └──────┬──────┘ │
│ ↓ │
│ ┌─────────┐ ┌─────────────┐ │
│ │ Hook │←───│ 原生解密 │ │
│ │ 注入点 │ │ 函数执行 │ │
│ └────┬────┘ └─────────────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 记录明文 │ ← 学习观察点 │
│ │ (仅本地测试)│ │
│ └─────────────┘ │
└─────────────────────────────────────────┘
Hook前端注入解密
javascript
// 在页面 JS 初始化完成后注入
var _orig = window['MuData_KXC'];
window['MuData_KXC'] = function() {
var decrypted = _orig.apply(this, arguments); // 原函数正常执行
try {
var parsed = JSON.parse(decrypted);
if (parsed && parsed.hasOwnProperty('ok')) {
window.__queryResult = parsed; // 把结果存到全局变量
window.__queryDone = true;
}
} catch(e) {}
return decrypted; // 原样返回,页面渲染不受影响
};
Python 端等待 __queryDone 变为 true,然后读取 __queryResult:
python
# 等待 JS 解密完成
for _ in range(25):
if page.run_js('return !!window.__queryDone'):
break
time.sleep(1)
# 读取已解密的数据
result_json = page.run_js('return JSON.stringify(window.__queryResult)')
result = json.loads(result_json)
不需要知道密钥,不需要理解 SM4,浏览器替你做了所有加解密工作。
技术理解:
- 浏览器环境已完成所有加解密工作
- 解密函数存在于页面 JS 上下文中
- 通过合法 Hook 技术,在数据渲染前观察结果
关键认知:
- 不解密算法:复用浏览器已加载的 SM4 实现
- 不伪造 Token:让 VMP 自动生成合法参数
- 不绕过验证:在验证通过后的数据层观察
学习收获:
- 理解前端加密的边界
- 掌握 JS Hook 技术的合法学习用途
- 认识到"逆向"的本质是理解原理,而非破解利用
六、方案对比总结
| 维度 | 方案一 HTTP | 方案二 Playwright | 方案三 CDP工具 | 方案四 Hook |
|---|---|---|---|---|
| 浏览器 | 无 | 有(带特征) | 有(原生) | 有(原生) |
| 瑞数绕过 | ❌ | ❌ 被检测 | ✅ | ✅ |
| 解密位置 | Python | Python | Python | 浏览器 |
| 解密成功 | ❌ | ❌0xf5错误 | ❌0xf5错误 | ✅ |
| 核心问题 | 无Token | 有特征+密钥错 | 密钥绑定 | - |
七、内容混淆识别
7.1 图片混淆原理
部分字段以 Base64 图片形式返回:
json
{
"fieldName": "b@se&64:iVBORw0KGgoAAAANSUhEUgAA..."
}
识别特征:
- 前缀标识:
b@se&64: - 编码格式:Base64
- 图片类型:PNG
7.2 OCR 识别方案
python
import ddddocr
import base64
def parse_image_field(value: str) -> str:
"""解析图片混淆字段"""
if not value.startswith('b@se&64:'):
return value
img_bytes = base64.b64decode(value[8:])
ocr = ddddocr.DdddOcr(show_ad=False)
return ocr.classification(img_bytes)
总结
核心知识点
- VMP 防护:虚拟化技术保护核心逻辑,静态分析困难
- SM4 加密:国密算法,通过标准常量可识别
- 浏览器特征:自动化工具需关注指纹一致性
- Hook 技术:在合规前提下用于学习理解
学习历程的启示
| 阶段 | 认知 | 方法 | 结果 |
|---|---|---|---|
| 初期 | 以为参数固定 | 直接请求 | ❌ 失败 |
| 中期 | 意识到动态生成 | 自动化工具 | ❌ 被检测 |
| 后期 | 理解环境绑定 | 原生浏览器 | ⚠️ 部分成功 |
| 最终 | 接受不可破解性 | 原理学习 | ✅ 理解本质 |
重要认知:安全防护设计的目的是让攻击成本远高于收益。作为学习者,理解原理比"成功绕过"更有价值。
学习资源
其他参考学习文档
【JS逆向系列】某海关公示平台分析_loadaesdecryptstr-CSDN博客
实战13-瑞数6(补环境) - 导弹* - 博客园
瑞数实例小记------某海关失信企业网址小记_瑞数6 加密的网站-CSDN博客