#!/usr/bin/env python3 """虎牙 udb 登录证书(biz_token)解析/铸造工具 —— 算法已完全闭环(2026-08-25)。 == 已证实的完整算法链 == 证书 = base64( [0x0c][key_idx][AES-128-ECB(key16, zeropad(P1))] ) P1 (187B) 结构(两份活体样本逐字段对齐验证): [0x01][0x04][0x00] 固定头 ["5008"] appId [u16_le=20][20B 随机数] 每次生成时随机 [u16_le=40][40B 指纹hex] 设备指纹ASCII("02df39879743..."), 设备绑定、跨会话不变 [u16_le=114][114B cred] hyCred 二进制 (=udb_cred base64解码) key16 来源: AESkeyMgr 密钥表。rodata 内置默认三条(0x1bc810, 注册id=3/4/5): 4VYcPdvKKqjBHZtCmbroRXHk <- 本次活体实测在用(取前16字节) xXEDWqiKLGwEZ6HubEiswCqK 3FMHubdKosFrhmXNLHTNHZwe 服务端可下发会话密钥(NEWKEY/key_id机制), 证书第二字节即 key_idx: 今日 getQUrlData 样本 idx=0x20 <-> "4VYcPdvKKqjBHZtC"; 更早样本 idx=0x50 用默认钥解不开 => 属服务端下发钥。 UdbAESUtil 本体为标准 AES-128-ECB(FIPS-197 KAT 逐位一致), 无魔改。 调用链: HandlerGetH5InfoEx::onHandler -> BusinessCfg::getOtp -> ... -> gen_biz_token(BIZTOKEN,P1部件...,out) @0x3307e4 -> enpack_header/enpack_body/cred_packls/xxtea_encrypt -> encode_aes(P1, key24, out) @0x330218 # C1 只读 key 前16字节 -> UdbAESUtil::encrypt @0x250038 # 补零到16倍数逐块 -> CBase64::Encode 验证脚本: scripts/hook_aes_diff.py(差分) / scripts/hook_cert_keycap.py(抓钥) 证据: evidence/aes_diff.json / evidence/cert_keycap.json """ from __future__ import annotations import base64 import os import struct import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from tools.udb_aes import udb_decrypt, udb_encrypt # noqa: E402 # so rodata 0x1bc810 起连排的三条24字符默认密钥(AESkeyMgr 注册 id=3/4/5) KEY_TABLE_RAW = [ "4VYcPdvKKqjBHZtCmbroRXHk", "xXEDWqiKLGwEZ6HubEiswCqK", "3FMHubdKosFrhmXNLHTNHZwe", ] DEFAULT_KEY16 = KEY_TABLE_RAW[0][:16].encode() # 本次活体验证在用 # ---------------------------------------------------------------- P1 组装/解析 def build_p1(app_id: bytes, fingerprint: bytes, cred: bytes, rnd: bytes | None = None) -> bytes: """按活体样本结构组装 P1 明文。 fingerprint: 40字节 ASCII hex 形如 b'02df39879743...'(设备绑定) cred: 114 字节 hyCred 二进制(base64(udb_cred) 解码后) rnd: 缺省用 os.urandom(20) """ assert len(fingerprint) == 40, f"指纹应40B, 得到{len(fingerprint)}" assert len(cred) == 114, f"cred应114B, 得到{len(cred)}" r = os.urandom(20) if rnd is None else rnd p1 = b"\x01\x04\x00" + app_id p1 += struct.pack(" dict: o = 0 def tk(n: int) -> bytes: nonlocal o b = data[o:o + n] o += n return b out = { "head": tk(3).hex(), "app_id": tk(4), } n = struct.unpack(" bytes: """P1 -> 二进制证书 [type][key_idx][192B AES 密文]。""" ct = udb_encrypt(key16, p1) assert len(ct) == 192, len(ct) return bytes([type_byte, key_idx]) + ct def cert_b64(cert: bytes) -> str: return base64.b64encode(cert).decode() def parse_cert(blob: bytes) -> tuple[int, int, bytes]: """[0x0c][idx][192B] -> (type, idx, ct)。""" assert blob[0] == 0x0C and len(blob) >= 194, "非证书结构" return blob[0], blob[1], blob[2:194] def decrypt_cert(blob: bytes, key16: bytes = DEFAULT_KEY16) -> bytes: _, _, ct = parse_cert(blob) return udb_decrypt(key16, ct) # ---------------------------------------------------------------- 兼容旧接口 def try_decrypt_all(ct192: bytes) -> list[tuple[str, bytes]]: return [(k[:16], udb_decrypt(k[:16].encode(), ct192)) for k in KEY_TABLE_RAW] if __name__ == "__main__": import json ev = json.load(open(Path(__file__).resolve().parent.parent / "evidence/cert_keycap.json")) enc_in = next(e for e in ev if e.get("type") == "enc_in") P1 = bytes.fromhex(enc_in["x1s"]["v"]) fields = parse_p1(P1) print("P1 解析:", {k: (v.hex()[:32] if isinstance(v, bytes) else v) for k, v in fields.items()}) # 用捕获的 P1 重铸并与活体密文比对 qurl = next(e["data"] for e in ev if e.get("type") == "qurl_done" and e.get("data")) import re raw = base64.b64decode(qurl) inner = [] for x in re.findall(rb"[A-Za-z0-9+/]{100,}={0,2}", raw): try: inner.append(base64.b64decode(x + b"=" * ((-len(x)) % 4))) except Exception: pass blob = max(inner, key=len) mine = forge_cert(P1) print("活体证书 :", blob.hex()[:64]) print("重铸证书 :", mine.hex()[:64]) print("FULL MATCH:", mine == blob)