313 lines
9.4 KiB
Python
313 lines
9.4 KiB
Python
"""
|
|
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]}...")
|