type: 收窄虎牙验证与设备注册类型

This commit is contained in:
yml2213
2026-08-30 20:07:12 +08:00
parent 563477fde8
commit 3f97dbe525
2 changed files with 152 additions and 72 deletions
+43 -17
View File
@@ -23,6 +23,7 @@
- ``_random_triple`` 的 64hex 占位只是 cw JSON 模板形态 — 真实 dfpReport 指纹 - ``_random_triple`` 的 64hex 占位只是 cw JSON 模板形态 — 真实 dfpReport 指纹
JSON (R36 解密 12 样本) 无 hdid 字段 (顶层 Athena/deviceinfo/terminal/version)。 JSON (R36 解密 12 样本) 无 hdid 字段 (顶层 Athena/deviceinfo/terminal/version)。
""" """
from __future__ import annotations from __future__ import annotations
import base64 import base64
@@ -74,23 +75,33 @@ def _load_chain() -> dict[str, tuple[bytes, bytes]]:
try: try:
data = json.loads(CHAIN_FILE.read_text(encoding="utf-8")) data = json.loads(CHAIN_FILE.read_text(encoding="utf-8"))
return { return {
name: (base64.b64decode(item["req_b64"]), base64.b64decode(item["resp_b64"])) name: (
base64.b64decode(item["req_b64"]),
base64.b64decode(item["resp_b64"]),
)
for name, item in data.items() for name, item in data.items()
} }
except (OSError, ValueError, KeyError) as exc: except (OSError, ValueError, KeyError) as exc:
raise DfpRegistrationError(f"注册链模板读取失败: {exc}") from exc raise DfpRegistrationError(f"注册链模板读取失败: {exc}") from exc
def _post(body: bytes, content_type: str = "application/octet-stream", def _post(
timeout: float = 20, proxies: Mapping[str, str] | None = None) -> bytes: body: bytes,
content_type: str = "application/octet-stream",
timeout: float = 20,
proxies: Mapping[str, str] | None = None,
) -> bytes:
try: try:
response = requests.post( response = requests.post(
WSAPI, WSAPI,
data=body, data=body,
headers={"Content-Type": content_type, "User-Agent": UA, headers={
"Accept-Encoding": "gzip"}, "Content-Type": content_type,
"User-Agent": UA,
"Accept-Encoding": "gzip",
},
timeout=timeout, timeout=timeout,
proxies=proxies, proxies=dict(proxies) if proxies else None,
) )
response.raise_for_status() response.raise_for_status()
return response.content return response.content
@@ -130,11 +141,16 @@ def _build_random_dfp_body() -> bytes:
json_sec = os.urandom(CW_JSON_LEN) json_sec = os.urandom(CW_JSON_LEN)
if len(seed) <= CW_JSON_LEN: if len(seed) <= CW_JSON_LEN:
mask = os.urandom(len(seed)) mask = os.urandom(len(seed))
json_sec = bytes(a ^ b for a, b in zip(seed, mask)) + json_sec[len(seed):] json_sec = bytes(a ^ b for a, b in zip(seed, mask)) + json_sec[len(seed) :]
cw = os.urandom(2) + json_sec + os.urandom(CW_COLL_LEN) + CW_TAIL cw = os.urandom(2) + json_sec + os.urandom(CW_COLL_LEN) + CW_TAIL
if len(cw) != CW_LEN: if len(cw) != CW_LEN:
raise DfpRegistrationError(f"dfpReport cw 长度异常: {len(cw)}") raise DfpRegistrationError(f"dfpReport cw 长度异常: {len(cw)}")
body = struct.pack(">I", len(TAF_HEAD) + len(MAGIC) + len(cw) + 4) + TAF_HEAD + MAGIC + cw body = (
struct.pack(">I", len(TAF_HEAD) + len(MAGIC) + len(cw) + 4)
+ TAF_HEAD
+ MAGIC
+ cw
)
if len(body) != 4226: if len(body) != 4226:
raise DfpRegistrationError(f"dfpReport body 长度异常: {len(body)}") raise DfpRegistrationError(f"dfpReport body 长度异常: {len(body)}")
return body return body
@@ -145,23 +161,34 @@ def _select_operator_request(template: bytes, fingerprint: str | None) -> bytes:
old = b"02df398797432eadefcc12767119ad5e80999389" old = b"02df398797432eadefcc12767119ad5e80999389"
index = template.find(old) index = template.find(old)
if index >= 0: if index >= 0:
return template[:index] + fingerprint.encode("ascii") + template[index + len(old):] return (
template[:index]
+ fingerprint.encode("ascii")
+ template[index + len(old) :]
)
return template return template
def _parse_response(data: bytes) -> tuple[str, str, str]: def _parse_response(data: bytes) -> tuple[str, str, str]:
try: try:
t1 = re.search(rb"\x16\x20([0-9a-f]{32})", data).group(1).decode() t1_match = re.search(rb"\x16\x20([0-9a-f]{32})", data)
t2 = re.search(rb"\x26\xb4([A-Za-z0-9+/=]{180})", data).group(1).decode("latin1") t2_match = re.search(rb"\x26\xb4([A-Za-z0-9+/=]{180})", data)
t5 = re.search(rb"\x56\x28([0-9a-f]{40})", data).group(1).decode() t5_match = re.search(rb"\x56\x28([0-9a-f]{40})", data)
except AttributeError as exc: if not t1_match or not t2_match or not t5_match:
raise ValueError("missing response fields")
t1 = t1_match.group(1).decode()
t2 = t2_match.group(1).decode("latin1")
t5 = t5_match.group(1).decode()
except (AttributeError, ValueError) as exc:
raise DfpRegistrationError("dfpReport 响应缺少 t1/t2/t5") from exc raise DfpRegistrationError("dfpReport 响应缺少 t1/t2/t5") from exc
return t1, t2, t5 return t1, t2, t5
def register_device(fingerprint: str | None = None, def register_device(
proxies: Mapping[str, str] | None = None, fingerprint: str | None = None,
timeout: float = 20) -> tuple[str, str, str]: proxies: Mapping[str, str] | None = None,
timeout: float = 20,
) -> tuple[str, str, str]:
"""执行新注册链,返回 ``(t1, safedeviceid, device_id)``。""" """执行新注册链,返回 ``(t1, safedeviceid, device_id)``。"""
chain = _load_chain() chain = _load_chain()
_post(chain["getDfpConfig"][0], timeout=timeout, proxies=proxies) _post(chain["getDfpConfig"][0], timeout=timeout, proxies=proxies)
@@ -169,4 +196,3 @@ def register_device(fingerprint: str | None = None,
_post(select_request, "application/x-wup", timeout, proxies) _post(select_request, "application/x-wup", timeout, proxies)
response = _post(_build_random_dfp_body(), timeout=timeout, proxies=proxies) response = _post(_build_random_dfp_body(), timeout=timeout, proxies=proxies)
return _parse_response(response) return _parse_response(response)
+109 -55
View File
@@ -10,6 +10,7 @@ import time
import uuid import uuid
from collections.abc import Mapping from collections.abc import Mapping
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from typing import Any, cast
from urllib.parse import parse_qs, quote, urlparse from urllib.parse import parse_qs, quote, urlparse
import cv2 import cv2
@@ -86,17 +87,19 @@ class HuyaVerificationSolver:
self.session.cookies.update(self.cookie) self.session.cookies.update(self.cookie)
if proxies: if proxies:
self.session.proxies = dict(proxies) self.session.proxies = dict(proxies)
self.session.headers.update({ self.session.headers.update(
"Accept-Language": "zh-CN,zh;q=0.9", {
"Cache-Control": "no-cache", "Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive", "Cache-Control": "no-cache",
"Origin": "https://aq.huya.com", "Connection": "keep-alive",
"Pragma": "no-cache", "Origin": "https://aq.huya.com",
"Referer": "https://aq.huya.com/", "Pragma": "no-cache",
"User-Agent": self.ua, "Referer": "https://aq.huya.com/",
"accept": "application/json, text/plain, */*", "User-Agent": self.ua,
"content-type": "application/json", "accept": "application/json, text/plain, */*",
}) "content-type": "application/json",
}
)
self.timeout = timeout self.timeout = timeout
self.ocr = ocr or default_ocr() self.ocr = ocr or default_ocr()
@@ -105,7 +108,7 @@ class HuyaVerificationSolver:
self.code = "" self.code = ""
self.url_param_map: dict[str, str] = {} self.url_param_map: dict[str, str] = {}
self.strategy_url = "" self.strategy_url = ""
self.js_ctx = None self.js_ctx: Any = None
self.behavior_events: list[dict] = [] self.behavior_events: list[dict] = []
self.app_id = app_id self.app_id = app_id
self.page_url = page_url or self._auth_page_url() self.page_url = page_url or self._auth_page_url()
@@ -125,9 +128,13 @@ class HuyaVerificationSolver:
return base64.b64decode(base64_string) return base64.b64decode(base64_string)
@staticmethod @staticmethod
def _vertical_concat_align_left(bg_bytes: bytes, tip_bytes: bytes, image_format: str = "JPEG") -> bytes: def _vertical_concat_align_left(
bg_bytes: bytes, tip_bytes: bytes, image_format: str = "JPEG"
) -> bytes:
bg = cv2.imdecode(np.frombuffer(bg_bytes, np.uint8), cv2.IMREAD_COLOR) bg = cv2.imdecode(np.frombuffer(bg_bytes, np.uint8), cv2.IMREAD_COLOR)
tip = cv2.imdecode(np.frombuffer(tip_bytes, np.uint8), cv2.IMREAD_COLOR) tip = cv2.imdecode(np.frombuffer(tip_bytes, np.uint8), cv2.IMREAD_COLOR)
if bg is None or tip is None:
raise HuyaVerificationError("验证码图片解码失败")
max_width = max(bg.shape[1], tip.shape[1]) max_width = max(bg.shape[1], tip.shape[1])
def pad_right(img, target_width): def pad_right(img, target_width):
@@ -162,7 +169,9 @@ class HuyaVerificationSolver:
return image.crop(bbox), black_border_height return image.crop(bbox), black_border_height
@classmethod @classmethod
def _composite_slider_images(cls, bg_bytes: bytes, tip_bytes: bytes, y_position: int) -> bytes: def _composite_slider_images(
cls, bg_bytes: bytes, tip_bytes: bytes, y_position: int
) -> bytes:
bg = Image.open(io.BytesIO(bg_bytes)).convert("RGB") bg = Image.open(io.BytesIO(bg_bytes)).convert("RGB")
tip = Image.open(io.BytesIO(tip_bytes)).convert("RGBA") tip = Image.open(io.BytesIO(tip_bytes)).convert("RGBA")
tip, black_border_height = cls._remove_black_borders(tip) tip, black_border_height = cls._remove_black_borders(tip)
@@ -174,8 +183,14 @@ class HuyaVerificationSolver:
@staticmethod @staticmethod
def _opencv_slider_x(target_bytes: bytes, background_bytes: bytes) -> int: def _opencv_slider_x(target_bytes: bytes, background_bytes: bytes) -> int:
target = cv2.imdecode(np.frombuffer(target_bytes, np.uint8), cv2.IMREAD_ANYCOLOR) target = cv2.imdecode(
background = cv2.imdecode(np.frombuffer(background_bytes, np.uint8), cv2.IMREAD_ANYCOLOR) np.frombuffer(target_bytes, np.uint8), cv2.IMREAD_ANYCOLOR
)
background = cv2.imdecode(
np.frombuffer(background_bytes, np.uint8), cv2.IMREAD_ANYCOLOR
)
if target is None or background is None:
raise HuyaVerificationError("滑块图片解码失败")
target = cv2.cvtColor(cv2.Canny(target, 100, 200), cv2.COLOR_GRAY2RGB) target = cv2.cvtColor(cv2.Canny(target, 100, 200), cv2.COLOR_GRAY2RGB)
background = cv2.cvtColor(cv2.Canny(background, 100, 200), cv2.COLOR_GRAY2RGB) background = cv2.cvtColor(cv2.Canny(background, 100, 200), cv2.COLOR_GRAY2RGB)
result = cv2.matchTemplate(background, target, cv2.TM_CCOEFF_NORMED) result = cv2.matchTemplate(background, target, cv2.TM_CCOEFF_NORMED)
@@ -188,7 +203,9 @@ class HuyaVerificationSolver:
try: try:
return response.json() return response.json()
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
raise HuyaVerificationError(f"虎牙验证接口返回不是 JSON: {response.text[:200]}") from exc raise HuyaVerificationError(
f"虎牙验证接口返回不是 JSON: {response.text[:200]}"
) from exc
def _compile_js(self, dx_js: str) -> None: def _compile_js(self, dx_js: str) -> None:
js_data = """ js_data = """
@@ -205,7 +222,9 @@ class HuyaVerificationSolver:
""" """
self.js_ctx = execjs.compile(js_data.replace("$$self_js$$", dx_js)) self.js_ctx = execjs.compile(js_data.replace("$$self_js$$", dx_js))
def _build_common_payload(self, extra: dict, info_override: str | None = None) -> dict: def _build_common_payload(
self, extra: dict, info_override: str | None = None
) -> dict:
payload = { payload = {
"urlParamMap": self.url_param_map, "urlParamMap": self.url_param_map,
"wupData": "", "wupData": "",
@@ -224,11 +243,17 @@ class HuyaVerificationSolver:
"""web 用 mouse 事件,App WebView 场景用 touch 事件。""" """web 用 mouse 事件,App WebView 场景用 touch 事件。"""
if not self.use_touch_events: if not self.use_touch_events:
return action return action
return {"mousedown": "touchstart", "mousemove": "touchmove", "mouseup": "touchend"}.get(action, action) return {
"mousedown": "touchstart",
"mousemove": "touchmove",
"mouseup": "touchend",
}.get(action, action)
def _encode_behavior(self) -> str: def _encode_behavior(self) -> str:
"""编码浏览器行为,失败重试时会携带前一次点击记录。""" """编码浏览器行为,失败重试时会携带前一次点击记录。"""
return quote(json.dumps(self.behavior_events, separators=(",", ":")), safe="~()*!.'") return quote(
json.dumps(self.behavior_events, separators=(",", ":")), safe="~()*!.'"
)
@staticmethod @staticmethod
def _extract_strategy_url(challenge_payload: dict | str) -> str: def _extract_strategy_url(challenge_payload: dict | str) -> str:
@@ -294,7 +319,9 @@ class HuyaVerificationSolver:
) )
self.behavior_events = [] self.behavior_events = []
def _restore_slider_background(self, bg_bytes: bytes, segment: str, huyapk: str) -> bytes: def _restore_slider_background(
self, bg_bytes: bytes, segment: str, huyapk: str
) -> bytes:
recover_list = self.js_ctx.call("get_regoin_arr", segment, huyapk) recover_list = self.js_ctx.call("get_regoin_arr", segment, huyapk)
image = Image.open(io.BytesIO(bg_bytes)).convert("RGB") image = Image.open(io.BytesIO(bg_bytes)).convert("RGB")
width, height = image.size width, height = image.size
@@ -320,15 +347,21 @@ class HuyaVerificationSolver:
logger.info("虎牙验证类型: 滑块") logger.info("虎牙验证类型: 滑块")
bg_bytes = self._decode_image(data.get("dtuStr")) bg_bytes = self._decode_image(data.get("dtuStr"))
tip_bytes = self._decode_image(data.get("puzzleStr")) tip_bytes = self._decode_image(data.get("puzzleStr"))
restored_bg = self._restore_slider_background(bg_bytes, segment, self.huyapk) restored_bg = self._restore_slider_background(
composite = self._composite_slider_images(restored_bg, tip_bytes, int(data.get("location_y") or 0)) bg_bytes, segment, self.huyapk
)
composite = self._composite_slider_images(
restored_bg, tip_bytes, int(data.get("location_y") or 0)
)
slider_box = self.ocr.ocr_slider_box(composite) slider_box = self.ocr.ocr_slider_box(composite)
yolo_x = int(slider_box[0] - 13) yolo_x = int(slider_box[0] - 13)
opencv_x = self._opencv_slider_x(tip_bytes, restored_bg) opencv_x = self._opencv_slider_x(tip_bytes, restored_bg)
if abs(opencv_x - yolo_x) < 10: if abs(opencv_x - yolo_x) < 10:
logger.debug("虎牙滑块识别距离: yolo={}, opencv={}", yolo_x, opencv_x) logger.debug("虎牙滑块识别距离: yolo={}, opencv={}", yolo_x, opencv_x)
return int((opencv_x + yolo_x) / 2) return int((opencv_x + yolo_x) / 2)
logger.debug("虎牙滑块识别距离: yolo={}, opencv={}, 使用yolo", yolo_x, opencv_x) logger.debug(
"虎牙滑块识别距离: yolo={}, opencv={}, 使用yolo", yolo_x, opencv_x
)
return yolo_x return yolo_x
logger.info("虎牙验证类型: 点选") logger.info("虎牙验证类型: 点选")
@@ -348,18 +381,20 @@ class HuyaVerificationSolver:
"endTime": int(track_points[-1][-1]) - int(track_points[0][-1]), "endTime": int(track_points[-1][-1]) - int(track_points[0][-1]),
} }
act = self.js_ctx.call("get_act", act_data, self.huyapk) act = self.js_ctx.call("get_act", act_data, self.huyapk)
self.behavior_events.append({ self.behavior_events.append(
"action": self._pointer_action("mousedown"), {
"id": "", "action": self._pointer_action("mousedown"),
"txt": "", "id": "",
"clientX": track_points[0][0], "txt": "",
"clientY": track_points[0][1], "clientX": track_points[0][0],
"clientWidth": 350, "clientY": track_points[0][1],
"clientHeight": 268, "clientWidth": 350,
"isTrusted": True, "clientHeight": 268,
"timeStamp": random.randint(1800, 5500), "isTrusted": True,
"nowTime": int(time.time() * 1000), "timeStamp": random.randint(1800, 5500),
}) "nowTime": int(time.time() * 1000),
}
)
payload = self._build_common_payload({"act": act, "ver": 5}) payload = self._build_common_payload({"act": act, "ver": 5})
return self._submit_verify(url, payload) return self._submit_verify(url, payload)
@@ -387,18 +422,20 @@ class HuyaVerificationSolver:
txt_list = ["", "1", "1\\n2"] txt_list = ["", "1", "1\\n2"]
behavior_list = [] behavior_list = []
for index, item in enumerate(click_data): for index, item in enumerate(click_data):
behavior_list.append({ behavior_list.append(
"action": self._pointer_action("mousedown"), {
"id": "", "action": self._pointer_action("mousedown"),
"txt": txt_list[index] if index < len(txt_list) else "", "id": "",
"clientX": item[0], "txt": txt_list[index] if index < len(txt_list) else "",
"clientY": item[1], "clientX": item[0],
"clientWidth": 350, "clientY": item[1],
"clientHeight": 268, "clientWidth": 350,
"isTrusted": True, "clientHeight": 268,
"timeStamp": random.randint(2000, 5000), "isTrusted": True,
"nowTime": item[2], "timeStamp": random.randint(2000, 5000),
}) "nowTime": item[2],
}
)
self.behavior_events.extend(behavior_list) self.behavior_events.extend(behavior_list)
payload = self._build_common_payload({"act": act, "ver": 5}) payload = self._build_common_payload({"act": act, "ver": 5})
@@ -426,8 +463,13 @@ class HuyaVerificationSolver:
dx_js = "" dx_js = ""
for index in range(2): for index in range(2):
logger.debug("虎牙验证获取config3 ({}/2)", index + 1) logger.debug("虎牙验证获取config3 ({}/2)", index + 1)
config = self._request_json("post", "https://udbrtt.huya.com/auth/client/config3", json=config_payload) config = self._request_json(
data = config.get("data") if isinstance(config.get("data"), dict) else {} "post",
"https://udbrtt.huya.com/auth/client/config3",
json=config_payload,
)
raw_data = config.get("data")
data = cast(dict[str, Any], raw_data) if isinstance(raw_data, dict) else {}
gurl = str(data.get("gurl") or "") gurl = str(data.get("gurl") or "")
vurl = str(data.get("vurl") or "") vurl = str(data.get("vurl") or "")
dx_js = str(data.get("js") or "") dx_js = str(data.get("js") or "")
@@ -440,7 +482,10 @@ class HuyaVerificationSolver:
len(dx_js), len(dx_js),
) )
if not (gurl and vurl): if not (gurl and vurl):
logger.warning("虎牙config3未下发验证会话, 完整响应: {}", json.dumps(config, ensure_ascii=False)[:1500]) logger.warning(
"虎牙config3未下发验证会话, 完整响应: {}",
json.dumps(config, ensure_ascii=False)[:1500],
)
if gurl and vurl and dx_js: if gurl and vurl and dx_js:
break break
if index == 0 and self.url_param_map.get("sceneType") != "4": if index == 0 and self.url_param_map.get("sceneType") != "4":
@@ -451,7 +496,9 @@ class HuyaVerificationSolver:
time.sleep(0.3) time.sleep(0.3)
if not gurl or not vurl or not dx_js: if not gurl or not vurl or not dx_js:
data_summary = {k: str(v)[:80] for k, v in (data or {}).items() if k != "js"} data_summary = {
k: str(v)[:80] for k, v in (data or {}).items() if k != "js"
}
raise HuyaNoSessionError( raise HuyaNoSessionError(
"config3未下发验证会话: " "config3未下发验证会话: "
f"returnCode={config.get('returnCode')}, " f"returnCode={config.get('returnCode')}, "
@@ -461,12 +508,17 @@ class HuyaVerificationSolver:
) )
self._compile_js(dx_js) self._compile_js(dx_js)
point_payload = self._build_common_payload({"ver": 5}, info_override=self._csid() if self.use_touch_events else None) point_payload = self._build_common_payload(
{"ver": 5}, info_override=self._csid() if self.use_touch_events else None
)
last_result: dict = {} last_result: dict = {}
for index in range(3): for index in range(3):
if index: if index:
time.sleep(0.4) time.sleep(0.4)
point_payload = self._build_common_payload({"ver": 5}, info_override=self._csid() if self.use_touch_events else None) point_payload = self._build_common_payload(
{"ver": 5},
info_override=self._csid() if self.use_touch_events else None,
)
logger.info("虎牙验证失败后重试: {}/3", index + 1) logger.info("虎牙验证失败后重试: {}/3", index + 1)
point = self._get_track_point(gurl, point_payload) point = self._get_track_point(gurl, point_payload)
@@ -476,7 +528,9 @@ class HuyaVerificationSolver:
else: else:
last_result = self._send_slider_verify(vurl, int(point)) last_result = self._send_slider_verify(vurl, int(point))
if isinstance(last_result, dict) and (last_result.get("authId") or last_result.get("nextAuthUrl")): if isinstance(last_result, dict) and (
last_result.get("authId") or last_result.get("nextAuthUrl")
):
return last_result return last_result
logger.info( logger.info(