diff --git a/dist/rainyun_qinglong/README.md b/dist/rainyun_qinglong/README.md deleted file mode 100644 index a9cdcc7..0000000 --- a/dist/rainyun_qinglong/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# 雨云 (Rainyun) 自动登录与每日签到 (青龙面板纯 ONNX 轻量部署包) - -本目录为纯净的**单层平铺目录**结构,已全面迁移至 **ONNX Runtime 轻量推理引擎**。 -彻底移除了体积庞大的 `torch`、`torchvision` 与 `ultralytics`(依赖体积从 1.5GB 骤降至 15MB),可在青龙面板或任何小内存 VPS 容器中秒级安装运行。 - ---- - -## 目录文件清单 - -```text -rainyun_qinglong/ -├── rainyun_checkin.py # 核心签到脚本 (青龙定时任务入口) -├── client.py # 腾讯验证码协议客户端 -├── crypto.py # 腾讯 TDC 混沌虚拟机 32 轮魔改 XTEA 纯 Python 还原 -├── solver.py # 纯 ONNX 视觉检测推理引擎 (无 torch 依赖) -├── slicer.py # 提示图投影切片 -├── server.py # (可选) 独立 HTTP 验证码微服务 -├── yolo_detector.onnx # YOLO 图标候选定位 ONNX 权重 (约 12MB) -├── icon_classifier.onnx # ResNet18 图标分类识别 ONNX 权重 (约 43MB) -├── .env # 本地配置文件 (格式: RAINYUN_ACCOUNT="账号#密码") -├── .env.example # 环境变量示例文件 -└── README.md # 本说明文档 -``` - ---- - -## 青龙面板极简部署步骤 - -### 1. 上传文件 -将本目录下的所有文件上传至青龙面板的脚本目录: -`/ql/data/scripts/` (新版青龙)或 `/ql/scripts/` (旧版青龙)。 - -### 2. 在青龙后台安装依赖 (体积仅数十 MB,极速安装) -进入青龙面板 ->【依赖管理】->【Python3】-> 点击【添加依赖】,依次添加并安装: -```text -httpx -onnxruntime -opencv-python-headless -pillow -numpy -``` -> **注意**:已彻底无需安装 `torch`、`torchvision`、`ultralytics`,免除了 G 级大包撑爆磁盘和内存的风险! - -### 3. 配置账号环境变量 -在青龙面板【环境变量】添加变量: -- **名称**:`RAINYUN_ACCOUNT` -- **值**:`账号#密码` (多个账号用 `&` 分隔,例如 `13700000000#pwd1&13800000000#pwd2`) - -### 4. 添加定时任务 -在青龙面板【定时任务】点击【新建任务】: -- **名称**:`雨云每日签到` -- **命令**:`task rainyun_checkin.py` -- **定时规则**:`30 8 * * *` (每天早上 8:30 自动执行) diff --git a/dist/rainyun_qinglong/client.py b/dist/rainyun_qinglong/client.py deleted file mode 100644 index d06adc2..0000000 --- a/dist/rainyun_qinglong/client.py +++ /dev/null @@ -1,323 +0,0 @@ -""" -腾讯天御点选验证码工业级纯协议客户端 (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 - -CURRENT_DIR = Path(__file__).resolve().parent -if str(CURRENT_DIR) not in sys.path: - sys.path.insert(0, str(CURRENT_DIR)) - -try: - from crypto import extract_eks, generate_collect - from solver import CaptchaSolver -except ImportError: - 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() diff --git a/dist/rainyun_qinglong/crypto.py b/dist/rainyun_qinglong/crypto.py deleted file mode 100644 index 4433e97..0000000 --- a/dist/rainyun_qinglong/crypto.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -Pure Python implementation of Tencent TDC Chaos VM (JSVMP) Cryptographic -and Telemetry generation algorithm. - -Eliminates the Node.js sandbox dependency by replicating the 32-round custom -XTEA block cipher, human-like mouse movement trajectory generation, and -telemetry payload serialization. -""" - -import base64 -import math -import random -import re -import struct -import time -from typing import List, Tuple - -# TDC VM Cryptographic Constants -DELTA = 2654435769 # 0x9E3779B9 -DEFAULT_KEY = [1448563793, 1847730015, 1733184827, 1364613714] - -DEFAULT_USER_AGENT = ( - "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" -) -DEFAULT_RENDERER = "Apple M1" -DEFAULT_VENDOR = "Apple" -DEFAULT_CANVAS_FP = ( - "GgoAAAANSUhEUgAAAAEAAAABCAYAAAUhEUgAAAAEAAAABCAYAAAA" - "fFcSJAAAADhgGAWjR9awAAAABJRU5ErkJggg==" -) - - -def xtea_encrypt_block(v0: int, v1: int, key: List[int]) -> Tuple[int, int]: - """ - Tencent custom 32-round XTEA block cipher for a 64-bit block (v0, v1). - Includes specific additive offsets (2829068 and 328986) depending on round index. - """ - v0 &= 0xFFFFFFFF - v1 &= 0xFFFFFFFF - total_sum = 0 - - for _ in range(32): - # Step 1: Update v0 - k_idx1 = total_sum & 3 - shift1 = (((v1 << 4) ^ (v1 >> 5)) + v1) & 0xFFFFFFFF - if k_idx1 == 1: - k_term1 = (total_sum + key[1] + 2829068) & 0xFFFFFFFF - elif k_idx1 == 2: - k_term1 = (total_sum + key[2] + 328986) & 0xFFFFFFFF - else: - k_term1 = (total_sum + key[k_idx1]) & 0xFFFFFFFF - v0 = (v0 + (shift1 ^ k_term1)) & 0xFFFFFFFF - - # Step 2: Update total_sum (standard XTEA delta increment) - total_sum = (total_sum + DELTA) & 0xFFFFFFFFFFFFFFFF - - # Step 3: Update v1 - k_idx2 = (total_sum >> 11) & 3 - shift2 = (((v0 << 4) ^ (v0 >> 5)) + v0) & 0xFFFFFFFF - if k_idx2 == 1: - k_term2 = (total_sum + key[1] + 2829068) & 0xFFFFFFFF - elif k_idx2 == 2: - k_term2 = (total_sum + key[2] + 328986) & 0xFFFFFFFF - else: - k_term2 = (total_sum + key[k_idx2]) & 0xFFFFFFFF - v1 = (v1 + (shift2 ^ k_term2)) & 0xFFFFFFFF - - return v0, v1 - - -def encrypt_string(plaintext: str, key: List[int] = DEFAULT_KEY) -> str: - """ - Encrypts an ASCII/Latin1 plaintext string into Base64 using the custom XTEA cipher. - Input is padded with null bytes to an 8-byte boundary if needed. - """ - data = plaintext.encode("latin1") - pad_len = (8 - (len(data) % 8)) % 8 - data += b"\x00" * pad_len - - out = bytearray() - for i in range(0, len(data), 8): - v0, v1 = struct.unpack(" List[Tuple[int, int]]: - """Generates cubic Bézier interpolation curve points with natural tremor.""" - dx = p1[0] - p0[0] - dy = p1[1] - p0[1] - dist = math.hypot(dx, dy) or 1.0 - - perp_x = -dy / dist - perp_y = dx / dist - curvature = (random.random() - 0.5) * min(50.0, dist * 0.4) - - cp1 = ( - p0[0] + dx * 0.3 + perp_x * curvature, - p0[1] + dy * 0.3 + perp_y * curvature, - ) - cp2 = ( - p0[0] + dx * 0.7 - perp_x * curvature * 0.5, - p0[1] + dy * 0.7 - perp_y * curvature * 0.5, - ) - - pts = [] - for i in range(steps + 1): - t = i / float(steps) - ease_t = t * t * (3.0 - 2.0 * t) - inv_t = 1.0 - ease_t - - bx = ( - inv_t**3 * p0[0] - + 3 * inv_t**2 * ease_t * cp1[0] - + 3 * inv_t * ease_t**2 * cp2[0] - + ease_t**3 * p1[0] - ) - by = ( - inv_t**3 * p0[1] - + 3 * inv_t**2 * ease_t * cp1[1] - + 3 * inv_t * ease_t**2 * cp2[1] - + ease_t**3 * p1[1] - ) - tremor = 0.0 if i in (0, steps) else (random.random() - 0.5) * 1.5 - pts.append((int(round(bx + tremor)), int(round(by + tremor)))) - - return pts - - -def generate_trajectory_payload( - click_points: List[Tuple[int, int]], - target_len: int = 600, -) -> str: - """ - Generates realistic human mouse movement trajectory and serializes it - into the TDC relative-displacement JSON format: - [[x0, y0, 2], [dx1, dy1, dt1], [dx2, dy2, dt2], ...] - - The string is padded with trailing spaces to target_len (multiple of 8). - """ - # Scale from original 672x480 coordinate space to 360x360 captcha iframe display space - scale = 320.0 / 672.0 - mapped_targets = [ - (int(round(20 + p[0] * scale)), int(round(60 + p[1] * scale))) - for p in click_points - ] - - # Starting position near entry area - current_pt = ( - int(round(150 + (random.random() - 0.5) * 60)), - int(round(320 + (random.random() - 0.5) * 20)), - ) - - events: List[List[int]] = [] - events.append([current_pt[0], current_pt[1], 2]) # 2 represents mouse movement event - - last_x, last_y = current_pt[0], current_pt[1] - - for target in mapped_targets: - steps = random.randint(16, 24) - curve = _bezier_interp((last_x, last_y), target, steps) - - for pt in curve[1:]: - dx = pt[0] - last_x - dy = pt[1] - last_y - dt = random.choice([0, 0, 0, 1]) - events.append([dx, dy, dt]) - last_x, last_y = pt[0], pt[1] - - # Dwell hover on target icon - dwell_count = random.randint(3, 5) - for _ in range(dwell_count): - dx = random.randint(-1, 1) - dy = random.randint(-1, 1) - events.append([dx, dy, random.choice([0, 1])]) - last_x += dx - last_y += dy - - import json - raw_json = json.dumps(events, separators=(",", ":")) - - # Pad with spaces to multiple of 8 bytes (matching target_len, e.g. 600) - desired_len = max(len(raw_json), target_len) - pad_needed = (8 - (desired_len % 8)) % 8 - total_len = desired_len + pad_needed - - return raw_json.ljust(total_len, " ") - - -def extract_eks(tdc_js_code: str) -> str: - """Extracts session public key (`eks`) from TDC.js script header.""" - match = re.search(r"window\.KQnlRHY\w+\s*=\s*'([^']+)'", tdc_js_code) - if match: - return match.group(1) - # Fallback to any base64 string assignment - match_fallback = re.search(r"window\.\w+\s*=\s*'([A-Za-z0-9+/=]{100,})'", tdc_js_code) - if match_fallback: - return match_fallback.group(1) - return "" - - -def generate_collect( - click_points: List[Tuple[int, int]], - aid: str = "2039519451", - user_agent: str = DEFAULT_USER_AGENT, - renderer: str = DEFAULT_RENDERER, - vendor: str = DEFAULT_VENDOR, - canvas_fp: str = DEFAULT_CANVAS_FP, -) -> str: - """ - Generates the complete encrypted telemetry payload (`collect`) in 100% pure Python. - - Assembles: - 1. cd prefix (48 bytes, space padded) - 2. mouse trajectory (space padded to 8-byte multiple) - 3. cd post-body (504 bytes, space padded) - 4. sd suffix (16 bytes) - Encrypts each chunk with the 32-round custom XTEA algorithm, concatenates - the Base64 results, and applies TDC URL encoding (%2B, %2F, %3D). - """ - now_sec = int(time.time()) - rand_salt = random.randint(100000000, 999999999) - - # 1. cd prefix (48 bytes) - cd_pre_raw = '{"cd":[[],"1920-1080-1055-24-*-*-|-*",0,0,"",' - cd_pre = cd_pre_raw.ljust(48, " ") - - # 2. Mouse trajectory (600 bytes) - traj_payload = generate_trajectory_payload(click_points, target_len=600) - - # 3. cd post-body (504 bytes) - cd_post_inner = ( - f',"",' - f'"undefined?rand=1519713624347",' - f"1," - f'"{canvas_fp}",' - f"{now_sec}," - f"{rand_salt}," - f"{now_sec}," - f'"",' - f"0," - f'"{user_agent}",' - f'"top",' - f'"MacIntel",' - f'"98k",' - f"40," - f'"",' - f'"{renderer}",' - f"0," - f'"{vendor}",' - f"{now_sec}," - f'"+08",' - f'["zh-CN","zh","en"],' - f"1422008362," - f"2," - f"3," - f'"https://turing.captcha.qcloud.com/cap_union_new_show?aid={aid}",' - f"0," - f"24," - f"22," - f"1920," - f"1," - f"[360,360]," - f"1080]," - ) - cd_post = cd_post_inner.ljust(504, " ") - - # 4. sd suffix (16 bytes) - sd = '"sd":{"od":"C"}}' - - # Encrypt all 4 parts - enc_cd_pre = encrypt_string(cd_pre) - enc_traj = encrypt_string(traj_payload) - enc_cd_post = encrypt_string(cd_post) - enc_sd = encrypt_string(sd) - - combined_b64 = enc_cd_pre + enc_traj + enc_cd_post + enc_sd - - # TDC URL encoding - collect = ( - combined_b64 - .replace("+", "%2B") - .replace("/", "%2F") - .replace("=", "%3D") - ) - return collect - - -if __name__ == "__main__": - # Self-test unit verification - test4 = '"sd":{"od":"C"}}' - exp4 = "nshM2N4iPvX6JRNXi68v5w==" - act4 = encrypt_string(test4) - assert act4 == exp4, f"XTEA unit test failed: {act4} != {exp4}" - - test2 = '{"cd":[[],"1920-1080-1055-24-*-*-|-*",0,0,"", ' - exp2 = "9qJOPk+13StWMa2FQroCP6yba6nGw0BmJaarCCtGtLi0Wfg7WQyVXwMoFAjWcXAj" - act2 = encrypt_string(test2) - assert act2 == exp2, f"XTEA unit test failed: {act2} != {exp2}" - - sample_pts = [(626, 253), (120, 120), (438, 33)] - sample_collect = generate_collect(sample_pts) - print(f"XTEA Unit Tests: PASSED") - print(f"Sample collect length: {len(sample_collect)}") - print(f"Sample collect prefix: {sample_collect[:80]}...") diff --git a/dist/rainyun_qinglong/icon_classifier.onnx b/dist/rainyun_qinglong/icon_classifier.onnx deleted file mode 100644 index ba3983e..0000000 Binary files a/dist/rainyun_qinglong/icon_classifier.onnx and /dev/null differ diff --git a/dist/rainyun_qinglong/rainyun_checkin.py b/dist/rainyun_qinglong/rainyun_checkin.py deleted file mode 100644 index 1793161..0000000 --- a/dist/rainyun_qinglong/rainyun_checkin.py +++ /dev/null @@ -1,497 +0,0 @@ -# -*- coding: utf-8 -*- -""" -cron: 30 8 * * * -new Env('雨云每日签到'); - -雨云 (Rainyun) 自动登录与每日签到脚本 (适用于青龙面板) -账号配置规范: - 环境变量 / 本地 .env 统一使用 RAINYUN_ACCOUNT="账号#密码" - 多账号分割: - - 青龙环境变量: 使用 & 分割 (如 RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2") - - 本地 .env 文件: 支持 & 分割或换行分割 -""" - -import json -import logging -import os -import random -import re -import sys -import time -import uuid -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple - -import httpx - -# 将项目根目录加入 sys.path 以便加载本地模块 -CURRENT_DIR = Path(__file__).resolve().parent -if str(CURRENT_DIR) not in sys.path: - sys.path.insert(0, str(CURRENT_DIR)) - -# 配置日志格式 -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%H:%M:%S", -) -logger = logging.getLogger("rainyun_checkin") - -# 青龙通知模块导入 -try: - from sendNotify import send -except Exception: - try: - from notify import send - except Exception: - def send(title: str, content: str): - pass - - -# 默认配置 -RAINYUN_API_BASE = "https://api.v2.rainyun.com" -TENCENT_AID = "2039519451" -DEFAULT_UA = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" -) - -# 积分提现比例: 2000 积分 = 1 元现金 (支持直接提现) -POINTS_PER_CNY = 2000.0 - - -def format_points(points: int) -> str: - """格式化积分及等值提现金额 (2000积分 = 1元)""" - cny = points / POINTS_PER_CNY - return f"{points} 积分 (≈ {cny:.2f} 元)" - - -def mask_field(val: str) -> str: - """脱敏账号信息""" - if not val: - return "" - if len(val) <= 4: - return "***" - if len(val) == 11 and val.isdigit(): - return f"{val[:3]}****{val[7:]}" - return f"{val[:2]}****{val[-2:]}" - - -class CaptchaManager: - """验证码求解调度器:支持本地 tcaptcha 模块或远程 HTTP 接口""" - - def __init__(self, api_url: Optional[str] = None): - self.api_url = api_url or os.getenv("TCAPTCHA_API_URL", "").strip() - self.local_client = None - - # 尝试初始化本地客户端 (平铺目录优先 import client,兼容 tcaptcha.client) - if not self.api_url: - try: - from client import TencentCaptchaClient - self.local_client = TencentCaptchaClient(aid=TENCENT_AID) - logger.info("✓ 成功加载本地纯算法求解引擎") - except Exception: - try: - from tcaptcha.client import TencentCaptchaClient - self.local_client = TencentCaptchaClient(aid=TENCENT_AID) - logger.info("✓ 成功加载本地纯算法求解引擎") - except Exception as e: - logger.warning(f"未能加载本地求解引擎 ({e}),将尝试远程接口或环境变量配置") - - def get_ticket(self) -> Tuple[Optional[str], Optional[str]]: - """获取腾讯天御点选验证码票据 (ticket, randstr)""" - # 1. 优先使用远程 API (若已配置) - if self.api_url: - logger.info(f"正在调用远程验证码求解接口: {self.api_url} ...") - try: - with httpx.Client(timeout=30.0, trust_env=False) as client: - resp = client.post(self.api_url, json={"aid": TENCENT_AID}) - if resp.status_code == 200: - data = resp.json() - # 兼容直接返回或嵌套在 data 中 - res_data = data.get("data", data) - ticket = res_data.get("ticket") - randstr = res_data.get("randstr") - if ticket and randstr: - logger.info(f"✓ 远程验证码求解成功 (randstr: {randstr})") - return ticket, randstr - else: - logger.error(f"远程验证码接口未返回有效票据: {data}") - else: - logger.error(f"远程验证码接口响应异常: {resp.status_code} {resp.text}") - except Exception as e: - logger.error(f"调用远程验证码接口失败: {e}") - - # 2. 本地求解引擎 - if self.local_client: - logger.info("正在调用本地 tcaptcha AI 纯协议求解器...") - try: - res = self.local_client.solve() - if res.get("success"): - logger.info(f"✓ 本地求解成功 (耗时: {res.get('elapsed_ms')}ms, randstr: {res.get('randstr')})") - return res.get("ticket"), res.get("randstr") - else: - logger.error(f"本地求解失败: {res.get('error')}") - except Exception as e: - logger.error(f"本地求解器执行异常: {e}") - - logger.error("未找到可用的验证码求解方案!请确认部署了 tcaptcha 本地模型或配置了 TCAPTCHA_API_URL 环境变量") - return None, None - - -class RainyunClient: - """雨云 API 客户端""" - - def __init__( - self, - user: str, - password: str, - captcha_mgr: Optional[CaptchaManager] = None, - ): - self.user = user - self.password = password - self.captcha_mgr = captcha_mgr or CaptchaManager() - - self.account_name = mask_field(user) if user else "未知账号" - self.points_before = 0 - self.points_after = 0 - - # 初始化 HTTP 客户端 - self.session = httpx.Client( - base_url=RAINYUN_API_BASE, - timeout=15.0, - follow_redirects=True, - trust_env=False, # 屏蔽环境代理干扰以兼容各类青龙网络 - ) - self._update_common_headers() - - def _update_common_headers(self): - rys = f"{uuid.uuid4().hex}{time.time():.3f}" - self.session.headers.update({ - "User-Agent": DEFAULT_UA, - "Origin": "https://app.rainyun.com", - "Referer": "https://app.rainyun.com/", - "Accept": "application/json, text/plain, */*", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", - "rys": rys, - }) - - def login(self) -> bool: - """执行登录并获取凭据""" - if not self.user or not self.password: - logger.error("缺少账号或密码!") - return False - - logger.info(f"正在登录雨云账号: {self.account_name} ...") - self._update_common_headers() - - payload = { - "field": self.user, - "password": self.password, - "verifyCode": "", - } - - resp = self.session.post("/user/login", json=payload) - res_json = {} - try: - res_json = resp.json() - except Exception: - pass - - # 检查是否需要验证码 - if resp.status_code == 400 and res_json.get("code") in [10004, 10005]: - logger.info("雨云登录要求完成人机验证,正在调用验证码求解器...") - ticket, randstr = self.captcha_mgr.get_ticket() - if not ticket or not randstr: - logger.error("登录验证码获取失败,无法继续登录") - return False - payload["vticket"] = ticket - payload["vrandstr"] = randstr - resp = self.session.post("/user/login", json=payload) - try: - res_json = resp.json() - except Exception: - pass - - if resp.status_code == 200 and res_json.get("code") == 200: - # 同步更新 CSRF Token - csrf = self.session.cookies.get("X-CSRF-Token") - if csrf: - self.session.headers["x-csrf-token"] = csrf - logger.info(f"✓ 账号 {self.account_name} 登录成功!") - return True - - err_msg = res_json.get("message") or resp.text - logger.error(f"❌ 账号 {self.account_name} 登录失败: {err_msg}") - return False - - def fetch_user_info(self) -> Optional[Dict[str, Any]]: - """获取当前用户信息(积分、用户名等)""" - self._update_common_headers() - try: - resp = self.session.get("/user/?no_cache=true") - if resp.status_code == 200: - data = resp.json() - if data.get("code") == 200: - info = data.get("data", {}) - self.account_name = info.get("Name") or self.account_name - self.points_before = info.get("Points", 0) - return info - except Exception as e: - logger.warning(f"获取用户信息异常: {e}") - return None - - def fetch_tasks(self) -> List[Dict[str, Any]]: - """获取任务列表""" - self._update_common_headers() - try: - resp = self.session.get("/user/reward/tasks") - if resp.status_code == 200: - data = resp.json() - if data.get("code") == 200: - return data.get("data", []) - except Exception as e: - logger.error(f"获取任务列表失败: {e}") - return [] - - def claim_daily_checkin(self) -> Dict[str, Any]: - """ - 执行每日签到核心业务逻辑: - 1. 检查任务列表与签到状态 - 2. 若 Status == 2: 今日已完成,直接跳过 - 3. 若 Status == 1: 待领取,获取验证码并提交 - """ - user_info = self.fetch_user_info() - user_display = f"{self.account_name} (ID: {user_info.get('ID') if user_info else '未知'})" - - tasks = self.fetch_tasks() - checkin_task = None - for t in tasks: - if t.get("Name") == "每日签到": - checkin_task = t - break - - if not checkin_task: - return { - "user": user_display, - "status": "error", - "message": "未在任务列表中找到【每日签到】任务", - "points_before": self.points_before, - "points_after": self.points_before, - } - - task_status = checkin_task.get("Status") - # Status 2: 今日已领取 - if task_status == 2: - logger.info(f"✨ 账号【{user_display}】今日已完成每日签到,当前总资产: {format_points(self.points_before)}。") - return { - "user": user_display, - "status": "already_done", - "message": "今日已签到 (无需重复领取)", - "points_before": self.points_before, - "points_after": self.points_before, - } - - # Status 1: 待签到 - logger.info(f"🎯 账号【{user_display}】今日尚未签到,开始启动签到流程...") - - # 1. 求解验证码 - ticket, randstr = self.captcha_mgr.get_ticket() - if not ticket or not randstr: - return { - "user": user_display, - "status": "fail", - "message": "验证码求解失败,签到中止", - "points_before": self.points_before, - "points_after": self.points_before, - } - - # 2. 提交签到请求 - self._update_common_headers() - payload = { - "task_name": "每日签到", - "verifyCode": "", - "vticket": ticket, - "vrandstr": randstr, - } - - resp = self.session.post("/user/reward/tasks", json=payload) - res_json = {} - try: - res_json = resp.json() - except Exception: - pass - - if resp.status_code == 200 and res_json.get("code") == 200: - reward_data = res_json.get("data", {}) - earned_points = reward_data.get("points", 500) - earned_cny = earned_points / POINTS_PER_CNY - - # 刷新最新积分 - latest_info = self.fetch_user_info() - self.points_after = latest_info.get("Points", self.points_before + earned_points) if latest_info else self.points_before + earned_points - - msg = f"签到成功!获得 +{earned_points} 积分 (≈ {earned_cny:.2f} 元)" - logger.info(f"🎉 账号【{user_display}】{msg},当前总资产: {format_points(self.points_after)}") - return { - "user": user_display, - "status": "success", - "message": msg, - "earned_points": earned_points, - "points_before": self.points_before, - "points_after": self.points_after, - } - else: - err_msg = res_json.get("message") or resp.text - logger.error(f"❌ 账号【{user_display}】签到失败: {err_msg}") - return { - "user": user_display, - "status": "fail", - "message": f"签到请求失败: {err_msg}", - "points_before": self.points_before, - "points_after": self.points_before, - } - - -def load_dotenv(env_path: Optional[Path] = None): - """自动加载本地 .env 文件到 os.environ,方便本地直接调试运行""" - search_paths = [ - env_path, - CURRENT_DIR / ".env", - Path.cwd() / ".env", - ] - for p in search_paths: - if p and p.is_file(): - try: - with open(p, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - if "=" in line: - k, v = line.split("=", 1) - k = k.strip() - v = v.strip() - if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")): - v = v[1:-1] - if k not in os.environ or not os.environ[k]: - os.environ[k] = v - logger.info(f"✓ 成功加载本地环境变量: {p}") - break - except Exception as e: - logger.warning(f"读取 .env 异常: {e}") - - -def parse_accounts() -> List[Dict[str, str]]: - """ - 解析雨云账号配置 (优先加载本地 .env): - 仅支持 RAINYUN_ACCOUNT="账号#密码" 格式 - 多账号使用 & 分割 (青龙面板推荐) 或 换行分割 (本地 .env 推荐) - 例如: - RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2" - """ - load_dotenv() - accounts = [] - - acc_env = os.getenv("RAINYUN_ACCOUNT", "").strip() - if not acc_env: - return [] - - # 去除外层引号 - if (acc_env.startswith('"') and acc_env.endswith('"')) or (acc_env.startswith("'") and acc_env.endswith("'")): - acc_env = acc_env[1:-1].strip() - - # 按照 & 或 换行 分割多账号 - raw_list = re.split(r"[&\n]", acc_env) - for item in raw_list: - item = item.strip() - if not item or "#" not in item: - continue - u, p = item.split("#", 1) - u, p = u.strip(), p.strip() - if u and p and not any(a.get("user") == u for a in accounts): - accounts.append({"user": u, "password": p}) - - return accounts - - -def main(): - print("=" * 60) - print(" 🚀 雨云 (Rainyun) 自动登录与每日签到任务启动") - print("=" * 60) - - accounts = parse_accounts() - if not accounts: - msg = ( - "未检测到雨云账号配置!请在青龙环境变量或本地 .env 中配置 RAINYUN_ACCOUNT\n" - "格式: 账号#密码 (多账号用 & 或换行分割)\n" - "例如: RAINYUN_ACCOUNT=\"13754650804#pwd1&13800000000#pwd2\"\n" - "如需远程验证码服务,可添加 TCAPTCHA_API_URL (如 http://x.x.x.x:8000/solve)" - ) - logger.warning(msg) - send("雨云每日签到异常", msg) - return - - logger.info(f"已成功加载 {len(accounts)} 个雨云账号配置") - captcha_mgr = CaptchaManager() - - results = [] - for idx, acc in enumerate(accounts, 1): - logger.info(f"\n--- [正在处理账号 {idx}/{len(accounts)}] ---") - client = RainyunClient( - user=acc["user"], - password=acc["password"], - captcha_mgr=captcha_mgr, - ) - - login_ok = client.login() - if not login_ok: - results.append({ - "user": client.account_name, - "status": "login_failed", - "message": "登录失败", - "points_before": 0, - "points_after": 0, - }) - continue - - res = client.claim_daily_checkin() - results.append(res) - - # 多账号之间随机休眠 2~4 秒 - if idx < len(accounts): - sleep_sec = random.uniform(2.0, 4.0) - time.sleep(sleep_sec) - - # 统计汇总 - print("\n" + "=" * 60) - print(" 📊 任务执行结果统计报表") - print("=" * 60) - - success_cnt = sum(1 for r in results if r["status"] in ["success", "already_done"]) - summary_lines = [] - total_points = 0 - - for idx, r in enumerate(results, 1): - status_icon = "✅" if r["status"] in ["success", "already_done"] else "❌" - pts = r.get("points_after", 0) - total_points += pts - cny_str = format_points(pts) - line = f"[{idx}] {status_icon} 账号: {r['user']} | 结果: {r['message']} | 当前账户: {cny_str}" - summary_lines.append(line) - print(line) - - total_cny = total_points / POINTS_PER_CNY - total_summary = f"💰 全部账号总资产: {total_points} 积分 (可提现 ≈ {total_cny:.2f} 元)" - summary_lines.append(total_summary) - print(total_summary) - print("=" * 60) - - # 推送青龙系统通知 - notify_title = f"雨云签到: 成功 {success_cnt}/{len(results)} | 总资产 ≈ {total_cny:.2f}元" - notify_content = "\n".join(summary_lines) - send(notify_title, notify_content) - - -if __name__ == "__main__": - main() diff --git a/dist/rainyun_qinglong/server.py b/dist/rainyun_qinglong/server.py deleted file mode 100644 index 43d902c..0000000 --- a/dist/rainyun_qinglong/server.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -""" -腾讯天御点选验证码纯协议 HTTP 微服务 (Tencent Turing Captcha HTTP Microservice) -用于在独立机器/宿主机/GPU服务器上运行,供青龙容器或其他轻量级客户端通过 HTTP API 跨机调用。 -基于 Python 原生 ThreadingHTTPServer 实现,零额外依赖 (无需额外安装 FastAPI/Flask)。 - -使用方式: - uv run python -m tcaptcha.server --port 8000 --host 0.0.0.0 -调用接口: - POST http://:8000/solve - 请求体 (可选): {"aid": "2039519451"} - 返回 JSON: - { - "code": 200, - "success": true, - "ticket": "tr03...", - "randstr": "@...", - "elapsed_ms": 820 - } -""" - -import argparse -import json -import logging -import sys -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path - -CURRENT_DIR = Path(__file__).resolve().parent -if str(CURRENT_DIR) not in sys.path: - sys.path.insert(0, str(CURRENT_DIR)) - -try: - from client import TencentCaptchaClient - from solver import CaptchaSolver -except ImportError: - from tcaptcha.client import TencentCaptchaClient - from tcaptcha.solver import CaptchaSolver - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%H:%M:%S", -) -logger = logging.getLogger("tcaptcha_server") - -GLOBAL_SOLVER = None - - -def get_solver() -> CaptchaSolver: - global GLOBAL_SOLVER - if GLOBAL_SOLVER is None: - logger.info("正在初始化并预加载 AI 求解推理引擎 (YOLOv8 + ResNet18)...") - GLOBAL_SOLVER = CaptchaSolver() - logger.info("✓ AI 推理引擎预加载完成!") - return GLOBAL_SOLVER - - -class CaptchaHttpHandler(BaseHTTPRequestHandler): - def _send_json(self, code: int, data: dict): - body = json.dumps(data, ensure_ascii=False).encode("utf-8") - self.send_response(code) - self.send_header("Content-Type", "application/json; charset=utf-8") - self.send_header("Content-Length", str(len(body))) - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Access-Control-Allow-Headers", "*") - self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - self.end_headers() - self.wfile.write(body) - - def do_OPTIONS(self): - self._send_json(200, {"status": "ok"}) - - def do_GET(self): - if self.path in ["/health", "/"]: - self._send_json(200, { - "status": "online", - "service": "Tencent Captcha Solver Microservice", - "endpoints": { - "solve": "POST /solve (body: {'aid': '2039519451'})", - "health": "GET /health" - } - }) - elif self.path.startswith("/solve"): - # 允许通过 GET 快速调试 - self._handle_solve({"aid": "2039519451"}) - else: - self._send_json(404, {"error": "Not Found"}) - - def do_POST(self): - if self.path.startswith("/solve"): - content_length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(content_length) if content_length > 0 else b"{}" - try: - params = json.loads(body.decode("utf-8")) if body else {} - except Exception: - params = {} - self._handle_solve(params) - else: - self._send_json(404, {"error": "Not Found"}) - - def _handle_solve(self, params: dict): - aid = params.get("aid") or "2039519451" - logger.info(f"收到求解请求 (AppID: {aid})...") - solver = get_solver() - client = TencentCaptchaClient(aid=aid, solver=solver) - result = client.solve() - - if result.get("success"): - self._send_json(200, { - "code": 200, - "success": True, - "ticket": result.get("ticket"), - "randstr": result.get("randstr"), - "elapsed_ms": result.get("elapsed_ms"), - "points": result.get("points"), - }) - else: - self._send_json(500, { - "code": 500, - "success": False, - "error": result.get("error"), - "elapsed_ms": result.get("elapsed_ms"), - }) - - def log_message(self, format, *args): - logger.info("%s - %s", self.address_string(), format % args) - - -def main(): - parser = argparse.ArgumentParser(description="Tencent Captcha Microservice") - parser.add_argument("--host", type=str, default="0.0.0.0", help="Listen host (default: 0.0.0.0)") - parser.add_argument("--port", type=int, default=8000, help="Listen port (default: 8000)") - args = parser.parse_args() - - # 预加载推理引擎 - get_solver() - - server_address = (args.host, args.port) - httpd = ThreadingHTTPServer(server_address, CaptchaHttpHandler) - logger.info(f"🚀 验证码微服务已启动: http://{args.host}:{args.port}") - logger.info("可用于青龙面板配置环境变量: TCAPTCHA_API_URL=http://:%d/solve", args.port) - - try: - httpd.serve_forever() - except KeyboardInterrupt: - logger.info("服务已停止") - finally: - httpd.server_close() - - -if __name__ == "__main__": - main() diff --git a/dist/rainyun_qinglong/yolo_detector.onnx b/dist/rainyun_qinglong/yolo_detector.onnx deleted file mode 100644 index 39e3923..0000000 Binary files a/dist/rainyun_qinglong/yolo_detector.onnx and /dev/null differ diff --git a/har/1ms_dianxuan/PROGRESS.md b/har/1ms_dianxuan/PROGRESS.md index 3c18da5..023a83f 100644 --- a/har/1ms_dianxuan/PROGRESS.md +++ b/har/1ms_dianxuan/PROGRESS.md @@ -8,6 +8,56 @@ --- +## 📁 2026-10-12 项目结构重组(下面的历史条目仍使用**旧路径**,对照本表) + +包从扁平改为分层,并删除了已废弃的整条路线(雨云线 / 模板回填 / Node 兜底 / +"补环境"时代的浏览器 JS / 旧 VM 探索工具 / 训练标注脚本)。 +回滚点:tag `backup/pre-refactor-20261012`。 + +``` +tcaptcha/ + protocol/ client.py transport.py proxy.py # 协议交互 + telemetry/ crypto.py values.py key.py opcodes.py # collect 纯离线生成 + disasm.py program.py layout.py modules.py # + 密钥静态还原 + solvers/ text_click.py icon_click.py icon_click_rotate.py slider.py + ocr/ engines.py slicer.py + adapters/ one_ms.py + models/ 权重(onnx) + tests/ +tools/ cdp_eval.js cdp_collect.js cdp_dump_scripts.js tdc_key_pure.py tdc_profile.py +scripts/verify/ align_session.py audit_names.py verify_values.py verify_offline.py send_test.py +``` + +| 历史条目里的旧路径 | 现在的位置 | +| --- | --- | +| `tcaptcha/client.py` | `tcaptcha/protocol/client.py` | +| `tcaptcha/http_transport.py` / `proxy.py` | `tcaptcha/protocol/transport.py` / `proxy.py` | +| `tcaptcha/crypto.py` | `tcaptcha/telemetry/crypto.py` | +| `tcaptcha/tdc_values.py` | `tcaptcha/telemetry/values.py` | +| `tcaptcha/tdc_key.py` / `tdc_opcodes.py` | `tcaptcha/telemetry/key.py` / `opcodes.py` | +| `tools/tdc_disasm.py` / `tdc_program.py` / `tdc_layout.py` / `tdc_modules.py` | `tcaptcha/telemetry/{disasm,program,layout,modules}.py` | +| `tcaptcha/glyph_solver.py` | `tcaptcha/solvers/text_click.py` | +| `tcaptcha/solver.py` | `tcaptcha/solvers/icon_click.py` | +| `tcaptcha/icon_click_solver.py` | `tcaptcha/solvers/icon_click_rotate.py` | +| `tcaptcha/slider_solver.py` | `tcaptcha/solvers/slider.py` | +| `tcaptcha/ocr_engines.py` / `slicer.py` | `tcaptcha/ocr/engines.py` / `slicer.py` | +| `tcaptcha/one_ms_adapter.py` | `tcaptcha/adapters/one_ms.py` | + +**已删除**(历史条目仍会提到,但要按"已不存在"来读): + +`tcaptcha/{chrome_fetch,manual_captcha,probe_captcha,solve_in_browser,tdc_browser}.js`、 +`tcaptcha/tdc_worker.js`、`models/success_collect_template.b64`、`tcaptcha/core_js/**`、 +`tcaptcha/TOOLCHAIN_REPORT.md`、 +`tools/{tdc_collectgen,tdc_slotmap,tdc_lift,tdc_vm_run,tdc_query,tdc_probe.js,tdc_trace.js,tdc_keytrace.js,tdc_vm_disasm.js,unpack_webpack.js}`、 +`tools/cdp_{capture,click,net_capture,script_source,shot,module_probe,show_captcha}.js`、 +`dist/rainyun_qinglong/**`、`rainyun_checkin.py`、 +`scripts/{analyze_collect_layout,rainyun_checkin,check_real_tencent_aid,experiment_layout_verify}.py`、 +`scripts/verify/{verify_segment,verify_seglen,verify_reorder,verify_pair_correct,verify_fp_consistency,verify_control_negative,verify_40_9_disamb,balance_names_values,label_plaintext}.py`、 +`tcaptcha/tests/{test_collectgen,test_xtea_key}.py`、 +`tcaptcha/{train_classifier,train_detector,prepare_yolo,web_labeler,collector,siamese,evaluate,benchmark_verify}.py` + +--- + ## ⚠️ 2026-10-12 路线修正(对照 `docs/tdc-案例.md`,全部基于**在线新抓样本**) 之前多轮「乱碰」的根因,是几条**基础假设错了**: @@ -300,46 +350,29 @@ TencentCaptchaClient(telemetry_mode="python_keyed") # 默认 ## 六、复验入口 ```bash -.venv/bin/python -m tcaptcha.tests.test_crypto_core # XTEA 往返 + encrypt_string 一致性 -.venv/bin/python -m tcaptcha.tests.test_xtea_key # XTEA + 密钥提取 (逐字节对拍, Node 参与) -.venv/bin/python -m tcaptcha.tests.test_tdc_key # **纯 Python** 密钥提取 (6 份构建, 零 Node) -.venv/bin/python -m tcaptcha.tests.test_glyph_solver # 文字点选求解 (8 张真实样本) -.venv/bin/python -m tcaptcha.tests.test_icon_click_solver # 图文点选求解 (真机样本) -.venv/bin/python -m tcaptcha.tests.test_slider_solver # 滑块缺口求解 (真机样本) -.venv/bin/python -m tcaptcha.selftest # 全部 - -.venv/bin/python tools/tdc_key_pure.py -v har/1ms_dianxuan/live/tdc_fresh.js # 单份构建 + 推导过程 -.venv/bin/python tools/tdc_key_pure.py --opcodes tcaptcha/tdc_sample.js # 打印按构建推导的 opcode 表 -.venv/bin/python scripts/analyze_collect_layout.py --json /tmp/cap.json # collect 槽位序对比 (已知 #31) -``` - -### 2026-10-12 新增的复验入口(按案例重做后) - -```bash -# ① 在线抓取全新样本(新研究的数据来源;同一个 URL 每次返回不同构建,见 #29) -.venv/bin/python scripts/fetch_online_samples.py --n 4 - -# ② 字符串+结构指纹识别 37 个采集器(应 37/37、0 unknown、0 重名) -.venv/bin/python tools/tdc_modules.py har/online//tdc_01_*.js - -# ③ 四段结构判决(不需要密钥:密文长度算术应精确等于 1048 / 该份的长度) -.venv/bin/python scripts/verify/verify_segment.py <已解密的 collect 明文文件> -.venv/bin/python scripts/verify/verify_seglen.py - -# ④ 指纹跨构建一致性(语义名集合应完全一致、单构建内 0 重名) -.venv/bin/python scripts/verify/verify_fp_consistency.py - -# ⑤ 配对正确性(实验组≈37/37,三个负对照应 2-3/37 —— 证明没过拟合) -.venv/bin/python scripts/verify/verify_pair_correct.py -.venv/bin/python scripts/verify/verify_control_negative.py - -# ⑥ 端到端重排(新数据,应 3/3 正确且非恒等) -.venv/bin/python scripts/verify/verify_reorder.py - -# ⑦ 回归测试(已并入 selftest,含「reorder 非恒等」强断言) -.venv/bin/python -m tcaptcha.tests.test_collectgen +# 一键全量 .venv/bin/python -m tcaptcha.selftest +# 单套 +.venv/bin/python -m tcaptcha.tests.test_crypto_core # XTEA 往返 + 四段切分回归 + 纯离线闭环 +.venv/bin/python -m tcaptcha.tests.test_tdc_key # 纯 Python 密钥提取(5 份构建,零 Node) +.venv/bin/python -m tcaptcha.tests.test_glyph_solver # 文字点选求解 +.venv/bin/python -m tcaptcha.tests.test_icon_click_solver # 图标点选求解 +.venv/bin/python -m tcaptcha.tests.test_slider_solver # 滑块缺口求解 + +# 静态分析 CLI(原来的 tools/tdc_*.py 已并入包里) +.venv/bin/python tools/tdc_key_pure.py -v tcaptcha/tdc_sample.js # 取密钥 + 推导过程 +.venv/bin/python tools/tdc_key_pure.py --opcodes tcaptcha/tdc_sample.js # 按构建推导的 opcode 表 +.venv/bin/python -m tcaptcha.telemetry.modules tcaptcha/tdc_sample.js # 37 个语义名识别 +.venv/bin/python -m tcaptcha.telemetry.layout tcaptcha/tdc_sample.js # 清单 require 顺序 + +# 在线抓全新样本(同一个 URL 每次返回不同构建,见 #29) +.venv/bin/python scripts/fetch_online_samples.py --n 4 +``` + +### 标定 / 闭环 / 真实发送(2026-10-12 起的主入口) + +```bash # ⑧ 「同会话构建 + 同会话 collect」的取值标定(这一步打通了纯离线生成的最后阻塞点) # 先备好同源配对(真 Chrome,9222): # node tools/cdp_dump_scripts.js --match drag_ele --outdir /tmp/1ms_bridge/scripts diff --git a/pyproject.toml b/pyproject.toml index 23049a4..92df1f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,20 @@ [project] name = "hello-js-reverse-skill" -version = "0.1.0" +version = "0.2.0" +description = "腾讯验证码(TDC)纯算还原 + 文字点选/图标点选/滑块求解" requires-python = ">=3.12" dependencies = [ - "curl-cffi>=0.16.3", - "ddddocr>=1.6.1", - "httpx>=0.28.1", + "curl-cffi>=0.16.3", # protocol.transport 的传输指纹实现(可选路径) + "ddddocr>=1.6.1", # ocr.engines 集成识别 + "httpx>=0.28.1", # protocol.client 默认传输 "numpy>=2.5.3", - "onnx>=1.22.0", + "onnxruntime>=1.20.0", # solvers.icon_click 的 YOLO/ResNet18 推理 "opencv-python-headless>=5.0.0.93", "pillow>=12.3.0", - "ultralytics>=8.4.147", ] + +# 说明: +# * 训练/标注用的 ultralytics + torch 已随 train_*/prepare_yolo/web_labeler 一并移除 +# —— 线上只跑 onnxruntime,不需要 PyTorch 栈。 +# * 唯一的外部运行时依赖是 **浏览器**(仅在做同会话标定时用): +# tools/cdp_*.js 通过 Chrome 的 --remote-debugging-port 工作,不参与出包。 diff --git a/rainyun_checkin.py b/rainyun_checkin.py deleted file mode 100644 index d089e32..0000000 --- a/rainyun_checkin.py +++ /dev/null @@ -1,492 +0,0 @@ -# -*- coding: utf-8 -*- -""" -cron: 30 8 * * * -new Env('雨云每日签到'); - -雨云 (Rainyun) 自动登录与每日签到脚本 (适用于青龙面板) -账号配置规范: - 环境变量 / 本地 .env 统一使用 RAINYUN_ACCOUNT="账号#密码" - 多账号分割: - - 青龙环境变量: 使用 & 分割 (如 RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2") - - 本地 .env 文件: 支持 & 分割或换行分割 -""" - -import json -import logging -import os -import random -import re -import sys -import time -import uuid -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple - -import httpx - -# 将项目根目录加入 sys.path 以便加载本地模块 -CURRENT_DIR = Path(__file__).resolve().parent -if str(CURRENT_DIR) not in sys.path: - sys.path.insert(0, str(CURRENT_DIR)) - -# 配置日志格式 -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%H:%M:%S", -) -logger = logging.getLogger("rainyun_checkin") - -# 青龙通知模块导入 -try: - from sendNotify import send -except Exception: - try: - from notify import send - except Exception: - def send(title: str, content: str): - pass - - -# 默认配置 -RAINYUN_API_BASE = "https://api.v2.rainyun.com" -TENCENT_AID = "2039519451" -DEFAULT_UA = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" -) - -# 积分提现比例: 2000 积分 = 1 元现金 (支持直接提现) -POINTS_PER_CNY = 2000.0 - - -def format_points(points: int) -> str: - """格式化积分及等值提现金额 (2000积分 = 1元)""" - cny = points / POINTS_PER_CNY - return f"{points} 积分 (≈ {cny:.2f} 元)" - - -def mask_field(val: str) -> str: - """脱敏账号信息""" - if not val: - return "" - if len(val) <= 4: - return "***" - if len(val) == 11 and val.isdigit(): - return f"{val[:3]}****{val[7:]}" - return f"{val[:2]}****{val[-2:]}" - - -class CaptchaManager: - """验证码求解调度器:支持本地 tcaptcha 模块或远程 HTTP 接口""" - - def __init__(self, api_url: Optional[str] = None): - self.api_url = api_url or os.getenv("TCAPTCHA_API_URL", "").strip() - self.local_client = None - - # 尝试初始化本地客户端 - if not self.api_url: - try: - from tcaptcha.client import TencentCaptchaClient - self.local_client = TencentCaptchaClient(aid=TENCENT_AID) - logger.info("✓ 成功加载本地 tcaptcha 纯算法求解引擎") - except Exception as e: - logger.warning(f"未能加载本地 tcaptcha 求解引擎 ({e}),将尝试远程接口或环境变量配置") - - def get_ticket(self) -> Tuple[Optional[str], Optional[str]]: - """获取腾讯天御点选验证码票据 (ticket, randstr)""" - # 1. 优先使用远程 API (若已配置) - if self.api_url: - logger.info(f"正在调用远程验证码求解接口: {self.api_url} ...") - try: - with httpx.Client(timeout=30.0, trust_env=False) as client: - resp = client.post(self.api_url, json={"aid": TENCENT_AID}) - if resp.status_code == 200: - data = resp.json() - # 兼容直接返回或嵌套在 data 中 - res_data = data.get("data", data) - ticket = res_data.get("ticket") - randstr = res_data.get("randstr") - if ticket and randstr: - logger.info(f"✓ 远程验证码求解成功 (randstr: {randstr})") - return ticket, randstr - else: - logger.error(f"远程验证码接口未返回有效票据: {data}") - else: - logger.error(f"远程验证码接口响应异常: {resp.status_code} {resp.text}") - except Exception as e: - logger.error(f"调用远程验证码接口失败: {e}") - - # 2. 本地求解引擎 - if self.local_client: - logger.info("正在调用本地 tcaptcha AI 纯协议求解器...") - try: - res = self.local_client.solve() - if res.get("success"): - logger.info(f"✓ 本地求解成功 (耗时: {res.get('elapsed_ms')}ms, randstr: {res.get('randstr')})") - return res.get("ticket"), res.get("randstr") - else: - logger.error(f"本地求解失败: {res.get('error')}") - except Exception as e: - logger.error(f"本地求解器执行异常: {e}") - - logger.error("未找到可用的验证码求解方案!请确认部署了 tcaptcha 本地模型或配置了 TCAPTCHA_API_URL 环境变量") - return None, None - - -class RainyunClient: - """雨云 API 客户端""" - - def __init__( - self, - user: str, - password: str, - captcha_mgr: Optional[CaptchaManager] = None, - ): - self.user = user - self.password = password - self.captcha_mgr = captcha_mgr or CaptchaManager() - - self.account_name = mask_field(user) if user else "未知账号" - self.points_before = 0 - self.points_after = 0 - - # 初始化 HTTP 客户端 - self.session = httpx.Client( - base_url=RAINYUN_API_BASE, - timeout=15.0, - follow_redirects=True, - trust_env=False, # 屏蔽环境代理干扰以兼容各类青龙网络 - ) - self._update_common_headers() - - def _update_common_headers(self): - rys = f"{uuid.uuid4().hex}{time.time():.3f}" - self.session.headers.update({ - "User-Agent": DEFAULT_UA, - "Origin": "https://app.rainyun.com", - "Referer": "https://app.rainyun.com/", - "Accept": "application/json, text/plain, */*", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", - "rys": rys, - }) - - def login(self) -> bool: - """执行登录并获取凭据""" - if not self.user or not self.password: - logger.error("缺少账号或密码!") - return False - - logger.info(f"正在登录雨云账号: {self.account_name} ...") - self._update_common_headers() - - payload = { - "field": self.user, - "password": self.password, - "verifyCode": "", - } - - resp = self.session.post("/user/login", json=payload) - res_json = {} - try: - res_json = resp.json() - except Exception: - pass - - # 检查是否需要验证码 - if resp.status_code == 400 and res_json.get("code") in [10004, 10005]: - logger.info("雨云登录要求完成人机验证,正在调用验证码求解器...") - ticket, randstr = self.captcha_mgr.get_ticket() - if not ticket or not randstr: - logger.error("登录验证码获取失败,无法继续登录") - return False - payload["vticket"] = ticket - payload["vrandstr"] = randstr - resp = self.session.post("/user/login", json=payload) - try: - res_json = resp.json() - except Exception: - pass - - if resp.status_code == 200 and res_json.get("code") == 200: - # 同步更新 CSRF Token - csrf = self.session.cookies.get("X-CSRF-Token") - if csrf: - self.session.headers["x-csrf-token"] = csrf - logger.info(f"✓ 账号 {self.account_name} 登录成功!") - return True - - err_msg = res_json.get("message") or resp.text - logger.error(f"❌ 账号 {self.account_name} 登录失败: {err_msg}") - return False - - def fetch_user_info(self) -> Optional[Dict[str, Any]]: - """获取当前用户信息(积分、用户名等)""" - self._update_common_headers() - try: - resp = self.session.get("/user/?no_cache=true") - if resp.status_code == 200: - data = resp.json() - if data.get("code") == 200: - info = data.get("data", {}) - self.account_name = info.get("Name") or self.account_name - self.points_before = info.get("Points", 0) - return info - except Exception as e: - logger.warning(f"获取用户信息异常: {e}") - return None - - def fetch_tasks(self) -> List[Dict[str, Any]]: - """获取任务列表""" - self._update_common_headers() - try: - resp = self.session.get("/user/reward/tasks") - if resp.status_code == 200: - data = resp.json() - if data.get("code") == 200: - return data.get("data", []) - except Exception as e: - logger.error(f"获取任务列表失败: {e}") - return [] - - def claim_daily_checkin(self) -> Dict[str, Any]: - """ - 执行每日签到核心业务逻辑: - 1. 检查任务列表与签到状态 - 2. 若 Status == 2: 今日已完成,直接跳过 - 3. 若 Status == 1: 待领取,获取验证码并提交 - """ - user_info = self.fetch_user_info() - user_display = f"{self.account_name} (ID: {user_info.get('ID') if user_info else '未知'})" - - tasks = self.fetch_tasks() - checkin_task = None - for t in tasks: - if t.get("Name") == "每日签到": - checkin_task = t - break - - if not checkin_task: - return { - "user": user_display, - "status": "error", - "message": "未在任务列表中找到【每日签到】任务", - "points_before": self.points_before, - "points_after": self.points_before, - } - - task_status = checkin_task.get("Status") - # Status 2: 今日已领取 - if task_status == 2: - logger.info(f"✨ 账号【{user_display}】今日已完成每日签到,当前总资产: {format_points(self.points_before)}。") - return { - "user": user_display, - "status": "already_done", - "message": "今日已签到 (无需重复领取)", - "points_before": self.points_before, - "points_after": self.points_before, - } - - # Status 1: 待签到 - logger.info(f"🎯 账号【{user_display}】今日尚未签到,开始启动签到流程...") - - # 1. 求解验证码 - ticket, randstr = self.captcha_mgr.get_ticket() - if not ticket or not randstr: - return { - "user": user_display, - "status": "fail", - "message": "验证码求解失败,签到中止", - "points_before": self.points_before, - "points_after": self.points_before, - } - - # 2. 提交签到请求 - self._update_common_headers() - payload = { - "task_name": "每日签到", - "verifyCode": "", - "vticket": ticket, - "vrandstr": randstr, - } - - resp = self.session.post("/user/reward/tasks", json=payload) - res_json = {} - try: - res_json = resp.json() - except Exception: - pass - - if resp.status_code == 200 and res_json.get("code") == 200: - reward_data = res_json.get("data", {}) - earned_points = reward_data.get("points", 500) - earned_cny = earned_points / POINTS_PER_CNY - - # 刷新最新积分 - latest_info = self.fetch_user_info() - self.points_after = latest_info.get("Points", self.points_before + earned_points) if latest_info else self.points_before + earned_points - - msg = f"签到成功!获得 +{earned_points} 积分 (≈ {earned_cny:.2f} 元)" - logger.info(f"🎉 账号【{user_display}】{msg},当前总资产: {format_points(self.points_after)}") - return { - "user": user_display, - "status": "success", - "message": msg, - "earned_points": earned_points, - "points_before": self.points_before, - "points_after": self.points_after, - } - else: - err_msg = res_json.get("message") or resp.text - logger.error(f"❌ 账号【{user_display}】签到失败: {err_msg}") - return { - "user": user_display, - "status": "fail", - "message": f"签到请求失败: {err_msg}", - "points_before": self.points_before, - "points_after": self.points_before, - } - - -def load_dotenv(env_path: Optional[Path] = None): - """自动加载本地 .env 文件到 os.environ,方便本地直接调试运行""" - search_paths = [ - env_path, - CURRENT_DIR / ".env", - Path.cwd() / ".env", - ] - for p in search_paths: - if p and p.is_file(): - try: - with open(p, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - if "=" in line: - k, v = line.split("=", 1) - k = k.strip() - v = v.strip() - if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")): - v = v[1:-1] - if k not in os.environ or not os.environ[k]: - os.environ[k] = v - logger.info(f"✓ 成功加载本地环境变量: {p}") - break - except Exception as e: - logger.warning(f"读取 .env 异常: {e}") - - -def parse_accounts() -> List[Dict[str, str]]: - """ - 解析雨云账号配置 (优先加载本地 .env): - 仅支持 RAINYUN_ACCOUNT="账号#密码" 格式 - 多账号使用 & 分割 (青龙面板推荐) 或 换行分割 (本地 .env 推荐) - 例如: - RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2" - """ - load_dotenv() - accounts = [] - - acc_env = os.getenv("RAINYUN_ACCOUNT", "").strip() - if not acc_env: - return [] - - # 去除外层引号 - if (acc_env.startswith('"') and acc_env.endswith('"')) or (acc_env.startswith("'") and acc_env.endswith("'")): - acc_env = acc_env[1:-1].strip() - - # 按照 & 或 换行 分割多账号 - raw_list = re.split(r"[&\n]", acc_env) - for item in raw_list: - item = item.strip() - if not item or "#" not in item: - continue - u, p = item.split("#", 1) - u, p = u.strip(), p.strip() - if u and p and not any(a.get("user") == u for a in accounts): - accounts.append({"user": u, "password": p}) - - return accounts - - -def main(): - print("=" * 60) - print(" 🚀 雨云 (Rainyun) 自动登录与每日签到任务启动") - print("=" * 60) - - accounts = parse_accounts() - if not accounts: - msg = ( - "未检测到雨云账号配置!请在青龙环境变量或本地 .env 中配置 RAINYUN_ACCOUNT\n" - "格式: 账号#密码 (多账号用 & 或换行分割)\n" - "例如: RAINYUN_ACCOUNT=\"13754650804#pwd1&13800000000#pwd2\"\n" - "如需远程验证码服务,可添加 TCAPTCHA_API_URL (如 http://x.x.x.x:8000/solve)" - ) - logger.warning(msg) - send("雨云每日签到异常", msg) - return - - logger.info(f"已成功加载 {len(accounts)} 个雨云账号配置") - captcha_mgr = CaptchaManager() - - results = [] - for idx, acc in enumerate(accounts, 1): - logger.info(f"\n--- [正在处理账号 {idx}/{len(accounts)}] ---") - client = RainyunClient( - user=acc["user"], - password=acc["password"], - captcha_mgr=captcha_mgr, - ) - - login_ok = client.login() - if not login_ok: - results.append({ - "user": client.account_name, - "status": "login_failed", - "message": "登录失败", - "points_before": 0, - "points_after": 0, - }) - continue - - res = client.claim_daily_checkin() - results.append(res) - - # 多账号之间随机休眠 2~4 秒 - if idx < len(accounts): - sleep_sec = random.uniform(2.0, 4.0) - time.sleep(sleep_sec) - - # 统计汇总 - print("\n" + "=" * 60) - print(" 📊 任务执行结果统计报表") - print("=" * 60) - - success_cnt = sum(1 for r in results if r["status"] in ["success", "already_done"]) - summary_lines = [] - total_points = 0 - - for idx, r in enumerate(results, 1): - status_icon = "✅" if r["status"] in ["success", "already_done"] else "❌" - pts = r.get("points_after", 0) - total_points += pts - cny_str = format_points(pts) - line = f"[{idx}] {status_icon} 账号: {r['user']} | 结果: {r['message']} | 当前账户: {cny_str}" - summary_lines.append(line) - print(line) - - total_cny = total_points / POINTS_PER_CNY - total_summary = f"💰 全部账号总资产: {total_points} 积分 (可提现 ≈ {total_cny:.2f} 元)" - summary_lines.append(total_summary) - print(total_summary) - print("=" * 60) - - # 推送青龙系统通知 - notify_title = f"雨云签到: 成功 {success_cnt}/{len(results)} | 总资产 ≈ {total_cny:.2f}元" - notify_content = "\n".join(summary_lines) - send(notify_title, notify_content) - - -if __name__ == "__main__": - main() diff --git a/scripts/analyze_collect_layout.py b/scripts/analyze_collect_layout.py deleted file mode 100644 index dd315b7..0000000 --- a/scripts/analyze_collect_layout.py +++ /dev/null @@ -1,182 +0,0 @@ -# -*- coding: utf-8 -*- -""" -collect 明文「字段布局」分析器 —— 回答一个关键问题: - - `collect` 解密后的 `cd` 数组, 槽位顺序到底是"按 tdc.js 版本固定"还是"按会话/构建随机"? - -为什么要问这个 (2026-10-11 排查 errorCode=12): - tcaptcha/models/success_collect_template.b64 存的是**明文**模板 (base64 封装), - generate_collect_success() 只回填 轨迹 / entry_url / 几个时间戳, 其余字段原样保留。 - 真机成功样本解密后发现: **同一批值, 顺序完全不同** —— 若服务端按"本次构建的槽位表" - 解释 cd, 那么我们用旧槽位序发出的 collect 会被读成"自相矛盾的环境" -> 12。 - -本脚本把多份样本 (HAR + CDP 抓包 + 仓库模板) 的 cd 槽位打成类型指纹并对比顺序。 - -用法: - .venv/bin/python scripts/analyze_collect_layout.py # 默认样本集 - .venv/bin/python scripts/analyze_collect_layout.py --json /tmp/cap2.json -""" -from __future__ import annotations - -import argparse -import base64 -import json -import os -import re -import sys -import urllib.parse -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from tcaptcha.crypto import xtea_decrypt_bytes # noqa: E402 -from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402 - -HARS = [ - ("har/原始抓包/签到成功-1.har", "签到成功-1"), - ("har/原始抓包/点选.har", "点选"), - ("har/login_checkin.har", "login_checkin"), -] - - -def _decrypt_collect(collect_b64: str, tdc_js: str) -> Optional[dict]: - key = extract_xtea_key_pure(tdc_js)["key"] - pt = xtea_decrypt_bytes(base64.b64decode(collect_b64), key) - text = pt.decode("latin1").rstrip("\x00").strip() - try: - return json.loads(text) - except Exception: - return None - - -def from_har(path: str) -> List[Tuple[str, dict]]: - if not os.path.exists(path): - return [] - data = json.load(open(path, encoding="utf-8")) - tdcs: Dict[str, str] = {} - sess2sid: Dict[str, str] = {} - verifies: List[dict] = [] - for e in data["log"]["entries"]: - url = e["request"]["url"] - content = (e["response"].get("content") or {}).get("text") or "" - if "tdc.js" in url and content: - q = urllib.parse.parse_qs(urllib.parse.urlsplit(url).query) - tdcs[(q.get("app_data") or [""])[0]] = content - elif "cap_union_prehandle" in url and content: - m = re.search(r"^[^(]*\((.*)\)\s*$", content, re.S) - if m: - try: - j = json.loads(m.group(1)) - sess2sid[j.get("sess", "")] = str(j.get("sid", "")) - except Exception: - pass - elif "cap_union_new_verify" in url: - pd = (e["request"].get("postData") or {}).get("text") or "" - if pd: - verifies.append(dict(urllib.parse.parse_qsl(pd, keep_blank_values=True))) - out = [] - for i, v in enumerate(verifies): - js = tdcs.get(sess2sid.get(v.get("sess", ""), "")) - if not js: - continue - j = _decrypt_collect(v["collect"], js) - if j is not None: - out.append((f"{Path(path).name}#{i}", j)) - return out - - -def from_cdp_capture(path: str) -> List[Tuple[str, dict]]: - if not os.path.exists(path): - return [] - d = json.load(open(path, encoding="utf-8")) - recs = d.get("records") or [] - tdcs = [(r["url"], r.get("body") or "") for r in recs if "tdc.js" in r["url"] and r.get("body")] - vers = [r for r in recs if "verify" in r["url"] and r.get("postData")] - out = [] - for i, v in enumerate(vers): - form = dict(urllib.parse.parse_qsl(v["postData"], keep_blank_values=True)) - sess = form.get("sess", "") - # sess 前 2 段与 tdc.js 的 app_data(sid) 无直接映射, 这里按时间就近取**最后一份**构建 - js = tdcs[-1][1] if tdcs else "" - if not js: - continue - j = _decrypt_collect(form["collect"], js) - if j is not None: - out.append((f"{Path(path).name}#{i}", j)) - return out - - -def slot_tags(cd: List) -> List[str]: - tags = [] - for v in cd: - if isinstance(v, str): - if v.startswith("http"): - tags.append("url") - elif "Mozilla" in v: - tags.append("UA") - elif v.startswith("GgoAAA"): - tags.append("png") - elif re.fullmatch(r"[-+]\d\d", v): - tags.append("tz") - elif v == "": - tags.append("empty") - elif len(v) <= 6 and v.isupper(): - tags.append("enc:" + v) - else: - tags.append("str:" + v[:10]) - elif isinstance(v, list): - if len(v) == 2 and all(isinstance(x, int) for x in v): - tags.append("vec2") - elif v and isinstance(v[0], list): - tags.append("traj") - else: - tags.append("list%d" % len(v)) - elif isinstance(v, bool): - tags.append("bool") - else: - tags.append("num") - return tags - - -def main() -> int: - ap = argparse.ArgumentParser(description="collect 明文字段布局分析") - ap.add_argument("--json", action="append", default=[], help="额外加入的 cdp_capture JSON") - args = ap.parse_args() - - samples: List[Tuple[str, dict]] = [] - for p in args.json: - samples += from_cdp_capture(p) - for p, tag in HARS: - for name, j in from_har(str(ROOT / p)): - samples.append((f"{tag}#{name.split('#')[-1]}", j)) - tpl_path = ROOT / "tcaptcha" / "models" / "success_collect_template.b64" - if tpl_path.exists(): - tpl = json.loads(base64.b64decode(tpl_path.read_text(encoding="ascii")) - .decode("latin1").rstrip("\x00").strip()) - samples.append(("TEMPLATE(仓库在用)", tpl)) - - if not samples: - print("没有可用样本") - return 1 - - for name, j in samples: - tags = slot_tags(j["cd"]) - print(f"\n[{name}] len(cd)={len(j['cd'])} sd={j.get('sd')}") - print(" " + " ".join(f"{i}:{t}" for i, t in enumerate(tags))) - - print("\n=== 槽位指纹两两对比 (看顺序是否随会话变化) ===") - sigs = [(n, slot_tags(j["cd"])) for n, j in samples] - base_name, base = sigs[0] - print(f" 基准 = {base_name}") - for n, s in sigs[1:]: - diff = [i for i in range(max(len(base), len(s))) - if (base[i] if i < len(base) else None) != (s[i] if i < len(s) else None)] - print(f" {n:22s} 顺序相同={s == base} 不同槽位数={len(diff)}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/bridge_1ms_captcha.py b/scripts/bridge_1ms_captcha.py index 0ad0ff8..87b6e71 100644 --- a/scripts/bridge_1ms_captcha.py +++ b/scripts/bridge_1ms_captcha.py @@ -10,8 +10,8 @@ 1. 用 CDP 在真实页面里刷新腾讯验证码 widget (`__cap.refresh()`); 2. 读 widget 内的题型/题面/几何 (滑块: slideBg + 拼图块 + 拖动柄; 点选: 提示图 + 背景图); 3. 求解: - * 滑块 -> tcaptcha.slider_solver.SliderSolver (ZNCC 找真缺口) - * 图文点选 -> tcaptcha.icon_click_solver.IconClickSolver (形状模板 + 旋转 IoU) + * 滑块 -> tcaptcha.solvers.slider.SliderSolver (ZNCC 找真缺口) + * 图文点选 -> tcaptcha.solvers.icon_click.IconClickSolver (形状模板 + 旋转 IoU) 4. 用 CDP `Input.dispatchMouseEvent` 派发**真实(trusted)鼠标事件** (点击 / 拖动); 5. 读回 widget 回调的 {ret, ticket, randstr}。 @@ -38,9 +38,9 @@ if str(ROOT) not in sys.path: import cv2 # noqa: E402 -from tcaptcha.glyph_solver import TextClickSolver # noqa: E402 -from tcaptcha.icon_click_solver import IconClickSolver # noqa: E402 -from tcaptcha.slider_solver import SliderSolver, sprite_region # noqa: E402 +from tcaptcha.solvers.text_click import TextClickSolver # noqa: E402 +from tcaptcha.solvers.icon_click_rotate import IconClickSolver # noqa: E402 +from tcaptcha.solvers.slider import SliderSolver, sprite_region # noqa: E402 NODE = "node" CDP_EVAL = str(ROOT / "tools" / "cdp_eval.js") diff --git a/scripts/check_real_tencent_aid.py b/scripts/check_real_tencent_aid.py deleted file mode 100644 index 0d452ea..0000000 --- a/scripts/check_real_tencent_aid.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -真机 aid 冒烟测试 —— 用**可反复测**的真实 aid 回归"纯算链路是否完好"。 - -背景 (har/1ms_dianxuan/PROGRESS.md 已知 #26/#27): - aid 195672344 (1ms.run) 对本客户端恒返回 errorCode=12 (aid 级风控, 与请求侧无关), - 且 1ms 签到每天只能试一次 —— 不适合做回归。 - 而抓包 `har/原始抓包/登录抓包-点选.har` 里的 aid **190758559** (mliev.login.wang 短信登录) - 可反复测: 本客户端 (纯算, 零 Node) 稳定拿到 errorCode=0 + 真 ticket。 - -用法: - .venv/bin/python scripts/check_real_tencent_aid.py # 默认 190758559 - .venv/bin/python scripts/check_real_tencent_aid.py --aid 195672344 # 对照: 1ms 会得到 12 - -注意: 只调用腾讯天御的验证码接口 (prehandle/tdc.js/getcapbysig/verify), - **不会**触发任何短信/签到等业务副作用。 -""" -from __future__ import annotations - -import argparse -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402 - -# (aid, entry_url, referer, wb) -TARGETS = { - "190758559": ("https://mliev.login.wang/auth/#/oidc/authorize", "https://mliev.login.wang/", "1"), - "195672344": ("https://1ms.run/console/personal/benefits/check-in", "https://1ms.run/", "1"), -} - - -def main() -> int: - ap = argparse.ArgumentParser(description="真机 aid 冒烟: 纯算链路端到端") - ap.add_argument("--aid", default="190758559", choices=sorted(TARGETS)) - ap.add_argument("--transport", default="httpx", choices=["httpx", "curl"]) - ap.add_argument("--refreshes", type=int, default=25) - args = ap.parse_args() - - entry_url, referer, wb = TARGETS[args.aid] - client = TencentCaptchaClient(aid=args.aid, entry_url=entry_url, referer=referer, wb=wb, - accept_kinds=(KIND_TEXT_CLICK,), telemetry_mode="python_keyed", - transport=args.transport) - try: - result = client.solve(max_retries=2, max_refreshes=args.refreshes) - finally: - client.close() - - raw = result.get("raw_response") or {} - print("\n=== 结果 ===") - print("aid :", args.aid) - print("transport :", args.transport) - print("success :", result.get("success")) - print("errorCode :", raw.get("errorCode")) - print("ticket :", (raw.get("ticket") or "")[:32]) - print("randstr :", raw.get("randstr")) - print("telemetry :", result.get("raw_response", {}).get("sess") and "python_keyed(纯算)") - return 0 if result.get("success") else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/experiment_h3_cookie_ab.py b/scripts/experiment_h3_cookie_ab.py index 17bff6d..2a166e0 100644 --- a/scripts/experiment_h3_cookie_ab.py +++ b/scripts/experiment_h3_cookie_ab.py @@ -35,7 +35,7 @@ ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402 +from tcaptcha.protocol.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402 def run_once(tag: str, cookies, args) -> dict: diff --git a/scripts/experiment_layout_verify.py b/scripts/experiment_layout_verify.py deleted file mode 100644 index f98737f..0000000 --- a/scripts/experiment_layout_verify.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -""" -实验 C —— 用「按本次构建生成的 collect」打一次完整 verify,检验 12 是否消失。 - -背景(PROGRESS.md #31/#32/#34/#35/#36/#37): - 槽位序按构建随机化;仓库里的模板是**某一次**的顺序; - `tools/tdc_collectgen.py` 已能按本次构建重排并加密(回环自洽)。 - -这一发要回答: - * 用「本次构建的槽位序」发出的 collect,aid 195672344 是否 **12 -> 0**? - - 若 0 -> H-LAYOUT 成立(12 的根因就是槽位序),这条链彻底闭合 - - 若仍 12 -> 槽位序**不是** 12 的自变量,H-LAYOUT 需要改, - 但我们已经知道该查什么(转去查请求方/会话绑定,即实验 A) - -⚠️ 样本要求(#31): 重排需要一份**同机**的真机明文(取值集合相同、只有顺序不同)。 - 本脚本的 --sample 就是它;本次样本恰好与本次构建同序,因此这一发是"原序复现"。 - -用法: - .venv/bin/python scripts/experiment_layout_verify.py \\ - --build /tmp/build.js --sample /tmp/sample_cd.json --kinds /tmp/items3.json -""" -from __future__ import annotations - -import argparse -import json -import random -import sys -import time -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) -if str(ROOT / "tools") not in sys.path: - sys.path.insert(0, str(ROOT / "tools")) - -import tdc_collectgen as cg # noqa: E402 -import tdc_layout # noqa: E402 -import tdc_program as tp # noqa: E402 -from tcaptcha.client import TencentCaptchaClient # noqa: E402 -from tcaptcha.crypto import extract_eks, extract_xtea_key # noqa: E402 - - -def one_verify(client, ch, tkid, collect: str, eks: str, pts) -> dict: - ans = json.dumps([{"elem_id": i + 1, "type": "DynAnswerType_POS", - "data": f"{p[0]},{p[1]}"} for i, p in enumerate(pts)], - separators=(",", ":")) - body = {"ans": ans, "collect": collect, "tlg": str(len(collect)), - "eks": eks, "sess": ch.sess, "tkid": tkid, - "pow_answer": "", "pow_calc_time": "1"} - r = client.http_client.post( - "/cap_union_new_verify", data=body, - headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - "Accept": "application/json, text/javascript, */*; q=0.01", - "Origin": "https://turing.captcha.gtimg.com", - "Referer": "https://turing.captcha.gtimg.com/"}) - return r.json() - - -def main() -> int: - ap = argparse.ArgumentParser(description="用按构建生成的 collect 打 verify(带对照组)") - ap.add_argument("--build", required=True, help="本次会话的 tdc.js 路径") - ap.add_argument("--sample", required=True, help="同机的真机 cd(json: {cd:[...]} 或 [...])") - ap.add_argument("--kinds", default=None, help="运行时取样的 items json(提供 kind/v0)") - ap.add_argument("--aid", default="195672344") - ap.add_argument("--entry-url", default="https://1ms.run/console/personal/benefits/check-in") - ap.add_argument("--n", type=int, default=12, help="每种 collect 各打几次(统计用)") - ap.add_argument("--solve", action="store_true", - help="真解题: 按题型用本地求解器算答案(12 只在答案正确时才判, 见说明)") - args = ap.parse_args() - - js = open(args.build, encoding="utf-8", errors="replace").read() - prog = tp.load(args.build) - _mid, _entry, seq = tdc_layout.find_manifest_module(prog) - key = extract_xtea_key(js) - eks = extract_eks(js) - - kinds, v0s = cg.load_shape(args.kinds, seq) if args.kinds else (None, None) - spec = cg.SlotSpec(seq, kinds, v0s) - sample = cg.load_sample_cd(args.sample) - cd = cg.reorder(sample, spec) - collect = cg.encrypt_plain(cg.build_plain(cd), key) - - print(f"[1] 构建 : {args.build}") - print(f" 清单模块 : registry[{_mid}] 项 {len(seq)}") - print(f" 槽位规格 : {spec.describe()}") - print(f" 密钥 : {key}") - print(f" eks : {len(eks)} 字符") - print(f"[2] collect : {len(collect)} 字符 (tlg={len(collect)}) 槽数 {len(cd)}") - - # 走一次完整协议: prehandle -> tdc(已在手) -> getcapbysig -> 解 -> verify, 但**替换 collect** - tkid = str(random.randint(100000000, 999999999)) - client = TencentCaptchaClient(aid=args.aid, entry_url=args.entry_url, - referer="https://1ms.run/") - # ⚠️ 12 是"答案正确之后"的风控判定, 答错一律 50/9, 两组就没区分度。 - # 所以 --solve 时按题型真解; 只统计**可解**的轮次。 - def solve_points(ch): - if not args.solve: - return [(120, 90), (180, 150), (240, 210)] - if ch.kind in ("text", "text_click"): - res = client.text_solver.solve_ex(client.fetch_image(ch.img_url, 1), - ch.instruction) - return res.points if res.ok else None - if ch.kind in ("icon", "icon_click"): - bg = client.fetch_image(ch.img_url, 1) - pr = client.fetch_image(ch.img_url, 0) - if bg and pr: - return client.icon_solver.solve(bg, pr) or None - return None - return None # 滑块: 本脚本不处理(用 --no-solve 时才有意义) - # 对照组: 仓库旧模板(某一次的槽位序), 同一份构建/同一 session 流程 - from tcaptcha.crypto import generate_telemetry - # 对照组只需"仓库旧模板的槽位序", 轨迹用固定点即可(不影响槽位序这一自变量) - tm = generate_telemetry(js, [(120, 90), (180, 150), (240, 210)], - aid=args.aid, user_agent=client.user_agent, - referrer=client.referer) - - tally = {"new(按构建)": {}, "old(旧模板)": {}} - try: - solved = 0 - for rnd in range(1, args.n + 1): - ch, _cfg, _sid = client.prehandle(tkid) - pts = solve_points(ch) - if not pts: - print(f" [{rnd:02d}] 跳过 (kind={ch.kind}, 本脚本不处理)") - continue - solved += 1 - print(f" [{rnd:02d}] kind={ch.kind} pts={pts}") - for tag, collect in (("new(按构建)", collect), ("old(旧模板)", tm["collect"])): - v = one_verify(client, ch, tkid, collect, eks, pts) - code = str(v.get("errorCode")) - tally[tag][code] = tally[tag].get(code, 0) + 1 - print(f" [{rnd:02d}] {tag:12s} -> {code}") - print(f"\n (有效轮次: {solved})") - for tag, d in tally.items(): - print(f" {tag:12s} 分布: {d}") - new12 = tally["new(按构建)"].get("12", 0) - old12 = tally["old(旧模板)"].get("12", 0) - print() - if new12 == 0 and old12 > 0: - print(">>> ✅ 按构建生成后 12 消失(对照组仍有 12) -> H-LAYOUT 成立, 槽位序就是根因") - elif new12 and old12: - print(">>> ❌ 两组都有 12 -> 槽位序不是 12 的自变量, H-LAYOUT 需修正") - elif not new12 and not old12: - print(">>> ⚠️ 两组都没有 12(多为 50/9=答案未命中) -> 本轮题型/条件下风控未介入, " - "需要换成能答对的题型或加大样本再判") - return 0 - finally: - client.close() - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/experiment_require_order.py b/scripts/experiment_require_order.py index 4d6abc1..8471cda 100644 --- a/scripts/experiment_require_order.py +++ b/scripts/experiment_require_order.py @@ -34,8 +34,8 @@ if str(ROOT) not in sys.path: if str(ROOT / "tools") not in sys.path: sys.path.insert(0, str(ROOT / "tools")) -import tdc_layout # noqa: E402 -import tdc_program as tp # noqa: E402 +from tcaptcha.telemetry import layout # noqa: E402 +from tcaptcha.telemetry import program as tp # noqa: E402 TRAP_JS = r"""(() => { if (window.__reqTrapInstalled) return {already: true}; diff --git a/scripts/experiment_transport_ab.py b/scripts/experiment_transport_ab.py index 1348f2b..8673a47 100644 --- a/scripts/experiment_transport_ab.py +++ b/scripts/experiment_transport_ab.py @@ -37,7 +37,7 @@ ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402 +from tcaptcha.protocol.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402 TRANSPORTS = {"a": ("httpx", None), "b": ("curl", "chrome")} diff --git a/scripts/fetch_online_samples.py b/scripts/fetch_online_samples.py index 5a8c353..11abc6d 100644 --- a/scripts/fetch_online_samples.py +++ b/scripts/fetch_online_samples.py @@ -25,9 +25,9 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "tools")) -from tcaptcha.client import TencentCaptchaClient # noqa: E402 -from tcaptcha.crypto import extract_eks # noqa: E402 -from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402 +from tcaptcha.protocol.client import TencentCaptchaClient # noqa: E402 +from tcaptcha.telemetry.crypto import extract_eks # noqa: E402 +from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402 OUT = ROOT / "har" / "online" diff --git a/scripts/rainyun_checkin.py b/scripts/rainyun_checkin.py deleted file mode 100644 index d089e32..0000000 --- a/scripts/rainyun_checkin.py +++ /dev/null @@ -1,492 +0,0 @@ -# -*- coding: utf-8 -*- -""" -cron: 30 8 * * * -new Env('雨云每日签到'); - -雨云 (Rainyun) 自动登录与每日签到脚本 (适用于青龙面板) -账号配置规范: - 环境变量 / 本地 .env 统一使用 RAINYUN_ACCOUNT="账号#密码" - 多账号分割: - - 青龙环境变量: 使用 & 分割 (如 RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2") - - 本地 .env 文件: 支持 & 分割或换行分割 -""" - -import json -import logging -import os -import random -import re -import sys -import time -import uuid -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple - -import httpx - -# 将项目根目录加入 sys.path 以便加载本地模块 -CURRENT_DIR = Path(__file__).resolve().parent -if str(CURRENT_DIR) not in sys.path: - sys.path.insert(0, str(CURRENT_DIR)) - -# 配置日志格式 -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%H:%M:%S", -) -logger = logging.getLogger("rainyun_checkin") - -# 青龙通知模块导入 -try: - from sendNotify import send -except Exception: - try: - from notify import send - except Exception: - def send(title: str, content: str): - pass - - -# 默认配置 -RAINYUN_API_BASE = "https://api.v2.rainyun.com" -TENCENT_AID = "2039519451" -DEFAULT_UA = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" -) - -# 积分提现比例: 2000 积分 = 1 元现金 (支持直接提现) -POINTS_PER_CNY = 2000.0 - - -def format_points(points: int) -> str: - """格式化积分及等值提现金额 (2000积分 = 1元)""" - cny = points / POINTS_PER_CNY - return f"{points} 积分 (≈ {cny:.2f} 元)" - - -def mask_field(val: str) -> str: - """脱敏账号信息""" - if not val: - return "" - if len(val) <= 4: - return "***" - if len(val) == 11 and val.isdigit(): - return f"{val[:3]}****{val[7:]}" - return f"{val[:2]}****{val[-2:]}" - - -class CaptchaManager: - """验证码求解调度器:支持本地 tcaptcha 模块或远程 HTTP 接口""" - - def __init__(self, api_url: Optional[str] = None): - self.api_url = api_url or os.getenv("TCAPTCHA_API_URL", "").strip() - self.local_client = None - - # 尝试初始化本地客户端 - if not self.api_url: - try: - from tcaptcha.client import TencentCaptchaClient - self.local_client = TencentCaptchaClient(aid=TENCENT_AID) - logger.info("✓ 成功加载本地 tcaptcha 纯算法求解引擎") - except Exception as e: - logger.warning(f"未能加载本地 tcaptcha 求解引擎 ({e}),将尝试远程接口或环境变量配置") - - def get_ticket(self) -> Tuple[Optional[str], Optional[str]]: - """获取腾讯天御点选验证码票据 (ticket, randstr)""" - # 1. 优先使用远程 API (若已配置) - if self.api_url: - logger.info(f"正在调用远程验证码求解接口: {self.api_url} ...") - try: - with httpx.Client(timeout=30.0, trust_env=False) as client: - resp = client.post(self.api_url, json={"aid": TENCENT_AID}) - if resp.status_code == 200: - data = resp.json() - # 兼容直接返回或嵌套在 data 中 - res_data = data.get("data", data) - ticket = res_data.get("ticket") - randstr = res_data.get("randstr") - if ticket and randstr: - logger.info(f"✓ 远程验证码求解成功 (randstr: {randstr})") - return ticket, randstr - else: - logger.error(f"远程验证码接口未返回有效票据: {data}") - else: - logger.error(f"远程验证码接口响应异常: {resp.status_code} {resp.text}") - except Exception as e: - logger.error(f"调用远程验证码接口失败: {e}") - - # 2. 本地求解引擎 - if self.local_client: - logger.info("正在调用本地 tcaptcha AI 纯协议求解器...") - try: - res = self.local_client.solve() - if res.get("success"): - logger.info(f"✓ 本地求解成功 (耗时: {res.get('elapsed_ms')}ms, randstr: {res.get('randstr')})") - return res.get("ticket"), res.get("randstr") - else: - logger.error(f"本地求解失败: {res.get('error')}") - except Exception as e: - logger.error(f"本地求解器执行异常: {e}") - - logger.error("未找到可用的验证码求解方案!请确认部署了 tcaptcha 本地模型或配置了 TCAPTCHA_API_URL 环境变量") - return None, None - - -class RainyunClient: - """雨云 API 客户端""" - - def __init__( - self, - user: str, - password: str, - captcha_mgr: Optional[CaptchaManager] = None, - ): - self.user = user - self.password = password - self.captcha_mgr = captcha_mgr or CaptchaManager() - - self.account_name = mask_field(user) if user else "未知账号" - self.points_before = 0 - self.points_after = 0 - - # 初始化 HTTP 客户端 - self.session = httpx.Client( - base_url=RAINYUN_API_BASE, - timeout=15.0, - follow_redirects=True, - trust_env=False, # 屏蔽环境代理干扰以兼容各类青龙网络 - ) - self._update_common_headers() - - def _update_common_headers(self): - rys = f"{uuid.uuid4().hex}{time.time():.3f}" - self.session.headers.update({ - "User-Agent": DEFAULT_UA, - "Origin": "https://app.rainyun.com", - "Referer": "https://app.rainyun.com/", - "Accept": "application/json, text/plain, */*", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", - "rys": rys, - }) - - def login(self) -> bool: - """执行登录并获取凭据""" - if not self.user or not self.password: - logger.error("缺少账号或密码!") - return False - - logger.info(f"正在登录雨云账号: {self.account_name} ...") - self._update_common_headers() - - payload = { - "field": self.user, - "password": self.password, - "verifyCode": "", - } - - resp = self.session.post("/user/login", json=payload) - res_json = {} - try: - res_json = resp.json() - except Exception: - pass - - # 检查是否需要验证码 - if resp.status_code == 400 and res_json.get("code") in [10004, 10005]: - logger.info("雨云登录要求完成人机验证,正在调用验证码求解器...") - ticket, randstr = self.captcha_mgr.get_ticket() - if not ticket or not randstr: - logger.error("登录验证码获取失败,无法继续登录") - return False - payload["vticket"] = ticket - payload["vrandstr"] = randstr - resp = self.session.post("/user/login", json=payload) - try: - res_json = resp.json() - except Exception: - pass - - if resp.status_code == 200 and res_json.get("code") == 200: - # 同步更新 CSRF Token - csrf = self.session.cookies.get("X-CSRF-Token") - if csrf: - self.session.headers["x-csrf-token"] = csrf - logger.info(f"✓ 账号 {self.account_name} 登录成功!") - return True - - err_msg = res_json.get("message") or resp.text - logger.error(f"❌ 账号 {self.account_name} 登录失败: {err_msg}") - return False - - def fetch_user_info(self) -> Optional[Dict[str, Any]]: - """获取当前用户信息(积分、用户名等)""" - self._update_common_headers() - try: - resp = self.session.get("/user/?no_cache=true") - if resp.status_code == 200: - data = resp.json() - if data.get("code") == 200: - info = data.get("data", {}) - self.account_name = info.get("Name") or self.account_name - self.points_before = info.get("Points", 0) - return info - except Exception as e: - logger.warning(f"获取用户信息异常: {e}") - return None - - def fetch_tasks(self) -> List[Dict[str, Any]]: - """获取任务列表""" - self._update_common_headers() - try: - resp = self.session.get("/user/reward/tasks") - if resp.status_code == 200: - data = resp.json() - if data.get("code") == 200: - return data.get("data", []) - except Exception as e: - logger.error(f"获取任务列表失败: {e}") - return [] - - def claim_daily_checkin(self) -> Dict[str, Any]: - """ - 执行每日签到核心业务逻辑: - 1. 检查任务列表与签到状态 - 2. 若 Status == 2: 今日已完成,直接跳过 - 3. 若 Status == 1: 待领取,获取验证码并提交 - """ - user_info = self.fetch_user_info() - user_display = f"{self.account_name} (ID: {user_info.get('ID') if user_info else '未知'})" - - tasks = self.fetch_tasks() - checkin_task = None - for t in tasks: - if t.get("Name") == "每日签到": - checkin_task = t - break - - if not checkin_task: - return { - "user": user_display, - "status": "error", - "message": "未在任务列表中找到【每日签到】任务", - "points_before": self.points_before, - "points_after": self.points_before, - } - - task_status = checkin_task.get("Status") - # Status 2: 今日已领取 - if task_status == 2: - logger.info(f"✨ 账号【{user_display}】今日已完成每日签到,当前总资产: {format_points(self.points_before)}。") - return { - "user": user_display, - "status": "already_done", - "message": "今日已签到 (无需重复领取)", - "points_before": self.points_before, - "points_after": self.points_before, - } - - # Status 1: 待签到 - logger.info(f"🎯 账号【{user_display}】今日尚未签到,开始启动签到流程...") - - # 1. 求解验证码 - ticket, randstr = self.captcha_mgr.get_ticket() - if not ticket or not randstr: - return { - "user": user_display, - "status": "fail", - "message": "验证码求解失败,签到中止", - "points_before": self.points_before, - "points_after": self.points_before, - } - - # 2. 提交签到请求 - self._update_common_headers() - payload = { - "task_name": "每日签到", - "verifyCode": "", - "vticket": ticket, - "vrandstr": randstr, - } - - resp = self.session.post("/user/reward/tasks", json=payload) - res_json = {} - try: - res_json = resp.json() - except Exception: - pass - - if resp.status_code == 200 and res_json.get("code") == 200: - reward_data = res_json.get("data", {}) - earned_points = reward_data.get("points", 500) - earned_cny = earned_points / POINTS_PER_CNY - - # 刷新最新积分 - latest_info = self.fetch_user_info() - self.points_after = latest_info.get("Points", self.points_before + earned_points) if latest_info else self.points_before + earned_points - - msg = f"签到成功!获得 +{earned_points} 积分 (≈ {earned_cny:.2f} 元)" - logger.info(f"🎉 账号【{user_display}】{msg},当前总资产: {format_points(self.points_after)}") - return { - "user": user_display, - "status": "success", - "message": msg, - "earned_points": earned_points, - "points_before": self.points_before, - "points_after": self.points_after, - } - else: - err_msg = res_json.get("message") or resp.text - logger.error(f"❌ 账号【{user_display}】签到失败: {err_msg}") - return { - "user": user_display, - "status": "fail", - "message": f"签到请求失败: {err_msg}", - "points_before": self.points_before, - "points_after": self.points_before, - } - - -def load_dotenv(env_path: Optional[Path] = None): - """自动加载本地 .env 文件到 os.environ,方便本地直接调试运行""" - search_paths = [ - env_path, - CURRENT_DIR / ".env", - Path.cwd() / ".env", - ] - for p in search_paths: - if p and p.is_file(): - try: - with open(p, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - if "=" in line: - k, v = line.split("=", 1) - k = k.strip() - v = v.strip() - if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")): - v = v[1:-1] - if k not in os.environ or not os.environ[k]: - os.environ[k] = v - logger.info(f"✓ 成功加载本地环境变量: {p}") - break - except Exception as e: - logger.warning(f"读取 .env 异常: {e}") - - -def parse_accounts() -> List[Dict[str, str]]: - """ - 解析雨云账号配置 (优先加载本地 .env): - 仅支持 RAINYUN_ACCOUNT="账号#密码" 格式 - 多账号使用 & 分割 (青龙面板推荐) 或 换行分割 (本地 .env 推荐) - 例如: - RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2" - """ - load_dotenv() - accounts = [] - - acc_env = os.getenv("RAINYUN_ACCOUNT", "").strip() - if not acc_env: - return [] - - # 去除外层引号 - if (acc_env.startswith('"') and acc_env.endswith('"')) or (acc_env.startswith("'") and acc_env.endswith("'")): - acc_env = acc_env[1:-1].strip() - - # 按照 & 或 换行 分割多账号 - raw_list = re.split(r"[&\n]", acc_env) - for item in raw_list: - item = item.strip() - if not item or "#" not in item: - continue - u, p = item.split("#", 1) - u, p = u.strip(), p.strip() - if u and p and not any(a.get("user") == u for a in accounts): - accounts.append({"user": u, "password": p}) - - return accounts - - -def main(): - print("=" * 60) - print(" 🚀 雨云 (Rainyun) 自动登录与每日签到任务启动") - print("=" * 60) - - accounts = parse_accounts() - if not accounts: - msg = ( - "未检测到雨云账号配置!请在青龙环境变量或本地 .env 中配置 RAINYUN_ACCOUNT\n" - "格式: 账号#密码 (多账号用 & 或换行分割)\n" - "例如: RAINYUN_ACCOUNT=\"13754650804#pwd1&13800000000#pwd2\"\n" - "如需远程验证码服务,可添加 TCAPTCHA_API_URL (如 http://x.x.x.x:8000/solve)" - ) - logger.warning(msg) - send("雨云每日签到异常", msg) - return - - logger.info(f"已成功加载 {len(accounts)} 个雨云账号配置") - captcha_mgr = CaptchaManager() - - results = [] - for idx, acc in enumerate(accounts, 1): - logger.info(f"\n--- [正在处理账号 {idx}/{len(accounts)}] ---") - client = RainyunClient( - user=acc["user"], - password=acc["password"], - captcha_mgr=captcha_mgr, - ) - - login_ok = client.login() - if not login_ok: - results.append({ - "user": client.account_name, - "status": "login_failed", - "message": "登录失败", - "points_before": 0, - "points_after": 0, - }) - continue - - res = client.claim_daily_checkin() - results.append(res) - - # 多账号之间随机休眠 2~4 秒 - if idx < len(accounts): - sleep_sec = random.uniform(2.0, 4.0) - time.sleep(sleep_sec) - - # 统计汇总 - print("\n" + "=" * 60) - print(" 📊 任务执行结果统计报表") - print("=" * 60) - - success_cnt = sum(1 for r in results if r["status"] in ["success", "already_done"]) - summary_lines = [] - total_points = 0 - - for idx, r in enumerate(results, 1): - status_icon = "✅" if r["status"] in ["success", "already_done"] else "❌" - pts = r.get("points_after", 0) - total_points += pts - cny_str = format_points(pts) - line = f"[{idx}] {status_icon} 账号: {r['user']} | 结果: {r['message']} | 当前账户: {cny_str}" - summary_lines.append(line) - print(line) - - total_cny = total_points / POINTS_PER_CNY - total_summary = f"💰 全部账号总资产: {total_points} 积分 (可提现 ≈ {total_cny:.2f} 元)" - summary_lines.append(total_summary) - print(total_summary) - print("=" * 60) - - # 推送青龙系统通知 - notify_title = f"雨云签到: 成功 {success_cnt}/{len(results)} | 总资产 ≈ {total_cny:.2f}元" - notify_content = "\n".join(summary_lines) - send(notify_title, notify_content) - - -if __name__ == "__main__": - main() diff --git a/scripts/verify/align_session.py b/scripts/verify/align_session.py index ff77a39..f43f765 100644 --- a/scripts/verify/align_session.py +++ b/scripts/verify/align_session.py @@ -23,9 +23,9 @@ from urllib.parse import unquote ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 -from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402 -from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402 +from tcaptcha.telemetry import modules as tm # noqa: E402 +from tcaptcha.telemetry.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402 +from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402 BUILD = "/tmp/1ms_bridge/session_build.js" SESS = "/tmp/1ms_bridge/session_collect.json" diff --git a/scripts/verify/audit_names.py b/scripts/verify/audit_names.py index 9e45ee7..6391490 100644 --- a/scripts/verify/audit_names.py +++ b/scripts/verify/audit_names.py @@ -30,10 +30,10 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 -import tdc_program as tp # noqa: E402 -from tcaptcha.crypto import xtea_decrypt_bytes # noqa: E402 -from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402 +from tcaptcha.telemetry import modules as tm # noqa: E402 +from tcaptcha.telemetry import program as tp # noqa: E402 +from tcaptcha.telemetry.crypto import xtea_decrypt_bytes # noqa: E402 +from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402 # 用于「这个模块到底像在采什么」的旁证关键词 HINTS = { diff --git a/scripts/verify/balance_names_values.py b/scripts/verify/balance_names_values.py deleted file mode 100644 index 2774b3d..0000000 --- a/scripts/verify/balance_names_values.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -"""全量配平:37 个语义名 <-> 真机明文的 38 个槽。 - -约束(这些是硬证据,不是猜): - 1. 值内容唯一确定语义名(MacIntel/UTF-8/ANGLE/GgoA.../98k/iframe...) - 2. screen_fingerprint `"1440-2560-2530-24-*-*-|-*"` 反解出 - screen_height=1440 / screen_width=2560 / screen_color_depth=24 - 3. itoken 有 `^\\d+:\\d+$` 特征 -> 占 **2 槽**(`641547489,1791732001`) - 4. 37 项 -> 38 槽(仅 itoken 多占 1 槽),可与明文槽数对账 - -输出: 每个语义名的取值 + 置信度,供人工核对。 -""" -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 - -RAW = "/tmp/1ms_bridge/real_collect_plain.txt" -raw = open(RAW, encoding="latin1").read().rstrip("\x00") -cd = json.loads(re.sub(r" {2,}", "", raw))["cd"] -print("明文 38 槽:") -for i, v in enumerate(cd): - print(" %2d %s" % (i, json.dumps(v, ensure_ascii=False)[:78])) -print() - -rows = tm.analyze("har/1ms_dianxuan/tdc.js") -names = [r["name"] for r in rows] -print("语义名 %d 个" % len(names)) -print() - -# ---- 按内容给每个槽打标签(不依赖位置)---- -def tags(v): - t = [] - if isinstance(v, str): - if "Mozilla" in v: t.append("user_agent") - if v == "MacIntel": t.append("platform") - if re.fullmatch(r"[+-]\d\d", v): t.append("timezone_offset") - if v.upper() == "UTF-8": t.append("charset") - if "ANGLE" in v: t.append("webgl_renderer") - if "Inc." in v: t.append("webgl_vendor") - if v.startswith("GgoA"): t.append("canvas_fingerprint") - if "rand=" in v and "captcha" in v: t.append("template_url") - if v == "98k": t.append("constant_98k") - if v == "iframe": t.append("iframe_detect") - if "*" in v and "-" in v: t.append("screen_fingerprint") - if v.startswith("http") and "1ms.run" in v: t.append("location_href") - elif isinstance(v, list): - if v and all(isinstance(x, list) and len(x) == 3 for x in v): t.append("trajectory") - if v and all(isinstance(x, str) for x in v): t.append("languages") - elif isinstance(v, int) and not isinstance(v, bool): - if 1_600_000_000 <= v <= 2_100_000_000: t.append("timestamp?") - elif v > 1_000_000: t.append("bigint?") - else: t.append("smallint?") - elif isinstance(v, str) and v == "": - t.append("emptystr?") - return t - -print("逐槽标签:") -for i, v in enumerate(cd): - print(" %2d %-22s %s" % (i, ",".join(tags(v)), json.dumps(v, ensure_ascii=False)[:50])) -print() - -# ---- 反解 screen_fingerprint 的约束 ---- -fp = next((v for v in cd if isinstance(v, str) and "*" in v and "-" in v), None) -if fp: - p = fp.split("-") - print("screen_fingerprint = %r -> 高%s 宽%s 色深%s" % (fp, p[0], p[1], p[3])) - for target, want in (("screen_height", int(p[0])), ("screen_width", int(p[1])), - ("screen_color_depth", int(p[3]))): - idxs = [i for i, v in enumerate(cd) if v == want] - print(" %-20s 期望 %s -> 明文里出现在槽 %s" % (target, want, idxs)) diff --git a/scripts/verify/label_plaintext.py b/scripts/verify/label_plaintext.py deleted file mode 100644 index fc474dc..0000000 --- a/scripts/verify/label_plaintext.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- -"""把真机明文的值,与「语义名」精确配对。 - -难点:真机明文的槽顺序 = **它自己那次构建**的 require 顺序, -而我手上只有 har/online/ 等若干构建;不知道明文属于哪一份。 - -解法(自洽性打分):把明文的值**按每个候选构建的语义名顺序**标注一遍, -再看标注结果是否语义自洽(platform 的位置该是 "MacIntel"、user_agent 该含 -"Mozilla"、charset 该是 "UTF-8" …)。自洽度最高者即为它所属的构建; -若都不匹配,也能从"哪些语义名被正确喂到"反推出每个名字的真实取值。 - -用法: - .venv/bin/python scripts/verify/label_plaintext.py <明文文件> <若干 tdc.js> -""" -import glob -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 - -# ---- 语义名 -> 期望值特征(这就是"画像"的定义雏形)---- -def likes(name: str, v) -> str: - """值是否与语义名相容。返回 OK / NO / ??(?? = 该名字无法从值判断)""" - s = json.dumps(v, ensure_ascii=False) - if name.startswith("user_agent"): - return "OK" if isinstance(v, str) and "Mozilla" in v else "NO" - if name == "platform": - return "OK" if isinstance(v, str) and v and " " not in v else "NO" - if name == "languages": - return "OK" if isinstance(v, list) and v and isinstance(v[0], str) else "NO" - if name == "hardware_concurrency": - return "OK" if isinstance(v, int) and 2 <= v <= 64 else "NO" - if name == "screen_width": - return "OK" if isinstance(v, int) and v > 500 else "NO" - if name == "screen_height": - return "OK" if isinstance(v, int) and v > 300 else "NO" - if name == "screen_color_depth": - return "OK" if isinstance(v, int) and 16 <= v <= 48 else "NO" - if name == "screen_fingerprint": - return "OK" if isinstance(v, str) and "-" in v and "*" in v else "NO" - if name == "timezone_offset": - return "OK" if isinstance(v, str) and re.fullmatch(r"[+-]\d\d", v) else "NO" - if name == "charset": - return "OK" if isinstance(v, str) and "UTF" in v.upper() else "NO" - if name == "webgl_renderer": - return "OK" if isinstance(v, str) and ("ANGLE" in v or "Renderer" in v) else "NO" - if name == "webgl_vendor": - return "OK" if isinstance(v, str) and ("Inc." in v or "Google" in v) else "NO" - if name == "canvas_fingerprint": - return "OK" if isinstance(v, str) and len(v) > 40 else "NO" - if name == "constant_98k": - return "OK" if v == "98k" else "NO" - if name == "template_url": - return "OK" if isinstance(v, str) and "captcha" in v and "rand=" in v else "NO" - if name in ("location_href", "referrer"): - return "OK" if isinstance(v, str) and v.startswith("http") else "??" - if name == "trajectory": - return "OK" if (isinstance(v, list) and v - and all(isinstance(x, list) and len(x) == 3 for x in v)) else "NO" - return "??" - - -def main() -> int: - if len(sys.argv) < 3: - print(__doc__) - return 2 - plain = open(sys.argv[1], encoding="latin1").read().rstrip("\x00") - logical = re.sub(r" {2,}", "", plain) - cd = json.loads(logical)["cd"] - print("明文 cd 槽数: %d" % len(cd)) - print() - - cands = [] - for a in sys.argv[2:]: - cands += sorted(glob.glob(a)) - if not cands: - print("没有候选构建") - return 1 - - def assign(names, nslots): - """按语义名顺序把 nslots 个槽分配给各名字(交错推进)。""" - out = {} - k = 0 - for nm in names: - if k >= nslots: - break - out.setdefault(nm, []).append(cd[k]) - k += 1 - return out - - best = None - for f in cands: - rows = tm.analyze(f) - names = [r["name"] for r in rows] - # 假设除 itoken(占 2 槽)外都是 1 槽 - itok = [i for i, n in enumerate(names) if n.startswith("itoken")] - n_extra = 1 if itok else 0 - # 逐个候选:把 cd 的槽按名字顺序切(itoken 那两个槽合并看第一个) - mapping = {} - k = 0 - for i, nm in enumerate(names): - if k >= len(cd): - break - if i in itok: - mapping[nm] = cd[k:k + 2] - k += 2 - else: - mapping[nm] = [cd[k]] - k += 1 - ok = no = unk = 0 - for nm, vals in mapping.items(): - st = likes(nm, vals[0]) - ok += st == "OK" - no += st == "NO" - unk += st == "??" - score = ok - no - print(" %-46s OK=%2d NO=%2d ??=%2d 分=%d" - % (Path(f).name, ok, no, unk, score)) - if best is None or score > best[0]: - best = (score, f, mapping, ok, no) - - print() - print(">>> 最自洽的构建: %s (OK=%d NO=%d)" % (Path(best[1]).name, best[3], best[4])) - print() - print("该假设下的 name -> 值 标注:") - for nm, vals in best[2].items(): - st = likes(nm, vals[0]) - mark = {"OK": " ", "NO": "!", "??": "?"}[st] - print(" %s %-40s %s" % (mark, nm, json.dumps(vals, ensure_ascii=False)[:70])) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/verify/send_test.py b/scripts/verify/send_test.py index 4af5754..5f8f952 100644 --- a/scripts/verify/send_test.py +++ b/scripts/verify/send_test.py @@ -24,8 +24,8 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "tools")) -import tcaptcha.client as client_mod # noqa: E402 -from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402 +import tcaptcha.protocol.client as client_mod # noqa: E402 +from tcaptcha.protocol.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402 AID = "195672344" ENTRY = "https://1ms.run/console/personal/benefits/check-in" @@ -56,7 +56,7 @@ def main() -> int: # 段几何(新链路才有;旧模板回填没有 segments) if out.get("source") == "python_values": try: - from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes + from tcaptcha.telemetry.crypto import split_collect_segments, xtea_decrypt_bytes import base64 plain = xtea_decrypt_bytes(base64.b64decode(out["collect"]), out["key"]).decode("latin1").rstrip("\x00") diff --git a/scripts/verify/verify_40_9_disamb.py b/scripts/verify/verify_40_9_disamb.py deleted file mode 100644 index 536fba2..0000000 --- a/scripts/verify/verify_40_9_disamb.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -"""验证 struct_40_9 的消歧特征是否跨构建稳定。 - -判别: - A 组: 闭包体最后写入 **空串** (PUSH_EMPTY_STR; SET_PROP) - B 组: 闭包体最后写入 **随机 int32** (PUSH_IMM ; SET_PROP) -""" -import glob -import sys -from collections import Counter, defaultdict -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 -import tdc_program as tp # noqa: E402 - -BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js"))) - - -def closure_body(p, entry): - """取模块唯一闭包的 target 体。""" - cls = [c for c in p.closures if p.owner(c.pc) == entry] - if not cls: - return None - return p.function_code(cls[0].target) - - -def disamb(p, entry): - """返回 'empty' | 'rand' | None""" - body = closure_body(p, entry) - if body is None: - return None - imms = [x["operands"][0] for x in body - if x["name"] == "PUSH_IMM" and x["operands"]] - big = [v for v in imms if v > 1_000_000] - if big: - return "rand" - return "empty" - - -stats = defaultdict(lambda: defaultdict(list)) -for f in BATCH: - p = tp.load(f) - reg = dict(p.module_registry(0)) - rows = tm.analyze(f) - for r in rows: - if r["name"] != "struct_40_9": - continue - d = disamb(p, reg[r["module"]]) - stats[f.split("/")[-1]][d or "?"].append((r["item"], r["module"])) - -print("%-24s %-8s %s" % ("构建", "判别", "项(模块)")) -print("-" * 70) -ok = True -for f, groups in stats.items(): - for d, members in sorted(groups.items()): - print("%-24s %-8s %s" % (f, d, members)) - # 每份构建必须 empty 1 个 + rand 1 个 - if len(groups.get("empty", [])) != 1 or len(groups.get("rand", [])) != 1: - ok = False -print() -print(">>> 每份构建都是 empty×1 + rand×1 :", ok) -print(">>> 结论:", "可静态消歧(写入空串 vs 写入随机 int32)" if ok else "仍不可分") diff --git a/scripts/verify/verify_control_negative.py b/scripts/verify/verify_control_negative.py deleted file mode 100644 index 4a9b2b0..0000000 --- a/scripts/verify/verify_control_negative.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -"""对照实验:证明「按语义名配对」不是归一化过拟合出来的。 - -风险: 归一化若抹掉太多,任何配对都会"全对",校验就失去意义。 - -做法(负对照,同一套归一化 + 同一套比对): - ① 按语义名配对(实验组) - ② 按清单下标配对(负对照:槽位序随机,必然错) - ③ 按模块 id 配对(负对照:id 每次重排,必然错) - ④ 随机配对(负对照基线) - -若①接近 37/37 而 ②③④ 明显更低 => 归一化没有过拟合,配对确实有效。 -""" -import glob -import random -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 -import tdc_program as tp # noqa: E402 - -# 带"代码地址"的操作数:每份构建的布局不同,必须抹掉 -ADDR_OPS = {"JUMP", "JUMP_IF_TRUE", "JUMP_IF_FALSE", "PUSH_TRY"} - - -def norm_code(p, entry, maxlen=400): - """归一化指令序列(与 verify_pair_correct2.py 同一套规则,此处内联避免导入副作用)。 - - 抹掉三类**会重排**的操作数,保留指令骨架与字符串的位置: - 1. 代码地址(JUMP / JUMP_IF_* / PUSH_TRY) - 2. require(N) 的模块号 N - 3. PUSH_STR_CHAR 的具体字符值(fp_salt 随机盐串、混淆变量名每次都变) - """ - code = p.function_code(entry) - out = [] - for k, inst in enumerate(code): - nm, ops = inst["name"], tuple(inst["operands"] or []) - if nm in ADDR_OPS: - ops = ("ADDR",) * len(ops) - elif nm == "PUSH_STR_CHAR": - ops = ("CH",) - elif nm == "PUSH_IMM" and ops: - if (k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM" - and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC"): - ops = ("MOD",) * len(ops) - out.append((nm, ops)) - return tuple(out[:maxlen]) - -BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js"))) - - -def load(f): - p = tp.load(f) - rows = tm.analyze(f) - reg = dict(p.module_registry(0)) - sig = {r["name"]: norm_code(p, reg[r["module"]]) for r in rows} - return rows, sig - - -A, fa = load(BATCH[0]) -print("基准 %s (归一化:抹掉地址/模块号/字符值,保留指令骨架与串的位置)" % BATCH[0].split("/")[-1]) -print() -print("%-22s %-22s %-8s %-8s" % ("对比构建", "配对方式", "一致", "不一致")) -print("-" * 66) - -for f in BATCH[1:]: - B, fb = load(f) - names = sorted(set(fa) & set(fb)) - - def score(pairs): - return sum(1 for a, b in pairs if a == b) - - # ① 实验组:语义名 - p1 = [(fa[n], fb[n]) for n in names] - # ② 负对照:清单下标 - p2 = [(fa[A[i]["name"]], fb[B[i]["name"]]) for i in range(min(len(A), len(B)))] - # ③ 负对照:模块 id(用 id 当 key 去查另一边同名集合的任意一项) - fb_by_id = {r["module"]: fb[r["name"]] for r in B} - p3 = [(fa[A[i]["name"]], fb_by_id.get(A[i]["module"], ())) for i in range(len(A))] - # ④ 负对照:随机配对 - rnd = random.Random(7) - shuf = names[:] - rnd.shuffle(shuf) - p4 = [(fa[n], fb[m]) for n, m in zip(names, shuf)] - - for label, pairs in (("①语义名(实验组)", p1), ("②清单下标(负对照)", p2), - ("③模块id(负对照)", p3), ("④随机配对(负对照)", p4)): - s = score(pairs) - print("%-22s %-22s %-8d %-8d" % (f.split("/")[-1], label, s, len(pairs) - s)) - print() - -print(">>> 判读: ①应≈37/37;②③④应明显更低 => 归一化未过拟合,配对有效") diff --git a/scripts/verify/verify_fp_consistency.py b/scripts/verify/verify_fp_consistency.py deleted file mode 100644 index 0bff1e0..0000000 --- a/scripts/verify/verify_fp_consistency.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -"""验证字符串+结构指纹方案的**跨构建一致性** —— 只基于 har/online/ 全新样本。 - -案例 §5.3 的验收标准: "3 个 tdc.js 版本、37 个模块、全部正确识别,无一 unknown"。 -本脚本再进一步:不仅要求"无 unknown",还要求 - ① 每份构建内部 37 项都有名字 - ② 各构建之间的**语义名集合**一致(说明识别的是同一批采集器) - ③ 语义名 -> 每项在 cd 里的**相对位置**可交叉核对 -""" -import glob -import sys -from collections import Counter -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 - -BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js"))) -print("全新在线样本 %d 份(只使用这批数据)" % len(BATCH)) -print() - -results = {} -for f in BATCH: - rows = tm.analyze(f) - names = [r["name"] for r in rows] - results[f] = rows - unk = sum(1 for n in names if n == "unknown") - via_struct = sum(1 for r in rows if r.get("via_struct")) - via_deleg = sum(1 for r in rows if r.get("via_delegate")) - print(" %-24s 项%2d unknown %d | 字符串直判 %2d 委派链 %d 结构兜底 %d" - % (f.split("/")[-1], len(rows), unk, - len(rows) - via_struct - via_deleg, via_deleg, via_struct)) - -print() -keys = list(results) -base = Counter(r["name"] for r in results[keys[0]]) -print("以 %s 为基准:%d 个语义名(去重 %d)" - % (keys[0].split("/")[-1], len(base), len(set(base)))) -print() -for f in keys[1:]: - c = Counter(r["name"] for r in results[f]) - same = sum((base & c).values()) - only_base = sum((base - c).values()) - only_it = sum((c - base).values()) - print(" vs %-22s 相同 %2d | 仅基准有 %2d | 仅有它有 %2d" - % (f.split("/")[-1], same, only_base, only_it)) - -print() -common = set(base) -for f in keys[1:]: - common &= set(Counter(r["name"] for r in results[f])) -allnames = set() -for f in keys: - allnames |= set(r["name"] for r in results[f]) -print(">>> 所有构建**共同**识别出的语义名: %d 个" % len(common)) -print(">>> 出现过的语义名总数 : %d 个" % len(allnames)) -print(">>> 差异名:", sorted(allnames - common) if allnames - common else "无(完全一致)") -print() - -# 唯一性检查:单构建内是否有重名(重名 = 锚点有歧义) -print("单构建内重名检查(重名说明该锚点有歧义):") -for f in keys: - c = Counter(r["name"] for r in results[f]) - dup = {k: v for k, v in c.items() if v > 1} - print(" %-24s 重名 %d 组 %s" - % (f.split("/")[-1], len(dup), dict(sorted(dup.items())))) diff --git a/scripts/verify/verify_offline.py b/scripts/verify/verify_offline.py index 50a9e7d..a5aeb29 100644 --- a/scripts/verify/verify_offline.py +++ b/scripts/verify/verify_offline.py @@ -26,11 +26,11 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 -from tcaptcha.crypto import (build_collect, split_collect_segments, # noqa: E402 +from tcaptcha.telemetry import modules as tm # noqa: E402 +from tcaptcha.telemetry.crypto import (build_collect, split_collect_segments, # noqa: E402 xtea_decrypt_bytes) -from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402 -from tcaptcha.tdc_values import Session, build_plain, generate_cd, trajectory_index # noqa: E402 +from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402 +from tcaptcha.telemetry.values import Session, build_plain, generate_cd, trajectory_index # noqa: E402 def main() -> int: diff --git a/scripts/verify/verify_pair_correct.py b/scripts/verify/verify_pair_correct.py deleted file mode 100644 index f4ffeb6..0000000 --- a/scripts/verify/verify_pair_correct.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -"""判决「按语义名配对」是否**正确** —— 用独立于指纹的方法。 - -⚠️ 上一轮我用「模块 id 相等」做校验,那个假设是**错的**: - 案例 §1.2 与账本 #29 都明确说「37 个模块的 ID 每次构建随机重新分配」。 - registry 全集交集 62/62 只说明编号**范围**相同(都是 0..61), - 不代表 id -> 语义 的映射相同。id 不一致是**预期行为**,不能用来否定配对。 - -本轮改用真正独立的证据:**函数体的归一化指令序列**。 - 指纹规则只用到「字符串集合」和「结构统计(指令数/直方图)」, - 没有用到「指令的完整顺序 + 操作数」。所以拿它做校验是独立的。 - - 归一化: 抹掉 JUMP 的绝对地址(每构建不同),保留其余操作数。 -""" -import glob -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools")) -import tdc_layout # noqa: E402 -import tdc_modules as tm # noqa: E402 -import tdc_program as tp # noqa: E402 - -BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js"))) - - -# 带"代码地址"的操作数:每份构建的布局不同,必须抹掉 -ADDR_OPS = {"JUMP", "JUMP_IF_TRUE", "JUMP_IF_FALSE", "PUSH_TRY"} - - -def norm_code(p, entry, maxlen=400): - """归一化指令序列。 - - 必须抹掉两类**会重排**的操作数,否则校验必然失败: - 1. 代码地址:JUMP / JUMP_IF_TRUE / JUMP_IF_FALSE / PUSH_TRY - 2. require(N) 的模块号 N:`LOAD_REG_IMM; PUSH_IMM N; CALL_FUNC` - (案例 §1.2:37 个模块的 ID 每次构建随机重新分配) - 其余操作数(字符串 charCode、小常量、寄存器号)跨构建稳定,保留。 - """ - code = p.function_code(entry) - out = [] - for k, inst in enumerate(code): - nm, ops = inst["name"], tuple(inst["operands"] or []) - if nm in ADDR_OPS: - ops = ("ADDR",) * len(ops) - elif nm == "PUSH_STR_CHAR": - # 只保留"这是一个字符"这件事,**不看具体是哪个字符**。 - # 这样 fp_salt 的随机盐串、混淆变量名这些「会变的内容」就不会干扰校验, - # 而"字符串的长度/位置"仍然参与比对 —— 足够独立,且仍然很强。 - ops = ("CH",) - elif nm == "PUSH_IMM" and ops: - # require(N) 形状:前一个 LOAD_REG_IMM,后一个 CALL_FUNC - if (k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM" - and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC"): - ops = ("MOD",) * len(ops) - out.append((nm, ops)) - return tuple(out[:maxlen]) - - -print("构建 : 每个模块的函数体归一化指令序列 -> 作为独立指纹") -print() - -A = tm.analyze(BATCH[0]) -pa = tp.load(BATCH[0]) -ra = dict(pa.module_registry(0)) -fa = {r["name"]: norm_code(pa, ra[r["module"]]) for r in A} - -print("基准 %s (%d 项)" % (BATCH[0].split("/")[-1], len(A))) -print() -print("%-24s %-8s %-8s %-8s %s" % ("对比构建", "配对", "序列一致", "不一致", "判决")) -print("-" * 66) -all_ok = True -for f in BATCH[1:]: - B = tm.analyze(f) - pb = tp.load(f) - rb = dict(pb.module_registry(0)) - fb = {r["name"]: norm_code(pb, rb[r["module"]]) for r in B} - common = set(fa) & set(fb) - same = [n for n in common if fa[n] == fb[n]] - diff = [n for n in common if fa[n] != fb[n]] - all_ok &= not diff - print("%-24s %-8d %-8d %-8d %s" - % (f.split("/")[-1], len(common), len(same), len(diff), - "✓" if not diff else "✗")) - for n in sorted(diff)[:3]: - a, b = fa[n], fb[n] - hit = next((k for k in range(min(len(a), len(b))) if a[k] != b[k]), -1) - print(" %-26s 首个分歧@%d A=%s B=%s" % (n, hit, - a[hit] if hit >= 0 else "-", b[hit] if hit >= 0 else "-")) - -print() -print(">>> 全部配对的函数体序列一致:", all_ok) -print(">>> 判决:", "语义名配对**正确**(独立证据)" if all_ok else "配对有误,需检查") diff --git a/scripts/verify/verify_reorder.py b/scripts/verify/verify_reorder.py deleted file mode 100644 index b87732f..0000000 --- a/scripts/verify/verify_reorder.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -"""用**全新在线样本**做端到端重排测试(不需要真机明文,自证)。 - -做法: - 给每个语义名编一个**假值** value_i,按构建 A 的清单顺序排成 cd_A。 - 再用锚点把它重排到构建 B 的顺序,得到 cd_B。 - 正确答案 = 按 B 的清单顺序、取各自语义名的假值。 - - 这个测试能抓住 reorder 的核心 bug(恒等映射): - 若 B 的顺序 ≠ A 的顺序,恒等映射必然错;只有真对齐才对。 -""" -import glob -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools")) -import tdc_collectgen as cg # noqa: E402 -import tdc_modules as tm # noqa: E402 - -BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js"))) -print("新样本 %d 份" % len(BATCH)) -print() - - -A = BATCH[0] -rowsA = tm.analyze(A) -namesA = [r["name"] for r in rowsA] -seqA = [r["module"] for r in rowsA] -n = len(namesA) -print("基准 %s 项 %d 唯一名 %d" % (A.split("/")[-1], n, len(set(namesA)))) - -# 假值:每项一个可区分的值 -fake = {nm: "V<%s>" % nm for nm in namesA} -cd_A = [fake[nm] for nm in namesA] - -src = cg.spec_from_seq(seqA, [None] * n, namesA, sample_cd=cd_A) - -print() -for f in BATCH[1:]: - rowsB = tm.analyze(f) - namesB = [r["name"] for r in rowsB] - seqB = [r["module"] for r in rowsB] - dst = cg.spec_from_seq(seqB, [None] * len(rowsB), namesB) - - # 正确答案:按 B 的顺序取各自语义名的假值 - truth = [fake[nm] for nm in namesB] - - try: - out = cg.reorder(cd_A, dst, src) - ok = out == truth - identical = out == cd_A - print(" vs %-24s" % f.split("/")[-1]) - print(" 重排结果 == 正确答案 : %s" % ok) - print(" 重排结果 == 原样(说明没重排): %s" % identical) - if not ok: - bad = [i for i, (a, b) in enumerate(zip(out, truth)) if a != b] - print(" 错位 %d 处, 前5: %s" % (len(bad), bad[:5])) - except Exception as e: - print(" vs %-24s %s: %s" % (f.split("/")[-1], type(e).__name__, str(e)[:110])) - print() diff --git a/scripts/verify/verify_seglen.py b/scripts/verify/verify_seglen.py deleted file mode 100644 index c8c0610..0000000 --- a/scripts/verify/verify_seglen.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -"""用真机 collect 密文的**长度与填充**判决「四段加密」结构。 - -不需要密钥:段结构在 base64 的长度与 '=' 分布上留下指纹。 - -判决依据(docs/tdc-案例.md §5.1): - chunk1/trajectory/chunk2 各 pad 到 24 字节 -> 密文长度是 24 的倍数 -> base64 无 '=' - sd 不 pad,只被 XTEA 补到 8 的倍数 -> 长度%3 != 0 -> base64 带 '=' - -用法: - .venv/bin/python scripts/verify/verify_seglen.py -""" -import base64 -import re -import sys -from pathlib import Path - -if len(sys.argv) < 2: - print("用法: verify_seglen.py ") - raise SystemExit(2) -_arg = sys.argv[1] -_p = Path(_arg) -b64 = (_p.read_text(encoding="ascii") if _p.exists() else _arg).strip() -print("真机 collect: %d 字符, 含 '=' : %s" % (len(b64), "=" in b64)) -print() - -raw = base64.b64decode(b64) -print("解码后密文: %d 字节" % len(raw)) -print() - -# 真机明文(另一份同会话样本)给出的各段长度 -segs = [("chunk1", 355, 5), ("trajectory", 62, 10), ("chunk2", 288, 24), ("sd", 33, None)] -print("%-11s %8s %6s %10s %12s %10s" % ("段", "内容", "pad", "密文字节", "base64字符", "含'='")) -print("-" * 66) -total = 0 -total_b64 = 0 -for name, content, pad in segs: - if pad is not None: - n = content + pad # 空格 pad 到 24 - assert n % 24 == 0 - else: - n = content + ((8 - content % 8) % 8) # sd: 只被 XTEA 补到 8 的倍数 - enc = base64.b64encode(b"\x00" * n).decode() - total += n - total_b64 += len(enc) - print("%-11s %8d %6s %10d %12d %10s" - % (name, content, pad if pad is not None else "(xtea)", - n, len(enc), "=" in enc)) -print("-" * 66) -print("%-11s %8s %6s %10d %12d" % ("合计", "", "", total, total_b64)) -print() -print(">>> 推算总密文字节 %d == 实际 %d : %s" % (total, len(raw), total == len(raw))) -print(">>> 推算 base64 字符 %d == 实际 %d : %s" % (total_b64, len(b64), total_b64 == len(b64))) -print() - -# 反过来看:把整串按 24 字节切,前三段应能整除 -print("[反向校验] 把密文按 24 字节切分:") -off = 0 -for name, content, pad in segs: - if pad is None: - print(" %-11s 剩余 %3d 字节 (sd, 非 24 倍数)" % (name, len(raw) - off)) - break - n = content + pad - print(" %-11s [%4d:%4d] %3d 字节 24倍数=%s" - % (name, off, off + n, n, n % 24 == 0)) - off += n -print() - -# 关键指纹: '=' 只应出现在最后(来自 sd 段) -idx = b64.find("=") -print("'=' 首次出现位置: %d / 总长 %d -> %s" - % (idx, len(b64), "在末尾区域(符合 sd 段不 pad)" - if idx >= len(b64) - 4 else "⚠️ 在中间(与四段结构矛盾)")) -if idx >= 0: - print(" 尾部填充: %r" % b64[idx:]) diff --git a/scripts/verify/verify_segment.py b/scripts/verify/verify_segment.py deleted file mode 100644 index c42f3d1..0000000 --- a/scripts/verify/verify_segment.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -"""按 docs/tdc-案例.md §5.1 验证 collect 的「四段加密」结构(真机明文判决)。 - -要判决的问题: - 项目现有 crypto.encrypt_string / tdc_collectgen.encrypt_plain 都是 - 「整体 JSON -> \x00 补齐到 8 -> 一次加密 -> 一个 base64」。 - 案例说「4 段独立加密,前 3 段空格 pad 到 24 字节对齐,sd 不 pad」。 - 两者只能有一个对 —— 用真机明文里的**空格填充串**来判。 - -用法: - .venv/bin/python scripts/verify/verify_segment.py <已解密的 collect 明文文件> -""" -import json -import re -import sys - -if len(sys.argv) < 2: - print("用法: verify_segment.py <已解密的 collect 明文文件>") - print(" (明文来源: 用同会话 tdc.js 的密钥解密 TDC.getData(true) 的产出)") - raise SystemExit(2) -RAW = sys.argv[1] - -data = open(RAW, encoding="latin1").read() -print("原始(含尾部 \\x00):", len(data), "字节") -stripped = data.rstrip("\x00") -print("去掉尾部 \\x00 后 :", len(stripped), "字节 (尾部 \\x00 个数 = %d)" % (len(data) - len(stripped))) -print() - -runs = [(m.start(), m.end()) for m in re.finditer(r" {2,}", stripped)] -print("[1] 连续空格串(只能来自「空格 pad」):") -for a, b in runs: - print(" 位置 %3d..%3d 长度 %2d" % (a, b, b - a)) -print() - -# 段边界 = 每个空格串的结束位置(若空格串在段尾) -bounds = [0] + [b for _a, b in runs[:-1]] if len(runs) > 1 else [0] -# 更稳: 段边界取每个空格串的"起点所在段"的末尾 -> 直接用空格串终点 -b1, b2, b3 = runs[0][1], runs[1][1], runs[2][1] -segs = [("chunk1", 0, b1), ("trajectory", b1, b2), ("chunk2", b2, b3), ("sd", b3, len(stripped))] -print("[2] 按空格串切出的 4 段:") -for name, a, b in segs: - s = stripped[a:b] - print(" %-11s [%3d:%3d] len=%3d 24倍数=%-5s 8倍数=%-5s" - % (name, a, b, len(s), len(s) % 24 == 0, len(s) % 8 == 0)) -print() - -print("[3] 空格 pad 公式校验 (pad_len = 24 - len%24,总是补 1..24 个):") -for name, a, b in segs[:3]: - s = stripped[a:b] - content = s.rstrip(" ") - pad = len(s) - len(content) - print(" %-11s 内容 %3d + pad %2d = %3d 公式给出 %2d -> %s" - % (name, len(content), pad, len(s), 24 - len(content) % 24, - "OK" if pad == 24 - len(content) % 24 else "FAIL")) -print() - -print("[4] sd 段不 pad(只被 XTEA 的 8 字节块 \x00 补齐):") -sd = stripped[b3:] -print(" sd = %r" % sd) -print(" sd 长度 %d (非 8 的倍数 -> 加密时被补 %d 个 \\x00)" - % (len(sd), (8 - len(sd) % 8) % 8)) -print(" 实际尾部 \\x00 个数 = %d" % (len(data) - len(stripped))) -print() - -print("[5] 逻辑整体是否是合法 JSON,以及轨迹在 cd 里的位置:") -obj = json.loads(stripped) -cd = obj["cd"] -traj_idx = [i for i, v in enumerate(cd) - if isinstance(v, list) and v and all(isinstance(x, list) and len(x) == 3 for x in v)] -print(" cd 槽数 = %d" % len(cd)) -print(" 轨迹槽下标 = %s" % traj_idx) -if traj_idx: - print(" 轨迹内容前 3 点 = %s" % cd[traj_idx[0]][:3]) -print() - -print("[6] 去 pad 后 4 段拼接 == 逻辑明文(应能解析成同一对象):") -rebuilt = "".join(stripped[a:b].rstrip(" ") for _name, a, b in segs[:3]) + stripped[b3:] -obj2 = json.loads(rebuilt) -print(" 可解析为 JSON :", True) -print(" 与原对象逐项相同 :", obj2 == obj) -print(" 重建长度 %d (原文去尾 \\x00 后 %d, 差 = pad 总量 %d)" - % (len(rebuilt), len(stripped), len(stripped) - len(rebuilt))) -print() -print("[7] 三段之间的连接处(验证 chunk1 以逗号结尾 / chunk2 以 '],' 结尾):") -print(" chunk1 内容末尾 : %r" % stripped[0:b1].rstrip(" ")[-24:]) -print(" trajectory 内容 : %r" % stripped[b1:b2].rstrip(" ")[:40]) -print(" chunk2 内容开头 : %r" % stripped[b2:b3].rstrip(" ")[:24]) -print(" chunk2 内容末尾 : %r" % stripped[b2:b3].rstrip(" ")[-24:]) -print(" sd 内容 : %r" % stripped[b3:]) diff --git a/scripts/verify/verify_values.py b/scripts/verify/verify_values.py index 4736f7e..c01090a 100644 --- a/scripts/verify/verify_values.py +++ b/scripts/verify/verify_values.py @@ -27,10 +27,10 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "tools")) -import tdc_modules as tm # noqa: E402 -from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402 -from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402 -from tcaptcha.tdc_values import Session, generate_cd # noqa: E402 +from tcaptcha.telemetry import modules as tm # noqa: E402 +from tcaptcha.telemetry.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402 +from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402 +from tcaptcha.telemetry.values import Session, generate_cd # noqa: E402 def slot_names(rows): diff --git a/tcaptcha/TOOLCHAIN_REPORT.md b/tcaptcha/TOOLCHAIN_REPORT.md deleted file mode 100644 index 953435f..0000000 --- a/tcaptcha/TOOLCHAIN_REPORT.md +++ /dev/null @@ -1,124 +0,0 @@ -# 腾讯验证码 (TCaptcha / TDC) 逆向工具链建设与协议解构报告 - -依据 `AGENTS.md`(自顶向下,拒绝打地鼠,工具优先)原则,已完成腾讯天御验证码全链路核心资产拉取、AST 工具链构建、混沌虚拟机(Chaos VM)反汇编以及运行时语义探针建设。 - ---- - -## 一、已建成的工具集清单 - -| 工具 | 路径 | 核心功能 | 解决什么问题 | -| :--- | :--- | :--- | :--- | -| **Webpack 解包索引器** | [`tools/unpack_webpack.js`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/unpack_webpack.js) | 将 230KB 单文件 bundle 拆解为 122 个独立模块,建立加签/风控关键词索引 | 圈定加签核心,剔除 80% 开源库与 UI 噪音 (L1 解构) | -| **Chaos VM 字节码反汇编器 (纯 Python)** | [`tools/tdc_disasm.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_disasm.py) | 纯 Python 提取 45,636 字节码 + 可达性 CFG 反汇编(26,168 条可达指令,**0 处非法 opcode**);opcode 表可**按构建注入**(见下) | 彻底透明化 TDC 字节码调度器 (L2/L3 解构) | -| **按构建推导 opcode 表** | [`tcaptcha/tdc_opcodes.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tcaptcha/tdc_opcodes.py) | 从解释器主循环 `X[Y[Z++]]()` 取出 handler 数组 → 解析 → 规范化指纹 → 跨构建匹配出**该构建的 opcode 表** | **opcode 编号每份构建整体重排**,写死即换脚本全错(6/6 构建 58/58 handler 匹配) | -| **纯 Python XTEA 密钥静态提取** | [`tcaptcha/tdc_key.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tcaptcha/tdc_key.py) + [`tools/tdc_key_pure.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_key_pure.py) | 反汇编 → 定位 XTEA 闭包(delta) → 两层捕获链选出真密钥数组 → 读 4 个字 + 折盐 | **零 Node** 拿到该构建 XTEA 密钥;6/6 构建与 Node 插桩逐字一致 | -| **深度 Proxy 语义探针** | [`tools/tdc_probe.js`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_probe.js) | 零 Headless/CDP 依赖,通过深度 Proxy 沙箱执行 `tdc.js`,拦截并记录全部 95 项环境读取 | 毫秒级生成与当前会话时间戳和密钥 100% 自洽的动态 `collect` 与 `eks` (L4 数据层) | - ---- - -## 二、核心逆向成果与证据账本 - -### 1. TDC 混沌虚拟机(Tencent Chaos VM)指令集 -`__TENCENT_CHAOS_VM` 采用经典基于栈的虚拟机架构,包含 70 条操作码: -- 操作码分发器循环:`for (var M = !1; !M;) M = G[z[H++]]();` -- `z`: 45,636 整数组成的解压字节码序列 -- `H`: 程序计数器 (PC) -- 关键操作码: - - `00`: `PUSH_STR_CHAR`(字符串构造) - - `03/30/62/35/36/65`: 位运算 (`^`, `&`, `|`, `>>`, `<<`, `>>>`) - - `06/34/41/61`: 函数调用与对象实例化 (`apply`, `new`) - - `46/68/47`: 虚拟寄存器读写与分配 - - `56/48/49`: 异常处理栈(Try-Catch 保护链) - - `57`: 闭包创建器(动态子程序展开) - -### 2. TDC 关键检测项白名单(已由探针全量捕获,共 95 项) -探针运行实测表明,TDC 实际探测的环境特征仅 95 项,绝非海量属性: -1. **自动化/反爬标记检测**: - - `document.$cdc_asdjflasutopfhvcZLmcfl_`(精准探测 ChromeDriver 变量) - - `window.navigator.webdriver` - - `window.domAutomation` - - `window.fxdriver_id` - - `window.callPhantom` / `casper` / `CasperError` / `patchRequire` - - `window.ubot` / `WebPage` -2. **显示与硬件画像**: - - `screen.width` (2560), `screen.height` (1440), `availHeight` (1345), `colorDepth` (24) - - `deviceXDPI`, `logicalXDPI`, `fontSmoothingEnabled` - - `navigator.hardwareConcurrency` (10), `platform` ("MacIntel"), `languages` -3. **渲染上下文**: - - WebGL Renderer: `"ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)"` - - WebGL Vendor: `"Google Inc. (Apple)"` - - Canvas Fingerprint: 成功样本原图 Base64 (368 字节) -4. **容器关系**: - - `window.top !== window`(识别是否运行在验证码 iframe 内) - - `document.getElementById("tCaptchaDyContent")` - - `window.TCaptchaReferrer`(宿主页面 URL 绑定) - -### 3. L4 遥测协议数据解密 (`cd` 38 维字段) -成功解密的 `collect` 明文格式为 `{"cd": [...], "sd": {"od": "C", "ft": "..."}}`: -- `cd[0..1]`: 屏幕宽、平台 (`2560`, `"MacIntel"`) -- `cd[2]`: Canvas 指纹 -- `cd[8]`: 宿主页面 URL (`document.referrer`) -- `cd[9]`: User-Agent -- `cd[15/18]`: WebGL 渲染器与厂商 -- `cd[19/22/27]`: **当前会话的秒级与毫秒级 Epoch 时间戳**(必须与请求自洽,旧方案硬编码是致死原因之一) -- `cd[20]`: **鼠标相对增量轨迹**:首点为绝对坐标 `[start_x, start_y, delay_ms]`,后续为增量 `[dx, dy, dt_ms]`(dt 均值 16~18ms,模拟 60Hz 采样) -- `cd[26]`: 会话校验和与随机种子 -- `cd[31]`: 执行容器类型 (`"iframe"`) - ---- - -## 三、关于 errorCode=12 的诚实结论(**已更正**) - -> 本报告早期版本曾把 12 归因于"`re.sub` 破坏校验和 / 时间戳错位 / 机械假轨迹 / CDP 特征泄露"。 -> 这 4 条**均与本项目自己的证据账本冲突**(见 `har/1ms_dianxuan/ANALYSIS.md` §7 与 `PROGRESS.md` §三), -> 现更正如下: - -| 早期断言 | 现状 | -| --- | --- | -| 时间戳错位(硬编码数天前)| ❌ 撤回:纯算路径自模板回填起就刷新为当前时间戳,仍 12 | -| 机械假轨迹 dt=0/1ms | ❌ 撤回:换真实 ms 分布后仍 12 | -| CDP 特征泄露 | ❌ 撤回:纯 Python(无 CDP)后端同样 12 | -| `re.sub` 破坏内部校验和 | ⚠️ 仅为假设,无对照实验 | - -**已确证**:12 与遥测后端 / 出口 IP / 静默时长均无关。真正的自变量 -(登录态 / 真实验证码页面上下文)尚未由单变量实验区分 —— 见 `ANALYSIS.md` §7.5。 - -### 关于深度 Proxy 探针(`tools/tdc_probe.js`)的定位 - -探针是**分析工具**(用于枚举 tdc.js 真实读取了哪些环境属性),**已不在遥测生成路径上** -(`crypto.generate_telemetry` 只保留纯算)。它注入的 Canvas / WebGL / 屏幕取值来自 -**单份成功样本**,属于"复刻一份画像"而非"真实环境",因此不作为交付依据。 - ---- - -## 四、一键复验入口 - -```bash -# 1. 验证 Webpack 解包与核心模块提取 -node tools/unpack_webpack.js - -# 2. 验证 TDC 虚拟机反汇编与字符串提取 -node tools/tdc_vm_disasm.js tcaptcha/core_js/tdc_sample.js # 已废弃, 转发到下面的纯 Python 实现 -.venv/bin/python tools/tdc_disasm.py tcaptcha/core_js/tdc_sample.js - -# 3. 验证深度 Proxy 语义探针执行并产出 100% 合法 collect 与 eks -node tools/tdc_probe.js tcaptcha/core_js/tdc_sample.js - -# 4. 验证探针生成的 collect 成功解密 -.venv/bin/python -c ' -import json, base64, urllib.parse, subprocess -from tcaptcha.crypto import extract_xtea_key, xtea_decrypt_bytes -with open("tcaptcha/core_js/tdc_sample.js") as f: code = f.read() -key = extract_xtea_key(code) -res = subprocess.check_output(["node", "-e", "const {probeTdc}=require(\"./tools/tdc_probe.js\"); const r=probeTdc(\"tcaptcha/core_js/tdc_sample.js\"); console.log(r.collect);"], text=True).strip() -raw = base64.b64decode(urllib.parse.unquote(res)) -plain = xtea_decrypt_bytes(raw, key).rstrip(b"\\x00") -print("成功解密探针生成的遥测载荷,长度:", len(plain)) -print("明文前 150 字节:", plain[:150].decode("latin1")) -' - -# 5. 纯 Python XTEA 密钥提取 (零 Node) + 跨构建回归 -.venv/bin/python tools/tdc_key_pure.py -v har/1ms_dianxuan/live/tdc_fresh.js -.venv/bin/python -m tcaptcha.tests.test_tdc_key # 6 份构建 (含实时新抓) 全部断言 -.venv/bin/python tools/tdc_key_pure.py --opcodes tcaptcha/tdc_sample.js # 打印该构建的 opcode 表 -``` diff --git a/tcaptcha/__init__.py b/tcaptcha/__init__.py index f16e32a..4889e84 100644 --- a/tcaptcha/__init__.py +++ b/tcaptcha/__init__.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- -"""Tencent Captcha Package +"""Tencent Captcha —— 纯算还原 + 视觉求解。 -核心能力: - * TencentCaptchaClient —— 纯协议客户端 (prehandle/PoW/tdc.js/图片/verify/getsig) - * TextClickSolver —— 文字点选 (2404) 求解器: 颜色分割 + OCR 集成指派 - * CaptchaSolver —— 图标点选 (2408) 求解器: YOLOv8 + ResNet18 - * EnsembleOcr —— ddddocr + PP-OCRv4 概率级集成识别 - * generate_telemetry(_node) —— collect/eks 遥测密文生成 (Node 沙箱执行真实 tdc.js) - * OneMsAdapter —— 毫秒 1ms.run 业务适配器 +分层(详见各子包 docstring): + * protocol/ TencentCaptchaClient —— prehandle / PoW / tdc.js / 题目图片 / verify + * telemetry/ collect 的**纯离线**生成(37 语义名 -> 38 槽 -> 4 段 XTEA)+ 密钥静态还原 + * solvers/ 文字点选 / 图标点选 / 滑块 求解器 + * ocr/ ddddocr + PP-OCRv4 集成识别、提示图切分 + * adapters/ 站点业务适配(1ms.run) """ -from .client import ( +# ---- 协议层 ---- +from .protocol.client import ( KIND_ICON_CLICK, KIND_SLIDER, KIND_TEXT_CLICK, @@ -18,19 +18,35 @@ from .client import ( TencentCaptchaClient, classify_challenge, ) -from .crypto import ( +from .protocol.proxy import check_proxy, fetch_proxy, parse_proxies + +# ---- 遥测层(纯算:零 Node、零真机明文)---- +from .telemetry.crypto import ( + build_collect, encrypt_string, extract_eks, extract_xtea_key, - generate_collect_success, generate_telemetry, + split_collect_segments, ) -from .glyph_solver import Glyph, SolveResult, TextClickSolver, parse_instruction -from .ocr_engines import DdddOcrEngine, EnsembleOcr, PpOcrEngine -from .one_ms_adapter import OneMsAdapter -from .proxy import check_proxy, fetch_proxy, parse_proxies -from .slicer import slice_prompt_image -from .solver import CaptchaSolver +from .telemetry.key import extract_xtea_key_pure +from .telemetry.values import ( + DEVICE_PROFILE, + Session, + generate_cd, + generate_collect, + trajectory_points, +) + +# ---- 求解器层 ---- +from .solvers.icon_click import CaptchaSolver +from .solvers.slider import SliderSolver +from .solvers.text_click import Glyph, SolveResult, TextClickSolver, parse_instruction + +# ---- 识别基座 / 业务适配 ---- +from .ocr.engines import DdddOcrEngine, EnsembleOcr, PpOcrEngine +from .ocr.slicer import slice_prompt_image +from .adapters.one_ms import OneMsAdapter __all__ = [ # 协议客户端 @@ -46,15 +62,24 @@ __all__ = [ "Glyph", "parse_instruction", "CaptchaSolver", + "SliderSolver", + # 识别基座 "slice_prompt_image", "EnsembleOcr", "DdddOcrEngine", "PpOcrEngine", - # 遥测 / 加密 (纯算: 模板回填 + XTEA, Node 仅提取密钥) - "generate_collect_success", + # 遥测 / 加密(纯算) "generate_telemetry", + "generate_collect", + "generate_cd", + "Session", + "DEVICE_PROFILE", + "trajectory_points", + "build_collect", + "split_collect_segments", "extract_eks", "extract_xtea_key", + "extract_xtea_key_pure", "encrypt_string", # 业务适配 / 代理 "OneMsAdapter", diff --git a/tcaptcha/adapters/__init__.py b/tcaptcha/adapters/__init__.py new file mode 100644 index 0000000..83791a5 --- /dev/null +++ b/tcaptcha/adapters/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +"""业务适配层:把通用客户端适配到具体站点的业务流程。 + + one_ms.py 毫秒镜像 1ms.run(签到 / 登录) +""" diff --git a/tcaptcha/one_ms_adapter.py b/tcaptcha/adapters/one_ms.py similarity index 97% rename from tcaptcha/one_ms_adapter.py rename to tcaptcha/adapters/one_ms.py index 234173f..63539a4 100644 --- a/tcaptcha/one_ms_adapter.py +++ b/tcaptcha/adapters/one_ms.py @@ -31,7 +31,7 @@ from typing import Dict, Optional, Tuple import httpx -from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient +from tcaptcha.protocol.client import KIND_TEXT_CLICK, TencentCaptchaClient logger = logging.getLogger("one_ms_adapter") @@ -118,7 +118,7 @@ class OneMsAdapter: proxy = self.proxy if not proxy and self.proxy_api: - from tcaptcha.proxy import fetch_proxy + from tcaptcha.protocol.proxy import fetch_proxy proxy = fetch_proxy(self.proxy_api) logger.info("从代理池初始化出口代理: %s", proxy) @@ -153,7 +153,7 @@ class OneMsAdapter: if not connectivity_error or not self.proxy_api: break - from tcaptcha.proxy import fetch_proxy + from tcaptcha.protocol.proxy import fetch_proxy new_proxy = fetch_proxy(self.proxy_api) if new_proxy: proxy = new_proxy diff --git a/tcaptcha/benchmark_verify.py b/tcaptcha/benchmark_verify.py deleted file mode 100644 index a5638aa..0000000 --- a/tcaptcha/benchmark_verify.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -腾讯天御点选验证码纯协议端到端实战压测基准脚本 -连续发起 N 轮真实线上验证请求,统计: -1. 首检通过率 (Success Rate) -2. 端到端耗时分布 (Min / Mean / Max Latency) -3. 异常与失败分类 -""" - -import argparse -import sys -import time -from pathlib import Path -from typing import List - -PROJECT_ROOT = Path(__file__).resolve().parent.parent -if str(PROJECT_ROOT) not in sys.path: - sys.path.insert(0, str(PROJECT_ROOT)) - -from tcaptcha.client import TencentCaptchaClient -from tcaptcha.solver import CaptchaSolver - - -def run_benchmark(rounds: int = 10, aid: str = "2039519451", interval: float = 3.0): - print("=" * 65) - print(f" 🚀 开始腾讯天御验证码纯协议线上压力基准测试 (AppID: {aid})") - print(f" 测试轮数: {rounds}, 请求间隔: {interval}s") - print("=" * 65) - - # 预加载模型,避免把模型初始化耗时算入单次请求 - print("[*] 正在预加载 YOLO 目标检测器与 ResNet18 分类器模型...") - solver = CaptchaSolver() - client = TencentCaptchaClient(aid=aid, solver=solver) - print("[*] 模型就绪,开始线上请求...\n") - - results: List[dict] = [] - latencies: List[float] = [] - successes = 0 - - for i in range(1, rounds + 1): - print(f"--- [Round {i}/{rounds}] ---") - res = client.solve() - results.append(res) - latencies.append(res["elapsed_ms"]) - - if res["success"]: - successes += 1 - print(f" ✅ 成功! 耗时: {res['elapsed_ms']}ms | randstr: {res['randstr']} | ticket: {res['ticket'][:24]}...") - else: - print(f" ❌ 失败! 耗时: {res['elapsed_ms']}ms | 原因: {res['error']}") - - # 间隔停顿模拟真实请求,避免瞬间高频触发频控 - if i < rounds and interval > 0: - time.sleep(interval) - - # 汇总报表 - success_rate = (successes / rounds) * 100 - avg_latency = sum(latencies) / len(latencies) if latencies else 0 - min_latency = min(latencies) if latencies else 0 - max_latency = max(latencies) if latencies else 0 - - print("\n" + "=" * 65) - print(" 📊 纯协议实战基准测试统计汇总") - print("=" * 65) - print(f"总测试轮数: {rounds}") - print(f"成功次数: {successes}") - print(f"失败次数: {rounds - successes}") - print(f"综合首检通过率: {success_rate:.2f}%") - print(f"平均单次耗时: {avg_latency:.1f} ms") - print(f"最短单次耗时: {min_latency} ms") - print(f"最长单次耗时: {max_latency} ms") - print("=" * 65) - - return success_rate, avg_latency - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Tencent Captcha Benchmark Tool") - parser.add_argument("--rounds", type=int, default=10, help="Number of test rounds (default: 10)") - parser.add_argument("--interval", type=float, default=3.0, help="Interval between rounds in seconds (default: 3.0)") - parser.add_argument("--aid", type=str, default="2039519451", help="AppID (default: 2039519451)") - args = parser.parse_args() - - run_benchmark(rounds=args.rounds, aid=args.aid, interval=args.interval) diff --git a/tcaptcha/chrome_fetch.js b/tcaptcha/chrome_fetch.js deleted file mode 100644 index 2ac5347..0000000 --- a/tcaptcha/chrome_fetch.js +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env node -/** - * 用真实 Chrome 的网络栈发一个请求 (TLS/HTTP2/header 顺序全是浏览器的) - * - * 用途: 验证服务端风控是否在按「客户端网络指纹」判定。 - * 从真实验证码域 https://turing.captcha.gtimg.com/ 发起 fetch, - * Origin/Referer/Sec-Fetch-* 都由浏览器自己填。 - * - * 用法: - * echo '' | node chrome_fetch.js [originUrl] - */ -const { spawn } = require('child_process'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -const CHROME = process.env.REA_BROWSER_EXECUTABLE || - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; - -function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } - -async function launchChrome() { - const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'chrome-fetch-')); - const proc = spawn(CHROME, [ - '--headless=new', '--remote-debugging-port=0', '--no-first-run', - '--no-default-browser-check', '--disable-gpu', '--disable-dev-shm-usage', - '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', - `--user-data-dir=${profile}`, '--window-size=1280,800', 'about:blank', - ], { stdio: ['ignore', 'pipe', 'pipe'] }); - const portFile = path.join(profile, 'DevToolsActivePort'); - let port = null; - for (let i = 0; i < 120; i++) { - if (fs.existsSync(portFile)) { - port = parseInt(fs.readFileSync(portFile, 'utf8').split('\n')[0], 10); - if (port) break; - } - await sleep(100); - } - if (!port) { proc.kill('SIGKILL'); throw new Error('Chrome 未就绪'); } - return { proc, port, profile }; -} - -class Cdp { - constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); } - static async connect(url) { - const ws = new WebSocket(url); - await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); - const c = new Cdp(ws); - ws.onmessage = ev => { - const m = JSON.parse(ev.data); - if (m.id && c.waits.has(m.id)) { - const { res, rej } = c.waits.get(m.id); - c.waits.delete(m.id); - m.error ? rej(new Error(JSON.stringify(m.error))) : res(m.result); - } - }; - return c; - } - send(method, params = {}, sessionId) { - const id = ++this.id; - return new Promise((res, rej) => { - this.waits.set(id, { res, rej }); - this.ws.send(JSON.stringify({ id, method, params, sessionId })); - }); - } -} - -async function main() { - const url = process.argv[2]; - const origin = process.argv[3] || 'https://turing.captcha.gtimg.com/'; - const body = fs.readFileSync(0, 'utf8').trim(); - const { proc, port, profile } = await launchChrome(); - try { - const ver = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json(); - const cdp = await Cdp.connect(ver.webSocketDebuggerUrl); - const { targetId } = await cdp.send('Target.createTarget', { url: origin }); - const { sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true }); - await cdp.send('Page.enable', {}, sessionId); - await sleep(2500); // 等页面(真实验证码域)加载完 - - const loc = await cdp.send('Runtime.evaluate', { expression: 'location.href', returnByValue: true }, sessionId); - process.stderr.write('[page] ' + loc.result.value + '\n'); - const expr = `(async () => { - const r = await fetch(${JSON.stringify(url)}, ${JSON.stringify(body)} - ? { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - body: ${JSON.stringify(body)}, credentials: 'include' } - : { method: 'GET', credentials: 'include' }); - const t = await r.text(); - return JSON.stringify({ status: r.status, text: t }); - })()`; - const out = await cdp.send('Runtime.evaluate', - { expression: expr, awaitPromise: true, returnByValue: true }, sessionId); - if (out.exceptionDetails) { - console.log(JSON.stringify({ _exception: out.exceptionDetails.text, - desc: (out.exceptionDetails.exception || {}).description })); - } else if (out.result && out.result.value !== undefined) { - console.log(out.result.value); - } else { - console.log(JSON.stringify({ _raw: out })); - } - cdp.ws.close(); - } finally { - proc.kill('SIGKILL'); - try { fs.rmSync(profile, { recursive: true, force: true }); } catch (_) {} - } -} - -main().catch(e => { console.error('ERR', e.message); process.exit(1); }); diff --git a/tcaptcha/collector.py b/tcaptcha/collector.py deleted file mode 100644 index 0122b30..0000000 --- a/tcaptcha/collector.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -腾讯天御点选验证码 (Tencent Turing Captcha) 样本纯协议采集器 -通过模拟前端两步 HTTP 接口直接拉取高清原图: -1. GET /cap_union_prehandle 获取初始会话 sess -2. POST /cap_union_new_getsig 获取题目配置与背景/提示图 URL -3. 并行下载 672x480 背景大图与 170x50 提示图标图 -""" - -import argparse -import asyncio -import json -import logging -import os -import random -import re -import time -from pathlib import Path -from typing import Optional, Tuple - -import httpx - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%H:%M:%S", -) -logger = logging.getLogger("tcaptcha_collector") - - -class TencentCaptchaCollector: - BASE_URL = "https://turing.captcha.qcloud.com" - DEFAULT_AID = "2039519451" - - def __init__(self, aid: str = DEFAULT_AID, output_dir: str = "tcaptcha/data/raw", timeout: float = 10.0): - self.aid = aid - self.output_dir = Path(output_dir) - self.output_dir.mkdir(parents=True, exist_ok=True) - self.index_file = self.output_dir / "index.jsonl" - self.timeout = timeout - - self.client = httpx.Client( - base_url=self.BASE_URL, - trust_env=False, - headers={ - "User-Agent": ( - "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" - ), - "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 _fetch_session(self) -> Tuple[Optional[str], Optional[str]]: - """第一步:预检请求获取 sess""" - cb_name = f"_aq_{random.randint(100000, 999999)}" - tkid = str(random.randint(100000000, 999999999)) - ua_b64 = "TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzE1Mi4wLjAuMCBTYWZhcmkvNTM3LjM2" - - params = { - "aid": self.aid, - "protocol": "https", - "accver": "1", - "showtype": "popup", - "ua": ua_b64, - "noheader": "1", - "fb": "1", - "grayscale": "1", - "clientype": "2", - "lang": "zh-cn", - "js": "/tcaptcha-frame.ba387dd1.js", - "wb": "2", - "tkid": tkid, - "subsid": "1", - "callback": cb_name, - } - - resp = self.client.get("/cap_union_prehandle", params=params) - if resp.status_code != 200: - logger.error("prehandle failed with status %d", resp.status_code) - return None, None - - match = re.search(rf"{cb_name}\((.*)\)", resp.text) - if not match: - logger.error("Failed to parse prehandle response: %s", resp.text[:100]) - return None, None - - try: - data = json.loads(match.group(1)) - sess = data.get("sess") - return sess, tkid - except Exception as e: - logger.error("Failed to decode JSON from prehandle: %s", e) - return None, None - - def _fetch_sig_and_images(self, sess: str, tkid: str) -> Optional[dict]: - """第二步:请求题目信息并下载图片对""" - resp = self.client.post( - "/cap_union_new_getsig", - data={"sess": sess, "tkid": tkid}, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - - if resp.status_code != 200: - logger.error("getsig failed with status %d", resp.status_code) - return None - - try: - res_json = resp.json() - except Exception as e: - logger.error("Failed to decode getsig JSON: %s", e) - return None - - if str(res_json.get("ret")) != "0": - logger.warning("getsig returned non-zero ret: %s", res_json) - return None - - cfg = res_json.get("data", {}) - instruction = cfg.get("instruction", "请依次点击:") - bg_cfg = cfg.get("bg_elem_cfg", {}) - img_url_bg = bg_cfg.get("img_url", "") - - if not img_url_bg: - logger.warning("No img_url found in getsig response") - return None - - img_url_prompt = img_url_bg.replace("img_index=1", "img_index=0") - - # 下载背景大图 (672x480) 与 提示图 (170x50) - bg_resp = self.client.get(img_url_bg) - prompt_resp = self.client.get(img_url_prompt) - - if bg_resp.status_code != 200 or prompt_resp.status_code != 200: - logger.error("Failed to download image pairs") - return None - - return { - "instruction": instruction, - "bg_bytes": bg_resp.content, - "prompt_bytes": prompt_resp.content, - "img_url_bg": img_url_bg, - "img_url_prompt": img_url_prompt, - "sess": sess, - "tkid": tkid, - } - - def fetch_one(self) -> Optional[str]: - """采集单组样本并保存到本地""" - sess, tkid = self._fetch_session() - if not sess or not tkid: - return None - - data = self._fetch_sig_and_images(sess, tkid) - if not data: - return None - - sample_id = f"{int(time.time() * 1000)}_{random.randint(1000, 9999)}" - bg_filename = f"{sample_id}_bg.png" - prompt_filename = f"{sample_id}_prompt.png" - meta_filename = f"{sample_id}_meta.json" - - bg_path = self.output_dir / bg_filename - prompt_path = self.output_dir / prompt_filename - meta_path = self.output_dir / meta_filename - - bg_path.write_bytes(data["bg_bytes"]) - prompt_path.write_bytes(data["prompt_bytes"]) - - meta_info = { - "sample_id": sample_id, - "timestamp": time.time(), - "aid": self.aid, - "instruction": data["instruction"], - "bg_file": bg_filename, - "prompt_file": prompt_filename, - "bg_size": len(data["bg_bytes"]), - "prompt_size": len(data["prompt_bytes"]), - "sess": data["sess"], - "tkid": data["tkid"], - } - - meta_path.write_text(json.dumps(meta_info, ensure_ascii=False, indent=2), encoding="utf-8") - - # 写入 index.jsonl - with open(self.index_file, "a", encoding="utf-8") as f: - f.write(json.dumps(meta_info, ensure_ascii=False) + "\n") - - logger.info( - "✓ 采集成功 [%s]: 大图 %d KB, 提示图 %d KB | 提示: %s", - sample_id, - len(data["bg_bytes"]) // 1024, - len(data["prompt_bytes"]) // 1024, - data["instruction"], - ) - return sample_id - - def collect_batch(self, count: int = 10, delay: float = 0.8) -> int: - """批量采集样本""" - logger.info("开始批量采集点选验证码样本,目标数量: %d, 间隔: %.2fs", count, delay) - success_count = 0 - - for i in range(count): - try: - sid = self.fetch_one() - if sid: - success_count += 1 - else: - logger.warning("第 %d/%d 个样本采集失败,准备重试", i + 1, count) - except Exception as e: - logger.error("采集异常: %s", e) - - if i < count - 1 and delay > 0: - # 随机微弱浮动,模拟正常采集 - actual_delay = delay + random.uniform(-0.2, 0.3) - time.sleep(max(0.2, actual_delay)) - - logger.info("批量采集完成: 成功 %d/%d (存储目录: %s)", success_count, count, self.output_dir) - return success_count - - def close(self): - self.client.close() - - -def main(): - parser = argparse.ArgumentParser(description="腾讯天御点选验证码纯协议批量采集器") - parser.add_argument("--count", type=int, default=10, help="采集样本数量 (默认: 10)") - parser.add_argument("--delay", type=float, default=0.8, help="每次请求间隔秒数 (默认: 0.8)") - parser.add_argument("--aid", type=str, default=TencentCaptchaCollector.DEFAULT_AID, help="业务 AppID") - parser.add_argument("--output-dir", type=str, default="tcaptcha/data/raw", help="保存路径") - - args = parser.parse_args() - collector = TencentCaptchaCollector(aid=args.aid, output_dir=args.output_dir) - try: - collector.collect_batch(count=args.count, delay=args.delay) - finally: - collector.close() - - -if __name__ == "__main__": - main() diff --git a/tcaptcha/core_js/frame_modules/manifest.json b/tcaptcha/core_js/frame_modules/manifest.json deleted file mode 100644 index c883a12..0000000 --- a/tcaptcha/core_js/frame_modules/manifest.json +++ /dev/null @@ -1,655 +0,0 @@ -[ - { - "id": 0, - "size": 7640, - "file": "module_000.js", - "keywords": [] - }, - { - "id": 1, - "size": 6222, - "file": "module_001.js", - "keywords": [ - "tdc", - "prehandle" - ] - }, - { - "id": 2, - "size": 3107, - "file": "module_002.js", - "keywords": [] - }, - { - "id": 3, - "size": 799, - "file": "module_003.js", - "keywords": [] - }, - { - "id": 4, - "size": 725, - "file": "module_004.js", - "keywords": [] - }, - { - "id": 5, - "size": 2500, - "file": "module_005.js", - "keywords": [] - }, - { - "id": 6, - "size": 1613, - "file": "module_006.js", - "keywords": [] - }, - { - "id": 7, - "size": 911, - "file": "module_007.js", - "keywords": [] - }, - { - "id": 8, - "size": 8871, - "file": "module_008.js", - "keywords": [] - }, - { - "id": 9, - "size": 77, - "file": "module_009.js", - "keywords": [] - }, - { - "id": 10, - "size": 1705, - "file": "module_010.js", - "keywords": [] - }, - { - "id": 11, - "size": 1047, - "file": "module_011.js", - "keywords": [] - }, - { - "id": 12, - "size": 2324, - "file": "module_012.js", - "keywords": [ - "verify", - "ticket" - ] - }, - { - "id": 13, - "size": 920, - "file": "module_013.js", - "keywords": [ - "verify" - ] - }, - { - "id": 14, - "size": 1034, - "file": "module_014.js", - "keywords": [] - }, - { - "id": 15, - "size": 23392, - "file": "module_015.js", - "keywords": [ - "pow" - ] - }, - { - "id": 16, - "size": 81616, - "file": "module_016.js", - "keywords": [ - "verify" - ] - }, - { - "id": 17, - "size": 669, - "file": "module_017.js", - "keywords": [ - "verify", - "ticket" - ] - }, - { - "id": 18, - "size": 286, - "file": "module_018.js", - "keywords": [] - }, - { - "id": 19, - "size": 5146, - "file": "module_019.js", - "keywords": [ - "verify", - "tdc", - "getsig", - "prehandle" - ] - }, - { - "id": 20, - "size": 447, - "file": "module_020.js", - "keywords": [] - }, - { - "id": 21, - "size": 1911, - "file": "module_021.js", - "keywords": [ - "verify" - ] - }, - { - "id": 22, - "size": 3537, - "file": "module_022.js", - "keywords": [ - "verify" - ] - }, - { - "id": 23, - "size": 2783, - "file": "module_023.js", - "keywords": [ - "prehandle" - ] - }, - { - "id": 24, - "empty": true - }, - { - "id": 25, - "size": 991, - "file": "module_025.js", - "keywords": [] - }, - { - "id": 26, - "size": 5018, - "file": "module_026.js", - "keywords": [] - }, - { - "id": 27, - "size": 2361, - "file": "module_027.js", - "keywords": [ - "verify" - ] - }, - { - "id": 28, - "size": 1510, - "file": "module_028.js", - "keywords": [ - "verify", - "prehandle" - ] - }, - { - "id": 29, - "size": 1526, - "file": "module_029.js", - "keywords": [ - "verify", - "prehandle" - ] - }, - { - "id": 30, - "empty": true - }, - { - "id": 31, - "empty": true - }, - { - "id": 32, - "empty": true - }, - { - "id": 33, - "empty": true - }, - { - "id": 34, - "empty": true - }, - { - "id": 35, - "empty": true - }, - { - "id": 36, - "empty": true - }, - { - "id": 37, - "empty": true - }, - { - "id": 38, - "empty": true - }, - { - "id": 39, - "size": 1065, - "file": "module_039.js", - "keywords": [] - }, - { - "id": 40, - "empty": true - }, - { - "id": 41, - "empty": true - }, - { - "id": 42, - "size": 666, - "file": "module_042.js", - "keywords": [] - }, - { - "id": 43, - "size": 443, - "file": "module_043.js", - "keywords": [ - "verify" - ] - }, - { - "id": 44, - "size": 1430, - "file": "module_044.js", - "keywords": [ - "verify" - ] - }, - { - "id": 45, - "empty": true - }, - { - "id": 46, - "empty": true - }, - { - "id": 47, - "size": 1244, - "file": "module_047.js", - "keywords": [] - }, - { - "id": 48, - "size": 4185, - "file": "module_048.js", - "keywords": [] - }, - { - "id": 49, - "empty": true - }, - { - "id": 50, - "empty": true - }, - { - "id": 51, - "empty": true - }, - { - "id": 52, - "empty": true - }, - { - "id": 53, - "size": 13636, - "file": "module_053.js", - "keywords": [ - "verify" - ] - }, - { - "id": 54, - "empty": true - }, - { - "id": 55, - "empty": true - }, - { - "id": 56, - "empty": true - }, - { - "id": 57, - "empty": true - }, - { - "id": 58, - "empty": true - }, - { - "id": 59, - "empty": true - }, - { - "id": 60, - "empty": true - }, - { - "id": 61, - "empty": true - }, - { - "id": 62, - "empty": true - }, - { - "id": 63, - "empty": true - }, - { - "id": 64, - "empty": true - }, - { - "id": 65, - "size": 713, - "file": "module_065.js", - "keywords": [] - }, - { - "id": 66, - "size": 10159, - "file": "module_066.js", - "keywords": [ - "verify", - "pow" - ] - }, - { - "id": 67, - "size": 4071, - "file": "module_067.js", - "keywords": [] - }, - { - "id": 68, - "size": 825, - "file": "module_068.js", - "keywords": [] - }, - { - "id": 69, - "size": 426, - "file": "module_069.js", - "keywords": [] - }, - { - "id": 70, - "empty": true - }, - { - "id": 71, - "empty": true - }, - { - "id": 72, - "empty": true - }, - { - "id": 73, - "empty": true - }, - { - "id": 74, - "empty": true - }, - { - "id": 75, - "empty": true - }, - { - "id": 76, - "empty": true - }, - { - "id": 77, - "empty": true - }, - { - "id": 78, - "empty": true - }, - { - "id": 79, - "empty": true - }, - { - "id": 80, - "empty": true - }, - { - "id": 81, - "empty": true - }, - { - "id": 82, - "empty": true - }, - { - "id": 83, - "empty": true - }, - { - "id": 84, - "empty": true - }, - { - "id": 85, - "empty": true - }, - { - "id": 86, - "size": 10832, - "file": "module_086.js", - "keywords": [ - "verify" - ] - }, - { - "id": 87, - "size": 1110, - "file": "module_087.js", - "keywords": [] - }, - { - "id": 88, - "size": 3631, - "file": "module_088.js", - "keywords": [] - }, - { - "id": 89, - "size": 6698, - "file": "module_089.js", - "keywords": [] - }, - { - "id": 90, - "size": 10256, - "file": "module_090.js", - "keywords": [ - "verify", - "prehandle" - ] - }, - { - "id": 91, - "size": 141, - "file": "module_091.js", - "keywords": [] - }, - { - "id": 92, - "size": 627, - "file": "module_092.js", - "keywords": [] - }, - { - "id": 93, - "size": 141, - "file": "module_093.js", - "keywords": [] - }, - { - "id": 94, - "size": 1265, - "file": "module_094.js", - "keywords": [ - "verify" - ] - }, - { - "id": 95, - "empty": true - }, - { - "id": 96, - "empty": true - }, - { - "id": 97, - "empty": true - }, - { - "id": 98, - "empty": true - }, - { - "id": 99, - "empty": true - }, - { - "id": 100, - "empty": true - }, - { - "id": 101, - "empty": true - }, - { - "id": 102, - "empty": true - }, - { - "id": 103, - "empty": true - }, - { - "id": 104, - "empty": true - }, - { - "id": 105, - "empty": true - }, - { - "id": 106, - "empty": true - }, - { - "id": 107, - "empty": true - }, - { - "id": 108, - "empty": true - }, - { - "id": 109, - "empty": true - }, - { - "id": 110, - "empty": true - }, - { - "id": 111, - "empty": true - }, - { - "id": 112, - "empty": true - }, - { - "id": 113, - "empty": true - }, - { - "id": 114, - "empty": true - }, - { - "id": 115, - "empty": true - }, - { - "id": 116, - "empty": true - }, - { - "id": 117, - "empty": true - }, - { - "id": 118, - "empty": true - }, - { - "id": 119, - "empty": true - }, - { - "id": 120, - "size": 13196, - "file": "module_120.js", - "keywords": [ - "verify", - "collect", - "prehandle", - "subcapclass", - "ticket" - ] - }, - { - "id": 121, - "size": 20314, - "file": "module_121.js", - "keywords": [ - "verify", - "prehandle", - "subcapclass", - "ticket" - ] - } -] \ No newline at end of file diff --git a/tcaptcha/core_js/frame_modules/module_000.js b/tcaptcha/core_js/frame_modules/module_000.js deleted file mode 100644 index 70b7afe..0000000 --- a/tcaptcha/core_js/frame_modules/module_000.js +++ /dev/null @@ -1,194 +0,0 @@ -function (e, t, i) { - "use strict"; - - var a = this && this.__importDefault || function (e) { - return e && e.__esModule ? e : { - "default": e - }; - }; - t.__esModule = !0, t.setScaleWidth = t.changeDomVisibility = t.domParserHtml = t.setVoiceOverFocus = t.bodyAriaHidden = t.addAriaModel = t.isTouchEventSupported = t.supportsPassive = t.isWindow = t.getWindow = t.getOffset = t.isDarkMode = t.shakeEl = t.addOnceAnimationClass = t.animationEndName = t.removeDomClass = t.addDomClass = t.setCss = t.getCSS = t.supportsCSS = t.createGeneralIframe = void 0; - var r = i(3), - n = a(i(20)), - o = i(21), - s = a(i(8)), - c = i(4); - t.createGeneralIframe = function (e) { - var t = document.createElement("div"); - (null === e || void 0 === e ? void 0 : e.name) ? t.innerHTML = '