签名公式:教科书级的"摘要套摘要"
知乎 API 的 x-zse-96 是逆向圈的经典案例,因为它的设计非常工整,拆开后每一步都清清楚楚:
x-zse-96 = "2.0_" + encrypt( md5( "101_3_3.0" + url_path + d_c0 + body_md5 ) )逐个字段说:
"101_3_3.0":写死在 JS 里的版本标识,历史上有过101_3_2.0等变体,以当前页面为准url_path:接口路径,不含域名和查询串(比如/api/v4/search_v3)d_c0:cookie 里的设备标识,登录/注册时服务端下发,形如ABCD1234...的一长串body_md5:POST 体的 md5;GET 请求没有 body,这一项为空串encrypt:知乎自定义加密,不是标准算法,下面细讲
定位:几乎没设防
知乎的签名 JS 混淆程度很低。Sources 里全局搜 x-zse-96,直接命中赋值点;往上一步就是生成函数,能看到 101_3_3.0 这个字符串字面量明晃晃地躺在拼接表达式里。从打开 DevTools 到找到签名公式,熟练的话十分钟。
真正要花时间的只有最后的 encrypt。
encrypt:异或 + S 盒置换
md5 算出来是 16 字节,encrypt 对这两轮操作:
- 固定表异或:16 字节逐字节与一个内置的 16 字节数组异或(这个数组在 JS 里以十进制数组字面量形式存在,搜
[开头的长数字数组能找到) - 置换编码:异或结果按一张固定的位置置换表重排,然后转 hex
这两步都是纯查表操作,没有任何密钥派生。工程上不建议读懂重写——直接把 encrypt 函数(连同它引用的两个表)原样抠到 Node/Python 里,一百来行的事:
# 结构示意:表数据从 JS 里抠出来填
XOR_TABLE = [0x00] * 16 # 实际值以抠取为准
PERM_TABLE = list(range(16)) # 实际值以抠取为准
def encrypt(md5_bytes):
xored = bytes(b ^ XOR_TABLE[i] for i, b in enumerate(md5_bytes))
permuted = bytes(xored[PERM_TABLE[i]] for i in range(16))
return permuted.hex()完整签名流程(Python)
import hashlib, re, requests
def md5_bytes(s):
return hashlib.md5(s.encode('utf-8')).digest()
def make_x_zse_96(path, d_c0, body=''):
body_md5_hex = hashlib.md5(body.encode('utf-8')).hexdigest() if body else ''
raw = "101_3_3.0" + path + d_c0 + body_md5_hex
return "2.0_" + encrypt(md5_bytes(raw))
# 使用:d_c0 从 cookie 取
session = requests.Session()
session.get('https://www.zhihu.com/') # 或登录流程,拿到 d_c0
d_c0 = session.cookies.get('d_c0')
path = '/api/v4/search_v3?t=general&q=逆向'
body = ''
headers = {
'x-zse-96': make_x_zse_96(path.split('?')[0], d_c0, body),
'x-zse-93': '101_3_3.0',
'Referer': 'https://www.zhihu.com/',
}精髓:版本号和 d_c0 的两个暗坑
x-zse-93头必须配套。它告诉服务端签名用的公式版本(就是101_3_3.0),只带 x-zse-96 不带它,服务端按错误版本校验,直接 403。两个头的版本字符串必须一致。- d_c0 是半永久设备指纹。它和账号弱绑定、和设备强绑定。协议池里每个账号要固定一个 d_c0 长期使用,频繁更换 d_c0 的账号会被标记"设备环境不稳定",后续所有请求的风控等级上调。
- GET 请求 body_md5 是空串,不是 url 的 md5。新手常犯的错是把查询串也算进去——公式里查询串完全不参与,只有 path。签不对先检查这一点。
总结
- 公式:
"2.0_" + encrypt(md5("101_3_3.0" + path + d_c0 + body_md5)) - encrypt 是异或+置换的查表操作,直接抠表别重写
- x-zse-93 版本头必须与公式版本一致
- d_c0 固定使用,它是设备指纹不是临时令牌