文章目录
验证码案例 【极验3滑动模式】
1 声明
本案例中所有内容仅供个人学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
2 案例目标
2.1 网站
aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby9zbGlkZS1mbG9hdC5odG1s
2.2 接口
aHR0cHM6Ly9hcGl2Ni5nZWV0ZXN0LmNvbS9nZXQucGhw
aHR0cHM6Ly9hcGkuZ2VldGVzdC5jb20vZ2V0LnBocA==
aHR0cHM6Ly9hcGkuZ2VldGVzdC5jb20vYWpheC5waHA=
2.3 参数
生成w以及相关参数
2.4 算法
RSA,AES,MD5
3 验证流程分析
3.1 抓包分析
1 register-slide?
返回challenge gt
2 gettype.php
返回一堆JS文件
3 get.php
请求参数:gt challenge w
返回一堆数据
4 ajax.php 获取滑块类型
请求参数:gt challenge w
返回滑块标记 {"status": "success", "data": {"result": "slide"}}
5 get.php
请求参数:gt challenge w
返回了验证码信息
6 ajax.php 提交验证
请求参数:gt challenge w
成功 message: "success"
失败 message: "fail"
通过分析我们只需要请求 1===>3===>4===>5===>6 (后面验证后3其实也可以省略)
3.2 获取验证码图片
3.2.1 大致流程
- 请求register-slide?接口,拿到gt和challenge
- 带上gt、challenge、w可置空请求get.php? 返回c、s
- 带上gt、challenge、w置空 请求ajax.php接口,返回验证码类型slide
- 带上gt、challeng再次请求get.php接口,验证码信息(包含图片背景图、缺口背景图),从json中取出新的c、s、challenge值 (c没变)
3.2.2 详细实现
首先,要拿到gt和challenge,直接请求获取
Query String Parameters、Form Data 和 Request Payload 都是 HTTP 请求中传递数据的方式,主要区别在于数据的格式和传递方式。
1. Query String Parameters:是通过 URL 的查询字符串传递数据的一种方式。查询字符串是 URL 中 ? 后面的部分,它包含了若干个键值对,用 & 分隔。例如,https://www.example.com/search?q=keyword&sort=desc 中的 q 和 sort 都是 Query String Parameters。这种方式适合于传递简单的数据,如搜索关键字、分页等。
2. Form Data:是通过 HTML 表单提交数据的一种方式。表单数据被编码为一系列键值对,通过 POST 请求发送到服务器。这种方式适合于传递更复杂的数据,如用户注册信息、文章发布等。
3. Request Payload:是通过 HTTP 请求主体传递数据的一种方式。它适用于传递复杂的结构化数据,如 JSON 或 XML 格式的数据。Request Payload 通常与 RESTful API 和 Ajax 交互中使用。
Query String Parameters 适合传递简单的数据,Form Data 适合传递表单数据,而 Request Payload 适合传递结构化数据,具有更大的灵活性和可扩展性。
第一个get.php,带上gt、challenge、callback是geetest+时间戳、w可置空请求get.php? 返回c、s
ajax.php:
带上gt、challenge、w置空 请求接口,返回验证码类型slide
第二次请求get.php
带上gt、challeng再次请求接口,验证码信息(包含图片背景图、缺口背景图),从json中取出新的c、s、challenge值 (c没变)
坑在这个返回的challenge,比之前的值后面多了两字符
看到获取的图片是乱码的,接下来需要对图片进行还原。
3.3底图还原
由于某验验证码是用canvas绘图的(F12查看验证码元素可以看出)
第一种方法:打上canvas断点,画布创建时会短住
for (var a = r / 2, _ = 0; _ < 52; _ += 1) {
// 取数组 Ut[_]对 26 取模,乘以 12 再加 1,结果赋值给变量 c
var c = Ut[_] % 26 * 12 + 1;
var u = 25 < Ut[_] ? a : 0;
// 在画布上将像素数据绘制到指定位置
var l = o['putImageData'](c, u, 10, a);
// 获取指定矩形区域内的像素数据 ImageData 对象
s['getImageData'](l, _ % 26 * 10, 25 < _ ? a : 0);
}
第二种方法
可以hook创建canvas对象的地方,直接百度参考一个:
// hook canvas
(function() {
'use strict';
let create_element = document.createElement.bind(document);
document.createElement = function (_element) {
console.log("create_element:",_element);
if (_element === "canvas") {
debugger;
}
return create_element(_element);
}
})();
这里逻辑还是很清晰的,但如果扣js的话相对来说要麻烦一些,还要依赖第三方canvas
库,不如直接用python复写逻辑(用到了PIL
库) Ut是定值
还原之后类似下图:
3.4 获取缺口距离生成轨迹
缺口距离识别,可以手动,也可以第三方平台,这里选择ddddocr识别效果还不错 直接上代码
def get_x(self):
slide = ddddocr.DdddOcr(det=False, ocr=False)
with open('bg.jpg', 'rb') as f:
target_bytes = f.read()
with open('full.jpg', 'rb') as f:
background_bytes = f.read()
res = slide.slide_comparison(target_bytes, background_bytes)
return res.get('target')[0]
通过缓动函数生成轨迹,参考https://easings.net/zh-cn#easeOutExpo
def __ease_out_expo(self,sep):
"""
缓动函数 easeOutExpo
参考:https://easings.net/zh-cn#easeOutExpo
"""
if sep == 1:
return 1
else:
return 1 - pow(2, -10 * sep)
def get_slide_track(self,distance):
"""
根据滑动距离生成滑动轨迹
:param distance: 需要滑动的距离
:return: 滑动轨迹<type 'list'>: [[x,y,t], ...]
x: 已滑动的横向距离
y: 已滑动的纵向距离, 除起点外, 均为0
t: 滑动过程消耗的时间, 单位: 毫秒
"""
if not isinstance(distance, int) or distance < 0:
raise ValueError(f"distance类型必须是大于等于0的整数: distance: {distance}, type: {type(distance)}")
# 初始化轨迹列表
slide_track = [
[random.randint(-50, -10), random.randint(-50, -10), 0],
[0, 0, 0],
]
# 共记录count次滑块位置信息
count = 30 + int(distance / 2)
# 初始化滑动时间
t = random.randint(50, 100)
# 记录上一次滑动的距离
_x = 0
_y = 0
for i in range(count):
# 已滑动的横向距离
x = round(self.__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
3.5 加密轨迹并提交
- 可以看到最后ajax.php带上新的gt,chanllenge,w参数想都不用想肯定是包含轨迹内容的。
直接搜索w是搜索不到的,JS
里面是被编码的,可以把W值编码,进行查找,在JS
里面搜索u0077":
或者通过解混淆,还原后搜索w,参考JS逆向:AST还原极验混淆JS实战 (qq.com)
然后搜索"w",可以看到"w": h + u ,h在5980行,u在5978行 还原如下:
var u = r[$_CAHJe(785)]()
, l = V['encrypt'](gt['stringify'](o), r[$_CAIAj(761)]())
, h = m[$_CAIAj(783)](l)
3.5.1 u参数
先解决u这个函数,滑动滑块之后短住,进去看看
进去之后发现是return的e **this$_CBFJv(761)**是生成的随机数 然后经过加密得到的
进去之后发现就在上面 返回的 **rt()**是16位随机数
里面又是由4个随机数**t()**相加生成的
还原之后Math.random() 是随机选取大于等于 0.0 且小于 1.0 的伪随机 double 值,toString(16) 为十六进制字符串
(65536 * (1 + Math["random"]()) | 0)["toString"](16)["substring"](1)
通过js复现,后续传入16位随机数可以直接通过python写死!方便调试
function random() {
var random_str = "";
for (var index = 0; index < 4; index++) {
random_str += (65536 * (1 + Math["random"]()) | 0)["toString"](16)["substring"](1);
}
return random_str;
}
接下来看U里面包括了三个方法 很显然是RSA加密 需要设置公钥
进入new U()[‘encrypt’] 来到lt()方法 2853行 往上找 ut方法 在2850打上断点重新拉滑块 发现了Invalid RSA public key字样
ut 函数传入了两个值,t 为公钥值,e 为公钥模数,都是固定值
t='00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81'
e='10001'
这里我们直接利用python RSA实现,通过rsa加密随机数就是U
def rsa_encrypt(self,random):
"""
rsa加密
:param random: 随机数
:return: 加密后的随机数
"""
#公钥模数
n ='00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81'
#公钥指数
e = '10001'
#构造公钥
key = rsa.PublicKey(e = int(e, 16), n = int(n, 16))
# print('key:',key)
#加密
message = rsa.encrypt(random.encode('utf-8'), key)
#转换成16进制
encrypt_data = message.hex()
return encrypt_data
3.5.2 L参数
l = V['encrypt'](gt['stringify'](o), r[$_CAIAj(761)]())
可以先看里面的参数 o是有一个参数对象组成 r$_CAIAj(761)为之前的16位随机数 有stringify方法所以gt肯定为JSON V为一个加密方法
3.5.2.1 o参数
这里先解决o参数,通过两次对比箭头所指的四个参数在变化 需要分析
- aa: “a()(T!!LKwswstssss*tstttsstssssts!*x(!!( ) 9 / 9 − − / 010 − . − 01 q 2 , 2 , − . − , 7 / 5 − 5 )9/9--/010-.-01q2,2,-.-,7/5-5 )9/9−−/010−.−01q2,2,−.−,7/5−5*R” //轨迹加密
- ep: {v: ‘7.9.0’, $_BIo: false, me: true, tm: {…}, td: -1} //可以随机或者写死
- h9s9: “1816378497” //可以写死
- imgload: 257 //图片加载时间可写死
- lang: “zh-cn”
- passtime: 406 //轨迹滑动时间
- rp: “bcb6b32f4b49185db3212719c93a9bef” //gt + 32 位 challenge + passtime,再经过 MD5 加密
- userresponse: “66666be4” //滑动距离x + challenge 的值
aa
往上找到o 在5917打上断点 aa是有e生成的
直接跳过了 发现在上边没有e 跟上个栈
跳第2个栈8066行、查看第2个参数l 就是这个参数
l = n['$_CICa']['$_BBEI'](n['$_CICa']['$_FDL'](), n['$_CJV']['c'], n['$_CJV']['s');
后面两个参数是前面get.php返回的新的c和s
外层** F D L ∗ ∗ 和 ∗ ∗ _FDL**和** FDL∗∗和∗∗_BBEI ** 直接进去全扣就行了
$_FDL转到3990行4030这里是轨迹
ct里面的**$_CAd**对轨迹进行了做差运算
passtime
去轨迹的最后一个列表的的最后一位数
passtime = track[-1][-1] #滑动时间
userresponse
t为滑动距离x **i[‘challenge’]**为get.php返回的新chanllenge
H(t, i['challenge'])
H()在668行全口还原一下就行
rp
这个rp为32位盲猜一波 MD5 控制台输出一下
还原之后 rp :gt + 32 位 challenge + passtime,再经过 MD5 加密
o['rp'] = X(i['gt'] + i['challenge']['slice'](0, 32) + o['passtime'])
3.5.2.2 V方法
进入V方法 控制台还原一下参数 很明显是AES加密 初始向量 iv 值为 “0000000000000000”:
这里通过引库实现
var CryptoJS = require('crypto-js')
function aesV(o_text, random_str) {
var key = CryptoJS.enc.Utf8.parse(random_str);
var iv = CryptoJS.enc.Utf8.parse("0000000000000000");
var srcs = CryptoJS.enc.Utf8.parse(o_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;
};
通过对比结果一致
3.5.3 h参数
通过**m[‘$_FEE’]**对l参数就行加密
h = m['$_FEE'](l)
进入$_FEE 跳转到1609行 发现最后是返回的e里面的两个参数
进去e里面看看 跳转**$_FCc**到1564行 返回的也是res:n 和 end:r
混淆还原之后改写一下方法
function $_FCc(t) {
// var o = this;
// i || (i = o);
for (var e = function(t, e) {
for (var n = 0, r = 24 - 1; 0 <= r; r -= 1)
1 === $_FBv(e, r) && (n = (n << 1) + $_FBv(t, r));
return n;
}, n = '', r = '', s = t['length'], a = 0; a < s; a += 3) {
var _;
if (a + 2 < s)
_ = (t[a] << 16) + (t[a + 1] << 8) + t[a + 2],
n += $_EJu(e(_, 7274496)) + $_EJu(e(_, 9483264)) + $_EJu(e(_, 19220)) + $_EJu(e(_, 235));
else {
var c = s % 3;
2 == c ? (_ = (t[a] << 16) + (t[a + 1] << 8),
n += $_EJu(e(_, 7274496)) + $_EJu(e(_, 9483264)) + $_EJu(e(_, 19220)),
r = ".") : 1 == c && (_ = t[a] << 16,
n += $_EJu(e(_, 7274496)) + $_EJu(e(_, 9483264)),
r = '.' + '.');
}
}
return n+r;
}
3.5.4 w
w = h+u
到此轨迹加密就完成了!!!!
4 结果展示
5 总结
在里面也遇到了很多坑,这种扣算法+js好处就在于不用不用补环境缺点就在于如果没有经验是不知道他的算法的,还有一种全扣的方法需要补环境可以上proxy()拦截环境进行补充比较简单,推荐新手尝试。