验证码的破解虽然整体难度不大,但某些细节处理不当可能会导致各种错误。在本文中,我们总结了一些常见问题及其解决方法,包括验证码识别方案和轨迹处理等。
关于 w 值
在某些验证码系统(如第三代系统)中,有几个请求接口会包含 w 值。需要特别注意的是,除了最后一个校验接口 ajax.php 外,其他接口的 w 值可以为空,但在某些模式下,如无感验证模式,在请求 get.php 接口获取 c 和 s 值时,也会校验 w 值。
常见错误:
json
{'status': 'error', 'error': 'param decrypt error', 'user_error': '网络不给力', 'error_code': 'error_03'}
关于时间间隔
验证码系统通常要求请求之间有一定的时间间隔。如果请求太快,验证会失败。例如,在第三代系统中,如果生成 w 值后没有随机停留 2 秒左右,验证可能会失败。
常见错误:
json
{'status': 'success', 'data': {'result': 'fail', 'msg': ['duration short']}}
关于 challenge
在第三代系统中,challenge 值会参与多次请求。第一次获取到一个 challenge,后续的 get.php 请求返回的数据中会包含一个新的 challenge,新 challenge 比第一次多了两位数。所有后续请求都必须使用新的 challenge。
常见错误:
json
{'success': 0, 'message': 'fail'}
关于 c 和 s
在第三代系统中,c 和 s 值会参与 w 的计算。点选和滑块验证码类型中,第一次 get.php 请求返回的 c 和 s 值,第二次 get.php 请求返回的 s 值会变化。生成 w 时需要使用第二次返回的 s 值。
常见错误:
json
{'success': 0, 'message': 'forbidden'}
关于两次 get.php 和 ajax.php 请求
在第三代系统中,点选和滑块验证码会有两次以 get.php 和 ajax.php 结尾的请求。第一次 get.php 返回主题、域名、提示文字等信息,第一次 ajax.php 返回验证码类型。这两次请求返回的数据虽然对我们没太大用处,但仍需发起请求,否则后续请求将失败。
关于智能组合验证
智能组合验证可以处理多种验证码类型。四代系统更为简洁,通过 load 接口的 captcha_type 字段直接告诉你验证码类型,如滑块、点选(及其子类型)、五子棋或九宫格等。
关于 w 值的算法
w 值生成算法中涉及一些细节,包括 passtime、pow_sign 和 pow_msg。
passtime
passtime 值在生成 w 时参与计算。对于滑块验证码,passtime 是滑动花费的时间,直接取轨迹的最后一个时间值。其他情况下,该值可设为一个随机值。
pow_sign 和 pow_msg
这两个参数是四代系统独有的。pow_msg 的格式如下:
text
1|0|md5|datetime|captcha_id|lot_number||随机字符串
pow_sign 是 pow_msg 经 MD5 加密后的值。需要注意,pow_sign 的生成必须符合特定条件,否则会导致验证失败。下面是一个处理示例:
javascript
var CryptoJS = require("crypto-js");
function getRandomString(){
function e(){
return (65536 * (1 + Math.random()) | 0).toString(16).substring(1);
}
return e() + e() + e() + e();
}
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
};
}
}
}
随机变化的字符串
在验证码生成过程中,通常会有一个 16 位随机字符串参与 w 的加密计算。这个字符串一般会用到两次,且两次必须相同。
常见错误:
json
{'status': 'error', 'error': 'param decrypt error', 'user_error': '网络不给力', 'error_code': 'error_03'}
随机变化的键值对
在三代和四代系统中,w 值的生成过程中会有一个随机键值对。以三代滑块为例,以下是一个动态获取该键值对的示例:
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_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){")
gct_js_new = gct_js[:break_position] + "window.gct=" + function_name + ";" + gct_js[break_position:]
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)
补环境中可能用到的方法
window.crypto.getRandomValues
在某些情况下,如三代滑块验证中,可能会用到 window.crypto.getRandomValues() 方法。
javascript
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;
}
// 测试
// var a = new Uint32Array(256);
// console.log(window.crypto.getRandomValues(a))
window.performance.timing
某些性能指标需要 window.performance.timing 方法来获取。例如:
javascript
window = global;
window.performance = {
timing: {
navigationStart: Date.parse(new Date()),
unloadEventStart: 0,
unloadEventEnd: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 0,
domainLookupStart: 0,
domainLookupEnd: 0,
connectStart: 0,
connectEnd: 0,
secureConnectionStart: 0,
requestStart: 0,
responseStart: 0,
responseEnd: 0,
domLoading: 0,
domInteractive: 0,
domContentLoadedEventStart: 0,
domContentLoadedEventEnd: 0,
domComplete: 0,
loadEventStart: 0,
loadEventEnd: 0,
}
}
完整的环境搭建代码示例
javascript
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
const CryptoJS = require('crypto-js');
function getRandomString(){
function e(){
return (65536 * (1 + Math.random()) | 0).toString(16).substring(1);
}
return e() + e() + e() + e();
}
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
};
}
}
}
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;
}
window.performance = {
timing: {
navigationStart: Date.parse(new Date()),
unloadEventStart: 0,
unloadEventEnd: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 0,
domainLookupStart: 0,
domainLookupEnd: 0,
connectStart: 0,
connectEnd: 0,
secureConnectionStart: 0,
requestStart: 0,
responseStart: 0,
responseEnd: 0,
domLoading: 0,更多内容联系1436423940
domInteractive: 0,更多内容访问ttocr.com或联系1436423940
domContentLoadedEventStart: 0,
domContentLoadedEventEnd: 0,
domComplete: 0,
loadEventStart: 0,
loadEventEnd: 0,
}
}
async function getCaptchaData() {
const response = await axios.get('https://api.example.com/captcha');
const captchaData = response.data;
return captchaData;
}
(async () => {
try {
const captchaData = await getCaptchaData();
const powResult = get_pow(captchaData.pow_detail, captchaData.captcha_id, captchaData.lot_number);
console.log('POW Result:', powResult);
} catch (error) {
console.error('Error fetching captcha data:', error);
}
})();