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