VAPTCHA这类手势验证码,很多做风控的同学应该不陌生。它是那种需要你在图片上按指定方式滑、点、画手势来证明"我是人"的验证码产品。我最早接触它,是在某次授权渗透测试里:一个业务系统的登录接口前挂了这层验证码,自动化脚本、撞库工具全被挡在门外。当时我就想,这东西到底是怎么判定"人"和"机器"的?它采集了哪些数据?这些数据是怎么被组装、加密、送往后端校验的?顺着这个思路往下挖,我陆续做了一些合规框架内的分析研究,今天这篇就专门聊聊VAPTCHA手势验证码的机制拆解、逆向分析思路,以及反过来看,验证码设计方应该怎么加固。
这文章不是教你绕过验证码去刷接口、薅流量,而是从安全研究的角度把机制摸清楚。适合三类人看:正在做风控对抗的研发、做安全测试的同行、以及研究验证码交互设计的同学。把对手的招数看明白,你才知道自己家的城墙该往哪儿修。
1. 为什么安全研究者会盯上VAPTCHA这款手势验证码
1.1 手势验证码在风控体系中的真实角色
很多非安全岗的同学容易把验证码理解成一个"人机测试题"——答对了就是人,答错了就是机器。实际上在成熟的风控体系里,验证码只是其中一个信号源。它要解决的核心矛盾是:业务方既需要拦截自动化流量,又不想让真人用户在登录、下单、领券环节流失太多。
VAPTCHA属于手势交互验证码,跟传统的字符识别、滑块拼图不太一样。它通常会在背景图上给出一个任务指令,比如"请按顺序点击星星"、"请沿轨迹滑动到指定位置",甚至是在图上画一个指定的图形。用户的手势轨迹、时间间隔、点击顺序、停留时长都会被采集下来,这些数据不只是用来判断"对没对",更重要的是用来评估"像不像真人"。
所以研究VAPTCHA的切入点就不该只盯着它的图片识别难度,而要看它整个前端采集机制和数据链路。这也是它作为研究样本比较典型的原囝:它代表了一类"行为式验证码"的设计范式——把用户手势当成多维特征向量,再结合设备环境、加密签名、服务端二次校验,形成一个闭环。把这个样本拆透,等于把市面上大多数行为式验证码的骨架都看了一遍。
1.2 逆向分析的合法边界与目标设定
先泼一盆冷水:任何验证码逆向分析,都必须发生在合规框架内。我自己做的这类研究,要么是在自家业务系统上搭环境跑,要么是在有书面授权的渗透测试项目里做。脱离授权去逆向别人线上生产环境的验证码,不管目的是什么,在合规层面都是不成立的。这一点想不清楚,后面技术做得再漂亮也没意义,反而可能给自己惹上麻烦。
在确认授权后,我的研究目标一般会拆成四层:
- 摸清VAPTCHA前端SDK的整体工作流:初始化、渲染、采集、加密、上报。
- 找到请求参数中跟手势相关的字段,理解它们的编码格式和含义。
- 分析加密签名的生成逻辑,确认它是纯前端签名还是结合了服务端下发的随机因子。
- 输出加固建议,反哺到自研验证码或风控体系的设计中。
换句话说,目标不是"绕过去",而是"读懂它"。这两个目标的技术路径有七成重合,但最终产出的价值完全不同。前者是一条用完即弃的攻击链,后者是一份能长期指导风控建设的分析报告。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先摸清工作链路:从页面渲染到后端校验
2.1 前端到底采集了哪些手势数据
我习惯先开浏览器开发者工具,切到Network面板,把VAPTCHA的初始化到提交这一步所有的请求都截下来看。你会发现它通常在页面加载时就先请求一批基础资源:验证码图片、背景图、字体文件、还有一段初始化配置。真正关键的提交请求一般发生在用户完成手势之后,接口路径往往带有"check"、"verify"、"submit"之类的字眼。
点开这个提交请求的Payload,能看到大量字段。排除掉那些明显是设备参数和环境参数的字段后,剩下的跟手势相关的核心数据有这么几类:
- 轨迹点集合:记录了从mousedown/touchstart到mouseup/touchend之间所有的坐标采样点,每个点通常包含x、y坐标和相对时间戳。
- 事件类型序列:down、move、up,以及可能的click、touch事件标记,相当于把用户操作过程完整回放了一遍。
- 时段统计:总耗时、每段轨迹的耗时分布、相邻采样点之间的间隔序列,这些数据对区分"人滑"还是"机器滑"特别关键。
- 操作顺序信息:如果是多点按序点击类型,还包含每个目标点的命中顺序和偏移量、停留时间。
这里有个容易被忽略的细节:轨迹点并不是每个像素都采样,而是按一定频率或者按移动距离阈值来采样。不同产品的采样策略不一样,有的按固定时间间隔(比如每20ms取一个点),有的按位移增量(比如每移动5个像素取一个点)。分析的时候要把采样策略还原出来,否则后面写轨迹相似度评估算法时容易算错。
除了轨迹数据本身,请求里还会带上验证码的会话标识、场景编号、随机token等。这些字段是用来把"这一次验证"和"这个用户会话"绑定起来的,防止有人把一次合法验证的参数复制重放到其他地方。
2.2 容易被忽略的隐藏环境参数
如果你是第一次分析这类验证码,很容易把注意力全放在轨迹和图片上,漏掉请求里那些看起来跟验证无关的环境参数。实际上,这些隐藏参数往往是风控模型里权重很高的特征。
我在VAPTCHA的提交请求里见过这样一个数据块:里面包含canvas指纹、WebGL渲染参数、字体列表、屏幕分辨率、时区、语言、还有一组经过计算的浏览器特征哈希。这套东西就是从浏览器环境里抠出来的"设备指纹"。它跟手势轨迹配合起来,能完成一项关键判断:这个手势是不是在这个真实浏览器环境里产生的。
举个例子:攻击者如果纯用代码去构造请求,他得先伪造这组环境参数。但如果他直接拿别人通过验证的完整请求来重放,又会因为环境参数和服务端记录的会话信息不匹配而被识别。VAPTCHA把"轨迹特征"和"环境指纹"绑在同一个加密参数里,本质上是在提高伪造和重放的联合成本。
分析的时候,建议把这些环境字段单独列一张表,标注它们的名称、类型、疑似来源(哪个JS函数生成的)。我自己的习惯是先在开发者工具里把每个字段的生成点通过搜索找到,再回到代码里去验证。不要靠猜,字段命名经常是误导性的,名字叫"width"的可能是canvas哈希的一部分。
3. 拆解前端SDK:定位关键代码的三条路径
3.1 通过网络请求反查加密参数生成点
拿到提交请求后,最笨也最可靠的定位方法就是"反查"。在开发者工具里选中那个verify请求,切到Initiator(发起者)面板,就能看到这个请求是从哪个JS函数里发出来的。顺着调用栈往上翻,能一路找到采集数据、组装参数、调用加密函数的位置。
这个方法看起来简单,但实际用的时候有个坎:SDK的代码通常是压缩混淆过的,函数名只可能是a、b、c这类短名称,调用栈里显示的内容几乎是不可读的。所以反查真正的价值不是直接读懂代码,而是帮你锁定"关键代码在哪个文件、大概在第几行、这个函数内部调用了哪些其他函数"。拿到这些坐标之后,再去源码里做局部还原,效率会高很多。
我在分析VAPTCHA时,用这个方式定位到了一个负责参数编码的函数。它接收一个包含轨迹数组的JSON对象,内部做了一次序列化,再交给另一个加密模块处理。这种"先序列化、再加密、最后拼进请求体"的模式在同类产品里很常见,定位到这一步,整个链路的主干就出来了。
3.2 从混淆JS中锁定核心函数
现实中不会有哪个验证码厂商把核心逻辑明文写在JS里,混淆是标配。VAPTCHA的SDK也差不多,我遇到的情况是:变量名短化、字符串数组化、控制流被拍平(把一个函数里的逻辑拆成switch循环块),一些敏感字符串做了十六进制编码。
面对混淆代码,不要一上来就想着完全还原,那是给自己挖坑。正确的姿态是带着问题去读:"我想找的是轨迹数据的组装函数",然后通过三条线索逼近:
- 线索一:特征字符串。比如"trail"、"point"、"x_coord"这类变量名虽然可能被混淆,但常数字符串有时还残留着。搜索请求体字段名,往往能直接跳到组装函数附近。
- 线索二:数组索引特征。字符串数组混淆很常见,代码里全是"arr[37] + arr[12]"这种形式。这时候可以用AST工具插桩,把数组索引对应的真实字符串打印出来,再去反查谁引用了某段关键字符串。
- 线索三:函数调用关系。在开发者工具里给"可疑函数"打上日志断点,看它被谁调用、传了什么参数、返回了什么值。多次提交验证码,观察轨迹数据传入和加密结果传出的变化,就能反推函数边界。
我自己比较喜欢用AST分析工具来做字符串解码。写一小段脚本,把混淆后的JS解析成AST,提取出字符串数组和解码函数,然后在所有字符串字面量的位置上标记出解码后的值。这个过程对理解代码逻辑帮助极大,比肉眼盯着压缩后的代码看半天有效得多。
3.3 动态执行与间接调用的分析技巧
有些SDK比纯静态混淆更进一步,会做动态生成代码:把核心函数体用一段加密字符串存着,运行时再通过eval或Function构造器还原执行。你静态分析的时候根本看不到函数内容,只有在浏览器运行到那一刻,代码才会在内存里现出原形。
面对这种情况,静态分析基本失灵,得换动态方案。我的做法是在代码执行到解密完成、函数即将调用前的位置下断点,等断点命中后,在控制台直接打印出被还原出来的函数体。因为Function构造器还原的代码本质上也是一个普通函数,你可以在它运行前用日志断点把它toString出来看。
VAPTCHA的SDK里也有类似的动态加载逻辑,不过它做得不算极端,大部分核心逻辑还是静态混淆为主。真正麻烦的是另一件事:环境检测。SDK会在运行过程中读取浏览器各种环境属性,如果发现是开发工具模拟的环境,会走一条假的逻辑分支,返回一个动态生成的假轨迹。第一次遇到这种情况,我还以为是自己定位错了函数,折腾了大半天才意识到是环境检测在起作用。
4. 核心逆向环节:手势轨迹的序列化与签名逻辑
4.1 轨迹特征如何被编码进请求
把关键函数定位出来后,下一步就是还原轨迹数据从采集到编码的全过程。我在VAPTCHA的代码里看到的大致流程是:
- 采集阶段:监听鼠标或触摸事件,将每次move的坐标和时间戳压入一个数组。
- 预处理阶段:对轨迹做抽稀,比如时间间隔小于某个阈值的相邻点会被合并;坐标值可能做了偏移处理,防止明文坐标被直接拿去重放。
- 序列化阶段:把轨迹数组转换成一个紧凑的字符串,通常是"x1,y1,t1;x2,y2,t2"这种格式,再用分隔符把事件类型、时段统计等字段拼接进去。
- 编码阶段:对这个序列化字符串做编码,可能是base64,也可能是自定义的字符映射。
这里有个值得注意的思路:VAPTCHA不一定把轨迹字段命名成"trail"或"track",它可能用一个无意义的字段名来装载编码后的数据,比如"cb_data"、"sec_data"。刚开始分析时我就是靠关键字找来找去找不到,后来换了思路——先在请求体里找出"值看起来像一段编码后的长字符串"的字段,再回去反推它是从哪个函数产生的,一下就锁定了。
轨迹编码的细节最花时间。不同手势类型的编码格式可能不一样:滑动手势的轨迹点逻辑简单,就是坐标+时间;按序点击的手势则要额外记录每个目标点的id和点击顺序;绘制类手势还要记录笔画路径的夹角变化。理解这些差异,你才能看懂为什么同一套SDK在不同验证场景下提交的请求体结构不一样。
4.2 签名算法的作用不只是防篡改
手势数据编码完后,还得经过一道签名或加密,才会被塞进请求体。VAPTCHA的签名逻辑,我理解它的核心目的有三个:防篡改、防重放、附带环境证据。
防篡改很好理解,签名确保服务端收到的轨迹数据在传输过程中没被中间人改过。它内部把编码后的参数字符串、会话token、一个时间戳拼在一起,做哈希或对称加密,生成一段签名串。任何字段发生变化,服务端重算签名就会对不上。
防重放则靠nonce和时间窗口。每次验证时,服务端会下发一个一次性token,前端签名时必须把它包含进去。服务端校验时检查这个token是否已使用过、是否在有效时间内,从机制上杜绝了直接把抓到的验证请求原样重发这种操作。
第三点最隐蔽也最关键:签名用的密钥或盐值,可能并不只是"一串固定的字符串",它可能是从环境参数里现场计算出来的。也就是说,同样一段轨迹数据,在浏览器A和浏览器B里签名出来的结果会不一样。这就把"轨迹数据"和"环境指纹"绑死在了一起。攻击者拿到一段合法签名,如果环境参数不匹配,签名并不能跨环境使用。这个设计思路非常值得借鉴,比单纯加个固定盐值要高明不少。
实际分析过程中,我会把签名模块单独隔离开来测试:固定一组轨迹输入,分别调整环境参数,观察输出签名是否变化;固定环境参数,轻量修改某个坐标点,观察签名是否变化。通过这种"控制变量法",就能确认签名算法里到底混入了哪些输入项,哪些输入项的权重更高。
4.3 一次完整的模拟请求验证
为了确认前面的分析结论,我做了这样一个验证实验(完全在自建的测试环境里跑的):先通过一个真实的浏览器操作走完一次VAPTCHA验证,把请求体完整记录下来。然后我用自动化脚本把轨迹数组里的某个坐标点做了微小修改,重新按照我推测的编码格式和签名流程生成请求,提交给测试服务端。
结果是:服务端返回了失败。这说明我的编码格式推测基本正确——服务端能解析这个结构,所以会继续往里挖;同时签名校验也确实存在——修改后的请求没能通过验签。
这个实验本身不复杂,但它的价值在于:它验证了整条分析链路是否闭环。如果你推测的编码格式不对,服务端大概率会返回"参数格式错误"之类的响应;如果你签名流程推测错了,则会在验签环节被拦下。通过响应差异,你就能反向定位自己哪个环节理解错了。
我建议做这类验证时,记录一个"对照表":
- 原始请求,什么都不改直接重放:预期是被拒(token已使用或时间窗失效)。
- 只改轨迹坐标,保留原签名:预期失败(验签不过)。
- 改了轨迹坐标,但用推测的逻辑重新生成签名:预期失败或成功,取决于你的逻辑是否还原正确。
- 全部用推测逻辑生成新请求,模拟新会话:预期失败,因为还缺服务端在下发阶段绑定的某些状态。
这几个对照项做完,你对VAPTCHA请求链路的理解就从"猜测"变成了"实证"。这个过程也是整个逆向分析里收获最大的部分。
5. 站在防御侧:理解了攻击手法之后,验证码该怎么加固
5.1 混淆与动态化:提高逆向成本
做过一轮逆向分析后,我对"前端安全到底应该做到什么程度"有了更实际的体会:前端代码在用户浏览器里运行,天然是透明的,你不可能让信息完全不可见,你能做的是大幅提高攻击者的分析成本。
VAPTCHA这一套方案里,有几处设计是值得借鉴的防御实践。比如关键字符串的数组化混淆,虽然最终能还原,但确实把静态分析的入门门槛抬高了一截;再比如把签名盐值跟环境参数动态绑定,让签名结果不可跨环境复用,这一招对批量脚本的杀伤力很大。
但也有一些可以做得更好的地方。我在分析时发现,轨迹数组的明文结构其实比较容易从内存中捞出来,因为它在序列化之前就是一个普通的数组对象。如果能把轨迹点的存储也做一层转换,比如先存进一个闭包内部的数据结构,到编码阶段再一次性取出,就能显著增加内存分析的难度。
另一个思路是"误导式设计":在代码里故意放几段看起来很像核心逻辑、实际上是僵尸代码的函数,或者把真正关键的函数切碎、穿插在无关逻辑中间。这种做法的目标不是让人读不懂,而是让人"读懂了也不敢确定"。逆向分析最消耗精力的就是确定性,一旦分析者没法确认"这就是核心函数",整个分析进度都会被打乱。
5.2 服务端校验:别把信任全部交给前端
分析VAPTCHA让我印象最深的一点,是它的服务端并没有信任前端传过来的任何单一字段。这不是它独有的优点,而是现在主流验证码产品的共识,但具体设计上有不少学问。
防御方在服务端至少要完成这几层校验:
- 协议层校验:字段是否存在、类型是否正确、长度是否在合理范围。这一点看似基础,却经常被绕过型攻击者利用——如果服务端对某些"装饰性字段"不做严格校验,攻击者就少了一段伪造成本。
- 业务逻辑校验:一次验证流程中的token、场景id、会话状态是否匹配。比如前端请求时的会话状态、下发的挑战信息、提交验证时的token,三者必须是同一链条。
- 时间窗口校验:从下发验证码到提交验证的时长是否有硬性上限,轨迹的总时长是否合理。人类完成一次手势验证通常在1到10秒之间,脚本可以伪造任何时长,但很难在服务端同时要求"总时长合理、每段间隔合理、采样点密度合理"的情况下都猜中。
- 行为统计校验:从同一个设备、同一个IP、同一个会话发起的验证频率是否异常。验证码本身挡不住有耐心的攻击者,但如果能提高单次尝试的代价,就能劝退大部分低收益攻击。
我特别想强调时间窗口这个点。很多自研验证码只校验了" token是否过期",却忘了校验"用户完成这个手势花了多长时间"。VAPTCHA对轨迹耗时的校验是分了段的,不同线段的时间比例关系也参与校验,因为真实人性的移动规律是"加速—匀速—减速",而脚本模拟出来的轨迹往往加速度曲线是异常的。
5.3 风控联动:轨迹质量比"结果正确"更值钱
做完这次分析,我最大的感触是:验证码的终极目标不是让人做对题目,而是让系统在用户做对题目的同时,采集到足够多的"人类行为证据"。VAPTCHA这类产品厉害的地方不是它的题目有多难,而是它把整个交互过程变成了一次行为采集的机会。
所以从防御方视角看,加固验证码最值得投入的方向是:建立基于真实人类手势特征的判别模型,而不是只做规则判断。简单说就是回答三个问题:
- 这个轨迹像不像人类画的?看曲率变化、加速度抖动、停顿模式。真人画线不是完美直线,总会有微小的手抖和停顿,而脚本生成的轨迹过于平滑。
- 这个操作过程符不符合这个场景的习惯?移动端用户触屏操作和桌面端鼠标操作的模式差异很大,桌面端鼠标轨迹往往有到达目标点前的小幅回弹,触屏则更直接。
- 这个设备、这个网络环境、这个账号之间有没有历史关联?风控模型如果能关联到一个设备指纹在过去几周内的可信度评分,那么仅凭"这一次验证通过"就想获得高信任分的脚本,就失去意义了。
把这些维度接入风控系统后,验证码就不是一个孤立的关卡,而是变成了风控决策的一个输入项。同样的验证码,在高风险设备上出现时,可以触发二次验证或人工审核;在低风险设备上出现时,可以简化交互甚至静默通过。这种动态策略兼顾了安全性和用户体验,也是我认为验证码产品未来的正确演进方向。
6. 逆向分析踩坑实录与效率提升方法
6.1 三个最耗时间的坑
这类逆向分析,真正耗时间的往往不是"读代码"本身,而是"找对代码"之前那段时间。我复盘了一下自己的分析过程,有三个坑印象特别深。
第一个坑是跟混淆代码死磕。有一段时间我试图把一整段混淆后的核心函数完整还原出来,为此折腾了大半天。后来发现完全没必要——我需要的只是轨迹数据从哪来、往哪去、经过哪个函数签名,这三个信息点在控制台打日志就能拿到,根本不需要完整读懂那段代码。跟混淆死磕就像想把整个迷宫的地图画出来再找出口,但其实你只需要感知左边那堵墙往哪里拐。
第二个坑是忽略环境检测导致的"假分支"。前面提到了,VAPTCHA的SDK会在异常环境下返回假轨迹。我在本地自动化环境里跑了半天,采集到的轨迹数据一看就不对劲——坐标全是整数序列、间隔完全均匀、和真实浏览器里的轨迹特征差异巨大。一开始我还以为是采样逻辑如此,后来对比真实浏览器里的输出才发现,是环境检测触发了假数据。这个坑的教训是:任何在模拟环境中观察到的行为,都要先在真实环境里做一次对照实验,否则你的分析对象根本不是真实逻辑。
第三个坑是服务端状态绑定。有一段时间我反复修改参数重新签名提交,但服务端一直返回同样的失败原因,我看得一头雾水。后来才意识到:每次验证的token是服务端在下发验证码时生成的,它不仅绑定会话,还绑定了下发给前端的挑战信息,包括图片内容、目标位置、甚至目标位置的顺位。如果你重新签名时用的图片或目标位置是旧一次的,那么即便签名正确也会校验失败。这类问题,只能靠拉长调试链路、分段验证输入输出来定位。
6.2 常用工具链与效率方法
最后整理一下我在这个分析过程中实际用到、觉得值得推荐的工具链。不是全面铺开介绍,只说那些真正提效的。
第一个是浏览器开发者工具。它的价值不用多说了,但我想提一个细节:Event Listener Breakpoints功能非常好用。你可以直接对所有mouse事件断点,然后开始操作验证码,代码会在事件处理函数入口处停下。从栈顶往下走一遍,就能看清"一次手势"从原生事件到业务逻辑函数的过程,这是理解采集逻辑的最佳入口。
第二个是代理调试工具。在需要抓取和修改请求时非常方便,它比浏览器面板好用的一点是:你可以对请求做批量修改和重放,而且能保存完整的请求历史,方便做对照实验。要注意的是,自己搭环境调试没问题,生产环境的抓包要严格在授权范围内使用。
第三类是AST相关工具。我只能说强烈建议学一点基础用法,不用研究很深,会写"把某几个字符串字面量打印出来"这个等级就够了。混淆JS在AST工具面前基本是透明的,因为语法结构是确定的,混淆只改变了名字和流程,没改变语法树骨架。
还有个效率上的习惯想分享:动手分析之前,先花二十分钟写一份"预期模型"。也就是凭你对这类验证码产品的了解,先写下"它应该怎么做":前端采集哪些事件、什么时机组装、用什么方式签名、服务端大概校验什么。然后再拿着模型去对比实际实现。你会发现大部分步骤都在预期内,真正的精力只需要花在"跟预期不一样"的少数几个点上。这个习惯帮我节省了大量盲目摸索的时间。VAPTCHA分析能顺利走通,很大程度也是靠这个思路,先搭骨架、再补血肉,比从零开始读代码高效得多。
