虎牙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,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
|
||||
Reference in New Issue
Block a user