
1. 项目背景与核心技术拆解1.1 VAPTCHA是什么为什么值得逆向分析VAPTCHAHandpress CAPTCHA手势验证码这几年在Web风控场景里出现得越来越频繁。它的核心设计不是让用户做选择题也不是拖动滑块拼图而是要求用户按照屏幕上的手势提示在指定区域内手动绘制一个特定的轨迹比如从上到下划一道弧线、在圆圈里画一个对勾。系统通过分析用户画出的轨迹是否符合预期来判断访问者到底是真人还是自动化脚本。我在做安全测试和爬虫兼容性研究时没少跟这类验证码打交道。很多业务系统尤其是登录、注册、下单、提交表单这些关键入口都会接入VAPTCHA来挡住机器流量。作为安全研究者我们好奇的不是怎么“绕过”它而是想搞清楚它的校验逻辑走到哪一步、前端把自己采集到的哪些特征传给了后端、哪些环节存在可以优化的空间。说白了逆向分析VAPTCHA核心目的是拆解其工作机制评估其抗攻击能力并反过来给风控团队提供加固建议。这个内容适合三类人看一是做Web安全测试的朋友需要快速理解目标站点验证码的实现细节二是前端开发或风控研发想知道自己集成的手势验证码到底是怎么工作的、可能被怎么分析三是对浏览器自动化、加密和反爬机制感兴趣的学习者想借VAPTCHA这个具体对象搞明白前端校验和后端校验的边界。不过先说清楚本文讲的是原理拆解和防御视角下的分析思路不会提供任何用于破坏验证码的成品脚本。理解了机制之后你反而应该更清楚怎么把手势验证码做得更可靠。1.2 核心知识储备手势验证码的三种主流实现在深入VAPTCHA之前得先知道它属于哪一类。市面上常见的手势验证码大致有三种实现方式实现方式典型特征优势劣势Canvas轨迹绘制用户按下鼠标或手指在canvas上连续绘制前端记录坐标序列实现简单兼容性好轨迹数据丰富前端代码高度集中容易被定位分析SVG轨迹绘制通过SVG元素捕获手势常用于轻量场景图形可缩放样式控制灵活轨迹数据一般也要转换成坐标数组本质和canvas类似WebGL手势行为融合用WebGL渲染背景同时采集鼠标移动、点击压力、设备倾斜等行为数据伪造成本高能捕获更细粒度的行为特征实现复杂性能消耗大VAPTCHA大多采用第一种方式也就是基于Canvas的轨迹绘制。它的核心逻辑可以拆成三步用户绘图、前端采集、后端校验。前两步在浏览器里完成后端负责最终判定。理解了这一点逆向分析的路径就很清晰了先找到绘制手势的Canvas元素再定位生成轨迹数据的JavaScript代码最后搞清楚这些数据经过怎样的处理被发送到服务端。整个过程中前端代码是全集后端校验逻辑只能通过修改前端数据、观察响应差异来推断。这一节先打个底子。后面所有实操步骤都围绕着“定位前端采集逻辑”和“摸清校验维度”这两件事展开。搞清楚这两件事VAPTCHA的机制在你眼里就不再是黑盒。2. 前端原理深度解析2.1 手势绘制与Canvas渲染机制先看用户视角。打开一个带VAPTCHA的页面你会看到一块矩形或者圆形的区域里面画着一个手势示例比如“从左到右画一条波浪线”。当你按住鼠标左键在区域内移动时屏幕上会实时出现一条跟随光标移动的轨迹。松开鼠标后轨迹停留片刻然后前端自动发起校验。这个过程在前端是如何实现的我用一个简化后的“伪代码逻辑”来说明注意这只是一个还原思路不是任何真实系统的完整源码// 监听鼠标事件 canvas.addEventListener(mousedown, function(e) { isDrawing true; points []; // 清空轨迹点 startTime Date.now(); // 记录开始时间 points.push({x: e.offsetX, y: e.offsetY, t: startTime}); }); canvas.addEventListener(mousemove, function(e) { if (!isDrawing) return; var point {x: e.offsetX, y: e.offsetY, t: Date.now()}; points.push(point); // 在canvas上绘制线段 drawLine(points[points.length - 2], point); }); canvas.addEventListener(mouseup, function(e) { isDrawing false; endTime Date.now(); // 采集结束准备提交 collectAndVerify(points, startTime, endTime); });从这段还原出来的逻辑里我们能看到几个关键信息点坐标序列points数组保存了用户绘制路径的原始坐标点(x, y)这是最核心的数据。服务端可以通过分析坐标序列判断轨迹是平滑的还是机械的。时间戳每个点都记录了按下鼠标那一刻的时间戳。通过点与点之间的间隔可以计算绘制速度、停顿、加速度。这些信息对区分真人动作和程序生成很有帮助。起点与终点mousedown和mouseup的位置天然形成了手势的起止特征。真人绘制通常有进入和离开区域的动作而脚本生成的轨迹往往是生硬的直线或完全模拟的曲线。在VAPTCHA这类验证码里Canvas只是呈现层。真正发挥校验作用的是后面针对这些points数据做的计算。很多前端会进一步做“轨迹平滑”把采集到的点用贝塞尔曲线拟合成更规整的序列这其实是为了兼容低精度设备同时也简化了后端判定的输入。理解Canvas渲染机制的价值在于我们知道用户数据是坐标数组所以逆向分析的主战场就是找到这些坐标是如何被收集、处理和上报的。只要找得到坐标数组的生成逻辑就找到了整个验证码的前端命脉。2.2 轨迹采集与加密传输的常见做法采集到原始坐标序列后前端通常不会直接明文把坐标数组POST给服务端。因为如果明文传输任何人都能在控制台里看到完整数据安全等级就太低了。常见的处理手段有这么几类第一先做特征压缩。原始坐标数量可能几百上千个直接传输数据量太大。前端会先抽取关键点比如只保留转折点、端点、每10像素取一个等。然后再计算一些全局特征比如轨迹的总长度、最大速度、平均速度、覆盖面积、斜率变化等。这些特征值会拼接到一个字符串里连同坐标一起传给后端。第二做数据编码。最简单的编码是base64把二进制数据转成字符串。稍微复杂一点的是自定义编码方式比如把坐标除以某个随机系数进行压缩再用可逆的算法还原。看起来像“乱码”的字符串往往就是编码后的坐标。第三对消息体加密。VAPTCHA这种成熟产品通常不会只用base64。它们会用对称加密算法如AES或者非对称加密算法如RSA的公开密钥加密将整个payload加密后放在请求体里。前端代码里能看到一个加密函数输入是轨迹特征数据输出是一串密文。这里有一个安全领域的老话题前端加密有意义吗坦率地说前端加密无法阻止真正懂技术的人逆向因为密钥和算法都暴露在浏览器代码里。但它能大幅提高分析门槛能过滤掉只会用浏览器自动化工具的初级脚本迫使攻击者投入更多精力。所以对分析者来说解密逆向的目标就是弄清楚三件事消息体里包含哪些字段、这些字段如何生成的、加密算法和密钥藏在哪。然后构造自己的请求时这些环节一个都不能错。从防御角度再看一眼轨迹采集与加密传输本质上是一个“提高门槛”的工程。设计得越好分析时间越长分析时间越长风控系统发现和拦截异常访问的机会越大。3. 逆向分析实操流程3.1 环境准备与工具选型做VAPTCHA手势验证码的逆向分析不需要特殊设备一台普通开发机和浏览器就够。我常用的工具组合如下Chrome DevTools最核心的工具用来断点调试、查看网络请求、检查JavaScript执行过程。Chromium Puppeteer用于自动加载页面辅助采集页面运行时的关键信息比如DOM结构和API调用记录。抓包工具Fiddler或Charles查看HTTPS请求的详细信息包括请求头、请求体、响应体。有些站点开了HTTPS需要安装证书才能解密流量。不过大部分时候DevTools的网络面板已经够用抓包工具作为辅助。Node.js用来写一些原型脚本模拟生成轨迹数据验证我们对前端逻辑的理解是否正确。工具选型的原则是“够用就好不追求大而全”。我见过有些新手一上来就上全局Hook、脱壳、二进制分析那完全是杀鸡用牛刀。VAPTCHA本质是前端逻辑验证码第一步先把手上的JS逻辑读懂比什么都强。开始分析前还有一个准备工作准备一个稳定的浏览器环境最好开启无痕模式避免插件干扰。同时把DevTools的“Sources”面板和“Network”面板打开准备好。3.2 定位关键JS逻辑的方法定位VAPTCHA前端逻辑常用的路径是“关键字搜索”。打开页面后在Sources面板里全局搜索以下几个高频关键字vaptcha、captcha、verifyhandpress、gesture、drawtrace、track、pointsstartTime、endTime、submit在压缩混淆过的代码里这些关键字也有可能被改名但通常万变不离其宗。比如变量名可能被替换成a、b、c但字符串常量如x、y、verify、trajectory不太可能被彻底混淆因为它们在传输时需要保持可读性。另一个非常高效的方法是“XHR断点”。在DevTools的Sources面板里找到XHR/fetch断点添加一个断点条件比如URL中包含verify或者vaptcha。然后手动绘制一次完整的手势验证码脚本就会在发起请求的那一刻停住。这时候再查看调用堆栈就能一步步回溯找到收集轨迹的函数和构造请求的函数。这个方法我强烈推荐它能把“从事件监听器到网络请求”的完整链路展示出来。为了更直观我举一个模拟场景假设页面加载了一个文件a.js压缩混淆后的代码长度大约是100KB。我们直接在文件里搜索XMLHttpRequest或fetch找到发起请求的位置。然后向上查找调用栈很快就能看到一个类似这样的函数function verifyGesture(data) { var req new XMLHttpRequest(); req.open(POST, /captcha/verify); req.setRequestHeader(Content-Type, application/json); req.send(JSON.stringify({token: token, trace: data})); }到了这一步说明已经锁定了传输层的代码。接下来要看的是data这个参数在调用verifyGesture之前是怎么被拼装出来的。3.3 提取手势特征与校验规则以实际案例模拟当我通过关键字搜索和XHR断点找到关键函数后一般会打一组“条件日志断点”来观察变量变化。还是以模拟案例来说明找到发送请求的代码后我在它前面一行添加断点然后刷新页面、重新绘制手势。断点触发后我可以在作用域面板里看到即将被发送的数据对象比如{ startPoint: {x: 112, y: 211}, endPoint: {x: 388, y: 211}, trace: [112, 211, 113, 212, ...], duration: 820, speed: [0.1, 0.3, 0.5, ...], area: 1560, token: a3f5c1d8... }这些字段就是前端采集的全部信息。有了这个结构后面就很好办了startPoint和endPoint是手势的起止坐标通常需要满足特定条件比如在手势区域内、与示例轨迹的起止点大致吻合。trace是原始坐标数组后端会对比轨迹形状是否跟示例手势相似。duration是绘制时长真人一般需要几百毫秒到几秒钟脚本生成的往往特别快或异常稳定。speed数组记录了各段速度真人手绘速度有波动脚本生成的速度可能非常均匀这是风控的重要特征。area是轨迹覆盖区域面积用于判断轨迹是否规范。token可能是前面加密过程的结果也可能是一个与用户会话绑定的标识。为了验证我推测的校验规则可以做一个“对照实验”只修改duration观察响应结果是否变化。这不需要任何特殊工具直接在DevTools的Console里调用一次发送函数用修改后的数据替换原数据就行。如果服务端返回“验证失败”而其他字段没变说明duration参与校验。这里必须提醒一下做这类实验目的是理解校验机制而不是为了破解验证码。对在线业务系统进行批量自动验证尝试哪怕只是为了实验在法律和道德上都是有风险的。请大家务必在本地测试环境或者自己搭建的模拟服务上操作。4. 常见问题与排查实战4.1 常见问题速查表在分析和调试VAPTCHA的过程中几乎每个环节都可能卡壳。这里我把高频踩坑点整理成一张速查表方便你照着排查。问题现象可能原因排查路径页面加载后找不到验证码相关JS文件代码被拆分成多个chunk动态加载Network面板里按JS类型排序寻找加载时机较晚的文件用Search全局搜索关键字搜索vaptcha关键字无结果字符串被编码比如unicode编码或Base64直接搜索123或.com等常见域名后缀或尝试搜索drawImage、getContext等绘图API绘制手势后控制台无请求记录校验逻辑里做了“本地先判定”不符合条件不发请求在Canvas的mouseup事件监听器处下断点逐步走到if分支看哪里拦截了提交找到了请求代码但数据被加密看不明白密钥和算法被故意混淆或使用了WebAssembly搜索AES、encrypt、CryptoJS关键字如果是Wasm先定位导出的内存函数再用对应的逆向工具链分析修改数据重放后响应一直报错服务端校验了请求签名、Token时效或用户行为序列对比正常请求和修改后请求的header差异检查Token生成逻辑确认重放时是否需要更新当前会话状态轨迹坐标点顺序乱序或缺失前端做了抽稀处理或数组压缩还原原始坐标时注意数组可能用步长编码比如每三位表示一个点首尾补上时间戳信息这张表是我在好几个真实项目里总结出来的不一定覆盖所有VAPTCHA变体但能应对大部分场景。4.2 逆向过程中容易踩的坑除了上面那些技术问题还有很多“经验坑”是光看文档学不来的。我挑三个印象最深的展开说说。第一个坑混淆代码里搜索关键字搜着搜着搜到广告变量。有些站点把验证码SDK跟业务代码打包在一起业务代码里本身就有token、time、url这类高频词。新手常被干扰把一堆无关内容当成验证码逻辑浪费大量时间。我的办法是优先搜索那些在业务代码里几乎不可能出现的组合词比如handpress、gestureVerify、vaptchaConfig。或者直接搜中文注释虽然代码混淆了但某些站点会残留中文提示比如“轨迹长度”或“验证码初始化”。第二个坑加密函数是动态的每次刷新页面密钥都变。VAPTCHA的进阶版本会在初始化时从服务端动态获取一个会话密钥用这个密钥加密轨迹数据。如果只分析单次请求能解出当次密文却无法复现第二次。正确的分析思路是找到“获取密钥”的那个网络请求看它的响应数据存放在哪个全局变量里然后在加密函数中观察它是如何被引用的。只要沿着“初始化请求 - 全局变量 - 加密函数”这条链路走下去动态密钥也就不再神秘。第三个坑后端的校验策略不只有一重。很多朋友做完前端分析构造了一个和真实请求一模一样的数据包发过去后却发现返回了“验证失败”。这不一定是前端逻辑没分析完而是后端还有一套独立校验。比如它可能根据你提交的坐标点计算轨迹的曲率、角度变化跟预设模板做相似度比对还可能根据浏览器指纹、IP历史行为做风险评分。前端逆向只是走通了一个途径不代表能完全模拟整个风控链路。在我自己的实操中处理这类问题常用的办法是把前端采集的逻辑完整梳理成一份文档标出哪些字段是必传的、哪些字段影响结果然后先做“减法实验”——每次删掉一个字段看服务端的反应。通过反应差异反向推断出后端最在意哪些数据。这个思路比瞎猜高效得多。5. 从防御角度看如何加固手势验证码逆向分析做到最后我的心态已经变了。与其成天想着怎么“绕过”不如想想如何“防住”。VAPTCHA这类验证码之所以存在是为了提高攻击者的成本而不是为了百分之百不可攻破。理解了原理设计的防御策略会更有针对性。5.1 增加服务端行为校验层次前端坐标系采集永远可以被人为篡改真正的防线应该在服务端。一个比较可靠的方案是在服务端保存用户手绘轨迹的原始特征比如绘制速度序列、加速度变化、轨迹与示例手势的相似度分数并把这些特征输入一个机器学习模型判断轨迹是人还是机器生成的。模型可以使用坐标序列作为输入输出一个“人机置信度”。由于模型参数和训练数据都在服务端攻击者很难直接逆向出准确规则。另外服务端可以反作弊地引入“会话绑定性”把验证码的校验结果绑定到当前用户会话的多个维度上比如IP、User-Agent、设备指纹、Cookie。只要其中一个维度与历史行为不一致即便验证码本身校验通过也会触发二次验证。5.2 动态更新与混淆策略静态前端代码一旦被分析透彻就很容易被批量利用。一个比较实用的加固手段是定期动态生成混淆代码每次加载页面时JS变量名、加密算法参数、甚至传输字段名都是随机的。这样分析者即使研究出一次逻辑下次又会遇到新变体维护成本会显著上升。还有一个小技巧在页面里故意埋入一些“诱饵”代码比如跟验证逻辑相同前缀、实际不参与校验的虚假函数。它们能混淆分析者的视线延长定位时间。当然不要因此给自己正常的维护带来负担这个尺度需要权衡。5.3 建立验证码失效机制最容易被大家忽略的其实是“时效性”。很多验证码请求只要拿到结果就能无限重放。合理的做法是验证码校验通过后服务端立即把校验Token标记为已消费短时间内只能成功一次同时给Token设置一个较短的过期时间比如2分钟。这样即使攻击者抓到了完整请求重放的价值也不大。从实际体验角度建议在“必须验证”和“用户体验”之间做平衡。不要每一次访问都要求画手势只在风险评分较高时弹出验证码比如在登录失败多次、短时间频繁操作等场景下触发。这样既减少了对真实用户的干扰也让攻击者的自动化流程更容易暴露。写在最后做了这么多VAPTCHA的分析我个人最大的体会是验证码本质上是一场攻防博弈没有一劳永逸的解决方案。作为安全研究者理解其原理会让你更敬畏这些看似简单的交互设计——开发展布一个手势验证码不难但要想让它可靠且友好确实需要花很多心思。逆向分析的过程恰恰是学习这些细节的最好机会。我始终坚信研究一个安全机制的最高境界不是找到绕开它的路而是理解它为什么会存在、它保护的是什么、它薄弱的地方在哪里。这篇分析里提到的很多方法换个角度就是防御方案的底座。希望大家在实操中守住底线把所有尝试都限制在受控环境里把学到的技术用在建设性的方向上。