【验证码逆向专栏】某验全家桶细节避坑总结
字数 2241 2025-08-11 01:06:30
某验验证码逆向分析与避坑指南
一、某验验证码概述
某验验证码分为多个版本(二代、三代、四代),主要包括以下几种类型:
- 滑块验证(slide)
- 点选验证(click):
- 文字点选(word)
- 图标点选(icon)
- 语序点选(phrase)
- 空间推理(space)
- 五子棋验证
- 九宫格验证
二、关键参数与常见错误
1. w值处理
- 三代验证中多个接口需要w值,除最后一个校验接口ajax.php外,其他接口w可置空
- 例外情况:三代一键通过模式(无感验证)在请求get.php接口时也校验w值
- 解决方案:需要获取两次w值,且两次生成方式不同
2. 时间间隔控制
- 三代验证流程不能过快,生成w值后需随机停留约2秒
- 错误提示:流程过快会导致验证失败
3. challenge参数
- 三代滑块验证中:
- 第一次获取challenge
- 第二次get.php请求返回新challenge(比第一次多两位数)
- 必须使用新challenge进行后续请求
- 错误提示:使用旧challenge会导致验证失败
4. c和s参数
- 参与w值计算
- 点选和滑块验证中:
- 第一次get.php返回c和s
- 第二次get.php返回新c和s(c通常不变,s会变)
- 必须使用第二次get.php返回的s生成w值
- 错误提示:使用错误的s值会导致验证失败
5. 请求顺序要求
- 三代点选和滑块验证必须按顺序发起请求:
- 第一次get.php(返回主题、域名等信息)
- 第一次ajax.php(返回验证码类型)
- 第二次get.php
- 第二次ajax.php
- 注意:即使前两次请求返回的数据看似无用,也必须发起
三、智能组合验证处理
1. 三代判断逻辑
- 第一次ajax.php接口返回验证码类型(click或slide)
- 如果是click类型,通过第二次get.php返回的pic_type字段判断具体点选类型:
- 文字点选(word)
- 图标点选(icon)
- 语序点选(phrase)
- 空间推理(space)
2. 四代判断逻辑
- load接口的captcha_type字段直接指明验证类型
四、w值算法细节
1. passtime参数
- 滑块验证:取轨迹最后一个时间值(track[track.length - 1][2])
- 其他验证:建议使用随机值:
Math.floor((Math.random()*500) + 4000) - 错误提示:滑块验证中passtime与轨迹时间不一致会导致失败
2. pow_sign和pow_msg(四代特有)
- pow_msg格式:
1|0|md5|datetime|captcha_id|lot_number||随机字符串 - pow_sign是pow_msg的MD5值
- 关键点:
- 随机字符串不能真随机,需满足特定条件
- 通过load接口返回的pow_detail字段判断算法类型(MD5/SHA1/SHA256)
算法实现代码
function get_pow(pow_detail, captcha_id, lot_number) {
var n = pow_detail.hashfunc;
var i = pow_detail.version;
var r = pow_detail.bits;
var s = pow_detail.datetime;
var o = "";
var a = r % 4;
var u = parseInt(r / 4, 10);
var c = function g(e, t) { return new Array(t + 1).join(e); }("0", u);
var _ = i + "|" + r + "|" + n + "|" + s + "|" + captcha_id + "|" + lot_number + "|" + o + "|";
while (1) {
var h = getRandomString(),
l = _ + h,
p = void 0;
switch (n) {
case "md5": p = CryptoJS.MD5(l).toString(); break;
case "sha1": p = CryptoJS.SHA1(l).toString(); break;
case "sha256": p = CryptoJS.SHA256(l).toString();
}
if (0 == a) {
if (0 === p.indexOf(c)) return { "pow_msg": _ + h, "pow_sign": p };
} else if (0 === p.indexOf(c)) {
var f = void 0, d = p[u];
switch (a) {
case 1: f = 7; break;
case 2: f = 3; break;
case 3: f = 1;
}
if (d <= f) return { "pow_msg": _ + h, "pow_sign": p };
}
}
}
3. 随机字符串
- 16位随机字符串参与w值加密
- 关键点:同一流程中该字符串需使用两次且保持一致
- 错误提示:
- 二/三代:随机字符串不一致会导致验证失败
- 四代:会报特定错误
4. 随机键值对
- 三四代生成w过程中有随机键值对(如
{'h9s9': '1803797734'}) - 解决方案:
- 从get.php请求获取gct.js路径
- 动态请求gct.js并导出获取键值对的方法
Python实现代码
import re
import execjs
import requests
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
}
# gct js路径
gct_path = "https://static.geetest.com/static/js/gct.b71a9027509bc6bcfef9fc6a196424f5.js"
gct_js = requests.get(gct_path, headers=headers).text
# 正则匹配需要调用的方法名称
function_name = re.findall(r"\)\)\{return", gct_js)[0]
# 查找需要插入全局导出代码的位置
break_position = gct_js.find("return function(t){")
# window.gct全局导出方法
gct_js_new = gct_js[:break_position] + "window.gct=" + function_name + ";" + gct_js[break_position:]
# 添加自定义方法调用window.gct获取键值对
gct_js_new = "window = global;" + gct_js_new + """
function getGct(){
var e = {"lang": "zh", "ep": "test data"};
window.gct(e);
delete e["lang"];
delete e["ep"];
return e;
}"""
gct = execjs.compile(gct_js_new).call("getGct")
print(gct) # 输出示例: {'h9s9': '1803797734'}
五、补环境方法
1. window.crypto.getRandomValues()
window = global;
window.crypto = {
getRandomValues: getRandomValues_
}
function randoms(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
function getRandomValues_(buf) {
var min = 0, max = 255;
if (buf.length > 65536) {
var e = new Error();
e.code = 22;
e.message = 'Failed to execute \'getRandomValues\' : The ' +
'ArrayBufferView\'s byte length (' + buf.length +
') exceeds the ' + 'number of bytes of entropy available via this API (65536).';
e.name = 'QuotaExceededError';
throw e;
}
if (buf instanceof Uint16Array) {
max = 65535;
} else if (buf instanceof Uint32Array) {
max = 4294967295;
}
for (var element in buf) {
buf[element] = randoms(min, max);
}
return buf;
}
2. window.performance.timing
function timing() {
var now = Date.now()
var tim = {
"navigationStart": now,
"unloadEventStart": now + 200,
"unloadEventEnd": now + 200,
"redirectStart": 0,
"redirectEnd": 0,
"fetchStart": now + 100,
"domainLookupStart": now + 150,
"domainLookupEnd": now + 250,
"connectStart": now + 30,
"connectEnd": now + 50,
"secureConnectionStart": now + 52,
"requestStart": now + 72,
"responseStart": now + 91,
"responseEnd": now + 92,
"domLoading": now + 99,
"domInteractive": now + 105,
"domContentLoadedEventStart": now + 105,
"domContentLoadedEventEnd": now + 111,
"domComplete": now + 111,
"loadEventStart": now + 111,
"loadEventEnd": now + 111,
}
return tim
}
六、验证码识别方案
1. OpenCV识别滑块缺口
import cv2
import numpy as np
from pathlib import Path
def get_distance(bg, tp, im_show=False, save_path=None):
"""
:param bg: 背景图路径或二进制
:param tp: 缺口图路径或二进制
:param im_show: 是否显示结果
:param save_path: 保存路径
:return: 缺口位置
"""
# 读取图片
bg_img = cv2_open(bg)
tp_gray = cv2_open(tp, flag=cv2.COLOR_BGR2GRAY)
# 金字塔均值漂移
bg_shift = cv2.pyrMeanShiftFiltering(bg_img, 5, 50)
# 边缘检测
tp_gray = cv2.Canny(tp_gray, 255, 255)
bg_gray = cv2.Canny(bg_shift, 255, 255)
# 目标匹配
result = cv2.matchTemplate(bg_gray, tp_gray, cv2.TM_CCOEFF_NORMED)
# 解析匹配结果
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
distance = max_loc[0]
if save_path or im_show:
# 绘制矩形框
tp_height, tp_width = tp_gray.shape[:2]
x, y = max_loc
_x, _y = x + tp_width, y + tp_height
bg_img = cv2_open(bg)
cv2.rectangle(bg_img, (x, y), (_x, _y), (0, 0, 255), 2)
if save_path:
cv2.imwrite(str(save_path), bg_img)
if im_show:
cv2.imshow('result', bg_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
return distance
2. 推荐方案
- 深度学习:使用OpenCV等库自行训练模型
- ddddocr:支持多种验证码类型和自定义训练
- 打码平台:如云码打码平台,支持多种验证码类型
七、轨迹生成方案
1. 基于缓动函数的轨迹生成
import random
def __ease_out_expo(sep):
"""缓动函数easeOutExpo"""
if sep == 1:
return 1
else:
return 1 - pow(2, -10 * sep)
def get_slide_track(distance):
"""
根据滑动距离生成滑动轨迹
:param distance: 需要滑动的距离
:return: 滑动轨迹[[x,y,t], ...]
"""
if not isinstance(distance, int) or distance < 0:
raise ValueError(f"distance类型必须是大于等于0的整数: {distance}")
# 初始化轨迹列表
slide_track = [
[random.randint(-50, -10), random.randint(-50, -10), 0],
[0, 0, 0],
]
# 记录次数
count = 30 + int(distance / 2)
# 初始化滑动时间
t = random.randint(50, 100)
# 记录上一次滑动的距离
_x = 0
_y = 0
for i in range(count):
# 已滑动的横向距离
x = round(__ease_out_expo(i / count) * distance)
# 滑动过程消耗的时间
t += random.randint(10, 20)
if x == _x:
continue
slide_track.append([x, _y, t])
_x = x
slide_track.append(slide_track[-1])
return slide_track
2. 其他方案
- 贝塞尔曲线:参考开源项目如gurs
- tanh和arctan函数:可参考相关技术论坛实现
八、常见错误代码
-
geetest_xxxxxxxxxxxxx({"status": "error", "error": "illegal challenge", "user_error": "网络不给力", "error_code": "error_23"})- 原因:challenge参数不正确
-
geetest_xxxxxxxxxxxxx({"status": "error", "error": "param decrypt error", "user_error": "网络不给力", "error_code": "error_03"})- 原因:w值生成不正确
-
geetest_xxxxxxxxxxxxx({"status": "error", "error": "not proof", "user_error": "网络不给力", "error_code": "error_21"})- 原因:滑动验证缺少轨迹
-
geetest_xxxxxxxxxxxxx({"success": 0, "message": "fail"})- 原因:轨迹、缺口距离或其他参数问题
-
geetest_xxxxxxxxxxxxx({"success": 0, "message": "forbidden"})- 原因:参数问题或验证失败