- 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 同步收尾
9.2 KiB
项目复盘:抖音「拖动重叠」验证码离线求解与自动化通过
本文是整个项目的经验总复盘:任务、四个阶段的演进、决定性发现、 踩坑清单、方法论沉淀与未竟事项。细节以各专题文档为准 (索引见 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 条路线全部失败或受限后, 才确认两条决定性事实(详见 findings、experiments):
- mark 的 RGB 是"捐赠补丁"——内部是场景纹理,与洞内内容无像素关系 (掩码 NCC 全图 ≤0.57),一切 RGB/灰度模板匹配原理上不可行;
- 唯一可靠信号是 alpha 轮廓 + 尺度 + 旋转——洞是叠加在照片上的实心 暗色覆盖物,形状=mark 的 alpha 轮廓,尺度≈1.0、旋转≈0;干扰项是同形的 缩放/旋转实例。
最终方案 L:方向感知倒角(9 法向桶 × 多尺度)+ 洞内部特征(边缘密度、 内-环亮度差)+ 局部旋转扫描,17 组样本 10/10 精确命中。
教训:12 条失败路线里没有一条是"实现 bug",全是信号假设错误。 早期就该做"掩码 NCC 上限"这类决定性实验先证伪 RGB 路线,而不是逐个换实现。
阶段二:浏览器触发与样本采集
- 抖音页面预加载验证码 iframe(display:none),「iframe 存在 ≠ 触发」,
判定要看
offsetWidth > 0; - wasm/GPU 渲染让
<img src>与截图法失效,但图片 URL 会正常发 HTTP 请求 → 网络层抓captcha/get响应 + 下载url1/url2即得与离线同规格的样本对; - 最可靠的触发法:密码登录弹窗(错误密码也必弹验证码);
- tip_y 服务端真值:
question.tip_y × 2 = 缺口顶 y(mark 画布顶坐标系)。
阶段三:拖动闭环与行为风控(HOOK 路线)
闭环管线(try/auto_solve_drag.py):点刷新 → 采 get → solve →
拟人路径 → CDP 拖动 → 读 verify,get→verify 全程 <8s(challenge 有效期约 60–90s)。
拟人轨迹三要素(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 增强
- pressure 指纹:CDP 缺省产生的 MouseEvent
pressure=0,W3C 规范里 真鼠标恒 0.5。三个鼠标事件全加force:0.5后,错误码从 5014(行为拒) 稳定变为 VerifyErr(位置判定被执行)——行为闸门通过。 - 错误码语义分层(多次连线实验归纳):
5014 行为拒→VerifyErr 位置判了但错→5009 频控(压住一切)→NotFoundChallengeId challenge 过期。任何改动的效果判定都必须在频控 冷却后做单次干净尝试,连环重试的 5014/5009 是无效信号。 - 拖动映射实测:按钮 1:1 带动 mark 预览;mark 初始相对大图
(0, tip_y×2)(每题垂直自动对齐,拖动只控 x);拖动距离 =solve.x × 340/552。 - 线上分布漂移与 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. 方法论沉淀
- 先证伪信号,再写实现:RGB 路线若有早期的"掩码 NCC 上限"实验, 方法 A–K 的大半工作量可以省掉;
- 无真值时用交叉验证建锚:SIFT 可用样本、bbox 与 alpha 尺寸一致性、 人工目检候选可视化;线上则直接用服务端 tip_y 当真值;
- 服务端下发的一切字段都是潜在真值(tip_y 是免费的 y 答案);
- 错误响应是免费的判层探针:5014/VerifyErr/5009 的分层让"行为关 过没过、位置关过没过"可独立观测;
- 离线评估 ≠ 线上分布:覆盖物样式一变(暗→亮),离线 10/10 的方法 也会跑飞,线上必须保留否决器(tip_y 一致性检查)。
5. 未竟事项
- 端到端一次通过未闭环:行为关(force=0.5)与 solve(tip_y 锚定)
都已各自验证,但本机 IP 已进深度频控,"冷启动单次拖动通过"需换环境
(新 IP / 无痕 profile)跑
try/solo_attempt.py验证; - AMBIGUOUS 题处理:solve 置信度低的题目前只能跳过,可积累样本 校准"可信分数"阈值或引入第二方法仲裁;
- 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_160548,s=1.0375 精修值,code=200「验证成功,正在为您登录」)。 社区资料确认 VerifyErr 语义 = 缺口距离错;剩余 VerifyErr 案例视觉确证为 候选整体选错(非小偏差)→ 后续方向是多候选质量评估 / 换题,而非拖动调参。 「拖动不跟手」指标作废(post_pos 读取晚于 SDK 重置滑块)。
专题文档:solution.md(算法与 API)、 experiments.md(方法演进)、 browser-automation/(触发、采集、拖动、CDP 经验)。