虎牙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:
yml2213
2026-08-25 02:59:20 +08:00
parent 06a0a1e7d3
commit ff99c036f8
14 changed files with 2185 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
"""虎牙 TAF(JCE) 纯字节流递归解码器 — 不依赖项目 read_* 高层 API。
独立实现 head/value 解析,直接按 dtype 读取 value(已消费 head 的前提下),
避免与 TafInputStream.read_string 等"内部再读 head"的方法冲突。
"""
from __future__ import annotations
import struct
# 类型标签(与 core/huya/taf_protocol.py TafType 一致)
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
FLOAT, DOUBLE = 0x04, 0x05
STRING1, STRING4 = 0x06, 0x07
MAP, LIST = 0x08, 0x09
STRUCT_BEGIN, STRUCT_END = 0x0A, 0x0B
ZERO, SIMPLE_LIST = 0x0C, 0x0D
_STR = (STRING1, STRING4)
_INTS = (INT8, INT16, INT32, INT64)
class Taf:
"""带游标的字节读取器"""
def __init__(self, data: bytes):
self.d = data
self.p = 0
def eof(self) -> bool:
return self.p >= len(self.d)
def peek_head(self):
if self.eof():
raise EOFError
b = self.d[self.p]
tag = (b >> 4) & 0x0F
dt = b & 0x0F
if tag == 15:
tag = self.d[self.p + 1]
return tag, dt
def read_head(self):
tag, dt = self.peek_head()
self.p += 1
if (self.d[self.p - 1] >> 4) & 0x0F == 15:
self.p += 1
return tag, dt
def read_len(self) -> int:
"""读 map/list 长度(int32 优化编码)"""
tag, dt = self.read_head()
return self.read_int_value(dt)
def read_int_value(self, dt: int) -> int:
if dt == ZERO:
return 0
if dt == INT8:
v = struct.unpack('b', self.d[self.p:self.p + 1])[0]; self.p += 1; return v
if dt == INT16:
v = struct.unpack('>h', self.d[self.p:self.p + 2])[0]; self.p += 2; return v
if dt == INT32:
v = struct.unpack('>i', self.d[self.p:self.p + 4])[0]; self.p += 4; return v
if dt == INT64:
v = struct.unpack('>q', self.d[self.p:self.p + 8])[0]; self.p += 8; return v
raise ValueError(f"期望整数, 实际 0x{dt:02x} @{self.p}")
def read_string_value(self, dt: int) -> str:
if dt == STRING1:
n = self.d[self.p]; self.p += 1
elif dt == STRING4:
n = struct.unpack('>I', self.d[self.p:self.p + 4])[0]; self.p += 4
else:
raise ValueError(f"期望 string, 实际 0x{dt:02x} @{self.p}")
v = self.d[self.p:self.p + n].decode('utf-8', errors='replace'); self.p += n
return v
def skip_value(self, dt: int):
if dt == ZERO:
return
if dt in _INTS:
self.p += {INT8: 1, INT16: 2, INT32: 4, INT64: 8}[dt]
elif dt in (FLOAT,):
self.p += 4
elif dt in (DOUBLE,):
self.p += 8
elif dt == STRING1:
self.p += 1 + self.d[self.p]
elif dt == STRING4:
self.p += 4 + struct.unpack('>I', self.d[self.p:self.p + 4])[0]
elif dt == SIMPLE_LIST:
self.read_head() # 元素类型
n = self.read_len()
self.p += n
elif dt == MAP:
n = self.read_len()
for _ in range(n):
_, kt = self.read_head(); self.skip_value(kt)
_, vt = self.read_head(); self.skip_value(vt)
elif dt == LIST:
n = self.read_len()
for _ in range(n):
_, it = self.read_head(); self.skip_value(it)
elif dt == STRUCT_BEGIN:
while True:
_, t = self.read_head()
if t == STRUCT_END:
break
self.skip_value(t)
# STRUCT_END 不需要处理
def decode_value(t: Taf, dt: int, depth: int = 0, max_depth: int = 6, max_items: int = 50):
"""给定已消费 head 的 dtype,递归读 value。"""
if dt == ZERO:
return 0
if dt in _INTS:
return t.read_int_value(dt)
if dt == FLOAT:
v = struct.unpack('>f', t.d[t.p:t.p + 4])[0]; t.p += 4; return round(v, 6)
if dt == DOUBLE:
v = struct.unpack('>d', t.d[t.p:t.p + 8])[0]; t.p += 8; return round(v, 6)
if dt in _STR:
return t.read_string_value(dt)
if dt == SIMPLE_LIST:
t.read_head() # 元素类型
n = t.read_len()
raw = t.d[t.p:t.p + n]; t.p += n
# 尝试 utf8 可读
try:
s = raw.decode('utf-8')
if s.isprintable() and n < 500:
return s
except Exception:
pass
return {"_bytes": n, "hex": raw[:48].hex()}
if dt == MAP:
n = t.read_len()
m = {}
for _ in range(min(n, max_items)):
kt_tag, kdt = t.read_head()
k = decode_value(t, kdt, depth + 1) if kdt != STRUCT_BEGIN else _decode_struct(t, depth + 1)
vt_tag, vdt = t.read_head()
v = decode_value(t, vdt, depth + 1) if depth + 1 < max_depth else _skip_str(t, vdt)
m[str(k)] = v
return m
if dt == LIST:
n = t.read_len()
items = []
for _ in range(min(n, max_items)):
it_tag, idt = t.read_head()
items.append(decode_value(t, idt, depth + 1) if depth + 1 < max_depth else _skip_str(t, idt))
return items
if dt == STRUCT_BEGIN:
return _decode_struct(t, depth + 1)
return f"<0x{dt:02x}>"
def _skip_str(t: Taf, dt: int):
t.skip_value(dt)
return f"<0x{dt:02x}>"
def _decode_struct(t: Taf, depth: int = 0):
"""已消费 STRUCT_BEGIN,读到 STRUCT_END。"""
out = {}
cnt = 0
while not t.eof() and cnt < 100:
try:
tag, dt = t.read_head()
except EOFError:
break
if dt == STRUCT_END:
break
key = f"t{tag}"
if depth < 6:
try:
out[key] = decode_value(t, dt, depth)
except Exception as e:
t.skip_value(dt)
out[key] = f"<err {e}>"
else:
t.skip_value(dt)
out[key] = f"<0x{dt:02x}>"
cnt += 1
return out
def decode_struct_bytes(data: bytes):
"""解析一个 TAF struct 字节串(可含或不含 STRUCT_BEGIN 头)。"""
t = Taf(data)
out = {}
cnt = 0
while not t.eof() and cnt < 100:
try:
tag, dt = t.read_head()
except EOFError:
break
if dt == STRUCT_END:
break
key = f"t{tag}"
try:
out[key] = decode_value(t, dt, 0)
except Exception as e:
t.skip_value(dt)
out[key] = f"<err {e}>"
cnt += 1
return out