""" 腾讯天御点选验证码工业级纯协议客户端 (Tencent Turing Captcha Protocol Client) 实现端到端全自动纯协议闭环: 1. GET /cap_union_prehandle 获取初始会话与 PoW 题目 2. 毫秒级求解 Proof of Work 哈希碰撞 3. 并发拉取动态混淆的 tdc.js 脚本 4. POST /cap_union_new_getsig 获取当前轮次题目与图片 5. YOLOv8n + ResNet18 模型进行视觉检测、分类与全局非冲突点选排序 6. 纯算法还原 (Custom XTEA + 贝塞尔人手微颤) 生成行为轨迹密文 collect 与会话密钥 eks (100% 纯 Python) 7. POST /cap_union_new_verify 提交验证,获取最终 ticket 与 randstr """ import hashlib import json import logging import random import re import sys import time from pathlib import Path from typing import Dict, List, Optional, Tuple import httpx PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from tcaptcha.crypto import extract_eks, generate_collect from tcaptcha.solver import CaptchaSolver logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("tcaptcha_client") class TencentCaptchaClient: BASE_URL = "https://turing.captcha.qcloud.com" DEFAULT_AID = "2039519451" def __init__( self, aid: str = DEFAULT_AID, solver: Optional[CaptchaSolver] = None, timeout: float = 15.0, user_agent: Optional[str] = None, ): self.aid = aid self.timeout = timeout self.user_agent = user_agent or ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36" ) self.solver = solver or CaptchaSolver() self.http_client = httpx.Client( base_url=self.BASE_URL, trust_env=False, # 屏蔽环境代理干扰 headers={ "User-Agent": self.user_agent, "Referer": "https://turing.captcha.qcloud.com/", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", }, timeout=self.timeout, follow_redirects=True, ) def _solve_pow(self, prefix: str, target_md5: str) -> Tuple[str, int]: """求解 Proof of Work 哈希碰撞""" t0 = time.time() for i in range(2000000): candidate = f"{prefix}{i}" if hashlib.md5(candidate.encode("utf-8")).hexdigest() == target_md5: calc_time = max(1, round((time.time() - t0) * 1000)) return candidate, calc_time raise RuntimeError(f"PoW MD5 collision search failed for prefix {prefix}") def _generate_telemetry(self, tdc_code: str, click_points: List[Tuple[int, int]]) -> Dict[str, str]: """纯 Python 算法生成真实行为轨迹与遥测密文 collect 及 eks (100% 纯算免 Node.js)""" eks = extract_eks(tdc_code) collect = generate_collect(click_points, aid=self.aid, user_agent=self.user_agent) return { "collect": collect, "tlg": str(len(collect)), "eks": eks, } def solve(self, max_retries: int = 2) -> Dict: """ 端到端全自动解题并提交验证 返回格式: { "success": bool, "ticket": str, "randstr": str, "elapsed_ms": int, "points": [(x1, y1), (x2, y2), (x3, y3)], "raw_response": dict, "error": str } """ start_time = time.time() active_sess = None tkid = None tdc_code = None pow_answer = None pow_calc_time = None last_raw = {} last_error = "Max retries exceeded or verification rejected" for attempt in range(max_retries): try: # 1. 预检请求 (Prehandle) 或在会话内刷新 cb_name = f"_aq_{random.randint(100000, 999999)}" if not tkid: tkid = str(random.randint(100000000, 999999999)) if not active_sess: # 首次进入:执行 Prehandle ua_b64 = "TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzE1Mi4wLjAuMCBTYWZhcmkvNTM3LjM2" params = { "aid": self.aid, "protocol": "https", "accver": "1", "showtype": "popup", "ua": ua_b64, "noheader": "1", "fb": "1", "aged": "0", "enableAged": "0", "enableDarkMode": "0", "grayscale": "1", "clientype": "2", "lang": "zh-cn", "entry_url": "https://app.rainyun.com/account/reward/earn#", "elder_captcha": "0", "js": "/tcaptcha-frame.ba387dd1.js", "wb": "2", "tkid": tkid, "subsid": str(random.randint(1, 5)), "callback": cb_name, } pre_resp = self.http_client.get( "/cap_union_prehandle", params=params, headers={"Referer": "https://app.rainyun.com/"}, ) match = re.search(rf"{cb_name}\((.*)\)", pre_resp.text) if not match: logger.warning("Failed to parse prehandle callback, retrying...") continue pre_data = json.loads(match.group(1)) initial_sess = pre_data.get("sess") comm_cfg = pre_data.get("data", {}).get("comm_captcha_cfg", {}) # 2. 计算 PoW pow_cfg = comm_cfg.get("pow_cfg", {}) pow_answer, pow_calc_time = self._solve_pow(pow_cfg["prefix"], pow_cfg["md5"]) # 3. 拉取当前会话专属动态 tdc.js tdc_path = comm_cfg.get("tdc_path", "") tdc_resp = self.http_client.get( tdc_path, headers={"Referer": "https://turing.captcha.gtimg.com/"}, ) tdc_code = tdc_resp.text current_sess = initial_sess else: # 会话内刷新:直接使用前一轮返回的 sess current_sess = active_sess # 4. 获取题目信息与图片 URL (POST /cap_union_new_getsig) sig_resp = self.http_client.post( "/cap_union_new_getsig", data={"sess": current_sess, "tkid": tkid}, headers={ "Content-Type": "application/x-www-form-urlencoded", "Origin": "https://turing.captcha.gtimg.com", "Referer": "https://turing.captcha.gtimg.com/", }, ) sig_json = sig_resp.json() if str(sig_json.get("ret")) != "0": logger.warning(f"getsig returned non-zero ret: {sig_json}") active_sess = None continue dyn_info = sig_json.get("data", {}) bg_url = dyn_info.get("bg_elem_cfg", {}).get("img_url", "") prompt_url = bg_url.replace("img_index=1", "img_index=0") # 提取图片 URL 中的最新有效 sess sess_match = re.search(r"sess=([^&]+)", bg_url) active_sess = sess_match.group(1) if sess_match else current_sess # 5. 下载背景大图与提示图标图 bg_bytes = self.http_client.get( bg_url, headers={"Referer": "https://turing.captcha.gtimg.com/"}, ).content prompt_bytes = self.http_client.get( prompt_url, headers={"Referer": "https://turing.captcha.gtimg.com/"}, ).content # 6. AI 模型高精度求解 pts = self.solver.solve(bg_bytes, prompt_bytes) if len(pts) < 3: logger.warning("Solver failed to find 3 valid points, retrying...") continue # 7. 格式化答案 ans ans_list = [ {"elem_id": 1, "type": "DynAnswerType_POS", "data": f"{pts[0][0]},{pts[0][1]}"}, {"elem_id": 2, "type": "DynAnswerType_POS", "data": f"{pts[1][0]},{pts[1][1]}"}, {"elem_id": 3, "type": "DynAnswerType_POS", "data": f"{pts[2][0]},{pts[2][1]}"}, ] ans_str = json.dumps(ans_list, separators=(",", ":")) # 8. 纯算法生成真实行为轨迹 collect 与会话密钥 eks (100% 纯 Python) telemetry = self._generate_telemetry(tdc_code, pts) # 9. 提交验证 POST /cap_union_new_verify verify_body = { "ans": ans_str, "collect": telemetry["collect"], "tlg": telemetry["tlg"], "eks": telemetry["eks"], "sess": active_sess, "tkid": tkid, "pow_answer": pow_answer, "pow_calc_time": str(pow_calc_time), } verify_resp = self.http_client.post( "/cap_union_new_verify", data=verify_body, headers={ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Origin": "https://turing.captcha.gtimg.com", "Referer": "https://turing.captcha.gtimg.com/", }, ) v_json = verify_resp.json() total_elapsed = int((time.time() - start_time) * 1000) if str(v_json.get("errorCode")) == "0": return { "success": True, "ticket": v_json.get("ticket", ""), "randstr": v_json.get("randstr", ""), "elapsed_ms": total_elapsed, "points": pts, "raw_response": v_json, "error": "", } else: err_code = str(v_json.get("errorCode")) err_msg = v_json.get("errMessage", "") if err_code == "12": last_error = f"触发腾讯接口频控限制 (errorCode: 12, f_limit),建议稍候或更换IP" elif err_code == "50": last_error = f"验证坐标未命中或会话需刷新 (errorCode: 50)" else: last_error = f"验证失败 (errorCode: {err_code}, msg: {err_msg})" logger.warning(f"Attempt {attempt + 1} verify: {last_error}") last_raw = v_json # 如果接口返回了新的会话 sess,下一轮直接沿用该会话重试刷新 new_sess = v_json.get("sess") if new_sess: active_sess = new_sess else: active_sess = None except Exception as e: last_error = f"请求异常: {e}" logger.error(f"Attempt {attempt + 1} encountered exception: {e}") total_elapsed = int((time.time() - start_time) * 1000) return { "success": False, "ticket": "", "randstr": "", "elapsed_ms": total_elapsed, "points": [], "raw_response": last_raw, "error": last_error, } def main(): import argparse parser = argparse.ArgumentParser(description="Tencent Captcha Pure Protocol Solver Client") parser.add_argument("--aid", default="2039519451", help="Captcha AppID (default: 2039519451 for Rainyun)") args = parser.parse_args() client = TencentCaptchaClient(aid=args.aid) print(f"[*] 正在发起腾讯天御验证码纯协议识别 (AppID: {args.aid})...") result = client.solve() if result["success"]: print("\n" + "=" * 50) print(" 🎉 验证码纯协议求解成功!") print("=" * 50) print(f"耗时: {result['elapsed_ms']} ms") print(f"点击坐标: {result['points']}") print(f"randstr: {result['randstr']}") print(f"ticket: {result['ticket']}") print("=" * 50) else: print(f"\n❌ 验证失败: {result['error']}") sys.exit(1) if __name__ == "__main__": main()