feat(huya): 整合 App 协议登录获取 Cookie 全链路并与 Web 登录解耦

- core/huya: 新增 app_login, wup_encoder, nonce_forge, cert_forge, envelope_forge, device_profile, udb_aes
- core/huya/__init__.py: 导出 login_huya_app_password 与 HuyaAppPasswordLogin
- web/backend: 新增 /accounts/app-password-login 与 /accounts/app-password-login/selected 路由及 Schema,与原 Web 密码登录独立分开
- web/frontend: 增加 App 登录 API 与前端界面“App 登录选中”操作,弹窗结果明确区分
- tests: 新增 test_huya_app_login.py 单元测试覆盖全链路
This commit is contained in:
yml2213
2026-08-26 15:51:30 +08:00
parent 4eec01568f
commit da2cf5a61d
22 changed files with 2572 additions and 11 deletions
+25
View File
@@ -12,11 +12,15 @@ __all__ = [
"HuyaLoginError",
"HuyaLoginResult",
"HuyaPasswordLogin",
"HuyaAppLoginError",
"HuyaAppQrAuthRequiredError",
"HuyaAppPasswordLogin",
"HuyaSmsCodeResult",
"HuyaSmsLogin",
"HuyaVerificationError",
"HuyaVerificationSolver",
"login_huya_password",
"login_huya_app_password",
"login_huya_sms",
"send_huya_sms_code",
"solve_huya_verification",
@@ -57,6 +61,27 @@ def __getattr__(name: str):
}
globals().update(values)
return values[name]
if name in {
"HuyaAppLoginError",
"HuyaAppQrAuthRequiredError",
"HuyaAppPasswordLogin",
"login_huya_app_password",
}:
from .app_login import (
HuyaAppLoginError,
HuyaAppQrAuthRequiredError,
HuyaAppPasswordLogin,
login_huya_app_password,
)
values = {
"HuyaAppLoginError": HuyaAppLoginError,
"HuyaAppQrAuthRequiredError": HuyaAppQrAuthRequiredError,
"HuyaAppPasswordLogin": HuyaAppPasswordLogin,
"login_huya_app_password": login_huya_app_password,
}
globals().update(values)
return values[name]
if name in {
"HuyaSmsCodeResult",
"HuyaSmsLogin",
+531
View File
@@ -0,0 +1,531 @@
"""虎牙 App 渠道协议登录模块。
流程:
1. 账号+密码 -> 独立设备画像 -> WUP 密码登录 (POST wup.huya.com)
2. safe_auth 滑块自动过验 -> 提取 fresh cred 与 真实 uid
3. 本地 XXTEA 算 nonce -> 铸造登录证书 (cert_forge) -> 补丁 WUP 信封 (envelope_forge)
4. 模拟扫码绑定四步流 (getQrId -> scanQrPicNotify -> bindQrLoginUser -> tryQrLogin) 获取 biztoken
5. POST /web/cookie/verify 兑换获取全套网页 Cookie
"""
from __future__ import annotations
import base64
import hashlib
import json
import random
import re
import struct
import time
import uuid
from typing import Mapping
from urllib.parse import parse_qs, quote, urlparse
import requests
from loguru import logger
from .cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1
from .cookie_utils import normalize_huya_cookie
from .device_fingerprint import get_huya_sdid
from .device_profile import get_profile
from .envelope_forge import Envelope
from .login import HuyaCredentialError, HuyaLoginError, HuyaLoginResult
from .nonce_forge import K1_DEFAULT, gen_nonce
from .wup_encoder import build_password_login_wup
WUP_URL = "https://wup.huya.com"
UDB_BASE = "https://udblgn.huya.com"
APP_SIGN_WEB = "1ce3bf682483d03f146f58232ec10635"
APP_SIGN_H5 = "0ba67962ab9e12387648efeae2750777"
UA_PC = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
)
APP_UA_MOBILE = (
"Mozilla/5.0 (Linux; Android 11; M2102J2SC Build/RKQ1.200826.002; wv) "
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 "
"Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/13.4.22/xiaomi/30"
)
RISK_URL_RE = re.compile(rb"https://aq\.huya\.com/p/safe_auth/[^\x00-\x20\"'\\<>]+")
_URL_TAIL_KEEP = set(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%"
)
DEFAULT_GOLDEN_DEV = {
"app_version": "13.4.22",
"sdk_version": "1.0.80138",
"vendor": "xiaomi",
"model": "M2102J2SC",
"os": "android",
"ip": "127.0.0.1",
"fingerprint": "02df398797432eadefcc12767119ad5e80999389",
"screen": "M2102J2SC,30,11",
"width": "1080",
"height": "2120",
"device_id": "7c5387e0539c023c31c4ff0e807e7256117385ee",
"hdid": "ed0db8334cadd236c00cadf7e11ab5a5",
"safedeviceid": (
"PQwemAN9NHkZKoMqVTFUZBIypqMTaQEOrmXr37xQVhQZqrL/gUKEQ11xvE0ju48V8O/"
"t9UBGSp27m4+6bP4IiAEnpaR5Rj1kHEfN2SPLPqYZW9vroxUSoAvjJn6ezTP9jWGxxlRDCbt"
"Py4Rd6MencYT/pNImVIWK+YbNKZt1O05bHUFhqHf3"
),
}
class HuyaAppLoginError(HuyaLoginError):
"""虎牙 App 登录失败。"""
class HuyaAppQrAuthRequiredError(HuyaAppLoginError):
"""风控要求扫码验证 (qr_auth)。"""
def _golden_session_assets() -> tuple[dict, str, str]:
mj = {
"associationId": 8193,
"funcName": "hypasswordLogin",
"group": 1,
"id": 4097,
"session": 3251699,
"step": 1,
"stillLogin": False,
"traceId": "0b8f098ff64a5bdc-23473-94783833787595172451",
"type": 3,
"uid": 0,
"userContext": "",
}
ua = (
'{"curl":"登录页","furl":"我的","latitude":"-1.0","longitude":"-1.0","ssid":"",'
'"user_action":[{"id":"24","time":"1787595171950","x":"277","y":"1057"},'
'{"id":"11","time":"1787595172444","x":"296","y":"952"}]}'
)
sd = (
"PQwemAN9NHkZKoMqVTFUZBIypqMTaQEOrmXr37xQVhQZqrL/gUKEQ11xvE0ju48V8O/"
"t9UBGSp27m4+6bP4IiAEnpaR5Rj1kHEfN2SPLPqYZW9vroxUSoAvjJn6ezTP9jWGxxlRDCbt"
"Py4Rd6MencYT/pNImVIWK+YbNKZt1O05bHUFhqHf3"
)
return mj, ua, sd
def wup_password_login_raw(
account: str,
password: str,
timeout: int = 15,
device_info: dict | None = None,
safedeviceid: str | None = None,
hdid: str | None = None,
proxies: dict | None = None,
) -> bytes:
"""发送 WUP 密码登录,返回原始响应字节。"""
uid_str = account[3:] if account.startswith("hy_") else account
mj, ua, sd = _golden_session_assets()
dev = device_info or DEFAULT_GOLDEN_DEV
pkt = build_password_login_wup(
uid_str,
hashlib.sha1(password.encode()).hexdigest(),
safedeviceid or dev.get("safedeviceid") or sd,
hdid or dev.get("hdid") or "ed0db8334cadd236c00cadf7e11ab5a5",
mj["session"],
mj["traceId"],
ua,
dev,
)
r = requests.post(
WUP_URL,
data=pkt,
headers={
"Content-Type": "application/multipart-formdata; charset=UTF-8",
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 11)",
"Accept-Encoding": "gzip",
},
proxies=proxies,
timeout=timeout,
)
if r.status_code != 200:
raise HuyaAppLoginError(f"登录 HTTP 状态异常: {r.status_code}")
return r.content
def parse_cred(resp: bytes) -> bytes | None:
"""从 WUP 响应中提取 114 字节的 cred。"""
s = resp.find(b"\x0a\x0a", 0x40)
e = resp.find(b"_wup_header")
if s < 0 or e < 0:
return None
d = resp[s:e - 6]
m = re.search(rb"\x3d\x00([\x00-\x03])(.)", d)
if not m:
return None
ln = m.group(2)[0]
st = m.start() + 4
cred = d[st:st + ln]
if len(cred) == 114 and cred[:1] == b"\x0a":
return cred
return None
def parse_risk_url(resp: bytes) -> str | None:
"""提取 safe_auth 风控 URL。"""
urls = []
for m in RISK_URL_RE.finditer(resp):
u = m.group().decode("utf-8", "ignore")
while u and u[-1] not in _URL_TAIL_KEEP:
u = u[:-1]
urls.append(u)
if not urls:
return None
pt = [u for u in urls if "pt_auth" in u]
return (pt or urls)[0]
def solve_safe_auth(risk_url: str, proxies=None, max_retry: int = 3) -> dict:
"""解 safe_auth 风控滑块。"""
from .verification.solver import (
HuyaQrAuthRequiredError,
HuyaVerificationSolver,
)
q = {k: v[0] for k, v in parse_qs(urlparse(risk_url).query, keep_blank_values=True).items()}
app_id = str(q.get("appId") or "5002")
last_err: Exception | None = None
for attempt in range(max_retry):
solver = HuyaVerificationSolver(
ua=APP_UA_MOBILE,
proxies=proxies,
app_id=app_id,
page_url=risk_url,
use_touch_events=True,
)
try:
result = solver.solve(risk_url)
except HuyaQrAuthRequiredError:
raise
except Exception as exc:
last_err = exc
logger.warning(f"[safe_auth] 第 {attempt + 1} 次过验异常: {exc}")
time.sleep(1.0)
continue
auth_id = str((result or {}).get("authId") or "")
if auth_id:
logger.info(f"[safe_auth] 滑块通过 authId={auth_id[:24]}...")
return result
raise HuyaAppLoginError(f"safe_auth 滑块过验失败: {last_err or '未返回 authId'}")
def parse_real_uid(resp: bytes) -> int:
"""从登录响应中提取 12 位真实 UID。"""
s = resp.find(b"\x0a\x0a", 0x40)
e = resp.find(b"_wup_header")
if s >= 0 and e >= 0:
chunk = resp[s:e]
if len(chunk) > 243:
uid = struct.unpack_from(">Q", chunk, 235)[0]
if 1_100_000_000_000 < uid < 1_300_000_000_000:
return uid
for off in range(len(chunk) - 8):
u = struct.unpack_from(">Q", chunk, off)[0]
if 1_100_000_000_000 < u < 1_300_000_000_000:
return u
raise HuyaAppLoginError("无法从登录响应解析真实 UID")
def login_cred_with_flow(
account: str,
password: str,
max_rounds: int = 3,
device_info: dict | None = None,
proxies: dict | None = None,
) -> tuple[bytes, int]:
"""账号密码 -> (新鲜cred, 真实uid)。自动过 safe_auth 滑块。"""
for rnd in range(max_rounds):
resp = wup_password_login_raw(account, password, device_info=device_info, proxies=proxies)
cred = parse_cred(resp)
if cred:
uid = parse_real_uid(resp)
return cred, uid
risk_url = parse_risk_url(resp)
if risk_url:
kind = "pt_auth(滑块)" if "pt_auth" in risk_url else ("qr_auth(扫码)" if "qr_auth" in risk_url else "未知")
logger.info(f"[huya-app] 第 {rnd + 1} 轮触发安全验证: {kind}")
if "qr_auth" in risk_url:
raise HuyaAppQrAuthRequiredError(
f"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
)
solve_safe_auth(risk_url, proxies=proxies)
logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...")
continue
raise HuyaAppLoginError("登录未返回凭据也无风控URL(密码错误或账号状态异常)")
raise HuyaAppLoginError(f"{max_rounds} 轮内未取得登录凭据")
class QrRole:
"""udblgn.huya.com/qrLgn/* 的 JSON 协议封装。"""
def __init__(self, pc: bool, sdid: str, proxies: dict | None = None):
self.pc = pc
self.sdid = sdid
self.s = requests.Session()
self.s.trust_env = False
if proxies:
self.s.proxies.update(proxies)
ctx_hex = uuid.uuid4().hex
tail = "CBC9F93DBEB000011DB11EC02A401BAD-" if pc else uuid.uuid4().hex.upper()
tail = tail if tail.endswith("-") else tail + "-"
prefix = "WB" if pc else "H5"
self.context = f"{prefix}-{ctx_hex}-{tail}"
self.page_id = random.randint(40_000_000, 41_000_000)
self.req_counter = random.randint(40_000_000, 41_000_000)
self.s.headers.update({
"User-Agent": UA_PC if pc else APP_UA_MOBILE,
"Origin": UDB_BASE,
"content-type": "application/json;charset=UTF-8",
"Accept": "*/*",
})
def _headers(self, uri: str) -> dict:
mid = "2.6" if self.pc else "2.5"
return {
"context": self.context,
"uri": uri,
"reqid": str(self.req_counter),
"lcid": "2052",
"Referer": f"{UDB_BASE}/web/middle/{mid}/{self.page_id}/https/{self.context.split('-')[1]}",
}
def call(self, path: str, uri: str, data: dict, cookies: dict | None = None) -> dict:
envelope = {
"uri": uri,
"version": "2.6" if self.pc else "2.5",
"context": self.context,
"appId": "5002" if self.pc else "5131",
"appSign": APP_SIGN_WEB if self.pc else APP_SIGN_H5,
"authId": "",
"sdid": self.sdid,
"lcid": "2052",
"byPass": "3",
"requestId": str(self.req_counter),
"data": data,
}
self.req_counter += random.randint(120, 400)
r = self.s.post(
f"{UDB_BASE}{path}",
json=envelope,
headers=self._headers(uri),
cookies=cookies,
timeout=20,
)
return r.json()
def web_behavior(page: str = "https://www.huya.com/g") -> tuple[str, str]:
now = int(time.time() * 1000) - random.randint(3000, 10000)
acts = []
d = random.randint(800, 1800)
for a in ("7", "7", "8", "8"):
now += random.randint(600, 2000)
d += random.randint(600, 2000)
acts.append({"id": a, "d": d, "time": now})
val = {"furl": page, "curl": page, "user_action": acts}
beh = quote(json.dumps(val, separators=(",", ":")), safe="~()*!.'")
return beh, quote(page, safe="")
class HuyaAppPasswordLogin:
"""虎牙 App 渠道纯协议密码登录器。"""
def __init__(
self,
username: str,
password: str,
proxies: Mapping[str, str] | None = None,
timeout: tuple[float, float] | None = None,
force_new_device: bool = False,
device_info: dict | None = None,
):
self.username = username.strip()
self.password = password
self.proxies = dict(proxies) if proxies else None
self.timeout = timeout or (10.0, 25.0)
self.force_new_device = force_new_device
self.device_info = device_info or get_profile(self.username, force_new=force_new_device)
def login(self) -> HuyaLoginResult:
"""执行完整 App 登录获取 Cookie 流程。"""
acct = self.username
logger.info(f"[huya-app] 开始登录账号 {acct} (机型: {self.device_info.get('model')})...")
# 1) 获取新鲜 cred 与 真实 uid (自动过 safe_auth 滑块)
try:
cred, uid = login_cred_with_flow(
acct,
self.password,
device_info=self.device_info,
proxies=self.proxies,
)
except HuyaAppQrAuthRequiredError as exc:
return HuyaLoginResult(
success=False,
message=str(exc),
code="QR_AUTH_REQUIRED",
)
except Exception as exc:
return HuyaLoginResult(
success=False,
message=f"App 登录凭证获取失败: {exc}",
code="LOGIN_FAILED",
)
logger.info(f"[huya-app] 成功获取 cred ({len(cred)}B), uid={uid}")
# 2) 本地生成 nonce 铸造证书 (P1 指纹与该账号设备画像一致)
try:
env = Envelope.load()
orig = base64.b64decode(env.cert_b64)
f = parse_p1(decrypt_cert(orig))
st = int(time.time() * 1000)
rnd = gen_nonce(uid, K1_DEFAULT, service_time_ms=st, counter=0)
fp_bytes = self.device_info["fingerprint"].encode("ascii")
p1 = build_p1(f["app_id"], fp_bytes, cred, rnd=rnd)
cert = base64.b64encode(forge_cert(p1, key_idx=orig[1])).decode()
# 3) 信封补丁
raw = bytearray(env.raw)
raw[env.cert_off:env.cert_off + env.cert_len] = cert.encode("ascii")
if env.uid != uid:
struct.pack_into(">Q", raw, env.uid_off, uid)
wup = base64.b64encode(bytes(raw)).decode("ascii")
except Exception as exc:
return HuyaLoginResult(
success=False,
message=f"证书铸造/信封补丁失败: {exc}",
code="CERT_FORGE_FAILED",
)
# 4) 模拟扫码绑定四步流获取 biztoken
try:
sdid_obj = get_huya_sdid(allow_fallback=True)
sdid = sdid_obj.sdid if sdid_obj else ""
pc = QrRole(pc=True, sdid=sdid, proxies=self.proxies)
ph = QrRole(pc=False, sdid=sdid, proxies=self.proxies)
beh, page = web_behavior()
# 4.1 获取 qrId
resp = pc.call(
"/qrLgn/getQrId",
"70001",
{"behavior": beh, "type": "", "domainList": "", "page": page},
)
qrid = (resp.get("data") or {}).get("qrId")
if not qrid:
return HuyaLoginResult(
success=False,
message=f"获取 qrId 失败: {resp.get('message') or resp}",
code="QR_ID_FAILED",
)
# 4.2 扫码与绑定
cp = f"https://aq.huya.com/r/confirm.html?k={qrid}&id=5002"
ph.call(
"/qrLgn/scanQrPicNotify",
"70005",
{
"qrId": qrid,
"wupData": wup,
"behavior": quote("[]", safe=""),
"page": quote(cp, safe=""),
},
)
r2 = ph.call(
"/qrLgn/bindQrLoginUser",
"70007",
{
"qrId": qrid,
"wupData": wup,
"behavior": quote("[]", safe=""),
"page": quote(cp, safe=""),
},
)
if r2.get("returnCode") not in (0, "0", None) and r2.get("returnCode") != 0:
logger.warning(f"[huya-app] bind 返回码: {r2.get('returnCode')} msg: {r2.get('message')}")
# 4.3 轮询 tryQrLogin
biztoken = None
for _ in range(12):
rt = pc.call(
"/qrLgn/tryQrLogin",
"70003",
{
"qrId": qrid,
"remember": "1",
"domainList": "",
"behavior": beh,
"page": page,
},
)
dt = rt.get("data") or {}
if dt.get("stage") == 2:
biztoken = dt.get("biztoken")
break
time.sleep(1.5)
if not biztoken:
return HuyaLoginResult(
success=False,
message="未能在轮询时间内获取到 biztoken (绑定超时或失败)",
code="BIZTOKEN_TIMEOUT",
)
# 5) POST /web/cookie/verify 兑换 Cookie
verify_resp = pc.s.post(
"https://udblgn.huya.com/web/cookie/verify",
json={"appId": 5002},
timeout=15,
)
if verify_resp.status_code != 200:
return HuyaLoginResult(
success=False,
message=f"Cookie verify 兑换 HTTP 失败: {verify_resp.status_code}",
code="VERIFY_FAILED",
)
cookie_str = normalize_huya_cookie(pc.s.cookies)
if "udb_cred" not in cookie_str and "yyuid" not in cookie_str:
return HuyaLoginResult(
success=False,
message="Cookie 兑换完成但缺失关键凭据 (udb_cred/yyuid)",
code="COOKIE_INCOMPLETE",
)
logger.info(f"[huya-app] 账号 {acct} 登录成功,获取完整 Cookie ({len(cookie_str)}B)")
return HuyaLoginResult(
success=True,
cookie=cookie_str,
message="App协议登录成功",
sdid=sdid,
context=pc.context,
)
except Exception as exc:
return HuyaLoginResult(
success=False,
message=f"扫码绑定兑换 Cookie 失败: {exc}",
code="BIND_FLOW_FAILED",
)
def login_huya_app_password(
username: str,
password: str,
proxies: Mapping[str, str] | None = None,
timeout: tuple[float, float] | None = None,
force_new_device: bool = False,
) -> HuyaLoginResult:
"""函数式入口:使用 App 协议执行虎牙密码登录并获取全套 Cookie。"""
return HuyaAppPasswordLogin(
username=username,
password=password,
proxies=proxies,
timeout=timeout,
force_new_device=force_new_device,
).login()
+90
View File
@@ -0,0 +1,90 @@
"""虎牙 udb 登录证书 (biz_token) 铸造与解析工具。
证书格式:base64( [0x0c][key_idx][AES-128-ECB(key16, zeropad(P1))] )
"""
from __future__ import annotations
import base64
import os
import struct
from .udb_aes import udb_decrypt, udb_encrypt
KEY_TABLE_RAW = [
"4VYcPdvKKqjBHZtCmbroRXHk",
"xXEDWqiKLGwEZ6HubEiswCqK",
"3FMHubdKosFrhmXNLHTNHZwe",
]
DEFAULT_KEY16 = KEY_TABLE_RAW[0][:16].encode()
def build_p1(
app_id: bytes,
fingerprint: bytes,
cred: bytes,
rnd: bytes | None = None,
) -> bytes:
"""按样本结构组装 P1 明文。
- app_id: b'5008' 等 4 字节
- fingerprint: 40 字节 ASCII hex
- cred: 114 字节 hyCred
- rnd: 20 字节 nonce,缺省用 os.urandom(20)
"""
assert len(fingerprint) == 40, f"指纹应为40B, 实际为 {len(fingerprint)}"
assert len(cred) == 114, f"cred应为114B, 实际为 {len(cred)}"
r = os.urandom(20) if rnd is None else rnd
p1 = b"\x01\x04\x00" + app_id
p1 += struct.pack("<H", len(r)) + r
p1 += struct.pack("<H", len(fingerprint)) + fingerprint
p1 += struct.pack("<H", len(cred)) + cred
assert len(p1) == 187, f"P1长度应为187,实际为 {len(p1)}"
return p1
def parse_p1(data: bytes) -> dict:
"""解析 P1 结构。"""
o = 0
def tk(n: int) -> bytes:
nonlocal o
b = data[o:o + n]
o += n
return b
out = {
"head": tk(3).hex(),
"app_id": tk(4),
}
n = struct.unpack("<H", tk(2))[0]
out["rnd"] = tk(n)
n = struct.unpack("<H", tk(2))[0]
out["fingerprint"] = tk(n)
n = struct.unpack("<H", tk(2))[0]
out["cred"] = tk(n)
out["trailing"] = data[o:]
return out
def forge_cert(
p1: bytes,
key16: bytes = DEFAULT_KEY16,
key_idx: int = 0x20,
type_byte: int = 0x0C,
) -> bytes:
"""P1 -> 二进制证书 [type][key_idx][192B AES 密文]。"""
ct = udb_encrypt(key16, p1)
assert len(ct) == 192, len(ct)
return bytes([type_byte, key_idx]) + ct
def parse_cert(blob: bytes) -> tuple[int, int, bytes]:
"""[0x0c][idx][192B] -> (type, idx, ct)。"""
assert blob[0] == 0x0C and len(blob) >= 194, "非证书结构"
return blob[0], blob[1], blob[2:194]
def decrypt_cert(blob: bytes, key16: bytes = DEFAULT_KEY16) -> bytes:
"""解密二进制证书为 P1 明文。"""
_, _, ct = parse_cert(blob)
return udb_decrypt(key16, ct)
+99
View File
@@ -0,0 +1,99 @@
"""虎牙多账号设备画像生成与管理。
为每个账号生成并持久化独立的设备身份画像(机型、屏幕、指纹、设备ID等)。
"""
from __future__ import annotations
import hashlib
import json
import os
import random
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
DATA_DIR = ROOT / "data"
PRIMARY_PROFILE_DB = DATA_DIR / "huya_device_profiles.json"
FALLBACK_PROFILE_DB = ROOT / "evidence" / "device_profiles.json"
REAL_MODELS = [
("xiaomi", "M2102J2SC", "M2102J2SC,30,11", (1080, 2120)),
("vivo", "V2370A", "V2370A,34,13", (1080, 2412)),
("oppo", "PFFM20", "PFFM20,34,13", (1080, 2412)),
("honor", "SDY-AN00", "SDY-AN00,31,12", (1080, 2400)),
("samsung", "SM-G9910", "SM-G9910,31,12", (1440, 3200)),
("oneplus", "PGZ110", "PGZ110,34,13", (1080, 2412)),
("redmi", "23049PCD8G", "23049PCD8G,34,13", (1080, 2400)),
("realme", "RMX3366", "RMX3366,34,13", (1080, 2412)),
("iqoo", "V2183A", "V2183A,34,13", (1080, 2400)),
("nubia", "NX729J", "NX729J,33,13", (1080, 2400)),
("motorola", "XT2301-5", "XT2301-5,33,13", (1080, 2400)),
("gionee", "GN9013", "GN9013,29,10", (720, 1560)),
]
HDID = "ed0db8334cadd236c00cadf7e11ab5a5"
APP_VERSION = "13.4.22"
SDK_VERSION = "1.0.80138"
SAFEDEVICEID_DEFAULT = (
"PQwemAN9NHkZKoMqVTFUZBIypqMTaQEOrmXr37xQVhQZqrL/gUKEQ11xvE0ju48V8O/"
"t9UBGSp27m4+6bP4IiAEnpaR5Rj1kHEfN2SPLPqYZW9vroxUSoAvjJn6ezTP9jWGxxlRDCbt"
"Py4Rd6MencYT/pNImVIWK+YbNKZt1O05bHUFhqHf3"
)
def _rand_sha1_hex() -> str:
return hashlib.sha1(os.urandom(20)).hexdigest()
def generate_profile(model_pick=None) -> dict:
"""生成一套随机设备画像。"""
vendor, model, screen, (w, h) = (
random.choice(REAL_MODELS) if model_pick is None else model_pick
)
return {
"app_version": APP_VERSION,
"sdk_version": SDK_VERSION,
"vendor": vendor,
"model": model,
"os": "android",
"ip": "127.0.0.1",
"fingerprint": _rand_sha1_hex(),
"screen": screen,
"width": str(w),
"height": str(h),
"device_id": _rand_sha1_hex(),
"hdid": HDID,
"safedeviceid": SAFEDEVICEID_DEFAULT,
}
def _load_db() -> dict:
if PRIMARY_PROFILE_DB.exists():
try:
return json.loads(PRIMARY_PROFILE_DB.read_text("utf-8"))
except Exception:
pass
if FALLBACK_PROFILE_DB.exists():
try:
return json.loads(FALLBACK_PROFILE_DB.read_text("utf-8"))
except Exception:
pass
return {}
def _save_db(db: dict) -> None:
try:
PRIMARY_PROFILE_DB.parent.mkdir(parents=True, exist_ok=True)
PRIMARY_PROFILE_DB.write_text(json.dumps(db, indent=2, ensure_ascii=False), encoding="utf-8")
except Exception:
pass
def get_profile(account: str, force_new: bool = False) -> dict:
"""按账号获取或创建画像(幂等:同账号复用同一套)。"""
db = _load_db()
if not force_new and account in db:
return db[account]
p = generate_profile()
db[account] = p
_save_db(db)
return p
+260
View File
@@ -0,0 +1,260 @@
"""wupData 信封构造与补丁工具。
解析与改写 WUP 信封中的 cert、uid、session 等字段。
"""
from __future__ import annotations
import base64
import json
import struct
from pathlib import Path
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
STRING1, STRING4 = 0x06, 0x07
MAP, LIST = 0x08, 0x09
STRUCT_BEGIN, STRUCT_END = 0x0A, 0x0B
ZERO, SIMPLE_LIST = 0x0C, 0x0D
DEFAULT_QURL_B64 = (
"AAAD5hADLDxCAFpBBVYMaHV5YXVkYndlYnVpZgdkZWZhdWx0fQABA7gIAAIGCV93dXBfZGF0YR0AAQOKCgoMFgMxLjAm"
"ynsiYXNzb2NpYXRpb25JZCI6MTg0NTQ5MzkyLCJmdW5jTmFtZSI6IiIsImdyb3VwIjowLCJpZCI6MTg0NTQ5MzkyLCJz"
"ZXNzaW9uIjo1OTE0ODg1LCJzdGVwIjowLCJzdGlsbExvZ2luIjpmYWxzZSwidHJhY2VJZCI6IjBiOGYwOThmZjY0YTVi"
"ZGMtMjY2OTItODI2MzkzOTQ3ODc2NjM1NTEzNjUiLCJ0eXBlIjoyLCJ1aWQiOjAsInVzZXJDb250ZXh0IjoiIn02BDUw"
"MDhAA1a0UFF3ZW1BTjlOSGtaS29NcVdNUW5WUkl5cHFNVGFRRU9ybVhyMzd4UVZoUVpxclA1aVVLRVExMXh2RTB2cE1n"
"a2xlajIzbEpGYmFHVW5LUEFhYnhWaUF0TnZyTkxBbXhBQzJyRGp6Qy9JSU0vSWFQeTdTcytvQXZqSmltR2pBS2RnVmpr"
"bUhWV2Q2bEw4cUZScU4zWkJMamt4c2xUQjczaXNvallWcjlrSFhWdUh3SkxoM3YzZgB2AIYAlgALGgYgZWQwZGI4MzM0"
"Y2FkZDIzNmMwMGNhZGY3ZTExYWI1YTUWBzEzLjQuMjImCTEuMC44MDEzODYARgkxMjcuMC4wLjFWBnhpYW9taWYACyoA"
"ARYJTTIxMDJKMlNDJigwMmRmMzk4Nzk3NDMyZWFkZWZjYzEyNzY3MTE5YWQ1ZTgwOTk5Mzg5NgdhbmRyb2lkRg9NMjEw"
"MkoyU0MsMzAsMTFmBDEwODB2BDIxMjCGKDdjNTM4N2UwNTM5YzAyM2MzMWM0ZmYwZTgwN2U3MjU2MTE3Mzg1ZWULMwAA"
"ARdRuGVvRwAAAQREQ0JHY0QyN0liSWEvZnZ0UHhNT2xZdGJUY0M3bWRaUlJ3YzIyc2NnQkFyRTJ5eTZwSUdIQjNMK0tr"
"MzVxVS9iaWc1Qk1TVVVSd3gzeS9wWVpWajRjd20rWEg1dnNrakR2SzNhUlhudEJGcURDQUUvaUVIbGs4ZXJ3VUJZdmJM"
"aXlIb0YrSytQam5GTGJRMmlzSHVhcUtqTHAvWmRETDlxSit3VEVSb3h0ZjFuUzlTZ0l2N3lCaVIyMjd4N3F3RjllUTVu"
"ckNaRitnRnczelVZb2N6Uk9jbHE1aXZDclhRZTVSZ3hOYkp5aWQ3ZkZqTVhYYlNQdHBIZ2p0TVJtdWp6RVRvPVYAZgAL"
"BhB3dXB1ZGJyZXF1ZXN0X3YwHQAABQIAWkEFjJgMqAw="
)
def _read_len_int(d: bytes, p: int) -> tuple[int, int]:
dt = d[p] & 0x0F
if dt == ZERO:
return 0, p + 1
if dt == INT8:
return struct.unpack_from(">b", d, p + 1)[0], p + 2
if dt == INT16:
return struct.unpack_from(">h", d, p + 1)[0], p + 3
if dt == INT32:
return struct.unpack_from(">i", d, p + 1)[0], p + 5
raise ValueError(f"长度int类型异常 {dt:#x}@{p}")
def _skip_value(d: bytes, p: int, dt: int) -> int:
if dt == ZERO:
return p
if dt == INT8:
return p + 1
if dt == INT16:
return p + 2
if dt == INT32:
return p + 4
if dt == INT64:
return p + 8
if dt == STRING1:
return p + 1 + d[p]
if dt == STRING4:
return p + 4 + struct.unpack_from(">i", d, p)[0]
if dt == SIMPLE_LIST:
p += 1
n, p = _read_len_int(d, p)
return p + n
if dt == MAP:
n, p = _read_len_int(d, p)
for _ in range(n):
h = d[p]
p += 1
kd = h & 0x0F
if kd == STRING1:
p += 1 + d[p]
elif kd == STRING4:
p += 4 + struct.unpack_from(">i", d, p)[0]
else:
raise ValueError(f"map key 类型 {kd:#x}")
vh = d[p]
p += 1
p = _skip_value(d, p, vh & 0x0F)
return p
if dt == LIST:
n, p = _read_len_int(d, p)
eh = d[p]
p += 1
edt = eh & 0x0F
for _ in range(n):
p = _skip_value(d, p, edt)
return p
if dt == STRUCT_BEGIN:
while True:
h = d[p]
p += 1
sdt = h & 0x0F
if sdt == STRUCT_END:
break
p = _skip_value(d, p, sdt)
return p
raise ValueError(f"未知类型 {dt:#x}@{p}")
class Envelope:
"""WUP 请求信封结构解析器与补丁器。"""
def __init__(self, raw_bytes: bytes):
self.raw = bytearray(raw_bytes)
self.tag4_span: tuple[int, int] | None = None
self.meta_json_span: tuple[int, int] | None = None
self.uid_off: int | None = None
self.cert_off: int | None = None
self.cert_len: int = 260
self._parse()
@classmethod
def load(cls, path: str | Path | None = None) -> "Envelope":
"""加载信封模板,支持从文件加载或使用内嵌金样本。"""
if path:
p = Path(path)
if p.exists():
return cls._load_from_path(p)
# 尝试查找 evidence/cert_keycap.json
candidate = Path(__file__).resolve().parent.parent.parent / "evidence" / "cert_keycap.json"
if candidate.exists():
try:
return cls._load_from_path(candidate)
except Exception:
pass
return cls(base64.b64decode(DEFAULT_QURL_B64))
@classmethod
def _load_from_path(cls, p: Path) -> "Envelope":
if p.suffix == ".json":
j = json.loads(p.read_text("utf-8"))
q = next(e["data"] for e in j if e.get("type") == "qurl_done" and e.get("data"))
return cls(base64.b64decode(q))
return cls(p.read_bytes())
def _parse(self) -> None:
d = self.raw
p = 4
# WUP header
while p < len(d):
h = d[p]
p += 1
tag, dt = (h >> 4) & 0x0F, h & 0x0F
if tag == 4 and dt == INT32:
self.tag4_span = (p, p + 4)
p += 4
elif tag == 7 and dt == SIMPLE_LIST:
p += 1 # 元素类型头
n, p = _read_len_int(d, p)
self._parse_sbuffer(p, n)
break
else:
p = _skip_value(d, p, dt)
def _parse_sbuffer(self, start: int, ln: int) -> None:
d = self.raw
p = start
h = d[p]
p += 1
assert (h & 0x0F) == MAP
cnt, p = _read_len_int(d, p)
for _ in range(cnt):
kh = d[p]
p += 1
kln = d[p]
p += 1
k = bytes(d[p:p + kln])
p += kln
vh = d[p]
p += 1
if k == b"_wup_data":
assert (vh & 0x0F) == SIMPLE_LIST
p += 1
wup_data_len, p = _read_len_int(d, p)
self._parse_wup_data_struct(p, wup_data_len)
p += wup_data_len
else:
p = _skip_value(d, p, vh & 0x0F)
def _parse_wup_data_struct(self, start: int, ln: int) -> None:
d = self.raw
q = start
assert (d[q] & 0x0F) == STRUCT_BEGIN
q += 1
while q < start + ln:
hh = d[q]
q += 1
tag, dt = (hh >> 4) & 0x0F, hh & 0x0F
if tag == 15:
tag = d[q]
q += 1
if dt == STRUCT_END:
break
if tag == 0 and dt == STRUCT_BEGIN:
while True:
h2 = d[q]
q += 1
tg2, dt2 = (h2 >> 4) & 0x0F, h2 & 0x0F
if tg2 == 15:
tg2 = d[q]
q += 1
if dt2 == STRUCT_END:
break
vs = q
q = _skip_value(d, q, dt2)
if tg2 == 2 and dt2 in (STRING1, STRING4):
off = vs + (4 if dt2 == STRING4 else 1)
self.meta_json_span = (off, q)
elif tag == 3 and dt == INT64:
self.uid_off = q
q += 8
elif tag == 4 and dt == STRING4:
ln_c = struct.unpack_from(">i", d, q)[0]
self.cert_off = q + 4
self.cert_len = ln_c
q = self.cert_off + ln_c
else:
q = _skip_value(d, q, dt)
if self.uid_off is None or self.cert_off is None:
raise ValueError("未在信封中定位到 t3(uid) 或 t4(cert)")
@property
def uid(self) -> int:
return struct.unpack_from(">Q", self.raw, self.uid_off)[0]
def patch_uid(self, uid: int) -> "Envelope":
struct.pack_into(">Q", self.raw, self.uid_off, uid)
return self
@property
def cert_b64(self) -> bytes:
return bytes(self.raw[self.cert_off:self.cert_off + self.cert_len])
def patch_cert(self, cert: bytes) -> "Envelope":
b64 = base64.b64encode(cert)
if len(b64) != self.cert_len:
raise ValueError(
f"证书b64长度不符: {len(b64)} != 模板 {self.cert_len} (cert {len(cert)}B)"
)
self.raw[self.cert_off:self.cert_off + self.cert_len] = b64
return self
def patch_session(self, session: int) -> "Envelope":
if self.tag4_span:
struct.pack_into(">I", self.raw, self.tag4_span[0], session & 0xFFFFFFFF)
d = self.raw
i = d.find(b"wupudbrequest_v0")
if i >= 0:
j = i + len(b"wupudbrequest_v0") + 4
if j + 4 <= len(d):
struct.pack_into(">I", d, j, session & 0xFFFFFFFF)
return self
def wup_b64(self) -> str:
return base64.b64encode(bytes(self.raw)).decode()
+78
View File
@@ -0,0 +1,78 @@
"""虎牙证书 nonce(rnd) 本地生成器。
基于 XXTEA 算法与 uid + k1 派生密钥。
"""
from __future__ import annotations
import hashlib
import struct
import time
K1_DEFAULT = "865a4924a40897ac1fcfe6b4c2cbb0e3"
def _xxtea_encrypt_words(v: list[int], k: list[int]) -> list[int]:
"""标准 XXTEA (delta=0x9e3779b9, rounds=6+52/n)。"""
n = len(v)
if n < 2:
return v
z = v[n - 1]
y = v[0]
s = 0
delta = 0x9E3779B9
q = 6 + 52 // n
while q > 0:
s = (s + delta) & 0xFFFFFFFF
e = (s >> 2) & 3
p = 0
while p < n - 1:
y = v[p + 1]
z = (v[p] + ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((s ^ y) + (k[(p & 3) ^ e] ^ z)))) & 0xFFFFFFFF
v[p] = z
p += 1
y = v[0]
z = (v[n - 1] + ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((s ^ y) + (k[((n - 1) & 3) ^ e] ^ z)))) & 0xFFFFFFFF
v[n - 1] = z
q -= 1
return v
def xxtea_encrypt(data: bytes, key16: bytes) -> bytes:
"""虎牙专用 XXTEA: 输出 words = ceil(len/4)+1, 末尾 word 存原始长度。"""
n = len(data)
nwords = (n // 4) + 1
v = [0] * nwords
for i in range(n // 4):
v[i] = struct.unpack("<I", data[i * 4:i * 4 + 4])[0]
v[nwords - 1] = n
k = [struct.unpack("<I", key16[i * 4:i * 4 + 4])[0] for i in range(4)]
_xxtea_encrypt_words(v, k)
return b"".join(struct.pack("<I", w & 0xFFFFFFFF) for w in v)
def pack_u64(v: int) -> bytes:
"""8字节小端裸值。"""
return struct.pack("<Q", v & 0xFFFFFFFFFFFFFFFF)
def gen_nonce(
uid: int,
k1: str = K1_DEFAULT,
service_time_ms: int | None = None,
counter: int = 0,
) -> bytes:
"""按设备算法生成 20B nonce(rnd)。
- uid: 虎牙 uid 数字(如 1199666914671
- k1: 设备常量 32hex
- service_time_ms: 缺省为当前毫秒时间戳
- counter: 计数器(通常为 0
"""
st = service_time_ms if service_time_ms is not None else int(time.time() * 1000)
st &= 0x7FFFFFFFFFFF
nonce_val = (counter & 0xFFFF) | (st << 16)
data = pack_u64(st) + pack_u64(nonce_val)
key = hashlib.md5((str(uid) + k1).encode()).hexdigest()[:16].encode()
return xxtea_encrypt(data, key)
+35
View File
@@ -0,0 +1,35 @@
"""虎牙 Udb AES 加解密工具。
基于 AES-128-ECB 与 0 填充。
"""
from __future__ import annotations
from Crypto.Cipher import AES
MASTER_KEY_STR = b"HuyaUdb1928374650qwertyuiop"
def encrypt_block(key16: bytes, block16: bytes) -> bytes:
return AES.new(key16, AES.MODE_ECB).encrypt(block16)
def decrypt_block(key16: bytes, block16: bytes) -> bytes:
return AES.new(key16, AES.MODE_ECB).decrypt(block16)
def zero_pad(data: bytes) -> bytes:
"""UdbAESUtil::encrypt 填充:补 '\\0' 到 16 的倍数。"""
if len(data) % 16 == 0:
return data
return data + b"\x00" * (16 - len(data) % 16)
def udb_encrypt(key16: bytes, plain: bytes) -> bytes:
"""AES-128-ECB 加密,带零填充。"""
p = zero_pad(plain)
return AES.new(key16, AES.MODE_ECB).encrypt(p)
def udb_decrypt(key16: bytes, ct: bytes) -> bytes:
"""AES-128-ECB 解密,去除尾部零填充。"""
return AES.new(key16, AES.MODE_ECB).decrypt(ct).rstrip(b"\x00")
+256
View File
@@ -0,0 +1,256 @@
"""虎牙 App 密码登录 WUP(TAF) 请求帧编码器。
纯 Python 构造符合 TAF/WUP 协议的请求二进制帧。
"""
from __future__ import annotations
import json
import random
import struct
import time as _time
from typing import Any, Dict
# TAF 类型标签
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
STRING1, STRING4 = 0x06, 0x07
MAP, LIST = 0x08, 0x09
STRUCT_BEGIN, STRUCT_END = 0x0A, 0x0B
ZERO, SIMPLE_LIST = 0x0C, 0x0D
class _Writer:
"""TAF 输出流。"""
def __init__(self) -> None:
self.buf = bytearray()
def get(self) -> bytes:
return bytes(self.buf)
def head(self, tag: int, dtype: int) -> None:
if tag < 15:
self.buf.append((tag << 4) | dtype)
else:
self.buf.append(0xF0 | dtype)
self.buf.append(tag)
def int8(self, tag: int, v: int) -> None:
if v == 0:
self.head(tag, ZERO)
else:
self.head(tag, INT8)
self.buf += struct.pack("b", v)
def int16(self, tag: int, v: int) -> None:
if -128 <= v <= 127:
self.int8(tag, v)
else:
self.head(tag, INT16)
self.buf += struct.pack(">h", v)
def int32(self, tag: int, v: int) -> None:
if -32768 <= v <= 32767:
self.int16(tag, v)
else:
self.head(tag, INT32)
self.buf += struct.pack(">i", v)
def int64(self, tag: int, v: int) -> None:
if -2147483648 <= v <= 2147483647:
self.int32(tag, v)
else:
self.head(tag, INT64)
self.buf += struct.pack(">q", v)
def string(self, tag: int, s: str) -> None:
b = s.encode("utf-8")
if len(b) > 255:
self.head(tag, STRING4)
self.buf += struct.pack(">I", len(b))
else:
self.head(tag, STRING1)
self.buf += struct.pack("B", len(b))
self.buf += b
def bytes(self, tag: int, b: bytes) -> None:
"""SIMPLE_LIST (byte[])。"""
self.head(tag, SIMPLE_LIST)
self.head(0, INT8)
self.int32(0, len(b))
self.buf += b
def struct_begin(self, tag: int) -> None:
self.head(tag, STRUCT_BEGIN)
def struct_end(self) -> None:
self.head(0, STRUCT_END)
def list_begin(self, tag: int, n: int) -> None:
self.head(tag, LIST)
self.int32(0, n)
def map_begin(self, tag: int, n: int) -> None:
self.head(tag, MAP)
self.int32(0, n)
def _build_meta_json(session: int, trace_id: str) -> str:
"""构造 _wup_data.t0.t2 元数据 JSON。"""
meta: Dict[str, Any] = {
"associationId": 8193,
"funcName": "hypasswordLogin",
"group": 1,
"id": 4097,
"session": session,
"step": 1,
"stillLogin": False,
"traceId": trace_id,
"type": 3,
"uid": 0,
"userContext": "",
}
return json.dumps(meta, ensure_ascii=False, separators=(",", ":"))
def _make_name(uid_str: str) -> str:
"""登录名 = "hy_" + 虎牙号。"""
if uid_str.startswith("hy_"):
return uid_str
return "hy_" + uid_str
def make_user_action(now_ms: int | None = None) -> str:
"""生成一条随机的 userAction 风控行为 JSON。"""
if now_ms is None:
now_ms = int(_time.time() * 1000)
t1 = now_ms
t2 = now_ms + random.randint(200, 900)
return json.dumps(
{
"curl": "登录页",
"furl": "我的",
"latitude": "-1.0",
"longitude": "-1.0",
"ssid": "",
"user_action": [
{"id": "24", "time": str(t1),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600))},
{"id": "11", "time": str(t2),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600))},
],
},
ensure_ascii=False,
separators=(",", ":"),
)
def make_trace_id(pid: int = 0) -> str:
"""生成 traceId,格式 `<hex16>-<pid>-<毫秒时间戳>`。"""
return f"{random.getrandbits(64):016x}-{pid}-{int(_time.time() * 1000)}"
def _build_wup_data(w: _Writer, uid_str: str, sha1_password: str,
safedeviceid: str, hdid: str, session: int,
trace_id: str, user_action_json: str,
device_info: Dict[str, str]) -> None:
"""编码 _wup_data struct。"""
meta_json = _build_meta_json(session, trace_id)
name = _make_name(uid_str)
w.struct_begin(0)
# -- t0: 请求头 struct --
w.struct_begin(0)
w.int8(0, 0)
w.string(1, "1.0")
w.string(2, meta_json)
w.string(3, "5008")
w.int8(4, 3)
w.string(5, safedeviceid)
w.string(6, "")
w.string(7, "")
w.string(8, user_action_json)
w.string(9, "")
w.struct_end()
# -- t1: 设备信息 struct --
di = device_info
w.struct_begin(1)
w.string(0, hdid)
w.string(1, di.get("app_version", "13.4.22"))
w.string(2, di.get("sdk_version", "1.0.80138"))
w.string(3, "")
w.string(4, di.get("ip", "127.0.0.1"))
w.string(5, di.get("vendor", "xiaomi"))
w.string(6, "")
w.struct_end()
# -- t2: 屏幕/设备特征 struct --
w.struct_begin(2)
w.int8(0, 1)
w.string(1, di.get("model", "M2102J2SC"))
w.string(2, di.get("fingerprint", ""))
w.string(3, di.get("os", "android"))
w.string(4, di.get("screen", "M2102J2SC,30,11"))
w.string(6, str(di.get("width", "1080")))
w.string(7, str(di.get("height", "2120")))
w.string(8, di.get("device_id", ""))
w.struct_end()
# -- 登录字段 --
w.string(3, name)
w.string(4, sha1_password)
w.list_begin(5, 1)
w.string(0, "5008")
w.int8(6, 1)
w.map_begin(7, 0)
w.bytes(8, b"")
w.struct_end()
def build_password_login_wup(
uid_str: str,
sha1_password: str,
safedeviceid: str,
hdid: str,
session: int,
trace_id: str,
user_action_json: str,
device_info: Dict[str, str],
) -> bytes:
"""构造密码登录的 WUP TAF 请求体。"""
wd = _Writer()
_build_wup_data(wd, uid_str, sha1_password, safedeviceid, hdid,
session, trace_id, user_action_json, device_info)
wup_data = wd.get()
req = _Writer()
req.int32(0, session)
wupdbreq_v0 = req.get()
sb = _Writer()
sb.map_begin(0, 2)
sb.string(0, "_wup_data")
sb.bytes(1, wup_data)
sb.string(0, "wupudbrequest_v0")
sb.bytes(1, wupdbreq_v0)
s_buffer = sb.get()
w = _Writer()
w.int16(1, 3)
w.int8(2, 0)
w.int8(3, 0)
w.int32(4, session)
w.string(5, "huyaudbwebui")
w.string(6, "hypasswordLogin")
w.bytes(7, s_buffer)
w.int32(8, 0)
w.map_begin(9, 0)
w.map_begin(10, 0)
wup_body = w.get()
return struct.pack(">I", 4 + len(wup_body)) + wup_body