极验验证码逆向分析:从加密原理到自动化破解实践

发布时间:2026/7/7 20:18:51
极验验证码逆向分析:从加密原理到自动化破解实践 1. 项目概述从“识别”到“破解”的思维跃迁在自动化测试、数据采集乃至一些合规的业务流程中验证码始终是横亘在程序与目标服务之间的一道坎。极验验证码以其动态交互、行为验证和强大的后台风控逻辑成为了业内公认的“硬骨头”。很多开发者初涉此领域往往止步于使用打码平台或简单的图像识别一旦遇到滑块轨迹加密、动态令牌验证等进阶防护便束手无策。今天要聊的正是如何从“知其然”到“知其所以然”深入极验验证码的内部分析其前端加密逻辑与关键参数生成机制。这不仅仅是写一个能跑通的脚本更是理解一套完整安全对抗体系的过程。无论你是为了提升自动化项目的健壮性还是纯粹对逆向工程感兴趣这篇内容都将带你绕过表象直击核心。简单来说我们将不再满足于“模拟滑动一下”而是要弄清楚为什么滑动轨迹需要被加密成一段乱码那个每次请求都不同的w参数到底是怎么算出来的前端 JavaScript 是如何收集用户设备信息并参与验证的理解这些意味着你能够应对更复杂的验证码变种甚至在对方更新算法时能快速定位关键点进行调整。接下来我会以一个典型的极验三代行为验证为例拆解其完整的验证流程并聚焦于最关键的加密环节分享一套可复现的分析与破解思路。2. 极验验证码核心流程与加密环节定位2.1 三代验证码交互流程全景图要破解加密必须先理解流程。极验三代验证码以滑动拼图为例的完整交互是一个多步骤的“挑战-响应”过程远比一个简单的图片比对复杂。其核心流程可以概括为以下四个阶段初始化获取挑战参数页面加载时前端会向极验服务器请求一次get.php或类似接口获取本次验证会话的唯一标识challenge、验证码图片资源地址以及一系列用于后续加密的初始参数如gt、s等。这个challenge至关重要它是整个验证会话的上下文 ID。用户交互与数据收集用户进行滑动操作时前端 JavaScript 会默默地做大量工作。它并非只记录滑块的最终位置而是以极高的频率例如每毫秒采集一个“轨迹点”数据包。每个数据包包含的内容远不止 X、Y 坐标它还包括但不限于当前时间戳、鼠标移动速度、加速度、在滑动过程中的相对停留时间、甚至是从多个传感器如果支持获取的细微抖动数据。同时前端会收集一份详尽的“设备指纹”包括浏览器用户代理UA、屏幕分辨率、插件列表、字体列表、Canvas 图像哈希、WebGL 渲染器信息等。这些数据共同构成了对“这是一个真人还是一条程序”的判断依据。前端数据加密与提交收集到的原始轨迹和指纹信息是明文且庞大的直接发送既不安全也容易被模拟。因此极验的前端代码通常是经过混淆和压缩的 JavaScript会执行一套加密算法。这套算法会将原始轨迹数组、challenge值、设备指纹信息等通过一系列复杂的变换可能是 AES、RSA也可能是自定义的混淆算法最终生成一个短短的、看似随机的字符串这就是那个关键的w参数。这个参数是前端所有工作的结晶也是提交给服务器进行判定的核心凭证。服务端验证与响应前端将challenge和加密后的w参数提交到ajax.php接口。服务器端利用对应的密钥解密w参数还原出轨迹和指纹数据并与预期的模型进行比对。验证不仅检查滑块是否对准更重点分析轨迹是否符合人类行为模型、设备指纹是否异常例如是否使用了无头浏览器、自动化框架的常见指纹。最后返回success或fail的结果。我们的主攻方向就是第 3 步——前端加密逻辑。w参数是连接用户行为与服务器验证的桥梁破解了它的生成逻辑就等于掌握了自主生成合法验证请求的能力。2.2 关键加密参数w的初步探查在浏览器开发者工具F12的网络Network选项卡中筛选 XHR 请求找到向ajax.php提交的请求。查看其请求负载Payload你通常会看到类似这样的结构{ gt: a1b2c3d4e5f6..., challenge: 7g8h9i0j1k2l3m4n5o6p7..., w: 1a2b3c4d5e6f...一长串密文, callback: geetest_xxx }其中gt是网站注册极验时获得的公钥标识challenge是本次会话 ID而w就是我们的目标。这个w值每次滑动都会变化即使滑动轨迹完全相同。这说明它的生成必然包含了随机因子如时间戳和动态的challenge。注意直接搜索w:或w在源代码中定位加密函数可能很困难因为生产环境的代码是高度混淆的。变量名可能被替换成a、b、c等单字母逻辑也被打散。我们需要更系统的方法。3. 逆向分析环境搭建与核心工具链3.1 浏览器开发者工具的深度使用技巧工欲善其事必先利其器。现代浏览器的开发者工具是我们静态和动态分析的基础。Sources 面板与代码格式化在 Sources 面板找到极验的 JavaScript 文件通常名为fullpage.xxx.js或gt.xxx.js。点击底部{}美化按钮将压缩的代码格式化使其可读。虽然变量名仍是混淆的但代码结构如函数定义、条件判断、循环清晰了。Network 面板的断点功能在 Network 面板中找到ajax.php请求右键选择 “Replay XHR” 有时不生效因为challenge是一次性的。更好的方法是在提交w参数之前打上 JavaScript 断点。在 Sources 面板的代码中搜索XMLHttpRequest的send方法或者搜索w被赋值的地方例如param.w ...然后打上断点。当滑动触发请求时执行流会在此暂停。Call Stack 调用堆栈当断点触发时右侧的 Call Stack调用堆栈窗口是无价之宝。它显示了当前断点函数是被谁调用的层层回溯可以让我们逆向找到w参数的生成源头函数。通常你需要从最顶层的send方法一步步向下查看找到那个将轨迹数组转换成w字符串的函数。3.2 插件与本地代理调试环境单纯靠浏览器工具面对高度混淆的代码可能效率低下。以下工具能极大提升效率Fiddler/Charles 抓包与修改除了抓包查看它们更强大的功能在于“断点修改”和“自动响应”。你可以将极验的 JavaScript 文件映射到本地一个修改过的版本方便你插入调试代码如console.log输出关键变量。具体操作是抓取到.js文件请求后设置一个 AutoResponder 规则将该线上 URL 映射到你本地保存的、已格式化并添加了调试语句的文件。油猴脚本辅助编写一个油猴脚本在极验 JS 加载后执行。你可以用这个脚本覆盖某些关键函数或者劫持JSON.stringify、Array.prototype.push等方法来监控轨迹数据的收集过程。例如你可以重写鼠标事件监听器记录下所有原始的mousemove坐标。Node.js 本地模拟环境这是最终将破解逻辑落地的关键。创建一个 Node.js 项目使用puppeteer或playwright这类无头浏览器库它们能真实执行页面 JavaScript同时暴露强大的 API 供你提取运行时数据。你可以让无头浏览器加载页面执行滑动然后在关键函数处通过page.evaluateOnNewDocument注入代码将加密函数的输入输出直接打印到 Node.js 控制台完全绕过浏览器混淆的干扰。我的常用组合是Charles 做文件映射 油猴脚本做初步数据采集 Node.js Puppeteer 做动态分析与算法复现。这套组合拳能应对绝大多数混淆场景。4. 动态调试追踪与加密函数定位4.1 基于调用堆栈的逆向追踪实战假设我们已经通过 Network 断点在send函数处暂停。查看 Call Stack可能会看到类似这样的调用链名称是混淆后的send (native) someFunctionA someFunctionB - 这里可能负责组装参数对象 {gt, challenge, w} encryptFunction - 疑似加密函数 trajectoryGenerator - 轨迹生成函数 eventListener - 鼠标事件监听器我们的目标是找到encryptFunction。在 Call Stack 中点击someFunctionB代码会跳转到对应的位置。在这个函数上下文中观察哪些变量最终被赋值给了param.w。通常你会看到类似param.w d(trajectory, challenge, otherData)的语句这个d函数就是我们的重点怀疑对象。在d函数内部打上断点重新滑动。断点触发后在 Console 面板输入console.log(JSON.stringify(arguments))查看传入的所有参数。同时使用 Step Over (F10) 和 Step Into (F11) 逐步执行观察每一步的变量变化。重点关注传入的轨迹数据 (trajectory) 结构是怎样的是一个二维数组[[t1,x1,y1], [t2,x2,y2]...]还是对象数组除了轨迹和challenge还有哪些参数被传入是否有设备指纹对象函数内部是否存在明显的加密操作特征例如是否有Array转String是否有encodeURIComponent、btoa或者是否有CryptoJS、AES、RSA等关键词即使变量名被混淆其常量字符串或特有方法名可能还在4.2 日志注入与关键数据捕获由于代码混淆单步调试可能非常耗时。更高效的方法是“日志注入”。通过 Fiddler/Charles 将 JS 文件映射到本地在疑似加密函数d的开头和结尾添加如下代码// 在函数开头添加 console.log([Encrypt Input], typeof trajectory, trajectory.length, JSON.stringify(trajectory.slice(0,2)), challenge); window._temp_input {t: trajectory, c: challenge}; // 暂存到全局方便提取 // 在函数返回前添加 console.log([Encrypt Output], typeof encryptedW, encryptedW); window._temp_output encryptedW;然后清空浏览器控制台执行一次完整的滑动操作。你将在控制台看到清晰的输入输出。重复几次观察相同的轨迹输入w输出是否变化如果变化说明函数内部引入了随机数或时间戳。在 Node.js Puppeteer 环境中这个操作更强大await page.evaluateOnNewDocument(() { // 覆盖或Hook疑似加密函数 const oldEncryptFunc window.某个混淆的加密函数; window.某个混淆的加密函数 function(...args) { console.log(加密函数被调用参数:, args); const result oldEncryptFunc.apply(this, args); console.log(加密函数结果:, result); // 将结果暴露给Node环境 window._lastEncryptionResult {args, result}; return result; }; });这样你可以在 Node.js 脚本中直接访问page.evaluate(() window._lastEncryptionResult)来获取纯净的数据。5. 加密逻辑拆解与算法还原5.1 常见加密模式识别通过动态调试我们可能发现几种常见的加密模式自定义混淆算法这是极验早期常用的方式。算法可能包括将数字转换成36进制字符串、进行位运算如 XOR、按特定规则打乱数组顺序、插入固定或随机的分隔符等。其特点是代码中看不到标准加密库而是一连串的for循环、charCodeAt、toString(36)等操作。标准加密算法如 AES可能会引入CryptoJS库或者使用浏览器的SubtleCryptoAPI。关键特征是会出现CryptoJS.AES.encrypt、mode: CBC、padding: Pkcs7等字符串常量或者window.crypto.subtle.encrypt的调用。此时核心是找到密钥key和初始化向量iv。它们可能硬编码在 JS 中也可能由challenge或其他参数动态生成。RSA 非对称加密用于加密对称加密的密钥或者直接加密核心数据。特征是有setPublicKey、encrypt等调用并且会有一个很长的公钥字符串以MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A开头。这种情况下你需要找到公钥并在自己的代码中实现 RSA 加密。5.2 算法还原实战一个简化案例假设我们通过日志发现一个简化的加密过程仅为示例真实情况复杂得多输入轨迹数组track挑战码challenge。输出一个字符串w。观察到的逻辑将track数组扁平化转换成x1,y1,t1;x2,y2,t2;...格式的字符串s1。将s1与challenge用|连接得到s2 s1 | challenge。对s2进行encodeURIComponent编码。将编码后的字符串每个字符的 charCode 与一个固定值如 0x17进行 XOR 运算。将 XOR 后的结果数组用toString(36)转换成 36 进制字符串。在字符串末尾附加一个由当前时间戳生成的校验码。还原此算法到 Python/Node.js 的伪代码import urllib.parse import time def generate_w(track, challenge): # 1. 扁平化轨迹 s1_parts [] for point in track: # 假设point是 [x, y, t] s1_parts.append(f{point[0]},{point[1]},{point[2]}) s1 ;.join(s1_parts) # 2. 连接 challenge s2 s1 | challenge # 3. URL编码 s3 urllib.parse.quote(s2) # 4. XOR 运算 xor_key 0x17 s4_chars [] for char in s3: s4_chars.append(chr(ord(char) ^ xor_key)) s4 .join(s4_chars) # 5. 转36进制 # 注意需要先将字符串转换成字节或数字。这里假设对每个字符的charCode操作。 s5_parts [] for char in s4: s5_parts.append(str(ord(char) % 36)) # 简化逻辑真实情况可能是大数转换 s5 .join(s5_parts) # 6. 附加时间戳校验码 timestamp int(time.time() * 1000) checksum str(timestamp % 1000).zfill(3) # 取后三位 w s5 checksum return w实操心得算法还原是一个反复比对的过程。你需要用自己还原的算法对多组已知的输入通过日志捕获的track和challenge进行计算将输出与捕获的真实w进行比对。必须保证多组数据都能完全匹配才能说明你的还原是正确的。一个字符的差异都意味着还有未发现的步骤或参数。5.3 处理动态密钥与随机因子更复杂的情况是加密密钥或 IV 是动态的。它们可能从第一次get.php请求的响应中获取藏在某个字段里。由challenge通过一个固定的算法派生出来。每次页面加载时由前端生成一个随机种子并可能通过 RSA 加密后发送到后端。应对策略是在动态调试时不仅要关注加密函数的输入输出还要关注函数内部引用的外部变量或函数返回值。这些可能就是动态生成的密钥。同样通过日志注入将这些变量的值也打印出来。6. 参数构造与请求模拟的完整实现6.1 轨迹模拟的生物学合理性即使你完美破解了加密一个不合理的滑动轨迹也会被服务器拒绝。轨迹模拟是另一个关键。人类滑动轨迹的特征包括先加速后减速起始和结束阶段速度慢中间阶段速度快。微小抖动路径不是完美的贝塞尔曲线会有细微的、随机的偏移。时间分布总时间通常在 1-3 秒过快或过慢都像机器。停留在接近缺口时可能会有极短的停顿思考时间。一个简单的轨迹生成函数Python可能长这样import random import math def generate_trajectory(distance, total_time_ms2000): 生成一条模拟人类滑动的轨迹。 distance: 需要滑动的总距离像素。 total_time_ms: 总耗时毫秒。 返回: 轨迹列表每个元素为 [时间偏移ms, x坐标, y坐标]。 trajectory [] current_x 0 current_time 0 # 使用匀加速和匀减速模型 # 假设加速时间占30%匀速占40%减速占30% t_acc total_time_ms * 0.3 t_const total_time_ms * 0.4 t_dec total_time_ms * 0.3 # 加速度和减速度 a_acc distance * 2 / (t_acc * (t_acc t_const)) # 简化计算 a_dec -distance * 2 / (t_dec * (t_dec t_const)) # 生成加速段轨迹 steps int(t_acc / 10) # 每10ms一个点 for i in range(steps): t i * 10 # 匀加速运动公式: s 0.5 * a * t^2 s 0.5 * a_acc * (t ** 2) trajectory.append([int(current_time t), int(s), 0]) # y坐标通常为0或微小抖动 current_x s # 生成匀速段轨迹 v_const a_acc * t_acc # 加速段末速度 steps int(t_const / 10) for i in range(steps): t i * 10 s current_x v_const * t trajectory.append([int(current_time t_acc t), int(s), 0]) current_x s # 生成减速段轨迹 steps int(t_dec / 10) for i in range(steps): t i * 10 # 匀减速运动公式: s v0*t 0.5*a*t^2 s current_x v_const * t 0.5 * a_dec * (t ** 2) trajectory.append([int(current_time t_acc t_const t), int(s), 0]) # 添加微小随机抖动 for point in trajectory: point[1] random.randint(-1, 1) # x坐标抖动 point[2] random.randint(-2, 2) # y坐标微小抖动 # 确保最后一个点精确到达目标距离 trajectory[-1][1] distance return trajectory6.2 完整请求模拟代码框架Node.js示例将轨迹生成和加密算法结合起来一个完整的模拟请求流程如下const axios require(axios); const { generateTrajectory } require(./trajectory); const { encryptW } require(./geetest_encrypt); // 这是你还原的加密算法 async function bypassGeetest(gt, challenge, slideDistance) { // 1. 生成模拟轨迹 const trajectory generateTrajectory(slideDistance, 1800 Math.random() * 500); // 2. 收集设备指纹 (这里需要模拟一个固定指纹或从真实浏览器环境获取) // 在实际项目中你可能需要运行一个无头浏览器来获取真实的指纹。 // 这里用一个简化版示例。 const fp { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., screenWidth: 1920, screenHeight: 1080, // ... 更多指纹信息 }; // 3. 使用还原的算法生成 w 参数 const w encryptW(trajectory, challenge, fp); // 4. 构造请求参数 const params new URLSearchParams(); params.append(gt, gt); params.append(challenge, challenge); params.append(w, w); params.append(callback, geetest_${Date.now()}); // 5. 发送验证请求 try { const response await axios.post(https://api.geetest.com/ajax.php, params, { headers: { Content-Type: application/x-www-form-urlencoded, User-Agent: fp.userAgent, // 可能还需要其他Headers如Referer } }); // 6. 解析响应 (通常是JSONP格式) const respText response.data; const jsonpMatch respText.match(/geetest_\d\((.*)\)/); if (jsonpMatch) { const result JSON.parse(jsonpMatch[1]); console.log(验证结果:, result); return result.status success; } } catch (error) { console.error(请求失败:, error); } return false; } // 使用示例 // gt和challenge从首次get请求获得slideDistance通过图像识别获得 // const success await bypassGeetest(1234567890abcdefg, challenge_string_here, 120);7. 常见问题排查与风控对抗策略7.1 验证失败原因深度排查表即使算法还原了请求也可能失败。以下是系统性的排查清单现象可能原因排查手段与解决方案w参数错误服务器返回固定错误码如-1001. 加密算法还原不完整或有误。2. 传入加密函数的参数不全漏了某个关键数据。3. 设备指纹缺失或格式错误。1.交叉验证用同一组输入轨迹、challenge在浏览器环境通过你的Hook脚本和本地还原算法各跑一次对比输出的w是否完全一致。必须二进制或字符串完全匹配。2.参数捕获在加密函数入口Hook打印arguments或...args确保你本地模拟时传入了完全相同的参数数量和结构。3.指纹模拟检查指纹对象是否包含了所有必要字段。极验可能依赖navigator.plugins.length、Canvas指纹等。使用puppeteer-extra-plugin-stealth等插件来模拟更真实的指纹。验证有时成功有时失败成功率低1. 轨迹模拟过于规律被行为风控识别。2. 请求频率过高触发IP或会话频率限制。3. 设备指纹不一致每次请求指纹变化。1.轨迹优化在轨迹生成函数中引入更多的随机性加速曲线变化、抖动幅度、总时间波动。可以采集大量真人滑动轨迹分析其统计特征进行模仿。2.请求节奏在关键操作间添加随机延迟模拟人类思考时间。避免在短时间内对同一challenge重复提交。3.指纹固化对于一次验证会话设备指纹应保持不变。可以将指纹信息持久化如存入文件在同一次任务中重复使用。首次get请求成功但ajax验证请求返回“challenge过期”或“无效请求”1.challenge与gt不匹配或已过期。2. 请求头Headers缺失或错误如Referer、Origin、X-Requested-With。3. Cookies 缺失。极验可能在第一次请求时设置了会话 Cookie。1.会话管理确保gt和challenge来自同一次get请求响应并且在有效期内使用通常几分钟。2.请求头复制使用抓包工具如Fiddler捕获一次成功的浏览器请求将它的所有Headers尤其是Referer、Origin、Accept-Language原样复制到你的模拟请求中。3.Cookie处理使用像axios的withCredentials: true或配合tough-cookie库来管理 Cookie Jar自动处理会话。完全无法触发验证码或加载失败1. 目标网站页面结构或极验初始化方式改变。2. 你的爬虫环境如无头浏览器被检测。1.动态适配极验的嵌入代码可能更新。需要重新分析页面找到初始化极验的 JavaScript 调用并确保你的脚本能正确触发它。2.环境伪装使用puppeteer-extra及其stealth插件来隐藏无头浏览器的特征。禁用webdriver属性覆盖navigator.languages等。7.2 对抗动态更新与长期维护极验的加密逻辑和前端代码并非一成不变。为了长期有效你需要建立一套监控和应对机制建立测试用例库保存多组有效的输入输出对轨迹challenge正确的w。当验证失败时首先用这些用例测试你的加密算法是否还正确。如果用例也失败了说明算法已更新。自动化差异对比定期例如每天用自动化脚本去真实浏览器中运行一次验证并捕获最新的 JS 文件。与本地备份的旧版本进行简单的文本差异对比如diff如果核心函数区域有大规模变动就需要启动重新分析。模块化设计将你的破解代码设计得高度模块化。将“轨迹生成”、“指纹收集”、“加密算法”分离。当加密算法更新时你只需要替换或更新encrypt.js模块其他部分如图像识别缺口、网络请求可以保持不变。关注核心而非表象极验的 UI 和交互可能会变如点选、文字点选、滑块图形但其核心的“收集行为数据-加密-提交验证”的流程范式是稳定的。你的分析工具链动态调试、Hook、日志注入应对这种范式是通用的可以快速应用到新变种上。最后必须强调所有技术分析应仅用于学习、安全研究或授权范围内的自动化测试。理解防御机制的本质是为了更好地构建健壮的系统而非用于破坏规则。这套分析方法本身是一种强大的逆向工程和逻辑思维训练其价值远超过破解某个特定验证码。