Files
hello_js_reverse_skill/tcaptcha/client.py
T

328 lines
13 KiB
Python

"""
腾讯天御点选验证码工业级纯协议客户端 (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()