feat(huya): nonce生成机制完全破解 - 任意账号零设备出完整CK闭环

动态hook实锤(scripts/hook_nonce_chain.py):
  rnd = XXTEA(pack(st) + pack(counter|st<<16), key=MD5hex(uid_str + k1)[:16])
  - st=毫秒时间戳, counter同st内递增
  - k1=设备常量865a4924...(不随账号变, evidence/nonce_k1.json)
  - Python复刻(tools/nonce_forge.py)对设备两轮rnd逐字节MATCH

推翻v2误判(X2'nonce账号绑定令牌'实为nonce内编码uid):
  - 换账号用目标uid重算nonce而非复用旧nonce!
  - 纯随机nonce必40020因解不出合法st/counter结构

实测: 同账号+跨账号(hy_300024708从未设备登录)本地nonce铸证bind均通过,
  full_web_cookie/full_auto_safe 任意账号密码->全套cookie(udb_cred/udb_biztoken)
This commit is contained in:
yml2213
2026-08-26 00:49:01 +08:00
parent da9937707c
commit 97e6d14139
16 changed files with 1831 additions and 127 deletions
+89 -37
View File
@@ -1,20 +1,20 @@
#!/usr/bin/env python3
"""一键全自动(含新账号safe_auth滑块): 账号密码 -> 页面cookie(biztoken)。
"""一键全自动(含新账号safe_auth滑块): 账号密码 -> 页面cookie(biztoken)。纯协议版。
与 full_auto_test.py 的区别: 可信账号直出cred之外,
2026-08-26: nonce 本地重算(tools/nonce_forge.py), 任意账号无需设备首登;
新/异地账号触发 safe_auth 时自动解 udbrtt 滑块并重发登录, 全程零人工。
用法:
.venv/bin/python tools/full_auto_safe.py <账号如hy_xxx或300023887> <密码>
前提: evidence/cert_keycap.json 内有较新鲜的 wupData 信封
(过期则先在设备上跑 scripts/hook_cert_keycap.py 刷新)
前提: evidence/cert_keycap.json 内有信封模板(任意账号共用, 仅需采集一次)
+ evidence/nonce_k1.json 或默认 k1 设备常量
"""
import base64
import json
import re
import subprocess
import struct
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
@@ -24,33 +24,34 @@ sys.path.insert(0, str(ROOT / "tools"))
from app_login_flow import QrAuthRequiredError, login_cred # noqa: E402
from cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1 # noqa: E402
from envelope_forge import Envelope # noqa: E402
from nonce_forge import K1_DEFAULT, gen_nonce # noqa: E402
def forge(cred: bytes) -> str:
"""复用信封原证书的nonce/指纹/key_idx仅换cred(纯随机nonce必40020)."""
env = Envelope.load()
def real_uid(acct: str, pwd: str) -> int:
"""登录响应解析真实uid(响应bean偏移235 u64be)。"""
from app_login_flow import wup_password_login_raw
resp = wup_password_login_raw(acct, pwd)
s = resp.find(b"\x0a\x0a", 0x40)
e = resp.find(b"_wup_header")
uid = struct.unpack_from(">Q", resp[s:e], 235)[0]
assert 1_100_000_000_000 < uid < 1_300_000_000_000, f"uid异常 {uid}"
return uid
def forge_for_uid(env: Envelope, cred: bytes, uid: int, k1: str) -> bytes:
"""本地重算nonce + 换cred 铸证(任意账号可用)。"""
orig = base64.b64decode(env.cert_b64)
f = parse_p1(decrypt_cert(orig))
P1 = build_p1(f["app_id"], f["fingerprint"], cred, rnd=f["rnd"])
return forge_cert(P1, key_idx=orig[1]).hex()
def bind_for_cookie(cred_json_path: Path) -> bool:
"""复用扫码绑定四步流(QrRole), 返回是否拿到biztoken。"""
probe = ROOT / "scripts/probe_forge_cert_bind.py"
src = probe.read_text()
src = re.sub(r'open\("evidence/hycred_[a-z_]+\.json"\)',
f'open("{cred_json_path.as_posix()}")', src)
tmp = ROOT / "scripts/_probe_auto.py"
tmp.write_text(src)
r = subprocess.run([sys.executable, str(tmp)], capture_output=True, text=True)
print(r.stdout[-800:])
return '"ok": true' in r.stdout or "'ok': True" in r.stdout
st = int(time.time() * 1000)
rnd = gen_nonce(uid, k1, service_time_ms=st, counter=0)
print(f" 本地nonce: uid={uid} st={st} rnd={rnd.hex()}")
P1 = build_p1(f["app_id"], f["fingerprint"], cred, rnd=rnd)
return forge_cert(P1, key_idx=orig[1])
def main() -> int:
acct, pwd = sys.argv[1], sys.argv[2]
print("[1/3] 纯Python密码登录 (遇safe_auth自动过滑块) ...")
print("[1/4] 纯Python密码登录 (遇safe_auth自动过滑块) ...")
try:
cred = login_cred(acct, pwd)
except QrAuthRequiredError as exc:
@@ -58,19 +59,70 @@ def main() -> int:
return 2
print(f" cred OK: {len(cred)}B {cred[:8].hex()}")
print("[2/3] 铸造证书 ...")
cert_hex = forge(cred)
out = ROOT / "evidence/auto_test_cred.json"
out.write_text(json.dumps({"p1": "", "cert_hex": cert_hex,
"cred_hex": cred.hex()}))
print(f" 证书 OK: {len(cert_hex)//2}B")
uid = real_uid(acct, pwd)
print(f"[2/4] 解析真实uid={uid}, 本地重算nonce 铸证 ...")
env = Envelope.load()
cert = forge_for_uid(env, cred, uid, K1_DEFAULT)
b64 = base64.b64encode(cert).decode()
if len(b64) != env.cert_len:
print(f" cert b64 长度不符 {len(b64)} != {env.cert_len}, 中止")
return 1
raw = bytearray(env.raw)
raw[env.cert_off:env.cert_off + env.cert_len] = b64.encode()
if env.uid != uid:
print(f" patch uid {env.uid} -> {uid}")
struct.pack_into(">Q", raw, env.uid_off, uid)
wup = base64.b64encode(bytes(raw)).decode()
print("[3/3] 扫码绑定取cookie ...")
if bind_for_cookie(out):
print("\n🎉 全链路成功: 密码已换出页面cookie(biztoken)!")
return 0
print("\n❌ 绑定失败——若40020多为信封过期, 请刷新wupData后重试")
return 1
print("[3/4] 扫码绑定四步流 ...")
from urllib.parse import quote
sys.path.insert(0, str(ROOT / "scripts"))
from probe_huya_qr_bind import QrRole, web_behavior
from core.huya.device_fingerprint import get_huya_sdid
sdid = get_huya_sdid(allow_fallback=False).sdid
pc = QrRole(pc=True, sdid=sdid)
ph = QrRole(pc=False, sdid=sdid)
beh, page = web_behavior()
resp = pc.call("/qrLgn/getQrId", "70001",
{"behavior": beh, "type": "", "domainList": "", "page": page})
qrid = resp["data"]["qrId"]
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="")})
print(f" bind rc={r2.get('returnCode')} {r2.get('message') or ''}")
biztoken = None
for _ in range(10):
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")
print(f" biztoken OK: {len(biztoken)}B")
break
time.sleep(2)
if not biztoken:
print("❌ 未取到biztoken")
return 1
print("[4/4] cookie verify 兑换 ...")
sess = getattr(pc, 'session', None) or getattr(pc, 's', None) or getattr(pc, '_sess', None)
r = sess.post("https://udblgn.huya.com/web/cookie/verify",
json={"appId": 5002}, timeout=15)
print(f" verify HTTP {r.status_code}")
jar = []
for c in sess.cookies:
print(f" {c.name} = {c.value[:50]}")
jar.append(f"{c.name}={c.value}")
out = ROOT / "evidence/web_cookies_full.txt"
out.write_text(f"# account: {acct}\n" + "\n".join(jar) + "\n")
print(f"\n🎉 纯协议全链路成功: 任意账号密码已换出全套cookie! ({len(jar)}个)")
print("已写", out)
return 0
if __name__ == "__main__":
+93 -60
View File
@@ -1,73 +1,106 @@
#!/usr/bin/env python3
"""一键: 账号密码 -> 完整网页Cookie全套
用法: python tools/full_web_cookie.py <账号> <密码>
"""一键: 账号密码 -> 完整网页Cookie全套 (纯协议, nonce本地重算).
2026-08-26 升级: nonce 生成机制已破解(tools/nonce_forge.py),
不再需要复用设备信封的原nonce —— 任意账号用 目标uid+k1 本地重算即过bind!
依赖: k1 设备常量(evidence/nonce_k1.json 或默认值, 不随账号变)。
用法: .venv/bin/python tools/full_web_cookie.py <账号> <密码>
产出: 打印全部cookie并写 evidence/web_cookies_full.txt
"""
import sys, json, re, hashlib
import sys, json, re, hashlib, time, base64
ACCT, PWD = sys.argv[1], sys.argv[2]
sys.argv = [sys.argv[0]]
import sys,json,time,base64
from urllib.parse import quote
sys.path.insert(0,'scripts'); sys.path.insert(0,'tools')
from probe_forge_cert_bind import load_capture, parse_p1, build_p1, forge_cert
sys.path.insert(0, 'scripts'); sys.path.insert(0, 'tools')
from cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1
from envelope_forge import Envelope
from nonce_forge import K1_DEFAULT, gen_nonce
from probe_huya_qr_bind import QrRole, web_behavior
from core.huya.device_fingerprint import get_huya_sdid
qurl,P1_old=load_capture()
fields=parse_p1(P1_old)
import time,base64
from urllib.parse import quote
# 登录取cred: 触发safe_auth时自动解udbrtt滑块并重发(app_login_flow闭环)
import app_login_flow as _flow
try:
cred = _flow.login_cred(ACCT, PWD)
except _flow.QrAuthRequiredError as e:
print('', e)
sys.exit(2)
except Exception as e:
print(f'❌ 登录未返回cred(密码错误/账号异常/风控过验失败): {e}')
sys.exit(1)
print('新鲜cred:',len(cred),cred[:8].hex())
P1n=build_p1(b"5008",fields["fingerprint"],cred,rnd=fields["rnd"])
cert=base64.b64encode(forge_cert(P1n,key_idx=fields and 0x20)).decode()
raw=base64.b64decode(qurl)
i=raw.find(base64.b64encode(forge_cert(build_p1(b"5008",fields["fingerprint"],b"\x0a"*114,rnd=fields["rnd"]),key_idx=0x20)))
# 直接替换原证书段: 找原cert b64 span——用原P1重建
old_b64=None
from probe_forge_cert_bind import find_cert_span as _fcs; j=_fcs(raw)
if True:
# 退路: 用find_cert_span
from probe_forge_cert_bind import find_cert_span
s0,s1,_=find_cert_span(raw); raw=raw[:s0]+cert.encode()+raw[s1:]
else:
raw=raw[:j]+cert.encode()+raw[j+len(old_b64):]
wup=base64.b64encode(raw).decode()
sdid=get_huya_sdid(allow_fallback=False).sdid
pc=QrRole(pc=True,sdid=sdid); ph=QrRole(pc=False,sdid=sdid)
beh,page=web_behavior()
resp=pc.call("/qrLgn/getQrId","70001",{"behavior":beh,"type":"","domainList":"","page":page})
qrid=resp["data"]["qrId"]
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="")})
print("bind rc",r2.get("returnCode"))
for i in range(10):
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:
print(json.dumps(dt,ensure_ascii=False,indent=1)[:1800]); break
time.sleep(2)
from app_login_flow import QrAuthRequiredError, login_cred
# 追加: cookie verify 兑换
sess = getattr(pc,'session',None) or getattr(pc,'s',None) or getattr(pc,'_sess',None)
r=sess.post("https://udblgn.huya.com/web/cookie/verify",json={"appId":5002},timeout=15)
print('verify HTTP',r.status_code)
for h in r.headers.get('Set-Cookie','').split('\n') or [r.headers.get('Set-Cookie','')]:
if h: print('SET:',h[:110])
print('cookie jar:')
jar=[]
def real_uid(acct: str) -> int:
"""登录响应解析真实uid(与信封t3一致的12位uid)。"""
import struct
from app_login_flow import wup_password_login_raw
resp = wup_password_login_raw(acct, PWD)
s = resp.find(b"\x0a\x0a", 0x40)
e = resp.find(b"_wup_header")
uid = struct.unpack_from(">Q", resp[s:e], 235)[0]
assert 1_100_000_000_000 < uid < 1_300_000_000_000, f"uid异常 {uid}"
return uid
print(f"== 账号 {ACCT} ==")
# 1) 登录取 cred (safe_auth滑块自动过)
try:
cred = login_cred(ACCT, PWD)
except QrAuthRequiredError as e:
print('', e); sys.exit(2)
except Exception as e:
print(f'❌ 登录未返回cred: {e}'); sys.exit(1)
print('新鲜cred:', len(cred), cred[:8].hex())
# 2) 取真实uid + 本地重算nonce 铸证
uid = real_uid(ACCT)
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)
print(f"本地nonce: uid={uid} st={st} rnd={rnd.hex()}")
P1 = build_p1(f["app_id"], f["fingerprint"], cred, rnd=rnd)
cert = base64.b64encode(forge_cert(P1, key_idx=orig[1])).decode()
assert len(cert) == env.cert_len, (len(cert), env.cert_len)
# 3) 信封: 换cert + (uid不同则patch)
raw = bytearray(env.raw)
raw[env.cert_off:env.cert_off + env.cert_len] = cert.encode()
if env.uid != uid:
import struct
print(f"patch uid {env.uid} -> {uid}")
struct.pack_into(">Q", raw, env.uid_off, uid)
wup = base64.b64encode(bytes(raw)).decode()
# 4) 扫码绑定四步流
sdid = get_huya_sdid(allow_fallback=False).sdid
pc = QrRole(pc=True, sdid=sdid); ph = QrRole(pc=False, sdid=sdid)
beh, page = web_behavior()
resp = pc.call("/qrLgn/getQrId", "70001",
{"behavior": beh, "type": "", "domainList": "", "page": page})
qrid = resp["data"]["qrId"]
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="")})
print("bind rc", r2.get("returnCode"), r2.get("message") or "")
biztoken = None
for i in range(10):
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")
print(f"biztoken OK: {len(biztoken)}B")
break
time.sleep(2)
if not biztoken:
print("❌ 未取到biztoken"); sys.exit(1)
# 5) cookie verify 兑换
sess = getattr(pc, 'session', None) or getattr(pc, 's', None) or getattr(pc, '_sess', None)
r = sess.post("https://udblgn.huya.com/web/cookie/verify", json={"appId": 5002}, timeout=15)
print('verify HTTP', r.status_code)
jar = []
for c in sess.cookies:
print(' ',c.name,'=',c.value[:50])
print(' ', c.name, '=', c.value[:50])
jar.append(f'{c.name}={c.value}')
out=__import__('pathlib').Path(__file__).resolve().parent.parent/'evidence/web_cookies_full.txt'
out = __import__('pathlib').Path(__file__).resolve().parent.parent / 'evidence/web_cookies_full.txt'
out.write_text(f'# account: {ACCT}\n' + '\n'.join(jar) + '\n')
print('已写', out)
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""虎牙证书 nonce(rnd) 生成器 —— 纯本地可算, 无需设备!
2026-08-26 动态hook实锤(scripts/hook_nonce_chain.py, /tmp/opencode/nonce_chain.json):
rnd = XXTEA(pack(serviceTime) + pack(nonce_next), key=MD5hex(uid_str + k1)[:16])
其中:
serviceTime = 当前毫秒时间戳 (int(time.time()*1000) & 0x7FFFFFFFFFFF)
nonce_next = counter | (serviceTime << 16)
(counter 为同 serviceTime 内递增计数, 从0开始)
k1 = 设备级常量 32位hex (865a4924a40897ac1fcfe6b4c2cbb0e3, 不随账号变)
key = hashlib.md5((uid_str + k1).encode()).hexdigest()[:16].encode()
pack(x) = 8字节小端 (cred_packlsEm: append 8B LE)
XXTEA 输出 = ceil(len/4)+1 words, 末尾 word 存原始长度 -> 16B输入 => 20B输出 = P1的rnd。
验证: 对设备实采两轮 rnd (80dd1dfa.../0ba8f8b7...) 逐字节复现一致。
k1 跨账号不变(uid=1199666914671 vs 1199666911746 均同值) => 任意账号可本地铸 nonce!
"""
from __future__ import annotations
import hashlib
import struct
import time
# 设备级常量: BusinessCfg::this+0x10 (动态hook自真机, 不随账号变化)
# TODO: 待确认其生成源(疑为设备安装时生成并持久化), 当前直接从真机提取
K1_DEFAULT = "865a4924a40897ac1fcfe6b4c2cbb0e3"
# ---------------------------------------------------------------- XXTEA
def _xxtea_encrypt_words(v: list[int], k: list[int]) -> list[int]:
"""标准 XXTEA (delta=0x9e3779b9, rounds=6+52/n), in-place 返回。"""
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 存原始长度。
data: 任意长度(实测16B); key16: 16字节。
返回: 20B (16B输入时)。
"""
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)
# ---------------------------------------------------------------- nonce
def pack_u64(v: int) -> bytes:
"""cred_packlsEm: 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: 同serviceTime内的递增计数(通常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)
# ---------------------------------------------------------------- 自检
if __name__ == "__main__":
import json
# 用设备实采数据验证 (成对遍历 xxtea_in/xxtea_out)
ev = json.load(open("/tmp/opencode/nonce_chain.json"))
ins = [e for e in ev if e.get("type") == "xxtea_in"]
outs = [e for e in ev if e.get("type") == "xxtea_out"]
for idx, e in enumerate(ins):
data = bytes.fromhex(e["data"]["v"])
key_str = bytes.fromhex(e["key"]["v"]).decode()
uid = key_str[:-32]
k1 = key_str[-32:]
st = struct.unpack("<Q", data[:8])[0]
nonce_val = struct.unpack("<Q", data[8:])[0]
counter = nonce_val & 0xFFFF
mine = gen_nonce(int(uid), k1, service_time_ms=st, counter=counter)
real = outs[idx]["out"]["v"]
print(f"uid={uid} st={st} counter={counter}")
print(f" 设备: {real}")
print(f" 复刻: {mine.hex()}")
print(" MATCH!" if mine.hex() == real else " MISMATCH!!")