虎牙App密码登录: 逆向WUP协议并实现纯Python登录编码器
- 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辅助函数 - 新增证据金标准 + 协议文档
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
虎牙 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())
|
||||
Reference in New Issue
Block a user