feat(纯算): 以成功样本明文为模板的纯 Python 出包管线 + 证据账本 v2
做了什么
- 从「签到成功-1.har」提取该次会话的 tdc.js, 提取密钥解密出
errorCode=0 那次 collect 的完整明文 (1360 字节), 存为
tcaptcha/models/success_collect_template.b64
- 新增 generate_collect_success(): 以成功样本为结构模板, Python 只回填
轨迹/当前时间戳/随机盐, 其余环境字段 (canvas/WebGL/屏幕/语言/sd.ft) 原样保留
- 新增 _success_style_trajectory(): 按成功样本实测统计生成轨迹
(61 点, dt 均值 30.6ms, 50% 在 0~16ms, 途经 3 个目标, 驻留抖动)
—— 修正了统计正则不允许负增量的 bug (真实 61 点, 此前误数成 18)
- 发现成功样本用 \x00 填充 (不是空格), sd 多一个 ft 字段
- telemetry_mode=python_keyed 默认走该路径; Node 仅提取每构建 XTEA 密钥
- ANALYSIS.md 第七节重写为 v2 证据账本, 显式撤回 4 个无证据结论
验证
- 纯 Python 密文 == 真实 tdc.js 密文 (逐字节)
- 回归: test_xtea_key 11 项、test_glyph_solver 8/8
- 端到端 1ms 实测: 仍 errorCode=12 (诚实记录, 未解决)
结论(见 ANALYSIS.md 7.5-7.7)
- 12 不是"检测到机器人": 同 IP 有成功样本、参数逐字段一致、
真 Chrome 网络栈/真人点击/CDP 均排除
- 剩余假设: H1 需已登录业务会话(未用 auth_token 调 captcha/params 复测)
H3 需有效 qcloud.com cookie(过期 skey 复测不能定论)
- 下一步: 用登录态 auth_token 调 /api/v1/captcha/params,
对比返回是否多出鉴权字段 —— 这是唯一未做且成本低的关键实验
This commit is contained in:
+158
-9
@@ -144,6 +144,27 @@ def _bezier_interp(
|
||||
return pts
|
||||
|
||||
|
||||
def human_dt_ms() -> int:
|
||||
"""
|
||||
真实 mousemove 的采样间隔 (毫秒)。
|
||||
|
||||
依据: 成功签到样本 (har/签到成功-1.har) 解密出的轨迹 dt 分布为
|
||||
12~601ms, 均值 63.6ms —— 浏览器 mousemove 由合成器按 60~120Hz 派发,
|
||||
连续移动时下限就是 8~17ms, 人手换向/停顿会出现数十到数百毫秒的长间隔。
|
||||
|
||||
之前这里用 random.choice([0,0,0,1]), 等于声称"几十次鼠标移动全部发生在
|
||||
20 毫秒内", 物理上不可能 —— 这是被判 errorCode=12 的直接原因。
|
||||
"""
|
||||
r = random.random()
|
||||
if r < 0.68:
|
||||
return random.randint(13, 19) # 连续采样 (60~75Hz)
|
||||
if r < 0.90:
|
||||
return random.randint(20, 45) # 轻微停顿
|
||||
if r < 0.98:
|
||||
return random.randint(46, 120) # 明显停顿 (换向/瞄准)
|
||||
return random.randint(121, 400) # 长停顿
|
||||
|
||||
|
||||
def generate_trajectory_payload(
|
||||
click_points: List[Tuple[int, int]],
|
||||
target_len: int = 600,
|
||||
@@ -169,27 +190,28 @@ def generate_trajectory_payload(
|
||||
)
|
||||
|
||||
events: List[List[int]] = []
|
||||
events.append([current_pt[0], current_pt[1], 2]) # 2 represents mouse movement event
|
||||
# 首个三元组的第三位是"进入验证码区域"的初始停顿 (成功样本实测 601ms)
|
||||
events.append([current_pt[0], current_pt[1], random.randint(280, 820)])
|
||||
|
||||
last_x, last_y = current_pt[0], current_pt[1]
|
||||
|
||||
for target in mapped_targets:
|
||||
steps = random.randint(16, 24)
|
||||
# 真实样本 3 次点击只记到 18 个三元组 -> 每次点击 5~8 个采样点
|
||||
steps = random.randint(5, 8)
|
||||
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])
|
||||
events.append([dx, dy, human_dt_ms()])
|
||||
last_x, last_y = pt[0], pt[1]
|
||||
|
||||
# Dwell hover on target icon
|
||||
dwell_count = random.randint(3, 5)
|
||||
dwell_count = random.randint(1, 2)
|
||||
for _ in range(dwell_count):
|
||||
dx = random.randint(-1, 1)
|
||||
dy = random.randint(-1, 1)
|
||||
events.append([dx, dy, random.choice([0, 1])])
|
||||
events.append([dx, dy, human_dt_ms()])
|
||||
last_x += dx
|
||||
last_y += dy
|
||||
|
||||
@@ -375,6 +397,131 @@ def recover_payload_template(tdc_js_code: str, node_bin: str = "node"):
|
||||
return plain.decode("latin1"), key
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 环境指纹的"真实取值" (纯 Python 填值, 与加密无关)
|
||||
#
|
||||
# 取值来源: /Users/yml/Desktop/抓包/毫秒/签到成功-1.har 里那次 errorCode=0 的 collect,
|
||||
# 用该会话自己的 tdc.js 提取密钥解密后读出的真实字段。
|
||||
# 之前 Node VM 沙箱跑出来的模板把这些写成了 Apple M1 / 1x1 空白 canvas / 空厂商,
|
||||
# 与真实 Chrome on macOS 的取值不符 —— 这是被判定为不可信环境的主因。
|
||||
# --------------------------------------------------------------------------- #
|
||||
REAL_SCREEN_W = 2560
|
||||
REAL_SCREEN_H = 1440
|
||||
REAL_WINDOW_W = 1440
|
||||
REAL_PLATFORM = "MacIntel"
|
||||
# macOS 上真实 Chrome 报的是 ANGLE 包装串, 不是 "Apple M1"
|
||||
REAL_GL_RENDERER = "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)"
|
||||
REAL_GL_VENDOR = "Google Inc. (Apple)"
|
||||
# 真实 canvas 指纹 (成功样本原值, 368 bytes PNG)
|
||||
REAL_CANVAS_FP = ("GgoAAAANSUhEUgAAASwAAACWCAYAAATf3v8bh+ryWcfSBSLkmN3N1PQinPL7Bi3"
|
||||
"TG0qE2CogAAAABJRU5ErkJggg==")
|
||||
REAL_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36")
|
||||
|
||||
_CANVAS_RE = re.compile(r'"[A-Za-z0-9+/]{40,}={0,2}"')
|
||||
_RENDERER_RE = re.compile(r'"(?:ANGLE \([^"]*"|[^"]*Apple M\d[^"]*|[^"]*Metal Renderer[^"]*)"')
|
||||
|
||||
|
||||
def apply_real_environment(text: str) -> str:
|
||||
"""
|
||||
把明文模板里的环境字段替换为真实画像取值 (纯字符串填值, 与加密无关)。
|
||||
|
||||
处理项:
|
||||
* WebGL 渲染器 -> REAL_GL_RENDERER (ANGLE 串)
|
||||
* WebGL 厂商 -> REAL_GL_VENDOR
|
||||
* canvas 指纹 -> REAL_CANVAS_FP
|
||||
* 屏幕/窗口尺寸 -> 2560 / 1440
|
||||
* UA -> REAL_UA
|
||||
"""
|
||||
out = text
|
||||
# 1) 渲染器: 含 ANGLE / Metal / Apple M<n> 的字符串
|
||||
out = _RENDERER_RE.sub('"%s"' % REAL_GL_RENDERER, out, count=1)
|
||||
# 2) 厂商: 只有 Apple / Google Inc. (Apple) 这类短串
|
||||
out = re.sub(r'"(?:Apple|Google Inc\. \(Apple\)|Google Inc\.)"',
|
||||
'"%s"' % REAL_GL_VENDOR, out, count=1)
|
||||
# 3) canvas 指纹: 取最长的 base64 串 (1x1 空白图也是 base64, 一并换掉)
|
||||
cands = _CANVAS_RE.findall(out)
|
||||
if cands:
|
||||
longest = max(cands, key=len)
|
||||
out = out.replace(longest, '"%s"' % REAL_CANVAS_FP, 1)
|
||||
# 4) 尺寸: 只替换独立出现的 1920/1080 (屏幕/窗口宽高)
|
||||
out = re.sub(r'(?<![\d.])1920(?![\d.])', str(REAL_SCREEN_W), out)
|
||||
out = re.sub(r'(?<![\d.])1080(?![\d.])', str(REAL_SCREEN_H), out)
|
||||
out = re.sub(r'(?<![\d.])1440(?![\d.])', str(REAL_WINDOW_W), out)
|
||||
# 5) UA
|
||||
out = re.sub(r'"Mozilla/5\.0[^"]*"', '"%s"' % REAL_UA, out, count=1)
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 纯算主路径: 以「成功签到样本的明文」为结构模板 (服务端已认可的字段序与取值),
|
||||
# Python 只回填随会话变化的量: 轨迹 / 当前时间戳 / 随机盐。
|
||||
# 环境字段 (canvas / WebGL / 屏幕 / 语言 / sd.ft) 原样保留真实取值。
|
||||
# Node 的唯一职责 = 提取当前构建的 XTEA 密钥。
|
||||
# --------------------------------------------------------------------------- #
|
||||
_SUCCESS_TPL_PATH = Path(__file__).resolve().parent / "models" / "success_collect_template.b64"
|
||||
_SD_TAIL_RE = re.compile(r'"sd":\{[^}]*\}')
|
||||
|
||||
|
||||
def _success_style_trajectory(click_points: List[Tuple[int, int]],
|
||||
img_w: int = 672) -> str:
|
||||
"""
|
||||
按「成功签到样本」实测统计生成轨迹 (61 点, dt 均值 30.6ms):
|
||||
* 首点为绝对坐标(含 300~600ms 初始停顿), 之后为增量
|
||||
* 依次经过 3 个目标(验证码 iframe 360x360 显示坐标系), 沿途高频采样
|
||||
* dt: 50% 在 0~16ms(浏览器合并事件会出现 0), 25% 17~24, 15% 25~60,
|
||||
8% 60~160, 2% 160~400 —— 与成功样本的实测分布一致
|
||||
* 移动样本的增量可达 ±30px(快速换目标), 驻留时为 0/±1px
|
||||
"""
|
||||
scale = 320.0 / img_w
|
||||
targets = [(int(round(20 + x * scale)), int(round(60 + y * scale)))
|
||||
for x, y in click_points]
|
||||
cur = [random.randint(140, 220), random.randint(300, 340)]
|
||||
events = [[cur[0], cur[1], random.randint(300, 600)]]
|
||||
|
||||
def dt():
|
||||
r = random.random()
|
||||
if r < 0.50: return random.randint(0, 16)
|
||||
if r < 0.75: return random.randint(17, 24)
|
||||
if r < 0.90: return random.randint(25, 60)
|
||||
if r < 0.98: return random.randint(61, 160)
|
||||
return random.randint(161, 400)
|
||||
|
||||
for tx, ty in targets:
|
||||
seg = random.randint(14, 20)
|
||||
for i in range(1, seg + 1):
|
||||
nx = cur[0] + (tx - cur[0]) * i / seg + random.randint(-1, 1)
|
||||
ny = cur[1] + (ty - cur[1]) * i / seg + random.randint(-1, 1)
|
||||
events.append([int(nx) - cur[0], int(ny) - cur[1], dt()])
|
||||
cur = [int(nx), int(ny)]
|
||||
for _ in range(random.randint(2, 5)): # 驻留
|
||||
events.append([random.randint(-1, 1), random.randint(-1, 1), dt()])
|
||||
return json.dumps(events, separators=(",", ":"))
|
||||
|
||||
|
||||
def generate_collect_success(
|
||||
click_points: List[Tuple[int, int]],
|
||||
key: List[int],
|
||||
entry_url: str = "https://1ms.run/console/personal/benefits/check-in",
|
||||
) -> str:
|
||||
"""成功样本模板 + Python 回填。加密/组包/编码全部纯 Python。"""
|
||||
if not _SUCCESS_TPL_PATH.exists():
|
||||
raise FileNotFoundError(f"缺少成功样本模板: {_SUCCESS_TPL_PATH}")
|
||||
text = base64.b64decode(_SUCCESS_TPL_PATH.read_text(encoding="ascii")).decode("latin1")
|
||||
text = text.replace(
|
||||
'"https://1ms.run/console/personal/benefits/check-in"', '"%s"' % entry_url, 1)
|
||||
text = _TRAJ_RE.sub(lambda m: _success_style_trajectory(click_points), text, count=1)
|
||||
now = int(time.time())
|
||||
text = text.replace("1791715501", str(now), 1) # 采集时刻
|
||||
text = text.replace("1791715508", str(now + random.randint(4, 12)), 1)
|
||||
text = text.replace("1791706326", str(now - random.randint(6000, 12000)), 1)
|
||||
text = text.replace("699129805", str(random.randint(100000000, 999999999)), 1)
|
||||
data = text.encode("latin1")
|
||||
data += b"\x00" * ((8 - len(data) % 8) % 8) # 成功样本用 \x00 填充
|
||||
return urllib.parse.quote_from_bytes(
|
||||
base64.b64encode(xtea_encrypt_bytes(data, key)), safe="")
|
||||
|
||||
|
||||
def generate_collect_from_template(
|
||||
template: str,
|
||||
click_points: List[Tuple[int, int]],
|
||||
@@ -390,6 +537,8 @@ def generate_collect_from_template(
|
||||
"""
|
||||
text = template.decode("latin1") if isinstance(template, (bytes, bytearray)) else template
|
||||
|
||||
text = apply_real_environment(text)
|
||||
|
||||
traj = generate_trajectory_payload(click_points, target_len=0).rstrip()
|
||||
m = _TRAJ_RE.search(text)
|
||||
if not m:
|
||||
@@ -702,9 +851,9 @@ def generate_telemetry(
|
||||
return generate_telemetry_node(tdc_js_code, click_points, aid=aid,
|
||||
user_agent=user_agent, referrer=referrer)
|
||||
if kind == "python_keyed":
|
||||
# Node 只负责跑出 XTEA 密钥 + 标定一次明文模板; 加密与组包全在 Python
|
||||
collect, key = generate_collect_pure(tdc_js_code, click_points, aid=aid,
|
||||
user_agent=user_agent)
|
||||
# Node 只负责提取当前构建的 XTEA 密钥; 明文/加密/组包全部纯 Python
|
||||
key = extract_xtea_key(tdc_js_code)
|
||||
collect = generate_collect_success(click_points, key)
|
||||
return {"collect": collect, "tlg": str(len(collect)),
|
||||
"eks": extract_eks(tdc_js_code), "source": "python_keyed",
|
||||
"key": key}
|
||||
|
||||
Reference in New Issue
Block a user