腾讯滑块验证码逆向解析
腾讯滑块验证码 · 技术逆向与验证逻辑文档
范围限定:本协议逆向分析与验证生命周期的技术实现。不含业务背景与非技术性描述。
验证码类型:腾讯TCaptcha拼图缺口型滑块,接入标识aid(固定值)。
一、协议总体生命周期
┌─ 客户端生成 ───────────────────────────────────────────────┐ │ prehandle(GET/JSONP) → 下发 sess / pow_cfg / dyn_show_info │ │ getcapbysig(GET) → 下发背景板(1) / 拼图块(0) 字节 │ │ 本地:PoW 爆破 → 缺口检测 → 轨迹合成 → TDC 加密 │ └───────────────────────────┬────────────────────────────────┘ │ cap_union_new_verify (POST, query==body) ┌─ 加密传输 ────────────────┴────────────────────────────────┐ │ 7 参数:collect / tlg / eks / sess / ans / pow_answer / │ │ pow_calc_time (collect 为 TEA 密文,eks 为密钥材料)│ └───────────────────────────┬────────────────────────────────┘ │ ┌─ 服务端校验 ──────────────┴────────────────────────────────┐ │ 会话(sess)信任 → PoW 重算 → 答案(缺口位置) → TEA 解密轨迹 │ │ 判定 errorCode:0 通过 / 9 失败 │ └───────────────────────────────────────────────────────────┘请求链路(按协议顺序):
cap_union_prehandle (GET) → 初始化会话,回传 sess cap_union_new_getcapbysig (GET, img_index=0/1, image=<令牌>, sess) → 图片字节 [cap_union_new_getsig (POST)] → 条件触发 cap_union_new_verify (POST) → 提交滑动结果(目标接口) cap_monitor (GET) → 遥测,不参与校验二、客户端生成阶段(逆向解析与复现)
2.1 prehandle 会话初始化
请求:HTTP GET,返回 JSONP(callback=_aq_<ts>包裹)。固定参数 28 个,核心:
aid, protocol, accver, showtype, ua, clientype, cap_cd, uid, lang, entry_url, js=/tcaptcha-frame.*.js, subsid, callback, sess=(空), ...请求侧sess参数为空;真实会话令牌在JSONP 响应对象e.sess中。
响应关键字段解析:
| 字段 | 含义 | 后续用途 |
|---|---|---|
state | 1=成功 | 流程闸门 |
sess | 会话令牌(≈506 字符) | 透传至 verify |
data.pow_cfg.prefix | PoW nonce | PoW 输入 |
data.pow_cfg.md5 | PoW target(隐藏答案的 md5) | PoW 校验目标 |
data.dyn_show_info.bg_elem_cfg.img_url | 背景图直链(含 sess) | 下载缺口图 |
data.dyn_show_info.fg_elem_list[id=1].size_2d[0] | 拼图块真实宽pieceW | 缺口检测参数 |
结构兼容:浏览器直出data.pow_cfg;某些链路嵌套在data.comm_captcha_cfg.pow_cfg,解析时双路径兜底。
关键约束(环境信任):sess与服务端"环境信任"绑定。只有由真实浏览器加载tcaptcha-frame.js后产生的sess才是可信的;纯 Node 自行拼装的 prehandle 虽能拿到完整响应,但其sess未被"激活",verify 必返回errorCode 9。这是整条逆向链唯一必须借助浏览器的环节。
2.2 图片挑战下发
两种下载路径(均无需 cookie,sess 已在 URL 中):
# A:显式 getcapbysig cap_union_new_getcapbysig?img_index=1&image=<图片令牌>&sess=<sess> # 背景板(带缺口) cap_union_new_getcapbysig?img_index=0&image=<图片令牌>&sess=<sess> # 拼图块 # B(实际采用):dyn_show_info 直链 data.dyn_show_info.bg_elem_cfg.img_url # 已含 sessimg_index:1=背景板,0=拼图块;image令牌由挑战加载时分配;sess与 verify 完全一致。
尺寸修正(逆向实测):背景图实际像素672×390;dyn_show_info.bg_elem_cfg.size_2d=[672,480]是含 padding 的显示画布,非图片像素。拼图块 sprite682×620,不透明 bbox≈x[157…242] y[507…592],宽≈85。缺口检测必须以真实图片像素为基准。
2.3 工作量证明 PoW
算法:暴力枚举u,使md5(prefix + u) === md5(target)。服务端将隐藏答案的 md5 作为 target 下发,客户端从 0 递增找回原值。
functionrunPow(prefix,target,timeoutMs=30000){constt0=Date.now();letu=0;while(md5(""+prefix+u)!==target){u+=1;if(Date.now()-t0>timeoutMs)break;}return{answer:""+u,duration:Date.now()-t0};}md5为标准 MD5(RFC1321),小写 hex,与 Nodecrypto实现一致。实测u量级数万,单次耗时 80~150ms。
产物:
pow_answer = prefix + u(如a1b2c373219)pow_calc_time = duration(毫秒)
2.4 缺口定位(垂直边缘能量法)
背景图缺口为"被挖掉的洞",左右竖直边产生强垂直边缘能量。
1. 逐列计算垂直边缘能量 edge[x] = Σ|L-R|(RGB 三通道) 2. 窄窗口(3) 平滑,保留尖锐边缘,压单像素噪点 3. 主体区(x∈[40, w-40]) 局部极大值,能量 > avg*1.3 → top 峰 4. 在 top 峰中找成对峰:间距 ∈ [pieceW-35, pieceW+35],能量和最大者 → 缺口左右缘 5. 退化:无成对峰时,以最高峰为右缘,左缘=右缘-pieceWpieceW必须取拼图块真实宽(fg_elem_list[id=1].size_2d[0]),不能用背景 canvas 宽(早期逆向误用背景宽导致定位错误)。
输出:GAP {left, right, width, center},拖动目标X = 缺口左缘(非中心)。实测标定:bg_live.png检测 left=471 ≈ 已知答案 470(误差 1px);bg_yiche2.png检测 left=458。
2.5 轨迹合成
明文格式(喂给 TDC 的轨迹点):
[ [4, 0, 0, t0, 0], [1, x, y, t, 0], ... ] type 4 = 起始标记;type 1 = 移动点;(x, y, t) 为坐标与累计毫秒坐标空间:图像像素空间(实测捕获真实点x=559超过显示宽 340,证明 SDK 以图像像素为单位)。因此targetX= 缺口左缘(图像空间),轨迹末点 x 必须等于targetX(与ans.x一致)。
拟人化生成:
- 缓动曲线
ease:ease-in-out cubic,末段叠加微小过冲(overshoot)后回稳; - 时间非均匀:起止慢、中段快;
- 竖直方向 ±2px 抖动;
- 末点强制对齐
targetX。
2.6 TDC 加密(tdc.js VM 字节码)
加密体:tdc.js内含VM 字节码解释器(window.TDC.setData/getData/getInfo/getKeyInfo均为while(!![]){switch(...)}形式的 VM 函数,源码层不可 step 进)。
算法:TEA(Tiny Encryption Algorithm)
- 32 轮 Feistel,
delta=0x9e3779b9,sum终值 ≈0xc6ef3720; - 64-bit / 8 字节分组;
- 密钥非静态常量,由会话材料
eks动态派生(用静态TDC_KEY解密 collect 得乱码,printable ratio=0.39,证实密钥非固定)。
调用链复现:
TDC.setData({ trackData, clientSize }) // 喂入合成轨迹 TDC.getData(true) → collect // TEA 加密轨迹,输出 url-encoded base64 TDC.getInfo().info → eks // 动态密钥材料(≈352 字符 base64) tlg = collect.length // 密文长度eks自包含:服务端收到eks后自行key=derive(eks)再解密collect,故在独立 Node VM 内生成collect+eks即构成合法 verify 请求体,无需手工反推密钥派生函数。
collect 结构:TEA 加密后的字节经 url-encode 的 base64;解码后为 8 字节整数倍的 TEA 密文(实测 2256 字节,对齐)。
三、加密传输阶段
3.1 verify 报文结构
cap_union_new_verify为 POST。query与body内容完全相同(u == f,协议强制):7 个参数同时出现在 URL 查询与请求体。
| 参数 | 编码方式 | 来源 |
|---|---|---|
collect | 已由getData(true)url-encode,原样拼接(不二次 encode) | TDC 密文 |
tlg | encodeURIComponent | collect.length |
eks | encodeURIComponent | TDC.getInfo().info |
sess | encodeURIComponent | prehandlej.sess透传 |
ans | encodeURIComponent | JSON.stringify([{elem_id:1,type:"DynAnswerType_POS",data:"X,100"}]) |
pow_answer | encodeURIComponent | prefix + u |
pow_calc_time | 数字原样 | duration |
3.2 参数编码与解密机制
collect由getData(true)内部完成 url-encode;buildVerify中原样拼接,服务端decodeURIComponent(collect)还原 TEA 字节后解密;其余参数正常encodeURIComponent。ans结构:[{"elem_id":1,"type":"DynAnswerType_POS","data":"<X>,100"}],X为缺口左缘像素(图像空间),y固定 100。- 传输层:
Content-Type: application/x-www-form-urlencoded,Referer/Origin指向t.captcha.qq.com。
四、服务端校验逻辑(逆向推演)
以下为基于客户端协议与实测反馈(errorCode)反推的服务端判定逻辑。
4.1 会话信任校验
- 校验
sess是否由可信环境(浏览器框架)激活; sess一次性、有时效:过期或被复用 → 拒绝(errorCode 9);- 纯 Node 自建
sess缺信任链 → 拒绝。
4.2 PoW 校验
- 重算
md5(prefix + pow_answer_without_prefix) === md5(target); - 校验
pow_calc_time合理性(与爆破耗时量级一致); - 不一致 → 拒绝。
4.3 答案校验
- 比对
ans.data的X与服务器侧真实缺口左缘; - 允许像素级误差(实测 ±1px 通过);
X偏差超阈值(缺口检测算偏)→ 拒绝。
4.4 加密轨迹校验
- 用回传
eks派生 TEA 密钥 → 解密collect→ 还原trackData; - 对轨迹做行为/设备指纹评估(速度曲线、分布等);
- 实测结论:轨迹的
isTrusted(是否真实浏览器鼠标事件)不是校验决定因素——纯 Node 无任何浏览器事件生成的collect/eks可被服务端接受并返回0。
4.5 errorCode 判定汇总
| 判定 | errorCode | 触发条件 |
|---|---|---|
| 通过 | 0 | sess 可信 + PoW 正确 + 答案命中 + 轨迹可解密 |
| 失败 | 9 | ① prehandle 非浏览器出生(缺信任);② 缺口检测偏差致距离错;③ sess 过期/复用 |
五、逆向工程底层实现
5.1 抓包解析
- prehandle 抓取:通过 CDP(
--remote-debugging-port)拉起 Chrome,监听Network.requestWillBeSent,正则/cap_union_prehandle/匹配并捕获完整请求 URL(含 28 参数)。该 URL 即"浏览器出生"的可信 prehandle。 - 请求链定位:在 DevTools Network 中按接口名过滤
cap_union_prehandle/cap_union_new_getcapbysig/cap_union_new_verify/cap_monitor,确定参数传递顺序与字段。 - JSONP 解析:
parseJsonp用正则/^\s*[_a-zA-Z][\w]*\(([\s\S]*)\)\s*;?\s*$/抽取回调包裹的 JSON 体。 - iframe 隔离:验证码在跨域 iframe 内加载,主页面无法读其响应体;故
pow_cfg通过算法复现(已知、自测 PASS),图片经dyn_show_info直链(Node 端下载,无需 cookie)绕开 CORS。
5.2 反混淆策略
- tdc.js VM 字节码:加密逻辑以
while(!![]){switch(...)}的 VM 解释器实现,源码不可直接读。逆向策略为忠实运行而非反编译——在 Nodevm沙箱中原样执行tdc.js,让其自身完成 TEA 加密,直接取collect/eks。 - 寄存器采样失败:试图用寄存器采样法从 VM 内存提取 128-bit 密钥(regs [16,25,34,62],代数签名
(v<<4)&(v>>>5)、窗口模式 mode ratio≈1.0)不稳定(解释器可能将移位折叠进加法),故放弃手工提密钥,改由忠实 VM 代劳加密。 - 最小 DOM 桩:为让
tdc.js在无浏览器环境运行,对document/navigator/screen/location等做最小桩(createElement返回elementStub、getContext/getBoundingClientRect返回固定值、定时函数为空操作),仅满足 VM 初始化与加密所需的最小 API 面。
5.3 绕过验证逻辑的底层实现
- 信任链绕过(唯一需浏览器处):用 CDP 拉一次浏览器,触发滑块并抓取真实 prehandle(含可信
sess),随后关闭浏览器。此步骤等价于"借用浏览器完成环境信任激活",是整条纯 Node 求解链成立的前提。 - 全链纯 Node 复现:prehandle GET → PoW 爆破 → 图片下载 → 缺口检测 → 轨迹合成 → TDC VM 加密 → POST verify,全程无浏览器依赖。
- 关键构造点:
collect/eks自包含:由 Node VM 内tdc.js生成,服务端用回传eks派生密钥解密,无需逆向密钥派生函数;query == body:协议强制 7 参数双份同值,构造时直接复用同一拼接串;ans坐标对齐:轨迹末点与ans.data的X均取缺口左缘(图像像素空间),保证答案与轨迹一致;pieceW真实来源:取fg_elem_list[id=1].size_2d[0],避免早期误用背景 canvas 宽导致的定位偏差。
- 绕过结果:在浏览器出生 prehandle 下,纯 Node 求解稳定返回
errorCode:"0"+ticket/randstr,完成验证闭环。
附:核心参数与算法速查
| 项 | 值 / 形式 |
|---|---|
| 加密算法 | TEA,32 轮 Feistel,delta=0x9e3779b9,sum终值0xc6ef3720 |
| 密钥 | 由eks会话级动态派生(非静态) |
| PoW | md5(prefix+u)===md5(target),暴力枚举 |
| 轨迹明文 | [[4,0,0,t,0],[1,x,y,t,0],...],图像像素空间 |
| verify 参数 | collect/tlg/eks/sess/ans/pow_answer/pow_calc_time(query==body) |
| 缺口输出 | X = 缺口左缘(图像像素),ans.data="X,100" |
| 图片真实尺寸 | 背景 672×390(size_2d 为含 padding 画布) |