
1. 这不是“破解”而是理解极验三代验证码的通信契约你打开一个电商下单页点击提交页面弹出滑块验证——背景是动态扭曲的拼图顶部提示“请完成验证”。你拖动滑块咔哒一声绿色对勾出现请求成功发出。整个过程不到3秒。但就在你松开鼠标的一瞬间浏览器控制台里一条带w参数的POST请求已悄然发出目标是https://api.geetest.com/ajax.php。这个w值长度约2000字符全由字母、数字和符号组成每次刷新都不同。它像一把一次性钥匙锁住了你这次行为的合法性。这就是极验三代Geetest v3验证码的核心机制服务端不直接校验用户是否“真的拖动了滑块”而是校验客户端生成的w参数是否符合当前上下文的加密契约。它不是传统意义上的“图片识别题”而是一套运行在浏览器沙箱内的轻量级JS虚拟机动态混淆环境指纹融合的综合验证协议。所谓“逆向”不是暴力爆破或OCR识别而是还原这套契约的生成逻辑——就像读懂一份用自定义密码本写就的密信关键不在猜字而在复原密码本本身。我第一次接触这个需求是在帮一家做跨境选品工具的团队处理登录自动化问题。他们需要批量获取商品价格但目标站启用了极验v3所有登录请求都被w参数拦截。当时网上能找到的资料90%停留在“用Selenium模拟拖动截图识别”的粗暴方案成功率不到40%且极易被识别为自动化流量剩下10%则直接给出一段混淆过的JS解密函数却不解释其中a、b、c三个核心变量从哪来、为何要这样异或、为什么时间戳要取毫秒后三位。结果就是代码能跑通但换一个站点、升级一次极验SDK版本立刻失效。这篇文章就是把那本被撕碎、烧掉、又重新拼起来的“密码本”一页一页摊开给你看。它不教你绕过风控而是带你真正理解极验v3如何定义“人”与“机器”的边界。你会看到抓包只是起点w的生成是终点而中间那条路——从HTML加载、JS初始化、行为采集、到最终加密签名——每一步都有其不可省略的因果链。适合正在做自动化测试、爬虫风控对抗、或前端安全研究的工程师。如果你只想要现成的w生成函数这篇文章可能让你失望但如果你希望下次遇到新版本时能自己定位到w生成函数的入口、读懂它的依赖、并快速适配那接下来的内容就是你真正需要的“地图”。2. 抓包只是表象极验v3的三重通信结构与w的真实角色很多人以为抓到那个带w参数的POST请求就等于拿到了通关密钥。这是最大的误解。w从来不是孤立存在的它是极验v3整套通信协议中最后一个环节输出的摘要凭证其有效性完全依赖于前两个环节的正确执行。把整个流程拆开它其实是一个典型的“三段式握手”2.1 第一段初始化请求init—— 获取挑战ID与公钥当你访问一个嵌入极验v3的页面时首先进入的是init阶段。浏览器会向https://api.geetest.com/get.php发起GET请求携带gt极验分配的全局公钥、challenge本次会话的随机挑战串、lang、pt等参数。服务器返回的不是验证码图片而是一段JSON核心字段包括success: 1 表示正常流程0 表示需走备用验证码如文字点选gt: 同请求参数用于后续校验challenge: 本次会话唯一ID形如a1b2c3d4e5f678901234567890abcdefnew_captcha: 1 表示启用v3新协议data: 一段经过Base64编码的字符串内含真正的公钥、AES加密的初始密钥、以及JS资源路径提示challenge是整个会话的“身份证号”后续所有操作都必须携带它。很多初学者直接复制别人抓到的w去发请求失败的根本原因就是challenge已过期或不匹配。2.2 第二段行为采集与本地计算ajax—— 构建环境指纹与行为特征init成功后页面会动态加载极验的主JS文件如https://static.geetest.com/.../geetest.6.0.0.js并执行初始化。此时JS引擎开始工作它做的第一件事不是渲染滑块而是静默采集12类环境特征包括浏览器User-Agent与navigator对象的完整属性树注意不是简单取值而是递归遍历所有可枚举属性屏幕分辨率、设备像素比、可用窗口尺寸Web Audio API生成的音频上下文指纹通过audioContext.createOscillator()生成特定频率波形并采样哈希Canvas绘图API的字体渲染差异指纹绘制隐藏文本读取像素数据生成MD5WebGL渲染管线的驱动信息与着色器编译能力本地存储localStorage/sessionStorage的键名列表与大小时序行为从JS加载完成到用户首次鼠标移动的毫秒级延迟、鼠标移动轨迹的加速度曲线拟合系数这些数据不会明文上传而是被送入一个名为getGtObj的内部对象经由一套多层嵌套的异或XOR、位移SHL/SHR、模加MOD运算与challenge、当前毫秒时间戳Date.now()、以及一个硬编码的种子字符串如geetest混合生成一个32字节的原始指纹fingerprint。2.3 第三段签名生成与提交register——w参数的诞生现场当用户完成滑块拖动或点选操作后极验JS会触发register流程。此时它将第二步生成的fingerprint连同以下关键数据一并输入到一个名为getW的函数中fingerprint: 上一步计算出的32字节环境指纹challenge: 初始化时获得的挑战IDuserresponse: 用户行为的结构化描述如滑块偏移量、点选坐标数组passtime: 从滑块加载完成到用户释放鼠标的毫秒数imgload: 图片资源加载完成耗时毫秒aa: 一个由fingerprint派生出的16字节密钥通过AES-128 ECB模式加密固定明文生成ep: 一个包含navigator、screen、location等对象序列化后的JSON字符串再经SHA256哈希getW函数的本质是一个高度混淆的、基于WebAssembly模块在新版中或纯JS实现的非对称签名算法模拟器。它并非使用RSA或ECDSA而是采用极验自研的GeetestCrypto协议先用aa密钥对ep进行AES-CBC加密再将加密结果与fingerprint、passtime等字段拼接最后用一个硬编码的256位密钥存储在JS内存中非明文进行HMAC-SHA256签名。最终签名结果被Base64编码并附加一个版本标识如v3和时间戳构成完整的w参数。注意w的长度约2000字符主要来自Base64编码后的二进制签名数据。它不是随机字符串而是对“当前环境当前行为当前时间”的一次确定性哈希。这意味着即使你完全模拟了用户操作只要浏览器环境指纹如Canvas指纹与真实环境存在微小差异w就会完全不同服务器校验必然失败。3. 定位getW函数从网络请求回溯到JS执行栈的完整路径找到w参数的生成函数是整个逆向过程中最耗时也最关键的一步。网上流传的“搜索getW字符串”方法在极验v3.5版本后已完全失效——因为getW本身就是一个被动态重命名的变量其真实函数名可能是_0x1a2b、aBcDeF甚至是window[‘\x67\x65\x74\x57’]Unicode转义。我们必须放弃字符串搜索转而采用行为驱动的动态追踪法。3.1 第一步锁定register请求的发起源头打开Chrome开发者工具切换到Network标签页勾选“Preserve log”。然后在页面上完成一次滑块验证。找到那个向https://api.geetest.com/ajax.php发送的POST请求右键选择“Reveal in Network Panel”再点击右侧的“Initiator”链接。你会看到一个调用栈形如geetest.6.0.0.js:12345 geetest.6.0.0.js:67890 geetest.6.0.0.js:24680这串数字代表JS文件中的行号。不要急着点进去先记下最后一行即最顶层的调用者比如geetest.6.0.0.js:24680。这个位置大概率就是register函数的入口。3.2 第二步在Sources面板中设置断点并观察执行流在Sources面板中找到并打开geetest.6.0.0.js文件。滚动到第24680行附近你会看到类似这样的代码function _0x4567(_0x1234, _0x5678) { var _0x89ab _0x1234[get](_0x5678); return _0x89ab _0x89ab[length] ? _0x89ab[0x0] : null; }这显然不是我们要找的。此时你需要做的是在这一行设置一个“条件断点”。右键该行选择“Edit breakpoint”输入条件request.url.includes(ajax.php) request.method POST然后刷新页面再次触发验证。当断点命中时暂停执行切换到Console面板输入console.log(new Error().stack)你会得到一个更清晰的、包含函数名的调用栈。重点寻找其中带有register、submit、verify、doRegister等语义的函数名。假设你看到at Object.register (geetest.6.0.0.js:11223) at HTMLDivElement.anonymous (geetest.6.0.0.js:54321)那么geetest.6.0.0.js:11223就是register函数的真正位置。3.3 第三步深入register函数追踪w的组装逻辑跳转到11223行你会看到register函数的主体。它的典型结构是register: function() { var _0x1234 this[getFingerprint](); // 关键获取指纹 var _0x5678 this[getUserResponse](); // 获取用户行为 var _0x9abc this[getPassTime](); // 获取耗时 var _0xdef0 this[getW](_0x1234, _0x5678, _0x9abc); // 核心生成w // ... 后续构造POST数据 }现在目标非常明确找到this[getW]的定义。在register函数内部向上搜索getW或者搜索this[getW] 、this.getW 。你可能会找到this[getW] function(_0x1234, _0x5678, _0x9abc) { var _0xdef0 _0x1234[slice](0x0, 0x20); var _0x1357 _0x5678[join](); var _0x2468 _0x9abc ; var _0x3579 _0xdef0 _0x1357 _0x2468; return btoa(_0x3579); // 错这只是简化版真实情况复杂得多 };这显然太简单了。真正的getW往往被包裹在一个立即执行函数IIFE中或者被赋值给一个全局变量如window._geetest_w_func。此时你需要回到Console在断点暂停状态下尝试执行this.getW.toString() // 或 window._geetest_w_func?.toString() // 或 Object.values(this).find(f typeof f function f.toString().length 1000)一旦你拿到getW函数的源码恭喜你已经站在了逆向的终点线上。接下来就是逐行解析它的加密逻辑。3.4 第四步解析getW—— 识别核心算法组件真实的getW函数通常包含以下可识别的“算法锚点”AES加密块查找CryptoJS.AES.encrypt、window.AES、_0x1234[encrypt]等调用其第二个参数往往是密钥aa第三个参数是配置对象指定CBC模式、PKCS7填充。HMAC-SHA256签名查找CryptoJS.HmacSHA256、window.CryptoJS.HmacSHA256、_0x5678[create]等其第一个参数是待签名数据第二个参数是256位密钥常以Uint8Array形式存在。Base64编码查找btoa、window.btoa、_0x9abc[toString](base64)这是w最终呈现的一步。时间戳处理查找Date.now()、new Date()并注意其是否被截取如Date.now().toString().slice(-3)这是反调试的关键特征。实操心得我曾在一个项目中发现getW函数里有一个setTimeout调用延迟100ms后才执行核心签名。起初以为是防抖后来调试发现这是为了等待WebGL上下文完全初始化确保Canvas指纹采集完整。忽略这个延迟会导致fingerprint缺失关键字段w永远无效。所以逆向不仅是读代码更是理解代码背后的业务意图和环境约束。4. 从JS到Pythonw参数生成的跨语言复现与关键陷阱当你在浏览器里成功定位并理解了getW函数后下一步就是将其逻辑翻译成Python或其他后端语言以便集成到你的自动化系统中。这一步看似简单实则暗藏大量“跨语言鸿沟”。我见过太多人JS里能完美生成wPython里却始终差几个字节最终归因于以下五个致命陷阱。4.1 陷阱一Date.now()的精度与时区陷阱JS中的Date.now()返回的是自1970年1月1日00:00:00 UTC以来的毫秒数这是一个绝对时间戳不受本地时区影响。但在Python中新手常犯的错误是# ❌ 错误time.time()返回的是秒级浮点数且受系统时区影响 import time timestamp int(time.time() * 1000) # ✅ 正确使用datetime强制UTC并精确到毫秒 from datetime import datetime, timezone timestamp int(datetime.now(timezone.utc).timestamp() * 1000)更隐蔽的陷阱在于极验JS有时会取Date.now()的后三位数字Date.now().toString().slice(-3)作为随机盐。Python中必须严格保证int(str(timestamp)[-3:])而不是timestamp % 1000因为当timestamp末尾是001时%1000得1而str(timestamp)[-3:]得001两者在后续字符串拼接中会产生完全不同的哈希结果。4.2 陷阱二navigator对象的深度序列化差异JS中navigator是一个复杂的对象其属性如plugins、mimeTypes是动态的、惰性加载的。极验JS采集时会递归调用JSON.stringify(navigator, (key, value) {...})并在replacer函数中过滤掉undefined、function并对ArrayBuffer、TypedArray等特殊类型进行Array.from()转换。Python中若直接用json.dumps(dict(navigator))会失败因为navigator不是Python字典。正确做法是预先构建一个与目标浏览器环境完全一致的navigator模拟字典其键名、嵌套结构、甚至属性的enumerable状态都必须与JS中Object.keys(navigator)的输出完全一致。例如# JS中 navigator.plugins 是一个 PluginArray 对象有 length 属性和索引访问 # Python中必须模拟 navigator { plugins: [ {name: Chrome PDF Plugin, filename: internal-pdf-viewer}, {name: Shockwave Flash, filename: pepflashplayer.dll} ], mimeTypes: [...], vendor: Google Inc., platform: Win32, # ... 必须包含所有极验JS会访问的127个属性 }经验技巧最省力的方法是在浏览器中执行JSON.stringify(navigator)将其结果保存为一个JSON文件然后在Python中json.load()它。但这仅适用于静态环境。对于需要动态变化的场景如不同UA必须编写一个NavigatorBuilder类按需生成。4.3 陷阱三Canvas指纹的像素级一致性这是最难攻克的陷阱。极验v3的Canvas指纹是通过在隐藏的canvas上绘制一段特定文本如BrowserCanvasTest然后调用ctx.getImageData(0, 0, 100, 100).data获取像素数组再对前1000个像素的RGBA值进行MD5哈希。Python中没有原生的Canvas API。常见方案是用Pillow库模拟from PIL import Image, ImageDraw, ImageFont import hashlib def canvas_fingerprint(): img Image.new(RGB, (100, 100), colorwhite) d ImageDraw.Draw(img) # ❌ 错误使用系统默认字体渲染效果与Chrome完全不同 # d.text((10, 10), BrowserCanvasTest, fillblack) # ✅ 正确必须使用Chrome实际使用的字体通常是Arial或Helvetica try: font ImageFont.truetype(arial.ttf, 12) except: font ImageFont.load_default() d.text((10, 10), BrowserCanvasTest, fillblack, fontfont) # 关键必须模拟抗锯齿、子像素渲染等Chrome特有行为 # Pillow默认是无抗锯齿的需手动开启 # 此处省略复杂配置实际项目中需用skia-python等更底层库 pixels list(img.getdata()) md5 hashlib.md5() for p in pixels[:1000]: md5.update(bytes(p)) return md5.hexdigest()但即便如此Pillow生成的像素与Chrome Canvas仍存在细微差异。我的解决方案是放弃纯Python模拟改用无头Chrome执行一小段JS脚本直接获取getImageData的原始数据。这牺牲了一点性能但换来100%的准确性from selenium import webdriver from selenium.webdriver.chrome.options import Options def get_canvas_fingerprint(): options Options() options.add_argument(--headless) options.add_argument(--no-sandbox) driver webdriver.Chrome(optionsoptions) driver.get(data:text/html,canvas idc/canvas) fingerprint driver.execute_script( const c document.getElementById(c); const ctx c.getContext(2d); ctx.font 12px Arial; ctx.fillText(BrowserCanvasTest, 10, 10); const data ctx.getImageData(0, 0, 100, 100).data; return Array.from(data.slice(0, 1000)); ) driver.quit() return hashlib.md5(bytes(fingerprint)).hexdigest()4.4 陷阱四AES与HMAC的密钥派生方式极验JS中aa密钥并非一个固定字符串而是由fingerprint通过AES-128 ECB加密一个固定明文如geetest_key_seed生成。Python中必须严格遵循加密模式AES.MODE_ECB填充方式PKCS7不是PKCS5虽然二者在128位块长下等价但库实现可能不同密钥长度必须为16字节128位不足则补零超长则截断from Crypto.Cipher import AES from Crypto.Util.Padding import pad import hashlib def derive_aa_key(fingerprint_bytes): # fingerprint_bytes 是32字节的原始指纹 # 取前16字节作为AES密钥 aes_key fingerprint_bytes[:16] # 固定明文 plaintext bgeetest_key_seed # ECB模式无需IV cipher AES.new(aes_key, AES.MODE_ECB) # PKCS7填充 padded pad(plaintext, AES.block_size) aa_key cipher.encrypt(padded) return aa_key而HMAC-SHA256的密钥通常是另一个256位32字节的硬编码值以十六进制字符串形式存在于JS中。Python中必须用bytes.fromhex()将其转为字节而非直接当作字符串传入hmac.new()。4.5 陷阱五w参数的最终组装与Base64变体最后一步w的组装。JS中常用btoa()它生成的是标准Base64编码。但极验v3的w在Base64编码后还会进行一次字符替换如→-/→_→.这是URL安全Base64Base64URL的标准做法。Python中不能直接用base64.b64encode()而必须用import base64 def urlsafe_b64encode(data): return base64.urlsafe_b64encode(data).decode(utf-8).replace(, .) # 最终w urlsafe_b64encode(signature) .v3. str(int(time.time()))此外w的末尾还附加了.v3.和一个时间戳这个时间戳是getW函数执行时的Date.now()而非请求发送时的时间。因此在Python中必须在hmac.new(...).digest()之后立即获取当前时间戳确保与JS行为一致。踩坑实录我在一个金融数据爬虫项目中曾因忽略了.v3.后的时间戳导致w在生成后10秒内有效超过即失效。而我们的请求队列有排队延迟结果大量请求因w过期被拒。最终解决方案是在生成w后立即将其缓存10秒并为每个challenge维护一个w池按需分发。5. 稳定性与维护应对极验v3版本迭代的防御性设计策略逆向成功w能稳定生成这只是一个开始。极验的SDK更新频率极高平均每月一次小版本如6.0.1 → 6.0.2每季度一次大版本如6.0.x → 6.1.x。每一次更新都可能重构getW的调用链、更换加密密钥、或增加新的环境检测项。指望一个“万能脚本”长期有效是不现实的。我们必须建立一套防御性、可监控、易升级的维护体系。5.1 策略一接口契约化—— 将w生成封装为独立服务不要把getW逻辑直接耦合在你的主业务代码中。应该将其抽象为一个独立的、有明确定义的HTTP服务例如POST /geetest/w Content-Type: application/json { gt: a1b2c3d4e5f678901234567890abcdef, challenge: f1e2d3c4b5a678901234567890abcdef, user_response: [120, 340], passtime: 1245, imgload: 892 } Response: { w: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., version: 6.0.2, timestamp: 1712345678901 }这个服务的内部实现可以是Node.js完美复现JS环境、Python用上述方案、甚至Go用golang.org/x/crypto库。关键是它对外暴露的只是一个稳定的JSON API。当极验SDK更新时你只需修改这个服务的内部实现而所有调用方爬虫、测试框架、自动化脚本完全无感。5.2 策略二自动化版本监控—— 让更新不再“突然”建立一个轻量级的监控脚本每天定时访问目标网站下载最新的极验JS文件如geetest.6.0.2.js并与本地存档的上一版本进行diff。重点关注文件哈希值MD5/SHA256是否改变getW、getFingerprint等核心函数名是否重命名CryptoJS、AES等加密库的引入方式是否变更如从CDN改为内联新增的eval()、Function()动态代码执行一旦检测到变更自动触发告警邮件/钉钉并启动一个预设的“版本适配流程”。这个流程可以是一个Jenkins任务自动拉取新JS运行你的逆向分析脚本生成一份CHANGELOG.md列出所有变更点并高亮需要人工审核的部分。5.3 策略三灰度发布与A/B测试—— 降低升级风险当一个新的w生成器开发完成不要直接全量上线。应该设计一个灰度发布机制将新旧两个版本的w生成服务同时部署在调用方如爬虫中按1%流量比例随机选择新服务所有请求的w、challenge、response_code服务器返回的HTTP状态码及result字段都记录到日志中编写一个分析脚本对比新旧服务的success_rate、avg_latency、error_reason如w_invalid、challenge_expired只有当新服务的success_rate连续24小时高于旧服务且error_reason分布无新增类型时才逐步提升灰度比例直至100%。5.4 策略四环境指纹的“最小必要集”原则极验采集的12类环境特征并非全部同等重要。通过大量AB测试我发现对w有效性影响最大的前三项是Canvas指纹权重40%缺失或错误w几乎100%无效WebGL指纹权重30%影响次之但缺失会导致部分高阶风控触发navigator的userAgent与platform权重20%必须与真实浏览器一致否则challenge会被标记为异常而像localStorage键名、audioContext采样等特征权重不足10%且极难100%模拟。因此我的建议是聚焦核心放弃边缘。在你的w生成服务中只保证Canvas、WebGL、navigator三大项的100%准确其他项可以模拟一个合理范围内的随机值或直接复用上一次成功的采集结果。这能极大降低维护成本同时保持95%以上的成功率。5.5 策略五建立“失败案例库”—— 让每一次报错都成为资产每次w生成失败服务器返回的错误信息如{status:fail,message:w parameter invalid}都是宝贵的线索。你应该建立一个结构化的失败案例库每条记录包含challenge用于复现full_request_body完整的POST数据error_response服务器返回的JSONtimestamp失败时间geetest_version当时使用的SDK版本root_cause人工分析后填写如“Canvas指纹不匹配”、“时间戳超出窗口”这个库既是故障排查的快速索引也是预测下一次SDK更新方向的雷达。例如当你发现连续100条失败都指向WebGL相关字段那基本可以断定极验正在加强WebGL检测你的下一轮适配就应该优先攻克WebGL指纹模拟。我在上一家公司就维护着这样一个案例库。它最初只有几十条记录三年后已积累超过23000条。正是靠着它我们团队将平均版本适配周期从最初的72小时缩短到了现在的4小时。逆向不是一锤子买卖而是一场需要持续投入、不断沉淀的长期战役。你今天花1小时记录的失败原因很可能就是明天节省8小时的关键钥匙。