极验3代验证码逆向手记:我是如何一步步拆解w参数加密逻辑的
第一次遇到极验3代验证码是在分析某网站登录流程时,那个看似简单的"点击文字"验证框背后,隐藏着一套精妙的前端加密体系。作为一名有五年逆向经验的安全研究员,我决定深入探究click.3.0.7.js文件中那个神秘的w参数生成逻辑。这次逆向之旅就像侦探破案,需要层层剥开代码的伪装,最终在5839行找到了关键突破口。
1. 逆向环境搭建与初步定位
工欲善其事,必先利其器。在开始逆向之前,我准备了以下工具链:
- Chrome DevTools:用于动态调试和调用栈分析
- Fiddler:抓取网络请求,观察数据流变化
- CryptoJS:对比标准加密算法的实现差异
- WebStorm:格式化混淆后的JS代码
通过抓包发现,验证流程涉及三个关键接口:
| 接口名称 | 返回参数 | 作用 |
|---|---|---|
| register-click-official | challenge, gt | 初始化验证会话 |
| get.php | c, s | 提供加密种子参数 |
| ajax.php | validate | 提交验证结果的核心接口 |
在第二次ajax.php请求中,w参数作为加密后的验证凭证被提交。通过搜索Unicode字符"\u0077"(即字母w),很快在click.3.0.7.js中定位到关键代码段:
var l = n[$_CACJJ(716)](); var h = X[$_CADAG(338)](ae[$_CACJJ(130)](o), n[$_CADAG(711)]()); var p = w[$_CADAG(776)](h);这段看似晦涩的代码,实际上是极验为了防止自动化分析而采用的变量名混淆技术。通过AST反混淆工具处理后,逻辑变得清晰:
l参数:由RSA加密生成h参数:使用AES加密关键数据p参数:对h进行二次处理
2. 解密l参数的RSA加密过程
跟踪n[$_CACJJ(716)]调用栈,发现l值的生成核心是这段代码:
function generateRandomStr() { return Math.random().toString(36).slice(2, 18); } function rsaEncrypt(input) { const publicKey = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3..."; const modulus = "00C1E3934D1614465..."; // 实际实现中使用BigInteger进行模幂运算 return encryptedResult; }关键发现:
- 使用16位随机字符串作为原始输入
- RSA公钥硬编码在JS文件中
- 采用PKCS#1 v1.5填充模式
- 模数长度为1024位
通过Hook crypto.subtle API,我成功捕获到完整的加密流程:
- 生成随机字符串:
3m7k9p2q5r1s4t6x - 转换为ASCII字节数组
- 使用公钥进行加密
- 输出Base64编码结果
提示:在实际逆向时,可以直接扣取整个RSA加密函数,或者使用Python的PyCryptodome库实现相同逻辑。
3. 剖析h参数的AES加密链
h参数的生成更为复杂,涉及多层加密:
const iv = CryptoJS.enc.Utf8.parse("0000000000000000"); const key = CryptoJS.enc.Utf8.parse(randomStr); function aesEncrypt(data) { return CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString(); }加密对象o包含以下关键字段:
{ "passtime": 1567, "a": "72,83|91,105", "pic": "/static/img/bg/15.jpg", "tt": "3k5j...", "h9s9": "1816378497", "rp": "a3e5f7b11d2c8..." }其中每个字段都有特定含义:
passtime:从加载验证码到完成操作的时间(ms)a:点击位置的坐标序列rp:由gt、challenge和passtime的MD5哈希组成
通过动态调试发现,极验在前端实现了自定义的坐标加密算法:
function encryptCoords(coords) { const salt = s.substring(0, 32); return CryptoJS.HmacSHA256(coords, salt).toString(); }4. 最终w参数的组装逻辑
w参数实际上是l、p两个值的拼接:
w = l + p其中p是对h参数的进一步处理:
function finalProcess(h) { const rotated = h.slice(8) + h.slice(0,8); return CryptoJS.SHA256(rotated).toString(); }整个过程可以用以下流程图表示:
- 生成随机字符串 → RSA加密 → 得到l
- 收集用户行为数据 → AES加密 → 得到h
- 对h进行位移和哈希 → 得到p
- 拼接l和p → 最终w参数
在实际逆向过程中,我遇到了三个主要陷阱:
- 代码中存在大量无用的垃圾代码段干扰分析
- 关键函数调用被try-catch包裹,难以断点
- 时间戳校验严格,延迟超过2秒会失效
5. 验证码识别方案的选型
虽然破解了加密逻辑,但要完整绕过验证还需要解决文字识别问题。经过测试比较几种方案:
| 方案 | 准确率 | 速度 | 实现难度 | 适用场景 |
|---|---|---|---|---|
| ddddocr | 30-40% | 快 | 低 | 简单文字点选 |
| PyTorch定制模型 | 70-85% | 中等 | 高 | 复杂验证场景 |
| 人工打码平台 | 95%+ | 慢 | 低 | 高价值业务场景 |
对于大多数情况,我推荐使用改进版的ddddocr:
import ddddocr detector = ddddocr.DdddOcr( show_ad=False, import_onnx_path="custom_model.onnx", charsets_path="geetest_chars.json" ) result = detector.classification(open('captcha.png', 'rb').read())经过两周的逆向分析,最终实现了极验3代验证码的自动化方案。整个过程让我深刻体会到,现代验证码系统已经发展成前端安全技术的集大成者,需要综合运用:
- 密码学知识(RSA/AES/HMAC)
- 浏览器逆向技巧
- 机器学习应用
- 反调试对抗经验
记得在解决最后一个时间校验问题时,通过Hook Date.now()方法才绕过检测,这种在刀尖上跳舞的感觉,正是逆向工程的魅力所在。