- hook WUP序列化层抓取TLS加密前明文,确认密码登录走 servant=huyaudbwebui / func=hypasswordLogin (msgType=0x1001) - 逆向JCE RequestPacket帧格式(1015字节TAF二进制),定稿三层结构: WUP header(tag1-10) -> sBuffer Map -> _wup_data业务struct - 澄清两个疑点字节: 0x66/0x7d 是TAF字段头(非分隔符) - 实现 tools/huya_wup_encoder.py,逐字节复现金标准 - 实测纯Python动态构造登录成功(返回uid+token),TLS指纹不阻断 - userAction随机坐标/时间戳同样通过,内置make_user_action辅助函数 - 新增证据金标准 + 协议文档
239 lines
9.4 KiB
Python
239 lines
9.4 KiB
Python
"""
|
||
虎牙 App 登录 — OTP/dckey 加密算法(已通过 Frida 动态双向验证)
|
||
|
||
完整加密链(libudbauthunify.so,com.duowan.kiwi 13.4.22 / versionCode 115315):
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ OTP 加密(hyudb_otp_encrypt) │
|
||
│ │
|
||
│ 1. XXTEA: xxtea_key = md5(uid字符串).hexdigest()[:16] │
|
||
│ xxtea_in = arg7(8字节小端) + 4字节原始长度 │
|
||
│ (标准 XXTEA, rounds = 6 + 52/n, DELTA=0x9e3779b9)│
|
||
│ │
|
||
│ 2. cred_pack 拼装: │
|
||
│ [0x02] # 固定 header │
|
||
│ [u16_le len][xxtea密文] # string 打包 │
|
||
│ [u16_le len][设备指纹] # string 打包 │
|
||
│ │
|
||
│ 3. AES-128-ECB + ZeroPadding: │
|
||
│ aes_key = md5(sessHex32 + AESKEYMGR_KEY[b])[4:12].hex() │
|
||
│ cipher = AES_ECB(拼装数据 + 补零) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
动态验证样本(两次 hook,两个独立 PID,结果一致):
|
||
uid = "1471238907296"
|
||
sessHex32 = "865a4924a40897ac1fcfe6b4c2cbb0e3" (固定)
|
||
AESKEYMGR = "MKDKeridjing7avnsasdSDHI" (b=2)
|
||
aes_key = "312334e88d8c35cb"
|
||
XXTEA 双向、AES 加密方向均已验证通过。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import struct
|
||
from binascii import unhexlify
|
||
|
||
from Crypto.Cipher import AES
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# AESkeyMgr 硬编码 key 表(按 b 参数索引,1-based)
|
||
# b=2..5 已通过 Frida hook AESkeyMgr::getkey(a=1, b) 动态确认;
|
||
# 其余为 rodata 相邻字符串推断,待最终确认顺序。
|
||
# ---------------------------------------------------------------------------
|
||
AESKEYMGR_KEYS: dict[int, str] = {
|
||
1: "owNMiaCgcHmqoTr3iRamFuHj",
|
||
2: "MKDKeridjing7avnsasdSDHI", # 已验证
|
||
3: "nskdI7MDGKSDJsnadjdoonvs", # 已验证
|
||
4: "whdFNGFdfJHMJbdfg6jhdsFG", # 已验证
|
||
5: "SHBfgytjtoikooruhogji7ER", # 已验证
|
||
6: "KNSDNjfohweeromnmkladj3g",
|
||
7: "KJNzmZaxUd9kGLoAbtefNzCv",
|
||
8: "YnzbiMPNXaGuxxuoNRd9Lzyj",
|
||
}
|
||
|
||
# 固定 32 位 hex 会话值(OTP AES key 源的第一半)
|
||
SESS_HEX32 = "865a4924a40897ac1fcfe6b4c2cbb0e3"
|
||
|
||
DELTA = 0x9E3779B9
|
||
|
||
|
||
def huya_arg7(now_ms: int | None = None) -> int:
|
||
"""arg7 = System.currentTimeMillis() << 16(低 16 位恒为 0)。
|
||
|
||
Frida 实测:arg7 >> 16 ≈ 事件时间戳(ms),误差为 hook 传播延迟(<600ms)。
|
||
"""
|
||
import time
|
||
if now_ms is None:
|
||
now_ms = int(time.time() * 1000)
|
||
return now_ms << 16
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# XXTEA(标准实现,已验证)
|
||
# ---------------------------------------------------------------------------
|
||
def _xxtea_mx(z, y, total, key, p, e):
|
||
return (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4))
|
||
^ ((total ^ y) + (key[(p & 3) ^ e] ^ z))) & 0xFFFFFFFF
|
||
|
||
|
||
def _xxtea_rounds(n: int) -> int:
|
||
return 6 + 52 // n
|
||
|
||
|
||
def xxtea_encrypt_u32(v: list[int], key: list[int]) -> list[int]:
|
||
n = len(v)
|
||
if n < 2:
|
||
return v
|
||
total = 0
|
||
z = v[n - 1]
|
||
for _ in range(_xxtea_rounds(n)):
|
||
total = (total + DELTA) & 0xFFFFFFFF
|
||
e = (total >> 2) & 3
|
||
for p in range(n):
|
||
y = v[(p + 1) % n]
|
||
v[p] = (v[p] + _xxtea_mx(z, y, total, key, p, e)) & 0xFFFFFFFF
|
||
z = v[p]
|
||
return v
|
||
|
||
|
||
def xxtea_decrypt_u32(v: list[int], key: list[int]) -> list[int]:
|
||
n = len(v)
|
||
if n < 2:
|
||
return v
|
||
rounds = _xxtea_rounds(n)
|
||
total = (rounds * DELTA) & 0xFFFFFFFF
|
||
y = v[0]
|
||
while total != 0:
|
||
e = (total >> 2) & 3
|
||
for p in range(n - 1, 0, -1):
|
||
z = v[p - 1]
|
||
v[p] = (v[p] - _xxtea_mx(z, y, total, key, p, e)) & 0xFFFFFFFF
|
||
y = v[p]
|
||
z = v[n - 1]
|
||
v[0] = (v[0] - _xxtea_mx(z, y, total, key, 0, e)) & 0xFFFFFFFF
|
||
y = v[0]
|
||
total = (total - DELTA) & 0xFFFFFFFF
|
||
return v
|
||
|
||
|
||
def xxtea_key_from_uid(uid: str) -> list[int]:
|
||
"""XXTEA key = md5(uid字符串).hexdigest()[:16] 的 16 个 ASCII 字节(4 个小端 u32)。"""
|
||
h = hashlib.md5(uid.encode("ascii")).hexdigest()[:16]
|
||
return list(struct.unpack("<4I", h.encode("ascii")))
|
||
|
||
|
||
def huya_xxtea_encrypt(data: bytes, uid: str) -> bytes:
|
||
"""XXTEA 加密:数据 + 4字节原始长度 padding,再标准 XXTEA。"""
|
||
key = xxtea_key_from_uid(uid)
|
||
pad = (-len(data)) % 4
|
||
buf = data + b"\x00" * pad + struct.pack("<I", len(data))
|
||
v = list(struct.unpack("<%dI" % (len(buf) // 4), buf))
|
||
enc = xxtea_encrypt_u32(v, key)
|
||
return b"".join(struct.pack("<I", x) for x in enc)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# AES(已验证)
|
||
# ---------------------------------------------------------------------------
|
||
def huya_aes_key(otp_input: bytes) -> bytes:
|
||
"""AES-128 key = md5(otp_input) digest 第 5~12 字节转 hex(16 ASCII 字符)。"""
|
||
d = hashlib.md5(otp_input).digest()
|
||
return d[4:12].hex().encode("ascii")
|
||
|
||
|
||
def huya_aes_encrypt(plain: bytes, otp_input: bytes) -> bytes:
|
||
key = huya_aes_key(otp_input)
|
||
pad = (-len(plain)) % 16
|
||
return AES.new(key, AES.MODE_ECB).encrypt(plain + b"\x00" * pad)
|
||
|
||
|
||
def huya_aes_decrypt(cipher: bytes, otp_input: bytes) -> bytes:
|
||
key = huya_aes_key(otp_input)
|
||
return AES.new(key, AES.MODE_ECB).decrypt(cipher).rstrip(b"\x00")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# cred_pack 打包
|
||
# ---------------------------------------------------------------------------
|
||
def cred_pack_string(data: bytes) -> bytes:
|
||
"""string = [2字节小端长度][数据](cred_pack::push_varstr)。"""
|
||
return struct.pack("<H", len(data)) + data
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 完整 OTP 加密(对应 hyudb_otp_encrypt)
|
||
# ---------------------------------------------------------------------------
|
||
def huya_password_sha1(password: str) -> str:
|
||
"""密码登录的 password 字段 = 标准 SHA1(明文密码) 的 hex,无盐。
|
||
|
||
Frida 抓包证据(msgType=0x1001 密码登录请求):
|
||
password = "772ed992b0e161276f44ec63671e60155c506294" (40 位 hex)
|
||
"""
|
||
return hashlib.sha1(password.encode("utf-8")).hexdigest()
|
||
|
||
|
||
def huya_name(hyname: str) -> str:
|
||
"""登录 name 字段 = "hy_" + 虎牙号(纯数字账号)。"""
|
||
return f"hy_{hyname}"
|
||
|
||
|
||
def huya_otp_encrypt(
|
||
uid: str,
|
||
arg7: int,
|
||
device_fingerprint: bytes,
|
||
b: int = 2,
|
||
sess_hex32: str = SESS_HEX32,
|
||
) -> bytes:
|
||
"""完整 OTP 加密,返回 AES 密文。
|
||
|
||
uid : 账号 uid 的十进制字符串(XXTEA key 源)
|
||
arg7 : 8 字节 ulong(时间戳/计数器)
|
||
device_fingerprint : 设备指纹二进制(arg5)
|
||
b : AESkeyMgr key 表索引(1..8)
|
||
sess_hex32 : 固定 32 位 hex 会话值
|
||
"""
|
||
# 1. XXTEA 加密 arg7
|
||
xxtea_ct = huya_xxtea_encrypt(struct.pack("<Q", arg7), uid)
|
||
|
||
# 2. cred_pack 拼装
|
||
packed = b"\x02" + cred_pack_string(xxtea_ct) + cred_pack_string(device_fingerprint)
|
||
|
||
# 3. AES-128-ECB
|
||
otp_input = (sess_hex32 + AESKEYMGR_KEYS[b]).encode("ascii")
|
||
return huya_aes_encrypt(packed, otp_input)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 自检 1: AES
|
||
otp_input = (SESS_HEX32 + AESKEYMGR_KEYS[2]).encode("ascii")
|
||
plain = unhexlify(
|
||
"020c00cf8b207ddeb2ec4ec2a9344772000a70686484dee8e70dd7649fa14e82"
|
||
"86caef42e5ddb9c380aef2485f87170507881b3e11b04c8dc976b8d056dab02"
|
||
"45672e3b5595f73d1803c123672fb3525bb3640b986a94d4c00349995e904a1"
|
||
"0b27b6d9d0078b1a0c85abdcd1747e588831370d553cf3bd9e3032a9537e30f"
|
||
"29dbb4866"
|
||
)
|
||
expected = unhexlify(
|
||
"b2f83ab1970e3d343935b502874a63e3e4a674c60827347a8b81186e11bfb44"
|
||
"3a95cd1252536822859daa76685317aab55ca6f50530b98b72122b61f774d6a"
|
||
"9bc515930d7a88f387efc6dac90379ee26b346a5a08e42546950b268a8a7f5c"
|
||
"ecfe8b93f2b1271c2c40ccddc8f805b2732e2d16e3e79fd526666b58a5fc3d0"
|
||
"af366cebfbeaa91089188cb5d7ed48661cbd"
|
||
)
|
||
assert huya_aes_key(otp_input) == b"312334e88d8c35cb", "AES key 派生失败"
|
||
assert huya_aes_encrypt(plain, otp_input) == expected, "AES 加密失败"
|
||
assert huya_aes_decrypt(expected, otp_input) == plain, "AES 解密失败"
|
||
|
||
# 自检 2: XXTEA
|
||
uid = "1471238907296"
|
||
xxtea_ct = huya_xxtea_encrypt(struct.pack("<Q", 0x01A034AD5ECA0000), uid)
|
||
assert xxtea_ct.hex() == "d24e6493d0ee53c395790d67", f"XXTEA 失败: {xxtea_ct.hex()}"
|
||
|
||
# 自检 3: 密码 SHA1(无盐)
|
||
assert huya_password_sha1("password123") == hashlib.sha1(b"password123").hexdigest()
|
||
|
||
print("自检全部通过")
|
||
print(" AES key =", huya_aes_key(otp_input).decode())
|
||
print(" XXTEA =", huya_xxtea_encrypt(struct.pack('<Q', 0x01A034AD5ECA0000), uid).hex())
|