某平台某验3全家桶逆向分析
目标网站:
aHR0cHM6Ly9qenNjLm1vaHVyZC5nb3YuY24vZGF0YS9jb21wYW55L2RldGFpbD9pZD0wMDIxMDUyOTEyMzk0NTEzMzk=
网站分析:
进入页面刷新,在加载数据前会过一道验证码,验证码左下角显示为极验验证码。经过几天观察发现,网站每天验证码类型随机,发现有五种类型验证码:滑块,文字点选,图标点选,九宫格和空间推理,验证码类型为极验3类型(极验3和极验四最大区别就是滑块,极验3需要检测滑块轨迹,极验四不需要,再就是混淆逻辑不同)。

流程分析:
先看验证码请求逻辑,验证码完整的请求逻辑应该是先通过start接口获取challenge和gt,然后请求第一个get和ajax完成challenge和gt初始化,然后请求第二个get获取验证码图片信息,最后请求ajax提交验证。但经过测试发现可以去掉第一个get请求,第一个ajax的w值可以置空。
然后观察请求发现,start接口有内容加密,最后一个ajax有参数加密

加密:
startCaptcha接口:
返回内容为加密内容,断点调试发现为aes加密,密钥和iv都固定,可使用标准库实现。

ajax接口:
w值加密,这里可以搜索"\u0077"定位,也可以断点跟栈定位,就不做描述。


观察发现,w的值由两个值构成:h和u,u为r[$_CAIAL(738)]()函数运行的产生的值,h为m[$_CAHJk(762)](l)运行产生的值。
u:
先看u,跟进r[$_CAIAL(738)],跟进去发现这个函数实际执行语句为:var e = new U()[$_CBFJy(326)](this[$_CBGAs(766)](t));
while (!e || 256 !== e[$_CBFJy(142)])
e = new U()[$_CBGAs(326)](this[$_CBFJy(766)](!0));
其中this[$_CBGAs(766)](t)是一个随机字符串生成函数,可以固定写死,new U()[$_CBFJy(326)]是ras加密操作,这里可以使用三方库还原,也可以扣函数代码,公钥为固定值。



h:
查看h之前需要处理l和o的值,因为h是对l处理加密得出,l是对o处理加密得出。
o值:向上跟栈定位o生成位置,发现o一共8个值,但其中只有u,passtime(这里取轨迹最后一个时间就行),aa和rp为变值,其余可以固定。
{
"lang": "zh-cn",
"userresponse": "2ddd22df8",
"passtime": 770,
"imgload": 204,
"aa": "U-,/-.04111-.(!!Stt(*((((((((((Rttttssssszstzsssust.(!!($))4,2h0111111111224$,-:-4I.7,2.667N91:$*p",
"ep": {
"v": "7.9.3",
"$_BIT": false,
"me": true,
"tm": {
"a": 1769566324330,
"b": 1769566324785,
"c": 1769566324789,
"d": 0,
"e": 0,
"f": 1769566324331,
"g": 1769566324331,
"h": 1769566324331,
"i": 1769566324331,
"j": 1769566324331,
"k": 0,
"l": 1769566324338,
"m": 1769566324777,
"n": 1769566324779,
"o": 1769566324797,
"p": 1769566331309,
"q": 1769566331309,
"r": 1769566331310,
"s": 1769566342149,
"t": 1769566342149,
"u": 1769566342149
},
"td": -1
},
"h9s9": "1816378497",
"rp": "e22583d1b402b5916eb5ee92ccf71b74"
}

u:先看u值,u值为 H(t, i[$_CAHJk(168)]),其中t为滑动距离,i[$_CAHJk(168)]为请求使用的challenge值,H函数直接扣下来即可。
aa:aa的值需要向前跟栈,发现是使用n[$_CJJJE(986)][$_DAAAF(1024)]函数对滑动轨迹,图片请求返回的c和s进行加密处理,这里直接将函数($_BBEl和$_FDd)扣下来即可。

rp:rp的值为X(i[$_CAHJk(176)] + i[$_CAIAL(168)][$_CAHJk(120)](0, 32) + o[$_CAIAL(714)]);三个值分别为gt,challenge和passtime,函数X可以直接扣代码。
l值:l = V[$_CAHJk(326)](yt[$_CAIAL(291)](o), r[$_CAHJk(766)]()),其中yt[$_CAIAL(291)]其实就JSON.stringify方法,r[$_CAHJk(766)]()就是之前生成的随机字符串。 V[$_CAHJk(326)]直接扣代码即可。
最后生成h值即可。
另外滑块的图片是乱序的(其中还原顺序是固定的,可以直接打canvas断点跟栈),这边直接给出还原代码,还原后直接使用ddddocr就可以获取滑动路径。
def restore_img_from_url(img_url, ut_array=None):
"""
从图片URL下载乱序图片,还原后返回二进制字节对象(适配ddddocr)
:param img_url: 图片的网络URL地址
:param ut_array: 拼图块的排列规则数组(默认使用原代码的Ut数组)
:return: 还原后的图片二进制字节对象 | None(失败时)
"""
# 默认排列规则(保留原代码的Ut数组)
default_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 = ut_array if ut_array is not None else default_ut
try:
# 1. 下载图片(设置超时和请求头)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
response = requests.get(img_url, headers=headers, timeout=10, verify=False)
response.raise_for_status()
# 2. 从字节流打开图片并转为RGB模式
im = Image.open(BytesIO(response.content)).convert("RGB")
# 3. 核心还原逻辑(不变)
canvas_width, canvas_height = 260, 160
new_image = Image.new("RGB", (canvas_width, canvas_height))
piece_height = canvas_height // 2 # 80px
for i in range(len(Ut)):
# 计算裁剪位置
crop_x = Ut[i] % 26 * 12 + 1
crop_y = piece_height if Ut[i] > 25 else 0
crop_box = (crop_x, crop_y, crop_x + 10, crop_y + piece_height)
piece = im.crop(crop_box)
# 计算粘贴位置
paste_x = i % 26 * 10
paste_y = piece_height if i > 25 else 0
new_image.paste(piece, (paste_x, paste_y))
# 4. 将还原后的Image对象转为二进制字节对象(关键修改)
img_byte_arr = BytesIO()
# 保存为JPEG格式(ddddocr兼容),quality=95保证清晰度且体积适中
new_image.save(img_byte_arr, format='JPEG', quality=95)
img_byte_arr = img_byte_arr.getvalue() # 获取二进制字节
# 5. 返回二进制对象
return img_byte_arr
except requests.exceptions.RequestException as e:
print(f"图片下载失败:{str(e)}")
return None
except Exception as e:
print(f"图片处理/转二进制失败:{str(e)}")
return None
滑块轨迹代码(网上copy大佬的):
def __ease_out_expo(sep):
'''
轨迹相关操作
'''
if sep == 1:
return 1
else:
return 1 - pow(2, -10 * sep)
def get_slide_track(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 = 10 + 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)
# y = round(__ease_out_expo(i / count) * 14)
# 滑动过程消耗的时间
t += random.randint(10, 50)
if x == _x:
continue
slide_track.append([x, _y, t])
_x = x
slide_track.append(slide_track[-1])
return slide_track
上面是滑块的加密流程,其他几种验证和滑块的区别是o值不一样,有3个变值(a,pic,tt)。其中pic为图片的path路径(返回的),e为点击的坐标(这里需要处理,可以打click断点进行单步调试,注意,网页调试是传入的是视图坐标,和本地下载识别坐标不一致,需要对处理逻辑简单处理一下(直接从图片对象坐标开始扣代码),还需要注意网页图片大小和下载图片大小进行坐标比例换算),tt由浏览器指纹(固定),返回的c和s加密得出。
{
"lang": "zh-cn",
"passtime": 1242, // 通过时间不校验
"a": e, // 坐标
"pic": pic_path, // 返回的图片路径
"tt": function (e, t, n) {
var $_CADFN = YDbxr.$_Ct
, $_CADEk = ['$_CADIZ'].concat($_CADFN)
, $_CADGD = $_CADEk[1];
$_CADEk.shift();
var $_CADHd = $_CADEk[0];
if (!t || !n)
return e;
var r, i = 0, s = e, o = t[0], _ = t[2], a = t[4];
while (r = n[$_CADFN(784)](i, 2)) {
i += 2;
var c = parseInt(r, 16)
, l = String[$_CADFN(181)](c)
, u = (o * c * c + _ * c + a) % e[$_CADFN(138)];
s = s[$_CADGD(784)](0, u) + l + s[$_CADFN(784)](u);
}
return s;
}(s, c, s_s),
"ep": {
"ca": [
{
"x": 894,
"y": 278,
"t": 1,
"dt": 1656
},
{
"x": 938,
"y": 193,
"t": 1,
"dt": 415
},
{
"x": 1025,
"y": 184,
"t": 1,
"dt": 392
},
{
"x": 1067,
"y": 426,
"t": 3,
"dt": 1068
},
{
"x": 920,
"y": 343,
"t": 1,
"dt": 89532
},
{
"x": 852,
"y": 195,
"t": 1,
"dt": 448
},
{
"x": 1046,
"y": 435,
"t": 3,
"dt": 789
}
],
"v": "3.1.2",
"$_FG": false,
"me": true,
"tm": {
"a": 1768372719938,
"b": 1768372720034,
"c": 1768372720036,
"d": 0,
"e": 0,
"f": 1768372719939,
"g": 1768372719940,
"h": 1768372719943,
"i": 1768372719943,
"j": 1768372720000,
"k": 1768372719970,
"l": 1768372720000,
"m": 1768372720030,
"n": 1768372720030,
"o": 1768372720042,
"p": 1768372720686,
"q": 1768372720686,
"r": 1768372720686,
"s": 1768372722999,
"t": 1768372722999,
"u": 1768372722999
}
} // 固定
}
结果:
返回为sucess就表明成功,score为轨迹人机分数,分数越低越像人为操作。

更多推荐




所有评论(0)