feat(tcaptcha): 完成腾讯天御TDC虚拟机纯算法逆向还原与100%纯Python独立客户端

This commit is contained in:
yml2213
2026-09-12 00:02:32 +08:00
parent 5220c67a99
commit c774d09bdd
2 changed files with 321 additions and 17 deletions
+9 -17
View File
@@ -15,18 +15,18 @@ 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
import httpx
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.crypto import extract_eks, generate_collect
from tcaptcha.solver import CaptchaSolver
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
@@ -36,7 +36,6 @@ 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,
@@ -76,20 +75,13 @@ class TencentCaptchaClient:
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())
"""纯 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": data.get("collect", ""),
"tlg": str(data.get("tlg", 0)),
"eks": data.get("eks", ""),
"collect": collect,
"tlg": str(len(collect)),
"eks": eks,
}
def solve(self, max_retries: int = 2) -> Dict:
+312
View File
@@ -0,0 +1,312 @@
"""
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("<II", data[i : i + 8])
ev0, ev1 = xtea_encrypt_block(v0, v1, key)
out.extend(struct.pack("<II", ev0, ev1))
return base64.b64encode(out).decode("ascii")
def _bezier_interp(
p0: Tuple[float, float],
p1: Tuple[float, float],
steps: int,
) -> 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]}...")