Files
douyin-captcha/docs/retrospective.md
T
杨豪 cae37465d2 feat: 尺度精修上线,首次机器 PASS(live_160548 s=1.0375)
- method_l_shape: OK 候选尺度 ±0.0375 精修(步长 0.0125 + ±6px 窗口),
  GT live_165204 Δ 179→176(真人 177),离线 9/10 保持
- 插桩 JSON.stringify 捕获 SDK 日志明文;指针 probe 排除 coalesced/pressure
- 拖动不跟手指标作废:post_pos 晚于 SDK 重置滑块,鼠标事件流始终完整
- 剩余 VerifyErr = 候选整体选错(视觉确证),方向:多候选质量评估
- docs: experiments/solution/retrospective 同步收尾
2026-09-15 16:20:05 +08:00

9.2 KiB
Raw Blame History

项目复盘:抖音「拖动重叠」验证码离线求解与自动化通过

本文是整个项目的经验总复盘:任务、四个阶段的演进、决定性发现、 踩坑清单、方法论沉淀与未竟事项。细节以各专题文档为准 (索引见 README.md)。

1. 任务与最终状态

任务:给定验证码大图(552×344)与 mark 小图(110×110 RGBA), 离线求出 mark 应拖动重叠的目标区域偏移;进一步在浏览器里自动完成 「触发 → 采题 → 求解 → 拖动 → 通过」闭环。

最终状态

层面 状态
离线求解(captchas/ 17 组样本) 方向感知倒角法 10/10 精确命中,src/solve.py 交付
线上求解(URL 采题) tip_y 锚定后 8 题 7 中(1 题差 1px 出容差)
拖动位置换算 实测校准(按钮 1:1 带 mark,x × 340/552
拟人轨迹生成 三要素路径(非直线/非匀速/过冲回拉),try/human_drag.py
行为风控(5014 定位关键指纹(CDP pressure=0)并修复(force=0.5
端到端一次通过 ⏸ 被本机 IP 深度频控(5009)阻塞,需换环境冷启动验证

2. 四个阶段的演进

阶段一:离线求解方法调研(A → L

从 RGB 模板匹配(A/A2)出发,历经 SIFT(B)、旋转 NCC(C)、倒角(D/E)、 轮廓匹配(F)、各类分割(G–K),共 12 条路线全部失败或受限后, 才确认两条决定性事实(详见 findingsexperiments):

  1. mark 的 RGB 是"捐赠补丁"——内部是场景纹理,与洞内内容无像素关系 (掩码 NCC 全图 ≤0.57),一切 RGB/灰度模板匹配原理上不可行;
  2. 唯一可靠信号是 alpha 轮廓 + 尺度 + 旋转——洞是叠加在照片上的实心 暗色覆盖物,形状=mark 的 alpha 轮廓,尺度≈1.0、旋转≈0;干扰项是同形的 缩放/旋转实例。

最终方案 L:方向感知倒角(9 法向桶 × 多尺度)+ 洞内部特征(边缘密度、 内-环亮度差)+ 局部旋转扫描,17 组样本 10/10 精确命中。

教训:12 条失败路线里没有一条是"实现 bug",全是信号假设错误。 早期就该做"掩码 NCC 上限"这类决定性实验先证伪 RGB 路线,而不是逐个换实现。

阶段二:浏览器触发与样本采集

  • 抖音页面预加载验证码 iframedisplay:none),「iframe 存在 ≠ 触发」, 判定要看 offsetWidth > 0
  • wasm/GPU 渲染让 <img src> 与截图法失效,但图片 URL 会正常发 HTTP 请求 → 网络层抓 captcha/get 响应 + 下载 url1/url2 即得与离线同规格的样本对;
  • 最可靠的触发法:密码登录弹窗(错误密码也必弹验证码);
  • tip_y 服务端真值question.tip_y × 2 = 缺口顶 ymark 画布顶坐标系)。

阶段三:拖动闭环与行为风控(HOOK 路线)

闭环管线(try/auto_solve_drag.py):点刷新 → 采 get → solve → 拟人路径 → CDP 拖动 → 读 verifyget→verify 全程 <8schallenge 有效期约 6090s)。

拟人轨迹三要素(try/human_drag.py):贝塞尔弓形骨架+高斯抖动(非直线)、 三段速度结构(非匀速)、过冲后回拉瞄准。但最初 CDP 拖动仍全部 5014。

SDK 逆向(webmssdk/bdms/captcha.js 反混淆)确认:mousemove 入列规则、 X-Bogus 行为位(kMoveFast=16 等)、isTrusted 采集、captchaBody 在 XHR.send 前已 wasm 加密。CDP 事件确实进入了 SDK 缓冲且 isTrusted=true——问题不在 事件送达,在事件属性。

阶段四:决定性突破与 solve 增强

  1. pressure 指纹CDP 缺省产生的 MouseEvent pressure=0,W3C 规范里 真鼠标恒 0.5。三个鼠标事件全加 force:0.5 后,错误码从 5014(行为拒) 稳定变为 VerifyErr(位置判定被执行)——行为闸门通过。
  2. 错误码语义分层(多次连线实验归纳): 5014 行为拒VerifyErr 位置判了但错5009 频控(压住一切)NotFoundChallengeId challenge 过期任何改动的效果判定都必须在频控 冷却后做单次干净尝试,连环重试的 5014/5009 是无效信号。
  3. 拖动映射实测:按钮 1:1 带动 mark 预览;mark 初始相对大图 (0, tip_y×2)(每题垂直自动对齐,拖动只控 x);拖动距离 = solve.x × 340/552
  4. 线上分布漂移与 tip_y 锚定:线上 URL 题的覆盖物是亮白半透明贴片 (离线样本是暗色洞),方法 L 分数从 1–3 恶化到 3.6–7.7。实验发现亮度项 在亮贴片题上是反作用,而服务端免费下发的 tip_y 是硬真值——把它做成 y 锚(±8px)在候选中选精修分最低者:离线 10/10 不回归,线上 8 题 7 中, 3 个原来的错误/AMBIGUOUS 全部修正。已落地 solve(big, mark, tip_y=...)

3. 踩坑清单(最有复用价值)

图像算法

  • mark RGB 与洞内容无关,别在 RGB 匹配上浪费时间——先用上限实验证伪信号;
  • TM_CCORR_NORMED 带 mask 在边界产生 inf,要手写 filter2D 掩码 NCC
  • 倒角会被密纹理击败,必须加方向约束 + 洞内部特征;
  • np.arange 浮点累积误差参与区间比较前先 round(s,3)

CDP / 浏览器自动化

  • Input.dispatchMouseEvent 缺省 pressure=0 是硬指纹,拖动类风控必补 force:0.5
  • button:"left"buttons:0 同时出现(矛盾状态)会毒化 SDK 拖动注册, verify 根本不发出——无按键时用 button:"none"
  • 按下前不要发"带着 left 键的 hover",按下前后 buttons 状态要严格一致;
  • 验证码「点刷新」不重建 iframe → iframe 内注入的 XHR hook 持续有效, 比 netcap 的 Network 域 attach(时机不稳)可靠得多;
  • 验证通过后 iframe 自毁,判断通过与否最硬的信号是 /json/list 里 rmc iframe 消失。

风控对抗

  • 5009 频控阈值苛刻(几十次尝试/小时后窗口 >20 分钟),实验要"省着打", 低质量题(solve 分差)宁可跳过也不出手;
  • 错误码会互相掩盖:频控期看到 5014 不代表行为判定就是这个结果。

4. 方法论沉淀

  1. 先证伪信号,再写实现:RGB 路线若有早期的"掩码 NCC 上限"实验, 方法 A–K 的大半工作量可以省掉;
  2. 无真值时用交叉验证建锚SIFT 可用样本、bbox 与 alpha 尺寸一致性、 人工目检候选可视化;线上则直接用服务端 tip_y 当真值;
  3. 服务端下发的一切字段都是潜在真值tip_y 是免费的 y 答案);
  4. 错误响应是免费的判层探针5014/VerifyErr/5009 的分层让"行为关 过没过、位置关过没过"可独立观测;
  5. 离线评估 ≠ 线上分布:覆盖物样式一变(暗→亮),离线 10/10 的方法 也会跑飞,线上必须保留否决器(tip_y 一致性检查)。

5. 未竟事项

  1. 端到端一次通过未闭环:行为关(force=0.5)与 solvetip_y 锚定) 都已各自验证,但本机 IP 已进深度频控,"冷启动单次拖动通过"需换环境 (新 IP / 无痕 profile)跑 try/solo_attempt.py 验证;
  2. AMBIGUOUS 题处理:solve 置信度低的题目前只能跳过,可积累样本 校准"可信分数"阈值或引入第二方法仲裁;
  3. captchaBody 明文结构wasm 加密未破解,若 pressure 修复后仍被 行为层拒绝,需回退到 hook SDK 内部轨迹采集(wasm 前的 JS 层)注入轨迹。

6. 交付物与文档索引

交付物 位置
求解 API(含 tip_y 锚定) src/solve.py
核心算法(方法 L src/method_l_shape.py
结果验证工具 src/verify_result.py
拟人路径生成 try/human_drag.py
自动闭环 / 单次尝试 try/auto_solve_drag.py / try/solo_attempt.py
网络层采集 try/captcha_net_capture.py

线上闭环最终状态(2026-05-16)

  • 位置层flatness 方法经真人 GT 实证系统性偏差后禁用(FLAT_DISABLED), tip_y 锚定 chamfer 为唯一选点路径(3 个可信 GT 全中);离线 9/10 保持;
  • 输入层:CDP 合成事件、事件属性指纹、xdotool OS 硬件级鼠标、真人轨迹 重放(含 0.7s 终点停顿修复)全部排除——不是根因;
  • 触发层:清 cookie 即解限流(无需等待)、错误正则去"验证码登录"误报、 清 cookie 后必须 Page.navigate 回站;
  • VerifyErr 收敛(2026-09-15 深夜):尺度精修上线后首次机器 PASS live_160548s=1.0375 精修值,code=200「验证成功,正在为您登录」)。 社区资料确认 VerifyErr 语义 = 缺口距离错;剩余 VerifyErr 案例视觉确证为 候选整体选错(非小偏差)→ 后续方向是多候选质量评估 / 换题,而非拖动调参。 「拖动不跟手」指标作废(post_pos 读取晚于 SDK 重置滑块)。

专题文档:solution.md(算法与 API)、 experiments.md(方法演进)、 browser-automation/(触发、采集、拖动、CDP 经验)。