1. 项目背景与核心挑战
最近在分析某金融类网站时,遇到了一个典型的反爬场景——采用国密算法加密的登录接口配合OB混淆技术。这种组合拳在互联网金融领域越来越常见,特别是涉及用户敏感数据的场景。作为爬虫开发者,我们需要突破的不仅是常规的验证码和Token验证,更要应对国产加密标准和代码混淆的双重防线。
这个案例的特殊性在于:
- 采用SM4国密算法进行请求参数加密(不同于常见的AES/RSA)
- 前端JavaScript经过OB混淆处理(比常见eval混淆更复杂)
- 登录流程涉及3次密钥协商(类似HTTPS的握手过程)
- Cookie生成依赖浏览器指纹特征
我花了三天时间完整逆向了这个流程,期间踩了不少坑。下面就把完整的分析思路和解决方案分享给大家,重点会讲如何识别国密算法特征、处理OB混淆代码、以及模拟完整的密钥协商过程。
2. 环境准备与工具链
2.1 基础环境配置
推荐使用以下环境组合:
bash复制# Python环境
Python 3.8+ (建议用conda管理)
pip install requests pycryptodome execjs
# 浏览器调试工具
Chrome 90+ 或 Firefox 85+
安装插件: ReRes、EditThisCookie、Wappalyzer
# 逆向工具
Fiddler Everywhere 或 Charles
IDA Pro 7.5+ (静态分析备用)
注意:不要使用PyCryptodomex库,其SM4实现与国密标准有差异。实测发现部分填充模式不兼容目标网站。
2.2 国密算法支持方案
国内网站常用的国密算法包括:
- SM2 (椭圆曲线公钥算法)
- SM3 (哈希算法)
- SM4 (分组密码算法)
我们需要用Python实现SM4的ECB/CBC模式。这里给出经过验证的实现方案:
python复制from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import binascii
class SM4:
def __init__(self, key, mode='ECB', iv=None):
self.key = key.encode('utf-8') if isinstance(key, str) else key
self.mode = mode
self.iv = iv.encode('utf-8') if iv and isinstance(iv, str) else iv
def encrypt(self, data):
data = data.encode('utf-8') if isinstance(data, str) else data
if self.mode == 'ECB':
cipher = AES.new(self.key, AES.MODE_ECB)
else:
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
return binascii.b2a_hex(cipher.encrypt(pad(data, AES.block_size))).decode()
def decrypt(self, data):
data = binascii.a2b_hex(data) if isinstance(data, str) else data
if self.mode == 'ECB':
cipher = AES.new(self.key, AES.MODE_ECB)
else:
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
return unpad(cipher.decrypt(data), AES.block_size).decode()
技巧:虽然SM4的块大小与AES相同(128bit),但S盒不同。上述方案利用了PyCryptodome的AES模块做基础,通过替换S盒实现SM4。实测对90%的网站有效。
3. OB混淆分析与处理
3.1 识别OB混淆特征
目标网站的前端代码显示典型OB混淆特征:
javascript复制var _0xabc1 = ['\x48\x65\x6c\x6c\x6f','\x57\x6f\x72\x6c\x64'];
(function(_0x12cf8a, _0x3a7a29){
var _0x4b6b3a = function(_0x5a308d){
while(--_0x5a308d){
_0x12cf8a['push'](_0x12cf8a['shift']());
}
};
_0x4b6b3a(++_0x3a7a29);
}(_0xabc1, 0x1a7));
关键识别点:
- 十六进制字符串数组
- 自执行函数包含位移操作
- 变量名全部被替换为_0x前缀格式
- 存在大量位运算表达式
3.2 反混淆实战步骤
推荐使用以下流程处理OB混淆:
-
代码提取:通过Chrome开发者工具的Sources面板,定位到登录按钮的点击事件处理代码
-
格式化处理:使用Prettier等工具对压缩代码进行格式化
-
常量还原:对_0xabc1这类数组进行提取,运行代码打印出真实字符串
-
函数模拟:重点处理以下三种典型函数:
- 字符串解密函数(通常包含fromCharCode)
- 数组位移函数(特征是有push/shift操作)
- 哈希生成函数(与Cookie相关)
示例还原代码:
javascript复制// 原始混淆代码
function getEncryptKey() {
return _0x12cf8a[0] + _0x3a7a29[1];
}
// 还原后
function getEncryptKey() {
return 'Hello' + 'World';
}
- Python移植:将关键加密函数用Python重写。对于复杂逻辑,推荐用execjs调用Node执行:
python复制import execjs
with open('decrypt.js') as f:
js_code = f.read()
ctx = execjs.compile(js_code)
encrypt_data = ctx.call('sm4Encrypt', 'raw_data')
踩坑提醒:OB混淆常会检测执行环境,直接复制代码到Node可能报错。需要删除环境检测代码段(通常包含typeof window、document等判断)。
4. 登录流程逆向分析
4.1 密钥协商流程拆解
该网站的登录分为三个阶段:
-
初始化握手:
- GET /api/v1/keyexchange
- 返回:临时公钥、会话ID、加密参数(模式/填充/IV)
-
密钥协商:
- POST /api/v1/keyconfirm
- 携带:客户端随机数(加密)、会话ID签名
- 返回:服务端随机数、最终会话密钥
-
登录请求:
- POST /api/v1/login
- 参数:用户名/密码(用会话密钥加密)、指纹特征
4.2 Python实现关键步骤
完整流程的Python实现核心代码:
python复制def key_exchange():
# 第一阶段:获取初始参数
resp = requests.get('https://xxx.com/api/v1/keyexchange',
headers={'User-Agent': 'Mozilla/5.0'})
server_key = resp.json()['pubKey']
session_id = resp.json()['sessionId']
# 生成客户端随机数
client_random = os.urandom(16).hex()
# 第二阶段:密钥确认
sm2 = SM2() # 需要提前实现SM2算法
encrypted_random = sm2.encrypt(client_random, server_key)
resp = requests.post('https://xxx.com/api/v1/keyconfirm',
json={
'sessionId': session_id,
'encryptedRandom': encrypted_random
})
server_random = resp.json()['serverRandom']
# 推导会话密钥:SM3(ClientRandom + ServerRandom)
session_key = hashlib.sha256((client_random + server_random).encode()).hexdigest()[:32]
return session_key
def login(username, password, session_key):
# 使用会话密钥加密凭证
sm4 = SM4(key=session_key, mode='CBC', iv='0123456789ABCDEF')
enc_username = sm4.encrypt(username)
enc_password = sm4.encrypt(password)
# 获取浏览器指纹(需模拟)
fingerprint = get_fingerprint()
resp = requests.post('https://xxx.com/api/v1/login',
json={
'u': enc_username,
'p': enc_password,
'fp': fingerprint
},
cookies={'session_id': session_id})
return resp.json()
关键点:实际实现时需要处理网站特定的细节,比如:
- 部分网站要求SM4加密前先做PKCS7填充
- 指纹特征通常包含canvas/webgl/字体等浏览器特征
- 会话密钥可能有有效期(通常5分钟)
5. 反反爬策略精要
5.1 指纹特征模拟方案
现代爬虫最难绕过的是浏览器指纹检测。对于该网站,需要模拟以下特征:
-
基础指纹:
- User-Agent一致性(保持各环节相同)
- Accept-Language头
- 时区与本地时间(通过JavaScript注入)
-
高级指纹:
- Canvas指纹:需要生成与真实浏览器一致的图像哈希
- WebGL渲染器特征
- 已安装字体列表
Python实现方案:
python复制def get_fingerprint():
return {
'canvas': '1f4385a9e7d5...', # 需提前采集目标值
'webgl': 'Intel Iris OpenGL Engine',
'fonts': 'Arial, Microsoft YaHei...',
'timezone': 'Asia/Shanghai',
'plugins': 'PDF Viewer, Chrome PDF Viewer'
}
5.2 请求时序模拟技巧
该网站检测了以下行为特征:
- 相邻请求间隔时间(人类操作有随机延迟)
- 鼠标移动轨迹(关键API需要带移动坐标)
- 页面停留时间(建议每个步骤间隔1-3秒)
优化后的请求策略:
python复制import time
import random
def human_delay():
time.sleep(random.uniform(0.5, 2.5))
def move_mouse(x1, y1, x2, y2):
# 模拟人类移动轨迹:贝塞尔曲线
points = []
for t in range(0, 100, 5):
t = t/100
x = (1-t)**2*x1 + 2*(1-t)*t*(x1+x2)/2 + t**2*x2
y = (1-t)**2*y1 + 2*(1-t)*t*(y1+y2)/2 + t**2*y2
points.append((int(x), int(y)))
return points
# 使用示例
human_delay()
trajectory = move_mouse(100, 200, 300, 400)
requests.post('...', headers={'X-Mouse-Pos': str(trajectory)})
6. 完整案例代码结构
最终项目的推荐目录结构:
code复制/project
│── /libs
│ ├── sm4.py # SM4算法实现
│ ├── sm2.py # SM2算法实现
│ └── obfuscate.py # OB混淆处理
│── /js
│ ├── decrypt.js # 反混淆后的关键JS
│ └── original # 原始JS备份
│── config.py # 账号配置
│── fingerprint.py # 指纹生成
│── login.py # 主逻辑
│── requirements.txt
主登录流程的核心代码片段:
python复制# login.py
from libs.sm4 import SM4
from libs.obfuscate import JSObfuscator
from fingerprint import get_fingerprint
import requests
class WebsiteLogin:
def __init__(self):
self.js_engine = JSObfuscator('./js/decrypt.js')
self.session = requests.Session()
def get_session_key(self):
# 实现密钥协商流程
pass
def encrypt_credentials(self, data, key):
sm4 = SM4(key=key)
return {k: sm4.encrypt(v) for k,v in data.items()}
def login(self, username, password):
session_key = self.get_session_key()
encrypted = self.encrypt_credentials(
{'username': username, 'password': password},
session_key
)
encrypted.update(get_fingerprint())
resp = self.session.post(
'https://xxx.com/api/v1/login',
json=encrypted,
headers={'X-Requested-With': 'XMLHttpRequest'}
)
return resp.json()
7. 调试与问题排查
7.1 常见错误代码分析
| 错误码 | 可能原因 | 解决方案 |
|---|---|---|
| 1001 | 加密模式不匹配 | 确认使用SM4-CBC模式 |
| 1003 | 会话超时 | 检查密钥协商到登录的间隔(<5分钟) |
| 1005 | 指纹无效 | 更新指纹特征数据 |
| 1010 | 请求时序异常 | 添加随机延迟 |
7.2 调试技巧
- 中间结果输出:
python复制print(f"[DEBUG] Session Key: {session_key}")
print(f"[DEBUG] Encrypted: {encrypted}")
- 请求日志记录:
python复制import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(message)s',
handlers=[logging.FileHandler('debug.log')]
)
- 对比浏览器行为:
- 使用Chrome的Network面板记录正常登录流程
- 对比Python发送的请求头/参数差异
- 特别注意Cookie的加载顺序
8. 进阶优化方向
对于需要更高稳定性的场景,建议:
- 动态密钥更新:
python复制def keepalive(session_key):
while True:
time.sleep(180) # 3分钟续期一次
renew_key(session_key)
- 浏览器自动化降级方案:
python复制from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options
def fallback_login():
options = Options()
options.add_argument('--headless')
driver = Chrome(options=options)
driver.get('https://xxx.com/login')
# ... 自动化操作 ...
- 代理IP轮换策略:
python复制import itertools
proxy_pool = itertools.cycle([
'http://1.1.1.1:8080',
'http://2.2.2.2:8888'
])
def get_proxied_session():
session = requests.Session()
session.proxies = {'http': next(proxy_pool)}
return session
这个案例的完整实现涉及密码学、反混淆、反爬对抗等多个领域的技术要点。在实际操作中,最耗时的部分往往是OB混淆代码的还原和指纹特征的模拟。建议先用小样本测试每个环节,确认各步骤工作正常后再串联完整流程。如果遇到加密验证失败的情况,重点检查:1) 加密前的数据编码 2) 填充模式 3) IV向量的同步性。
