虎牙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
+157
View File
@@ -0,0 +1,157 @@
"""解析虎牙 App HAR:解码 WUP(TAF) 全字段 + dckey/check 分析。
用法:
.venv/bin/python tools/hy_har_parse.py <file.har> [--req] [--resp]
"""
from __future__ import annotations
import base64
import json
import sys
sys.path.insert(0, ".")
from tools.taf_decode import ( # noqa: E402
Taf, STRUCT_END, MAP, SIMPLE_LIST, decode_struct_bytes,
)
STR = (6, 7) # STRING1, STRING4
SIMPLE = 13
def norm_payload(data: bytes) -> bytes:
"""去掉可选 4 字节 WUP 长度前缀。"""
if len(data) < 4:
return data
declared = int.from_bytes(data[0:4], "big")
if declared in (len(data), len(data) - 4):
return data[4:]
return data
def decode_wup_header(data: bytes):
"""返回 (servant, func, sBuffer_bytes)。"""
t = Taf(norm_payload(data))
servant = ""
func = ""
sbuf = b""
while not t.eof():
try:
tag, dt = t.read_head()
except EOFError:
break
if tag > 10:
break
if tag == 5 and dt in STR:
servant = t.read_string_value(dt)
elif tag == 6 and dt in STR:
func = t.read_string_value(dt)
elif tag == 7 and dt == SIMPLE:
t.read_head()
n = t.read_len()
sbuf = t.d[t.p:t.p + n]
t.p += n
else:
t.skip_value(dt)
return servant, func, sbuf
def decode_sbuffer_map(sbuf: bytes):
"""sBuffer 是一个 Map<str, bytes>,返回 {key: bytes}。"""
if not sbuf:
return {}
t = Taf(sbuf)
out = {}
try:
tag, dt = t.read_head()
if not (tag == 0 and dt == MAP):
return {"<raw>": sbuf}
n = t.read_len()
for _ in range(min(n, 20)):
_, kt = t.read_head()
k = t.read_string_value(kt) if kt in STR else f"<k{kt}>"
_, vt = t.read_head()
if vt == SIMPLE:
t.read_head()
ln = t.read_len()
out[k] = t.d[t.p:t.p + ln]; t.p += ln
else:
t.skip_value(vt)
except Exception as e:
out["<err>"] = str(e).encode()
return out
def dump_har(path: str, show_req: bool = True, show_resp: bool = True):
with open(path, encoding="utf-8", errors="ignore") as fp:
h = json.load(fp)
ents = h["log"]["entries"]
targets = ("wup.huya.com", "statwup.huya.com", "wsapi.huya.com",
"udbdf.huya.com", "udblgn.huya.com", "udbreg.huya.com")
for i, e in enumerate(ents):
req = e["request"]
u = req["url"]
if req["method"] != "POST" or not any(x in u for x in targets):
continue
pd = req.get("postData", {})
txt = pd.get("text", "")
mime = pd.get("mimeType", "")
print("=" * 80)
print(f"#{i} {u[:110]}")
print(f" mime={mime} textLen={len(txt)}")
# ---- 请求 ----
if txt:
try:
raw = base64.b64decode(txt)
except Exception:
raw = txt.encode("latin1")
if len(raw) >= 6 and raw[4:6] == b"\x10\x03" and show_req:
servant, func, sbuf = decode_wup_header(raw)
print(f" [REQ WUP] {servant}.{func}")
for k, v in decode_sbuffer_map(sbuf).items():
print(f" {k}: {json.dumps(decode_struct_bytes(v), ensure_ascii=False)[:700]}")
else:
# 加密 binary(如 dckey/check、statwup 应用层加密)
print(f" [REQ BINARY] {len(raw)}B hex={raw[:32].hex()}")
analyze_binary(raw)
# ---- 响应 ----
resp = e.get("response", {})
c = resp.get("content", {})
rtext = c.get("text", "")
if rtext and show_resp:
if rtext.lstrip().startswith("{"):
print(f" [RESP JSON] {rtext[:220]}")
else:
try:
rraw = base64.b64decode(rtext)
except Exception:
rraw = rtext.encode("latin1")
if len(rraw) >= 6 and rraw[4:6] == b"\x10\x03":
servant, func, sbuf = decode_wup_header(rraw)
print(f" [RESP WUP] {servant}.{func}")
for k, v in decode_sbuffer_map(sbuf).items():
print(f" {k}: {json.dumps(decode_struct_bytes(v), ensure_ascii=False)[:700]}")
else:
print(f" [RESP BINARY] {len(rraw)}B hex={rraw[:32].hex()}")
elif rtext:
print(f" [RESP raw] {rtext[:200]!r}")
def analyze_binary(raw: bytes):
"""对加密 body 做基础分析:长度/对齐/头部熵。"""
print(f" len={len(raw)} mod16={len(raw) % 16} mod64={len(raw) % 64}")
# 头部是否像有固定结构(前几字节恒定)
print(f" head32={raw[:32].hex()}")
print(f" tail16={raw[-16:].hex()}")
if __name__ == "__main__":
args = [a for a in sys.argv[1:] if not a.startswith("--")]
path = args[0] if args else "-"
show_req = "--req" not in sys.argv or True
show_resp = "--resp" not in sys.argv or True
dump_har(path)