百趣云 百趣云的博客

知乎 x-zse-96 签名算法逆向详解

签名公式:教科书级的"摘要套摘要"

知乎 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 对这两轮操作:

  1. 固定表异或:16 字节逐字节与一个内置的 16 字节数组异或(这个数组在 JS 里以十进制数组字面量形式存在,搜 [ 开头的长数字数组能找到)
  2. 置换编码:异或结果按一张固定的位置置换表重排,然后转 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 的两个暗坑

  1. x-zse-93 头必须配套。它告诉服务端签名用的公式版本(就是 101_3_3.0),只带 x-zse-96 不带它,服务端按错误版本校验,直接 403。两个头的版本字符串必须一致。
  2. d_c0 是半永久设备指纹。它和账号弱绑定、和设备强绑定。协议池里每个账号要固定一个 d_c0 长期使用,频繁更换 d_c0 的账号会被标记"设备环境不稳定",后续所有请求的风控等级上调。
  3. GET 请求 body_md5 是空串,不是 url 的 md5。新手常犯的错是把查询串也算进去——公式里查询串完全不参与,只有 path。签不对先检查这一点。

总结

  1. 公式:"2.0_" + encrypt(md5("101_3_3.0" + path + d_c0 + body_md5))
  2. encrypt 是异或+置换的查表操作,直接抠表别重写
  3. x-zse-93 版本头必须与公式版本一致
  4. d_c0 固定使用,它是设备指纹不是临时令牌
By 百趣云 阅读量:1 On