feat(tcaptcha): 完成腾讯天御点选纯协议全自动化客户端与雨云业务适配器

This commit is contained in:
yml2213
2026-09-11 23:36:23 +08:00
parent 8df4493994
commit 5220c67a99
6 changed files with 889 additions and 15 deletions
+83
View File
@@ -0,0 +1,83 @@
"""
腾讯天御点选验证码纯协议端到端实战压测基准脚本
连续发起 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)
+327
View File
@@ -0,0 +1,327 @@
"""
腾讯天御点选验证码工业级纯协议客户端 (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. Node.js 沙箱环境运行 tdc.js 并拟合生成拟人化贝塞尔轨迹密文 collect 与 eks
7. POST /cap_union_new_verify 提交验证,获取最终 ticket 与 randstr
"""
import hashlib
import json
import logging
import random
import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
import httpx
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"
WORKER_SCRIPT = PROJECT_ROOT / "tcaptcha/tdc_worker.js"
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]:
"""通过 Node.js 沙箱运行 tdc.js 生成真实行为轨迹遥测数据"""
points_arg = json.dumps([[int(p[0]), int(p[1])] for p in click_points])
proc = subprocess.run(
["node", str(self.WORKER_SCRIPT), "-", points_arg],
input=tdc_code,
text=True,
capture_output=True,
check=True,
)
data = json.loads(proc.stdout.strip())
return {
"collect": data.get("collect", ""),
"tlg": str(data.get("tlg", 0)),
"eks": data.get("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. Node 沙箱环境生成真实轨迹 collect 与 eks
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()
+115
View File
@@ -0,0 +1,115 @@
"""
雨云 (Rainyun) 业务接口适配器 (Rainyun Captcha Adapter)
将腾讯天御点选验证码纯协议客户端无缝注入雨云核心业务流程:
1. 任务签到与积分领取: POST /user/reward/tasks (带 vticket 与 vrandstr)
2. 账号密码登录: POST /user/login (带 vticket 与 vrandstr)
"""
import json
import logging
from typing import Dict, Optional, Tuple
import httpx
from tcaptcha.client import TencentCaptchaClient
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("rainyun_adapter")
class RainyunAdapter:
API_BASE = "https://api.v2.rainyun.com"
RAINYUN_AID = "2039519451"
def __init__(
self,
api_key: Optional[str] = None,
cookie: Optional[str] = None,
timeout: float = 15.0,
):
self.api_key = api_key
self.cookie = cookie
self.timeout = timeout
self.captcha_client = TencentCaptchaClient(aid=self.RAINYUN_AID)
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://app.rainyun.com/",
"Origin": "https://app.rainyun.com",
"Accept": "application/json, text/plain, */*",
}
if self.api_key:
headers["X-Api-Key"] = self.api_key
if self.cookie:
headers["Cookie"] = self.cookie
self.http_client = httpx.Client(
base_url=self.API_BASE,
trust_env=False,
headers=headers,
timeout=self.timeout,
)
def get_captcha_ticket(self) -> Tuple[Optional[str], Optional[str]]:
"""调用纯协议验证码求解管线,获取有效 ticket 与 randstr"""
logger.info("正在调用腾讯天御验证码求解客户端...")
res = self.captcha_client.solve()
if res["success"]:
logger.info(f"验证码求解成功 (耗时: {res['elapsed_ms']}ms, randstr: {res['randstr']})")
return res["ticket"], res["randstr"]
else:
logger.error(f"验证码求解失败: {res['error']}")
return None, None
def claim_reward_task(self, task_name: str = "加入用户群") -> Dict:
"""
带验证码提交雨云积分任务
接口: POST https://api.v2.rainyun.com/user/reward/tasks
"""
ticket, randstr = self.get_captcha_ticket()
if not ticket or not randstr:
return {"code": -1, "message": "验证码获取失败"}
payload = {
"task_name": task_name,
"verifyCode": "",
"vticket": ticket,
"vrandstr": randstr,
}
logger.info(f"正在提交雨云任务: {task_name}...")
resp = self.http_client.post("/user/reward/tasks", json=payload)
try:
res_json = resp.json()
logger.info(f"雨云任务接口返回: {res_json}")
return res_json
except Exception as e:
logger.error(f"雨云任务接口解析失败: {e}")
return {"code": resp.status_code, "text": resp.text}
def login(self, field: str, password: str) -> Dict:
"""
雨云用户登录 (自动注入验证码凭证)
接口: POST https://api.v2.rainyun.com/user/login
"""
ticket, randstr = self.get_captcha_ticket()
if not ticket or not randstr:
return {"code": -1, "message": "验证码获取失败"}
payload = {
"field": field,
"password": password,
"verifyCode": "",
"vticket": ticket,
"vrandstr": randstr,
}
logger.info(f"正在发起雨云登录: {field}...")
resp = self.http_client.post("/user/login", json=payload)
try:
return resp.json()
except Exception as e:
return {"code": resp.status_code, "text": resp.text}
+15 -3
View File
@@ -9,12 +9,19 @@ import numpy as np
from PIL import Image
def slice_prompt_image(prompt_img_path: str | Path, padding: int = 2) -> List[Tuple[Image.Image, Tuple[int, int, int, int]]]:
import io
def slice_prompt_image(prompt_img: str | Path | bytes | Image.Image, padding: int = 2) -> List[Tuple[Image.Image, Tuple[int, int, int, int]]]:
"""
自动识别并切割提示图中的 3 个字符/图标
返回: [(PIL_Image, (x1, y1, x2, y2)), ...]
"""
img = Image.open(prompt_img_path).convert("L")
if isinstance(prompt_img, Image.Image):
img = prompt_img.convert("L")
elif isinstance(prompt_img, bytes):
img = Image.open(io.BytesIO(prompt_img)).convert("L")
else:
img = Image.open(prompt_img).convert("L")
arr = np.array(img)
h, w = arr.shape
@@ -48,7 +55,12 @@ def slice_prompt_image(prompt_img_path: str | Path, padding: int = 2) -> List[Tu
segments = [(i * step + 5, (i + 1) * step - 5) for i in range(3)]
# 原始彩色图用于切片
orig_img = Image.open(prompt_img_path).convert("RGBA")
if isinstance(prompt_img, Image.Image):
orig_img = prompt_img.convert("RGBA")
elif isinstance(prompt_img, bytes):
orig_img = Image.open(io.BytesIO(prompt_img)).convert("RGBA")
else:
orig_img = Image.open(prompt_img).convert("RGBA")
crops = []
for seg_start, seg_end in segments[:3]:
+17 -12
View File
@@ -7,11 +7,12 @@
4. 全局匹配: 全局最大后验概率分配 -> 输出毫秒级精准有序点击坐标 [(x1, y1), (x2, y2), (x3, y3)]
"""
import io
import itertools
import logging
import sys
from pathlib import Path
from typing import List, Tuple
from typing import List, Tuple, Union
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
@@ -75,11 +76,11 @@ class CaptchaSolver:
bin_t = cv2.resize(((arr < bg - 20).astype(np.uint8) * 255), (48, 48), interpolation=cv2.INTER_AREA)
self.templates.append((cid, bin_t))
def parse_prompt_targets(self, prompt_img_path: Path) -> List[int]:
def parse_prompt_targets(self, prompt_img: str | Path | bytes | Image.Image) -> List[int]:
"""
自动识别并解析提示图中 3 个目标图标的真实类别 ID (0~11)
"""
crops = slice_prompt_image(prompt_img_path)
crops = slice_prompt_image(prompt_img)
target_classes = []
for crop_img, _ in crops[:3]:
arr = np.array(crop_img.convert("L"))
@@ -92,12 +93,19 @@ class CaptchaSolver:
return target_classes
def _extract_candidates(self, bg_img_path: Path) -> List[Tuple[int, int, Image.Image]]:
def _extract_candidates(self, bg_img: str | Path | bytes | Image.Image) -> List[Tuple[int, int, Image.Image]]:
"""从背景图中提取所有去重后的候选图标 (cx, cy, patch)"""
results = self.detector(str(bg_img_path), conf=0.03, verbose=False)
if isinstance(bg_img, Image.Image):
bg_pil = bg_img.convert("RGB")
elif isinstance(bg_img, bytes):
bg_pil = Image.open(io.BytesIO(bg_img)).convert("RGB")
else:
bg_pil = Image.open(bg_img).convert("RGB")
# Ultralytics detector natively accepts PIL.Image
results = self.detector(bg_pil, conf=0.03, verbose=False)
boxes = results[0].boxes.xyxy.cpu().numpy()
bg_pil = Image.open(bg_img_path).convert("RGB")
w_img, h_img = bg_pil.size
raw_cands = []
@@ -120,22 +128,19 @@ class CaptchaSolver:
return candidates
def solve(self, bg_img_path: str | Path, prompt_img_path: str | Path) -> List[Tuple[int, int]]:
def solve(self, bg_img: str | Path | bytes | Image.Image, prompt_img: str | Path | bytes | Image.Image) -> List[Tuple[int, int]]:
"""
端到端求解验证码点选坐标
返回 3 个目标的像素坐标: [(x1, y1), (x2, y2), (x3, y3)]
"""
bg_path = Path(bg_img_path)
prompt_path = Path(prompt_img_path)
# 1. 确定提示图的目标序列 [T1, T2, T3]
target_classes = self.parse_prompt_targets(prompt_path)
target_classes = self.parse_prompt_targets(prompt_img)
if len(target_classes) < 3:
logger.warning("提示图未成功切出 3 个目标")
return []
# 2. 定位背景图所有候选图标
candidates = self._extract_candidates(bg_path)
candidates = self._extract_candidates(bg_img)
if len(candidates) < 3:
logger.warning(f"背景图检出候选图标数量不足: {len(candidates)}")
return []
+332
View File
@@ -0,0 +1,332 @@
/**
* Tencent TDC.js Sandboxed Worker & Mouse Trajectory Generator
* Runs TDC.js in an isolated mock DOM environment to generate authentic
* encrypted telemetry (`collect`) and session encryption key (`eks`).
*/
const fs = require('fs');
const vm = require('vm');
class Element {
constructor(tagName) {
this.tagName = (tagName || 'div').toUpperCase();
this.children = [];
this.style = {};
this.attributes = {};
}
appendChild(child) {
this.children.push(child);
return child;
}
removeChild(child) {
const idx = this.children.indexOf(child);
if (idx !== -1) this.children.splice(idx, 1);
return child;
}
insertBefore(newChild, refChild) {
const idx = this.children.indexOf(refChild);
if (idx !== -1) this.children.splice(idx, 0, newChild);
else this.children.push(newChild);
return newChild;
}
cloneNode(deep) {
const el = new Element(this.tagName);
el.style = { ...this.style };
return el;
}
setAttribute(k, v) { this.attributes[k] = String(v); }
getAttribute(k) { return this.attributes[k] || null; }
hasAttribute(k) { return k in this.attributes; }
removeAttribute(k) { delete this.attributes[k]; }
getBoundingClientRect() {
return { top: 0, left: 0, width: 360, height: 360, bottom: 360, right: 360 };
}
}
class CanvasElement extends Element {
constructor() {
super('CANVAS');
this.width = 300;
this.height = 150;
}
getContext(type) {
if (type.includes('webgl')) {
return {
getSupportedExtensions: () => [
'ANGLE_instanced_arrays', 'EXT_blend_minmax', 'EXT_texture_filter_anisotropic',
'OES_element_index_uint', 'OES_standard_derivatives', 'OES_texture_float',
'OES_texture_half_float', 'OES_vertex_array_object', 'WEBGL_compressed_texture_s3tc',
'WEBGL_debug_renderer_info', 'WEBGL_depth_texture', 'WEBGL_lose_context'
],
getExtension: (ext) => {
if (ext === 'WEBGL_debug_renderer_info') {
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
}
return null;
},
getParameter: (p) => {
if (p === 37445) return 'Apple';
if (p === 37446) return 'Apple M1';
if (p === 7936) return 'WebKit';
if (p === 7937) return 'WebKit WebGL';
return 0;
},
createBuffer: () => ({}),
bindBuffer: () => {},
bufferData: () => {},
createProgram: () => ({}),
createShader: () => ({}),
shaderSource: () => {},
compileShader: () => {},
attachShader: () => {},
linkProgram: () => {},
useProgram: () => {},
drawArrays: () => {},
viewport: () => {},
clearColor: () => {},
clear: () => {}
};
}
return {
fillRect: () => {},
fillText: () => {},
getImageData: () => ({ data: new Uint8ClampedArray(4) }),
measureText: (txt) => ({ width: txt.length * 10 }),
beginPath: () => {},
arc: () => {},
fill: () => {},
stroke: () => {}
};
}
toDataURL() {
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
}
}
/**
* Generate human-like mouse movement trajectory using cubic Bézier curves and Gaussian tremor.
*/
function generateBezierTrajectory(p0, p1, steps = 25) {
// Control points with natural curve deviation
const dx = p1.x - p0.x;
const dy = p1.y - p0.y;
const dist = Math.hypot(dx, dy);
// Lateral deviation perpendicular to direct vector
const perpX = -dy / (dist || 1);
const perpY = dx / (dist || 1);
const curvature = (Math.random() - 0.5) * Math.min(60, dist * 0.4);
const cp1 = {
x: p0.x + dx * 0.3 + perpX * curvature + (Math.random() - 0.5) * 10,
y: p0.y + dy * 0.3 + perpY * curvature + (Math.random() - 0.5) * 10,
};
const cp2 = {
x: p0.x + dx * 0.7 + perpX * curvature * 0.6 + (Math.random() - 0.5) * 10,
y: p0.y + dy * 0.7 + perpY * curvature * 0.6 + (Math.random() - 0.5) * 10,
};
const points = [];
for (let i = 0; i <= steps; i++) {
const t = i / steps;
// Ease in-out parameterization
const easeT = t * t * (3 - 2 * t);
const invT = 1 - easeT;
const bx = invT * invT * invT * p0.x +
3 * invT * invT * easeT * cp1.x +
3 * invT * easeT * easeT * cp2.x +
easeT * easeT * easeT * p1.x;
const by = invT * invT * invT * p0.y +
3 * invT * invT * easeT * cp1.y +
3 * invT * easeT * easeT * cp2.y +
easeT * easeT * easeT * p1.y;
// Subtle tremor (except start and end)
const tremor = (i === 0 || i === steps) ? 0 : (Math.random() - 0.5) * 1.5;
points.push({
x: Math.round(bx + tremor),
y: Math.round(by + tremor)
});
}
return points;
}
function runTdcCode(rawCode, clickPoints) {
const listeners = [];
function addL(type, fn) {
listeners.push({ type, fn });
}
const docBody = new Element('BODY');
const docElement = new Element('HTML');
const baseWindow = {
location: {
href: 'https://turing.captcha.qcloud.com/cap_union_new_show?aid=2039519451',
protocol: 'https:',
host: 'turing.captcha.qcloud.com',
hostname: 'turing.captcha.qcloud.com',
pathname: '/cap_union_new_show',
search: '?aid=2039519451',
hash: ''
},
document: {
createElement: (tag) => {
if ((tag || '').toLowerCase() === 'canvas') return new CanvasElement();
return new Element(tag);
},
addEventListener: addL,
removeEventListener: () => {},
body: docBody,
documentElement: docElement,
referrer: 'https://app.rainyun.com/',
cookie: '',
getElementsByTagName: (t) => [t === 'body' ? docBody : docElement],
getElementById: () => null,
title: '安全验证',
head: new Element('HEAD')
},
navigator: {
userAgent: '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',
platform: 'MacIntel',
languages: ['zh-CN', 'zh', 'en'],
language: 'zh-CN',
cookieEnabled: true,
plugins: [{ name: 'PDF Viewer' }],
mimeTypes: []
},
screen: {
width: 1920,
height: 1080,
availWidth: 1920,
availHeight: 1055,
colorDepth: 24,
pixelDepth: 24
},
performance: {
now: () => Date.now(),
timing: {
navigationStart: Date.now() - 2500,
loadEventEnd: Date.now() - 800
}
},
history: { length: 2 },
indexedDB: {},
sessionStorage: { getItem: () => null, setItem: () => {} },
localStorage: { getItem: () => null, setItem: () => {} },
Date: Date,
Math: Math,
setTimeout: setTimeout,
clearTimeout: clearTimeout,
setInterval: setInterval,
clearInterval: clearInterval,
console: { log: () => {}, error: () => {}, warn: () => {} },
innerWidth: 360,
innerHeight: 360,
outerWidth: 1920,
outerHeight: 1080,
devicePixelRatio: 2,
addEventListener: addL,
removeEventListener: () => {}
};
baseWindow.window = baseWindow;
baseWindow.self = baseWindow;
baseWindow.top = baseWindow;
baseWindow.parent = baseWindow;
// Execute TDC.js
vm.runInNewContext(rawCode, baseWindow);
// Map 672x480 coordinates to 360x360 iframe display space
const scale = 320.0 / 672.0;
const targetPoints = clickPoints.map(p => ({
x: Math.round(20 + p[0] * scale),
y: Math.round(60 + p[1] * scale)
}));
// Generate path: entrance -> P1 -> P2 -> P3
let currentPoint = {
x: Math.round(150 + (Math.random() - 0.5) * 60),
y: Math.round(320 + (Math.random() - 0.5) * 20)
};
const mmListener = listeners.find(l => l.type === 'mousemove');
let currentTime = Date.now() - 1500;
if (mmListener) {
for (const target of targetPoints) {
// Leg trajectory
const steps = Math.floor(18 + Math.random() * 12);
const leg = generateBezierTrajectory(currentPoint, target, steps);
for (const pt of leg) {
currentTime += Math.floor(12 + Math.random() * 16);
mmListener.fn({
clientX: pt.x,
clientY: pt.y,
pageX: pt.x,
pageY: pt.y,
screenX: pt.x + 300,
screenY: pt.y + 200,
timeStamp: currentTime,
type: 'mousemove',
isTrusted: true
});
}
currentPoint = target;
// Dwell pause on icon (150ms ~ 350ms)
const dwellSteps = Math.floor(4 + Math.random() * 5);
for (let i = 0; i < dwellSteps; i++) {
currentTime += Math.floor(25 + Math.random() * 30);
mmListener.fn({
clientX: currentPoint.x + Math.round((Math.random() - 0.5) * 2),
clientY: currentPoint.y + Math.round((Math.random() - 0.5) * 2),
pageX: currentPoint.x,
pageY: currentPoint.y,
screenX: currentPoint.x + 300,
screenY: currentPoint.y + 200,
timeStamp: currentTime,
type: 'mousemove',
isTrusted: true
});
}
}
}
const collect = baseWindow.TDC.getData();
const info = baseWindow.TDC.getInfo();
const eks = info ? info.info : '';
return {
collect,
tlg: collect ? collect.length : 0,
eks
};
}
function runTdc(tdcFilePath, clickPoints) {
const rawCode = fs.readFileSync(tdcFilePath, 'utf8');
return runTdcCode(rawCode, clickPoints);
}
if (require.main === module) {
const tdcPath = process.argv[2];
const pointsArg = process.argv[3];
const points = pointsArg ? JSON.parse(pointsArg) : [[626, 253], [120, 120], [438, 33]];
if (tdcPath === '-') {
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', () => {
const result = runTdcCode(input, points);
console.log(JSON.stringify(result));
});
} else {
const result = runTdc(tdcPath, points);
console.log(JSON.stringify(result));
}
}
module.exports = { runTdc, runTdcCode };