免责声明:本文仅供技术交流与安全研究使用,严禁用于任何非法抓取、商业牟利等破坏目标网站正常运行的行为。
在系列的第一篇文章中,我们通过环境伪装和无头浏览器技术,成功突破了 ZLibrary 的前端反爬盾,拿到了可以正常访问 HTML 页面的权限。然而,对于一个现代化的 Web 应用,核心数据(如搜索结果、书籍详情、下载链接)通常是通过 Ajax/XHR 异步加载的。
当你满怀欣喜地使用拿到的 Cookie 去请求 ZLibrary 的 API 接口时,却大概率会再次碰壁,收到 401 Unauthorized 或 400 Bad Request。打开网络面板一看,请求头中多出了一些奇怪的字段(例如 X-Signature、X-Timestamp、X-Req-Token),并且 POST 的表单数据竟然是一串毫无规律的乱码。
这就是我们本篇要攻克的堡垒:API 请求签名与参数动态加密机制。
一、 请求分析:寻找加密的蛛丝马迹
在浏览器控制台的 Network 面板中,我们抓取一个搜索请求。观察其 Request Headers 和 Payload。
特征 1:动态请求头 通常会包含时间戳和一个长度固定的哈希字符串(Signature)。
-
X-Timestamp:1711234567(当前的 UNIX 时间戳) -
X-Signature:a8f3b2...c9d1(看起来像是 MD5 或 SHA256) -
X-Nonce: 随机字符串,用于防止重放攻击 (Replay Attack)。
特征 2:参数加密 原本应该是 {"query": "python", "page": 1} 的 JSON 格式,变成了:
data:U2FsdGVkX1+9x/k...(典型的 AES 加密后的 Base64 编码,或者是自定义混淆格式)。
如果签名算法不对,或者参数没有正确加密,服务器直接在网关层就会将请求丢弃。
二、 逆向解析:抽丝剥茧找算法
要伪造这个请求,我们就必须找出生成 Signature 和加密 data 的 JavaScript 代码。
1. XHR 断点拦截
这是逆向 API 最有效的手法。在 Chrome DevTools 的 Sources 面板右侧,找到 XHR/fetch Breakpoints。添加一个包含请求 URL 关键字的断点(比如 /api/search)。 触发搜索操作,浏览器会瞬间停在发起 XMLHttpRequest.send() 或是 fetch() 的那行代码上。
2. 调用栈回溯 (Call Stack Trace)
停在发送网络请求的地方还不够,此时数据通常已经加密好了。我们需要顺着右侧的 Call Stack(调用栈)一层层往上回溯。 不断点击调用栈中的上一层函数,观察局部变量的作用域(Scope)。你会发现某个时刻,原始的明文 JSON {"query": "python"} 存在于变量中,然后经过了一个类似于 Object(r.a)(t) 这样的恶心函数调用,就变成了乱码。
3. Webpack 模块提取与解包
现代前端往往使用 Webpack 或 Vite 进行打包。刚才提到的 Object(r.a) 其实就是 Webpack 加载的某个加密模块。 难点 :直接抠代码会遇到环境依赖问题(缺少其他模块)。 解决思路:
-
全扣法 :把整个 Webpack 的入口
window.webpackJsonp结构扣下来,在本地 Node.js 环境中模拟执行,暴露出加密函数。 -
打桩插桩 :通过编写 Chrome 插件或者使用抓包工具(如 Charles/Mitmproxy)拦截 JS 文件,在加密函数的入口和出口处加上
console.log(),动态打印出其加密过程和密钥。
三、 算法还原:AES 与 HMAC 的交响曲
经过漫长的抠代码和调试,我们通常会发现其加密与签名的本质逻辑:
1. 参数加密 (通常是对称加密 AES)
-
密钥 (Key):通常通过前端的某个全局变量,或者由服务器在初始 HTML 源码中下发,亦或是通过时间戳和某个固定盐值 (Salt) 计算得出。
-
向量 (IV):为了保证每次加密结果不同,IV 往往是随机生成的,并附加在密文前或者作为单独的 Header 传输。
-
算法往往是
AES-128-CBC配合Pkcs7填充。
2. 请求签名 (HMAC) 为了防止中间人篡改数据,ZLibrary 会要求对请求进行签名。计算公式大致如下: Signature = HMAC_SHA256(SecretKey, URL_Path + Timestamp + Nonce + MD5(RequestBody))
四、 可复用的绕过思路:Python 还原与 RPC 方案
面对找出的加密逻辑,我们有两种可复用的应对方案。
方案一:纯 Python 算法还原 (推荐,性能最高)
如果我们彻底搞懂了 JS 里的 AES 和 HMAC 逻辑,以及 Key 的生成规则,我们就可以脱离浏览器,用 Python 的 cryptography 或 PyCryptodome 库完全重写一遍。
# 伪代码:Python 还原 ZLibrary 请求签名与加密过程
import time
import hashlib
import hmac
import json
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
# 假设逆向分析出的常量和盐值
SECRET_KEY = b"zlib_super_secret_salt_2024"
AES_KEY = b"dynamic_key_from_js_1234" # 实际情况中可能需要动态计算
AES_IV = b"1234567890123456"
def encrypt_payload(data_dict):
cipher = AES.new(AES_KEY, AES.MODE_CBC, AES_IV)
json_str = json.dumps(data_dict)
padded_data = pad(json_str.encode('utf-8'), AES.block_size)
encrypted = cipher.encrypt(padded_data)
return base64.b64encode(encrypted).decode('utf-8')
def generate_signature(path, timestamp, payload_str):
# 构造待签名的基础字符串
message = f"{path}|{timestamp}|{payload_str}"
# 使用 HMAC-SHA256 生成签名
signature = hmac.new(SECRET_KEY, message.encode('utf-8'), hashlib.sha256).hexdigest()
return signature
def fetch_data():
timestamp = str(int(time.time()))
api_path = "/api/v1/search"
payload = {"query": "deep learning", "limit": 20}
encrypted_data = encrypt_payload(payload)
sign = generate_signature(api_path, timestamp, encrypted_data)
headers = {
"X-Timestamp": timestamp,
"X-Signature": sign,
"Content-Type": "application/json"
}
# 使用 requests 发送完全伪造的合法请求
# response = requests.post("[https://zlibrary-domain.com](https://zlibrary-domain.com)" + api_path, headers=headers, json={"data": encrypted_data})
方案二:浏览器 RPC (Remote Procedure Call) 方案
如果 JS 代码混淆程度太深(比如使用了高度定制的 WebAssembly 或者 OLLVM 混淆),纯算法还原的时间成本过高,我们可以采用 RPC 方案。
原理是:使用 Playwright 挂载真实的 ZLibrary 页面,在页面内注入 WebSocket 客户端。Python 脚本作为服务端。当 Python 需要发请求时,通过 WebSocket 把明文参数发给浏览器,利用浏览器页面内自带的、现成的 JS 加密函数进行加密和签名,把结果传回给 Python,Python 再去发送 HTTP 请求。
这种方案完美绕过了逆向算法的过程,降维打击,实现真正意义上的"可复用"。
总结:突破了请求签名与加密,我们已经可以将 ZLibrary 的数据转化为结构化的 JSON。但不要高兴得太早。当我们开始高并发抓取时,网络层的死神正在逼近。下一篇,我们将探讨最后也是最难的关卡:TLS 指纹与 IP 频率风控的突破。