feat(huya): 证书算法完全闭环 - 差分法证明标准AES-128-ECB并复现铸证

- NativeFunction直调差分: FIPS-197 KAT逐位一致, UdbAESUtil无魔改
- 证书 = b64([0x0C][key_idx][AES128-ECB(key16, zeropad(P1_187B))])
- P1结构: [010400][5008][u16+20B随机][u16+40B指纹hex][u16+114B hyCred]
- 密钥: rodata默认三条(AESkeyMgr id=3/4/5) + 服务端下发会话钥(key_idx区分)
- tools/cert_forge.py 重铸证书与活体密文 FULL MATCH
This commit is contained in:
yml2213
2026-08-25 15:21:11 +08:00
parent 2076510df9
commit a0ba2fe020
7 changed files with 771 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
#!/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("<H", len(r)) + r
p1 += struct.pack("<H", len(fingerprint)) + fingerprint
p1 += struct.pack("<H", len(cred)) + cred
assert len(p1) == 187, len(p1)
return p1
def parse_p1(data: bytes) -> 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("<H", tk(2))[0]
out["rnd"] = tk(n)
n = struct.unpack("<H", tk(2))[0]
out["fingerprint"] = tk(n)
n = struct.unpack("<H", tk(2))[0]
out["cred"] = tk(n)
out["trailing"] = data[o:]
return out
# ---------------------------------------------------------------- 证书层
def forge_cert(p1: bytes, key16: bytes = DEFAULT_KEY16,
key_idx: int = 0x20, type_byte: int = 0x0C) -> 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)
+114
View File
@@ -0,0 +1,114 @@
"""UdbAESUtil 参考实现与设备实测对拍。
2026-08-25 Frida 直调差分结论(scripts/hook_aes_diff.py + evidence/aes_diff.json:
- UdbAESUtil::Cipher == 标准 AES-128 加密 (FIPS-197 KAT 逐位一致)
- UdbAESUtil::KeyExpansion == 标准展开,轮密钥按「列序/转置」存储
- C1(uchar* key) 只读 key 前 16 字节
即: 不存在自定义 AES;任何"解密对不上"都是密钥来源/调用方的问题。
"""
from __future__ import annotations
from Crypto.Cipher import AES
MASTER_KEY_STR = b"HuyaUdb1928374650qwertyuiop"
_SBOX = bytes.fromhex(
'637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0'
'b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275'
'09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf'
'd0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2'
'cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb'
'e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08'
'ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e'
'e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16')
_INV = bytes.fromhex(
'52096ad53036a538bf40a39e81f3d7fb733317225e060830ca22c1e0941053ff'
'5013071554c6935c9b6b478e49af25ebd6d13ca8d1b972e183a15b8e35997231'
'd18def6e281286be5532e377b905b09393c428c0aab8858fddec191bdbaff54c'
'bb617a475e8db4f89d389128723abbd6142cbbb8da24014361a9cf83d0d90c0b'
'93ae82fdf3a50165c19fd02fa1ca67094c40b5f9bbc9ecd454f75bc46caa5e58'
'f4fc6d7893fa59957c04c0a3a8ef7a3493163b536fceaac2d940e70fa65048e9'
'4bccd5d83760a68309a2ded466b75c8976ee06753c70336a124b2042b8db9a8f'
'58eec4f73d116f33766f2cc10ed1233b84f21f2679120569ad47eb4e30665ff1')
def _xtime(a: int) -> int:
a <<= 1
return (a ^ 0x1B) & 0xFF if a & 0x100 else a
def expand_schedule_transposed(key16: bytes) -> bytes:
"""标准 AES-128 轮密钥,按 UdbAESUtil 的存储序输出(176B)。
设备布局: rk[round] 为 16B,按 state 列序存放(rk0..3 = key 的第0列)。
"""
assert len(key16) == 16
words = [list(key16[4 * i:4 * i + 4]) for i in range(4)]
rcon = 1
for i in range(4, 44):
t = list(words[i - 1])
if i % 4 == 0:
t = t[1:] + t[:1]
t = [_SBOX[b] for b in t]
t[0] ^= rcon
rcon = _xtime(rcon)
words.append([words[i - 4][j] ^ t[j] for j in range(4)])
# 设备实测布局: 每轮16B按字表转置存放 dev[r*4+c] = words[rnd*4+c][r]
out = bytearray()
for rnd in range(11):
blk = bytearray(16)
for c in range(4):
for r in range(4):
blk[r * 4 + c] = words[rnd * 4 + c][r]
out += blk
return bytes(out)
def encrypt_block(key16: bytes, block16: bytes) -> bytes:
return AES.new(key16, AES.MODE_ECB).encrypt(block16)
def decrypt_block(key16: bytes, block16: bytes) -> bytes:
return AES.new(key16, AES.MODE_ECB).decrypt(block16)
def zero_pad(data: bytes) -> bytes:
"""UdbAESUtil::encrypt 的填充:补 '\\0' 到 16 的倍数(不追加整块)。"""
if len(data) % 16 == 0:
return data
return data + b"\x00" * (16 - len(data) % 16)
def udb_encrypt(key16: bytes, plain: bytes) -> bytes:
p = zero_pad(plain)
return AES.new(key16, AES.MODE_ECB).encrypt(p)
def udb_decrypt(key16: bytes, ct: bytes) -> bytes:
return AES.new(key16, AES.MODE_ECB).decrypt(ct).rstrip(b"\x00")
def selftest() -> None:
# FIPS-197
kat_k = bytes(range(16))
kat_p = bytes.fromhex("00112233445566778899aabbccddeeff")
assert encrypt_block(kat_k, kat_p).hex() == "69c4e0d86a7b0430d8cdb78070b4c55a"
# 与设备 dump 的轮密钥对拍
import json
from pathlib import Path
j = Path(__file__).resolve().parent.parent / "evidence" / "aes_diff.json"
if j.exists():
dev = json.loads(j.read_text())["sched_kat"]["rk"]
assert expand_schedule_transposed(kat_k).hex() == dev, "schedule mismatch!"
dev_ct = json.loads(j.read_text())["cipher_huyaudb_first16"]["after"]
# harness 该样本实际喂入的 key = "4875796155646231"(hex串被二次编码, 见脚本注释)
ref = encrypt_block(b"4875796155646231", b"\x00" * 16).hex()
assert dev_ct == ref, f"huyaudb mismatch {dev_ct} vs {ref}"
# 主密钥前16字节的标准指纹(供真实样本比对)
ref_master = encrypt_block(MASTER_KEY_STR[:16], b"\x00" * 16).hex()
print("master first16 enc(zeros) =", ref_master)
print("udb_aes selftest OK")
if __name__ == "__main__":
selftest()