目标地址
aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby9zbGlkZS1mbG9hdC5odG1s
请求流程
1、https://www.geetest.com/demo/gt/register-slide
注册验证码,第一次返回 challenge 和 gt
2、https://apiv6.geetest.com/gettype.php
获取资源路径,此接口可以忽略
3、https://apiv6.geetest.com/get.php
此接口需要传入第一个接口中的challenge、gt和w,w一共需要构造3次,该请求是第一次。返回了c和s
4、https://apiv6.geetest.com/ajax.php
此接口也需要传入第一个接口中的challenge、gt和w,该请求是第二次构造w参数。
5、https://apiv6.geetest.com/get.php
此地址为第二次请求,在第3步的时候,请求了第一次,该接口返回了验证码图片的地址,以及新的challenge和s值,该challenge为原来旧的challenge加上两位新的字符串。值得注意的是,该验证码图片顺序被打乱需要裁切还原。
6、https://apiv6.geetest.com/ajax.php
第二次请求该地址,并且也是最终的验证方法。需要传入gt、challenge和w,返回{"validate": "202cb962ac59075b964b07152d234b70"}形式,即代表成功。
难点
一、第一次构造w
搜索关键字\u0077,即可看到参数w的生成逻辑,w = i + r。
跟栈进入 t[$_CEEJl(1169)] ,发现r实际上等于new X()[$_CGCJs(17)](this[$_CGCI_(1119)](e));扣出X和this[$_CGCI_(1119)](e)即可
this[$_CGCI_(1119)](e) 为16位字符串,由e方法连续执行4次拼接而成
X直接整个扣出即可
来到下一步,开始处理参数o,$_BF_()[$_CEEIh(1184)] 为AES加密,初始向量是 0000000000000000,扣代码或者用库都行,he[$_CEEIh(491)]为JSON.stringify()方法,不用扣,t[$_CEEIh(1119)]() 为rsa加密需要用到的key,须与上一步r中得到的16为字符串保持一致。
唯一需要注意的是,这里的 t[$_CEEJl(363)] 为Config对象,注意该对象中, challenge 和 gt 需要使用第一个接口中返回的,其他参数固定即可。
最后一个i参数,直接将p对象全部抠出来即可
至此,第一个w参数,已经全部逆向完毕。主要代码如下:
function Aes_encrypt(text, key_value) {
var key = CryptoJS.enc.Utf8.parse(key_value);
var iv = CryptoJS.enc.Utf8.parse("0000000000000000");
var srcs = CryptoJS.enc.Utf8.parse(text);
var encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
for (var r = encrypted, o = r.ciphertext.words, i = r.ciphertext.sigBytes, s = [], a = 0; a < i; a++) {
var c = o[a >>> 2] >>> 24 - a % 4 * 8 & 255;
s.push(c);
}
return s;
}
function first_w(register_info){
config = {
gt: register_info["gt"],
challenge: register_info["challenge"],
offline: false,
new_captcha: true,
product: "float",
width: "300px",
https: true,
api_server: "apiv6.geetest.com",
protocol: "https://",
type:"fullpage",
aspect_radio: {slide: 103, click: 128, voice: 128, beeline: 50},
beeline: "/static/js/beeline.1.0.1.js",
cc: 20,
click: "/static/js/click.3.1.0.js",
fullpage: "/static/js/fullpage.9.1.9-crdubp.js",
geetest: "/static/js/geetest.6.0.9.js",
i:"-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1",
slide: "/static/js/slide.7.9.2.js",
static_servers: ['static.geetest.com/', 'static.geevisit.com/'],
voice: "/static/js/voice.1.2.4.js",
ww: true
}
// 根据aeskey 生成256位字符串
var r = new X()['encrypt'](register_info["aeskey"]);
// AES加密 生成944位数组
o = Aes_encrypt(JSON['stringify'](config), register_info["aeskey"])
i = p['$_HEz'](o)
// w长度固定 1516
w = i + r
return w
}
二、第二次构造w
第二个w,与另外两个略有不同,无法通过字符串直接找到,所以直接在栈里面找到如下记录:
即可找到参数w的生成逻辑,这里的代码比较绕,建议拿出来,格式化后再看。t[$_CFCJJ(1147)]为最终的w值,t[$_CFCIL(1160)] 为w的生成规则,跟栈进入。
首先,需要找到这个对象,该对象中的passtime比较重要,rp的加密过程中,也需要用到passtime,两者必须保持一致,rp为gt+challenge+passtime的MD5加密,即下图中的H方法。
解决完该参数后,继续往下,即可看到 t[$_CFCJJ(1147)] 的完整逻辑,p对象和,在构造第一个w的时候,已经扣过了,可以直接用;c[$_CFFII(17)]为AES加密,也可以直接用;i[$_CFFJL(1119)]()为16位随机字符串,继续用第一步生成的那个原字符串。
值得注意的是,在构造以上r对象的时候,以下[$_CFEEQ(1053)][$_CFEEQ(1003)]方法,仅在第一次执行时,能获取到正确结果。
至此,第二个w参数也构造完毕,主要代码如下所示:
function second_w(register_info){
// 这里的aeskey与第一次的aeskey 需要保持一致
r = {
"lang": "zh-cn",
"type": "fullpage",
// 省略部分参数...请自行补齐
"tt": "M3d8Pjp8Pjp8Pjp7.*M/SQe5e,85((n(5((((bn,5nbe,9(((.9(((()-1-)3,(@M9-U-)3)(?/)(NMM3)M?MM-N1?-U1)ME2@MRM91d-N6:5?EU-)61-*MM-N3)M?N9-j/*N1-jI)1E/,/)(R3)):JE9c:OJ4RH2cCeOQ5V.:MPDGb-h(B1,5b8q5b,5,bb(e5,qn(b((eb(((eV*(E/(/)M)NU(9/)M5-)M9,)(E1(/)()OU(9-19",
},
"dnf": "dnf",
"by": 0
},
"passtime": 2077,
"rp": CryptoJS.MD5(register_info["gt"] + register_info["challenge"] + 2077).toString(),
"captcha_token": "1407818801",
"pwsu": "czuubzkl"
}
// console.log(r);
w = p['$_HEz'](Aes_encrypt(JSON.stringify(r), register_info["aeskey"]))
return w
}
三、第三次构造w
第三个w参数,与前两个基本大同小异。
首先,仍然通过关键字搜索,定位到以下位置,得到
u = r[$_CAHJS(737)]()
l = V[$_CAHJS(392)](gt[$_CAIAK(254)](o), r[$_CAIAK(744)]())
h = m[$_CAIAK(792)](l)
w = h+u 的基本逻辑
u = r[$_CAHJS(737)](),与构造第一个w时的方式相同,不多赘述。
l = V[$_CAHJS(392)](gt[$_CAIAK(254)](o), r[$_CAIAK(744)]()),这一步构造o对象时,需要注意其中的userresponse、passtime、rp、aa四个参数
先找到o的定义位置,即可知道o对象各个参数的生成规则,
userresponse: H(滑块距离, new_challenge),
passtime: 滑块通过时间
aa: e,
e由外部传入,跟栈进入,这里生成的l,即为上一步需要的e。
仔细观察,可以发现,n[$_CJJJU(67)][$_CJJJU(1033)]和n[$_DAAAU(67)][$_CJJJU(345)]分别为第五步接口中获取到的c和s,不多赘述。着重看下n[$_DAAAU(985)][$_CJJJU(1075)]和n[$_CJJJU(985)][$_CJJJU(1073)]两个方法。这两个方法,前者为加密方法,直接将n对象整体抠出来。
后者稍微复杂一点,跟栈进入后,可以发现,该方法对轨迹进行加密,需要改造该方法为接收外部传入的轨迹。
改造后,如下图所示,在调用时传入轨迹即可得到结果
到这里,只剩下最后的h参数,跟前面一样的操作,直接将整个对象扣下来即可
最后拼接h+u参数,即完成了第三个w的参数的构造。需要注意的是,在构造第三个参数的o对象的时候,轨迹最后一组的z值要与o的passtime相同,轨迹最后一组的x值,要与滑动距离相同。
最后,放上需要用到的一些工具代码
import requests, time, random, json, execjs, base64
from PIL import Image
import io
import ddddocr, cv2
class Captcha:
#还原乱序底图
def ImgRestore(self):
# 打开PNG图片
image = Image.open("极验/三代2.0/pic/乱序.webp")
# 创建新的空白图片
new_image = Image.new('RGBA', (260, 160))
# 坐标
k = 0
# 定值, 在极验3.0版本中, 通过画布断点获取数据
ut = [39, 38, 48, 49, 41, 40, 46, 47, 35, 34, 50, 51, 33, 32, 28, 29, 27, 26, 36, 37, 31, 30, 44, 45, 43, 42,
12, 13, 23, 22, 14, 15, 21, 20, 8, 9, 25, 24, 6, 7, 3, 2, 0, 1, 11, 10, 4, 5, 19, 18, 16, 17]
# ut有多少参数, 就有多少个小碎片拼图
for _ in range(len(ut)):
# 通过计算得出 小碎片的起始坐标
c = ut[_] % 26 * 12 + 1
# 计算是否当前小碎片在第一层还是第二层, 三元表达式
u = (lambda: 80 if 25 < ut[_] else 0)()
# 将裁切下来小碎片的数据存储
image2 = image.crop((c, u, c + 10, u + 80))
# 当存储量达到26, 则重置小碎片的放置坐标, 每层只能放26小碎片
if _ == 26:
k = 0
if _ > 25:
# 第二层
new_image.paste(image2, (k, 80))
k += 10
else:
# 第一层
new_image.paste(image2, (k, 0))
k += 10
# 保存最终图片 随机名称
new_image.save("极验/三代2.0/pic/顺序.png")
buffered = io.BytesIO()
# 将数据写入buffered里
new_image.save(buffered, format="PNG")
return {'base64': base64.b64encode(buffered.getvalue()).decode()}
# 保存乱序验证码底图
def downloadImage(self, bgPath, slicePath, session):
path1 = bgPath.replace("jpg", "webp") #webp为乱序底图
url1 = "https://static.geetest.com/"+path1
response1 = session.get(url1)
filename1 = '极验/三代2.0/pic/乱序.webp'
# 使用'wb'模式打开文件用于写入二进制数据
with open(filename1, 'wb') as f:
# 将请求返回的内容写入文件
f.write(response1.content)
f.close()
url2 = "https://static.geetest.com/"+slicePath#png为滑块图片
response2 = session.get(url2)
filename2 = '极验/三代2.0/pic/滑块.png'
# 使用'wb'模式打开文件用于写入二进制数据
with open(filename2, 'wb') as f:
# 将请求返回的内容写入文件
f.write(response2.content)
f.close()
# 识别距离
def getDIstance(self):
with open("极验/三代2.0/pic/顺序.png", "rb") as f:
bg_content = f.read()
with open("极验/三代/pic/滑块.png", "rb") as f:
slider_content = f.read()
slider = ddddocr.DdddOcr(det=False, ocr=False, show_ad=False)
ret = slider.slide_match(slider_content, bg_content, simple_target=True)
target = ret["target"]
return target[0]
def random_x(self, count, max):
res_arr = [random.randint(1, max) for _ in range(count)]
res_arr.sort()
return res_arr
def random_y(self, count, max):
res_arr = [random.randint(0, max) * -1 for _ in range(count)]
res_arr.sort(reverse=True)
return res_arr
def random_z(self, count, max):
res_arr = set()
while len(res_arr) < count:
# 生成一个随机数并添加到集合中,集合会自动去重
res_arr.add(random.randint(0, max - 1))
# 将集合转换为列表,因为集合是无序的,需要排序
sorted_res_arr = sorted(res_arr)
return sorted_res_arr
def gen_track(self, distance, time):
# 轨迹的前两项和最后一项 需要手动调整
track = [
[random.randint(-50, -10), random.randint(-50, -10), 0],
[0, 0, 0]
]
count = int(1.46 * dis + 8.36)
xs = self.random_x(count, distance)
ys = self.random_y(count, random.randint(5, 15))
zs = self.random_z(count, time)
res = [list(t) for t in zip(xs, ys, zs)]
track += res
track.append([distance, track[-1][1], track[-1][2] + random.randint(100, 200)])
print("轨迹长度-->",len(track))
return track
至此,整个过程全部结束。