☰
极验验证码逆向分析:从加密原理到自动化破解实践
2026/10/19 22:11:51 网站建设 项目流程

1. 项目概述:从“识别”到“破解”的思维跃迁

在自动化测试、数据采集乃至一些合规的业务流程中,验证码始终是横亘在程序与目标服务之间的一道坎。极验验证码,以其动态交互、行为验证和强大的后台风控逻辑,成为了业内公认的“硬骨头”。很多开发者初涉此领域,往往止步于使用打码平台或简单的图像识别,一旦遇到滑块轨迹加密、动态令牌验证等进阶防护,便束手无策。今天要聊的,正是如何从“知其然”到“知其所以然”,深入极验验证码的内部,分析其前端加密逻辑与关键参数生成机制。这不仅仅是写一个能跑通的脚本,更是理解一套完整安全对抗体系的过程。无论你是为了提升自动化项目的健壮性,还是纯粹对逆向工程感兴趣,这篇内容都将带你绕过表象,直击核心。

简单来说,我们将不再满足于“模拟滑动一下”,而是要弄清楚:为什么滑动轨迹需要被加密成一段乱码?那个每次请求都不同的w参数到底是怎么算出来的?前端 JavaScript 是如何收集用户设备信息并参与验证的?理解这些,意味着你能够应对更复杂的验证码变种,甚至在对方更新算法时,能快速定位关键点进行调整。接下来,我会以一个典型的极验三代行为验证为例,拆解其完整的验证流程,并聚焦于最关键的加密环节,分享一套可复现的分析与破解思路。

2. 极验验证码核心流程与加密环节定位

2.1 三代验证码交互流程全景图

要破解加密,必须先理解流程。极验三代验证码(以滑动拼图为例)的完整交互是一个多步骤的“挑战-响应”过程,远比一个简单的图片比对复杂。其核心流程可以概括为以下四个阶段:

  1. 初始化获取挑战参数:页面加载时,前端会向极验服务器请求一次get.php或类似接口,获取本次验证会话的唯一标识challenge、验证码图片资源地址以及一系列用于后续加密的初始参数(如gt、s等)。这个challenge至关重要,它是整个验证会话的上下文 ID。

  2. 用户交互与数据收集:用户进行滑动操作时,前端 JavaScript 会默默地做大量工作。它并非只记录滑块的最终位置,而是以极高的频率(例如每毫秒)采集一个“轨迹点”数据包。每个数据包包含的内容远不止 X、Y 坐标,它还包括但不限于:当前时间戳、鼠标移动速度、加速度、在滑动过程中的相对停留时间、甚至是从多个传感器(如果支持)获取的细微抖动数据。同时,前端会收集一份详尽的“设备指纹”,包括浏览器用户代理(UA)、屏幕分辨率、插件列表、字体列表、Canvas 图像哈希、WebGL 渲染器信息等。这些数据共同构成了对“这是一个真人还是一条程序”的判断依据。

  3. 前端数据加密与提交:收集到的原始轨迹和指纹信息是明文且庞大的,直接发送既不安全也容易被模拟。因此,极验的前端代码(通常是经过混淆和压缩的 JavaScript)会执行一套加密算法。这套算法会将原始轨迹数组、challenge值、设备指纹信息等,通过一系列复杂的变换(可能是 AES、RSA,也可能是自定义的混淆算法),最终生成一个短短的、看似随机的字符串,这就是那个关键的w参数。这个参数是前端所有工作的结晶,也是提交给服务器进行判定的核心凭证。

  4. 服务端验证与响应:前端将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 插件与本地代理调试环境

单纯靠浏览器工具,面对高度混淆的代码可能效率低下。以下工具能极大提升效率:

  1. Fiddler/Charles 抓包与修改:除了抓包查看,它们更强大的功能在于“断点修改”和“自动响应”。你可以将极验的 JavaScript 文件映射到本地一个修改过的版本,方便你插入调试代码(如console.log)输出关键变量。具体操作是:抓取到.js文件请求后,设置一个 AutoResponder 规则,将该线上 URL 映射到你本地保存的、已格式化并添加了调试语句的文件。
  2. 油猴脚本辅助:编写一个油猴脚本,在极验 JS 加载后执行。你可以用这个脚本覆盖某些关键函数,或者劫持JSON.stringify、Array.prototype.push等方法,来监控轨迹数据的收集过程。例如,你可以重写鼠标事件监听器,记录下所有原始的mousemove坐标。
  3. 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 常见加密模式识别

通过动态调试,我们可能发现几种常见的加密模式:

  1. 自定义混淆算法:这是极验早期常用的方式。算法可能包括:将数字转换成36进制字符串、进行位运算(如 XOR)、按特定规则打乱数组顺序、插入固定或随机的分隔符等。其特点是代码中看不到标准加密库,而是一连串的for循环、charCodeAt、toString(36)等操作。
  2. 标准加密算法(如 AES):可能会引入CryptoJS库,或者使用浏览器的SubtleCryptoAPI。关键特征是会出现CryptoJS.AES.encrypt、mode: CBC、padding: Pkcs7等字符串常量,或者window.crypto.subtle.encrypt的调用。此时,核心是找到密钥(key)和初始化向量(iv)。它们可能硬编码在 JS 中,也可能由challenge或其他参数动态生成。
  3. RSA 非对称加密:用于加密对称加密的密钥,或者直接加密核心数据。特征是有setPublicKey、encrypt等调用,并且会有一个很长的公钥字符串(以MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A开头)。这种情况下,你需要找到公钥,并在自己的代码中实现 RSA 加密。

5.2 算法还原实战:一个简化案例

假设我们通过日志,发现一个简化的加密过程(仅为示例,真实情况复杂得多):

  • 输入:轨迹数组track,挑战码challenge。
  • 输出:一个字符串w。
  • 观察到的逻辑:
    1. 将track数组扁平化,转换成"x1,y1,t1;x2,y2,t2;..."格式的字符串s1。
    2. 将s1与challenge用|连接,得到s2 = s1 + "|" + challenge。
    3. 对s2进行encodeURIComponent编码。
    4. 将编码后的字符串每个字符的 charCode 与一个固定值(如 0x17)进行 XOR 运算。
    5. 将 XOR 后的结果数组,用toString(36)转换成 36 进制字符串。
    6. 在字符串末尾附加一个由当前时间戳生成的校验码。

还原此算法到 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_ms=2000): """ 生成一条模拟人类滑动的轨迹。 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 trajectory

6.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参数错误,服务器返回固定错误码(如-100)1. 加密算法还原不完整或有误。
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 对抗动态更新与长期维护

极验的加密逻辑和前端代码并非一成不变。为了长期有效,你需要建立一套监控和应对机制:

  1. 建立测试用例库:保存多组有效的输入输出对(轨迹,challenge,正确的w)。当验证失败时,首先用这些用例测试你的加密算法是否还正确。如果用例也失败了,说明算法已更新。
  2. 自动化差异对比:定期(例如每天)用自动化脚本去真实浏览器中运行一次验证,并捕获最新的 JS 文件。与本地备份的旧版本进行简单的文本差异对比(如diff),如果核心函数区域有大规模变动,就需要启动重新分析。
  3. 模块化设计:将你的破解代码设计得高度模块化。将“轨迹生成”、“指纹收集”、“加密算法”分离。当加密算法更新时,你只需要替换或更新encrypt.js模块,其他部分(如图像识别缺口、网络请求)可以保持不变。
  4. 关注核心,而非表象:极验的 UI 和交互可能会变(如点选、文字点选、滑块图形),但其核心的“收集行为数据->加密->提交验证”的流程范式是稳定的。你的分析工具链(动态调试、Hook、日志注入)应对这种范式是通用的,可以快速应用到新变种上。

最后必须强调,所有技术分析应仅用于学习、安全研究或授权范围内的自动化测试。理解防御机制的本质,是为了更好地构建健壮的系统,而非用于破坏规则。这套分析方法本身,是一种强大的逆向工程和逻辑思维训练,其价值远超过破解某个特定验证码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询