本文章所有内容仅供学习和研究使用,本人不提供具体模型和源码。若有侵权,请联系我立即删除!维护网络安全,人人有责。
前言
手势验证码是一种绘制轨迹进行人机的验证码,我曾经遇到过几次。
查了查网上没有较好的教程,便打算自己写一篇文章。
目标网址:
aHR0cHM6Ly93d3cudmFwdGNoYS5jb20vI2RlbW8=
手势验证码长这样
1.png (246.65 KB, 下载次数: 0)
下载附件
1
2024-2-8 00:51 上传
我将该验证码的协议逆向分析和验证码识别分为2篇文章,本文主要研究该验证码的逆向分析。
目录
请求分析
准备工作
4种请求
config请求
get请求
validate请求
获取验证码
en参数分析
fp指纹分析
fp指纹分析2
构造en参数
获取数据
图片还原
还原代码分析
获取图片还原顺序
python还原图片
图片识别
关于图片识别
构造请求
请求分析
轨迹分析
轨迹加密
请求验证
总结
请求分析
准备工作
首先,每个验证码平台都有唯一的业务标识,用于区分不同网站。我们应该养成看文档的好习惯,一般情况下直接在该验证码的官网找到Web端开发文档
2.png (101.16 KB, 下载次数: 0)
下载附件
2
2024-2-8 00:51 上传
可以看到可以看到验证码初始化的代码,vid就是唯一标识,然后在验证码出现的页面全局搜索vaptcha(
3.png (165.06 KB, 下载次数: 0)
下载附件
3
2024-2-8 00:51 上传
然后记住该id为59b252ed57f5a2111xxxxxxx(为了脱敏,部分真实数据我将用xxx代替)
4种请求
该验证码没有debugger反爬措施,直接打开浏览器开发者工具抓包即可。
4.png (30.97 KB, 下载次数: 0)
下载附件
4
2024-2-8 00:51 上传
可以看到一共有4种请求,其中59b252ed57f5a2111xxxxxxx接口就是用户的vid,用于获取验证服务器。
5.png (40.94 KB, 下载次数: 0)
下载附件
5
2024-2-8 00:51 上传
返回的api就是下面3个请求的域名。
config请求
该请求是获取验证码配置的,即一些sdk地址和版本。
请求数据
6.png (23.44 KB, 下载次数: 0)
下载附件
6
2024-2-8 00:52 上传
响应数据
7.png (34.09 KB, 下载次数: 0)
下载附件
7
2024-2-8 00:52 上传
vi参数就是vid
其中我们只需要响应数据的knock值
那么u值是怎么生成的呢?
我们开始跟栈
8.png (34.24 KB, 下载次数: 0)
下载附件
8
2024-2-8 00:52 上传
getConfig这个函数名那么明显,我们进去看看
9.png (34.54 KB, 下载次数: 0)
下载附件
9
2024-2-8 00:52 上传
可以明显看到,u值的来源是localStorage中的vaptchanu,那么意味着可以是空值。
当把该页面的缓存清空后,可以发现u值为空,这里我就不放图了,有兴趣大家试试。
get请求
顾名思义,该请求是获取验证码数据的。
请求数据
10.png (25.32 KB, 下载次数: 0)
下载附件
10
2024-2-8 00:52 上传
响应数据
11.png (31.68 KB, 下载次数: 0)
下载附件
12
2024-2-8 00:53 上传
可以看到k参数就是config接口返回的knock
而en参数包含一些环境检测,我将在下文分析
validate请求
该请求用于验证是否正确。
请求数据
12.png (41.08 KB, 下载次数: 0)
下载附件
13
2024-2-8 00:53 上传
正确的响应数据
13.png (17.29 KB, 下载次数: 0)
下载附件
14
2024-2-8 00:53 上传
返回的token值就是我们的目标,我们携带该值请求需要验证的接口就可以返回数据了。
获取验证码
en参数分析
我们进入post的前面的栈
14.png (38.83 KB, 下载次数: 0)
下载附件
15
2024-2-8 00:53 上传
16.png (112.48 KB, 下载次数: 0)
下载附件
16
2024-2-8 00:53 上传
由于switch的存在,代码是分步执行的,我们在switch打上记录点,记录执行顺序
15.png (122.37 KB, 下载次数: 0)
下载附件
11
2024-2-8 00:53 上传
可以看到,获取了一些localStorage的内容,
hex_md5其实就是md5加密 (严格来说不算加密,因为md5无法解密)
我们清空缓存后重新请求可以发现执行顺序。
6
0
1
2
5
我们只需要把执行过的函数扣下来即可。
我只说几个重点
[ol]
获取了localStorage的变量都是三元运算符,当localStorage里的值为空时就会设置默认值
_0x502684['GenerateFP']和_0x5d0164['GenerateFP']都是浏览器指纹,但是后者是Promise异步任务,我接下来会分析这两个的区别。
_0x4f54b3['secretC']是一个固定值。
[/ol]
17.png (48.13 KB, 下载次数: 0)
下载附件
17
2024-2-8 00:54 上传
[/ol]
18.png (81.34 KB, 下载次数: 0)
下载附件
18
2024-2-8 00:54 上传
19.png (144.83 KB, 下载次数: 0)
下载附件
19
2024-2-8 00:54 上传
将返回值的值相加即可得到该字符串。由于js的相加和python不太一样,我们可以使用以下代码在python中实现
def sort_dict_by_key(input_dict):
"""字典按照键排序"""
sorted_keys = sorted(input_dict.keys())
sorted_dict = {key: input_dict[key] for key in sorted_keys}
return sorted_dict
def splicing_obj(obj: dict):
obj = sort_dict_by_key(obj)
result = ""
for k, v in obj.items():
if isinstance(v, str):
result += v
elif isinstance(v, int):
result += str(v)
elif isinstance(v, float):
result += str(v)
elif isinstance(v, bool):
result += "true" if v else "false"
elif isinstance(v, list):
result += ",".join(v)
else:
result += "[object Object]"
return result
fp指纹分析
_0x502684['GenerateFP']函数主要通过canvas绘制特定字符再转换为base64来计算浏览器指纹。
传入一个参数,该参数将会绘制在canvas上。
20.png (101.97 KB, 下载次数: 0)
下载附件
20
2024-2-8 00:53 上传
该方法在不同浏览器上的结果不相同,这就意味着我们可以不用canvas来计算指纹,但是要求同一参数的计算值相同。
我们可以使用加盐的哈希算法来计算,在同一次请求中盐值应当相同(要注意CRC32校验,否则验证不通过)。
由于本文是教程,所以我就直接使用canvas来计算指纹(nodejs安装canvas库有些麻烦)
21.png (98.96 KB, 下载次数: 0)
下载附件
21
2024-2-8 00:55 上传
其他就是一些转换了,包括CRC32校验,直接扣下来即可。
fp指纹分析2
_0x5d0164['GenerateFP']是个异步函数
22.png (10.75 KB, 下载次数: 0)
下载附件
22
2024-2-8 00:55 上传
我们跟进函数内查看,在返回部分打个断点重新请求。
23.png (160.2 KB, 下载次数: 0)
下载附件
23
2024-2-8 00:55 上传
可以看到,_0x33ff5b是浏览器环境,经过hashComponents后返回一个哈希值(我没见过这种加密,有懂的大佬可以说说)
查看代码得知该GenerateFP函数可以传入两个参数:
// _0x41154d存在时将加入浏览器环境
if (_0x41154d) {
_0x33ff5b = _0x3633c3(_0x3633c3({}, _0x2d65f5['components']), {
'param': {
'value': _0x41154d,
'duration': 0x0
}
});
} else {
_0x33ff5b = _0x3633c3({}, _0x2d65f5['components']);
}
// 计算哈希值
_0x4c267d = _0x5bf32f['hashComponents'](_0x33ff5b);
// 是否截取前八位,_0x596253是回调函数
if (_0x5799b7)
_0x596253(_0x4c267d);
else
_0x596253(_0x4c267d['slice'](0x0, 0x8));
_0x596253是回调函数,在下一步代码的_0x4f1db8['sent']()可以获取该值
24.png (125.17 KB, 下载次数: 0)
下载附件
24
2024-2-8 00:55 上传
我们直接将hashComponents全部扣下来即可。
关于浏览器环境
就是检测一些字体、插件、设置等,完全可以弄成定值,同样的相同的参数返回的值应该相同。
构造en参数
encryFunc和selectFrom没有特殊含义,直接扣下来即可
最后,把扣下来的代码整合可以得到en值
25.png (98.01 KB, 下载次数: 0)
下载附件
25
2024-2-8 00:55 上传
获取数据
最后把en携带参数去请求服务器即可获得验证码数据
26.png (70.76 KB, 下载次数: 0)
下载附件
26
2024-2-8 00:48 上传
图片还原
还原代码分析
27.png (224.05 KB, 下载次数: 0)
下载附件
27
2024-2-8 00:48 上传
可以看到,接口返回的图片是乱序的。
我们可以在事件侦听器中勾选canvas创建,因为浏览器还原乱序图片肯定会用到canvas。
28.png (20.9 KB, 下载次数: 0)
下载附件
28
2024-2-8 00:48 上传
跳过几个检测指纹的canvas,我们成功断住了还原图片的地方
29.png (65.38 KB, 下载次数: 0)
下载附件
29
2024-2-8 00:48 上传
我们只需要注意传入的_0x8782d1即可,该变量就是图片顺序。
获取图片还原顺序
向前跟栈找到生成顺序的地方
30.png (68.87 KB, 下载次数: 0)
下载附件
30
2024-2-8 00:48 上传
大体就是一个计算得到的整数_0x19be99和接口返回的img_order经过一个_0x2f94f6['Decrypt']方法得到
和en一样,找到switch的执行顺序以后可以得到0x19be99的值
_0x19be99 = hex2int(GenerateFP(ha)) + hex2int(hashComponents(hb)) + parseInt(secretC) + pow(r);
r、ha和hb就是get接口返回的内容
当有一项为空值时,就直接设为0.
pow为work2.js返回的内容,直接扣下来即可
31.png (89.71 KB, 下载次数: 0)
下载附件
31
2024-2-8 00:48 上传
整合后
function get_order(img_order, r, hb, ha){
let hb_en = 0;
let ha_en = 0;
if (hb !== "" && hb) {
hb_en = hex2int(hashComponents(hb))
}
if (ha !== "" && ha) {
ha_en = hex2int(GenerateFP(ha))
}
let _0x28c598 = ha_en + hb_en + 8549731620 + pow(r);
return Decrypt(img_order, _0x28c598)
}
python还原图片
from PIL import Image
from io import BytesIO
def restore(img, order):
"""还原图片"""
img = Image.open(BytesIO(img))
new_img = Image.new('RGB', (400, 230))
width = 80
height = 115
def drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight):
"""
param image:
param sx: 开始剪切的 x 坐标位置。
param sy: 开始剪切的 y 坐标位置。
param sWidth: 被剪切图像的宽度。
param sHeight: 被剪切图像的高度。
param dx: 在画布上放置图像的 x 坐标位置。
param dy: 在画布上放置图像的 y 坐标位置。
param dWidth: 要使用的图像的宽度
param dHeight: 要使用的图像的高度
"""
split_img = image.crop((sx, sy, sx + sWidth, sy + sHeight))
new_img.paste(split_img, (dx, dy))
for i in range(10):
if i
传入2个参数即可还原,img是图片二进制数据,order为图片还原顺序
图片识别
关于图片识别
图片识别我将在另一个专题详细讲
大家可以稍等
构造请求
请求分析
和get请求一样,在post前面断住
32.png (123.06 KB, 下载次数: 0)
下载附件
32
2024-2-8 00:48 上传
我们可以看到,和get请求的en加密基本相同,只不过多了几个参数。
其中_0x13cb8b变量包含验证信息:
33.png (100.46 KB, 下载次数: 0)
下载附件
33
2024-2-8 00:48 上传
轨迹分析
而_0x491ed2就是轨迹信息。
34.png (118.02 KB, 下载次数: 0)
下载附件
34
2024-2-8 00:49 上传
可以看到这个轨迹有点迷糊,有很多小数。我们找到生成轨迹的地方。
35.png (29.26 KB, 下载次数: 0)
下载附件
35
2024-2-8 00:49 上传
可以看到,就是鼠标位置减去绘制的起始位置。
这里有个坑。
36.png (219.85 KB, 下载次数: 0)
下载附件
36
2024-2-8 00:49 上传
这里区域实际上比事件位置多出30px,(验证图片原高230px,宽400px)
所以我们在原来的位置加上30px即可。
其他注意事项:
当轨迹x或y间隔小于5时,不会加入轨迹列表
37.png (49.89 KB, 下载次数: 0)
下载附件
37
2024-2-8 00:49 上传
38.png (42.12 KB, 下载次数: 0)
下载附件
38
2024-2-8 00:49 上传
轨迹加密
assemblyCoordData就是轨迹加密函数
39.png (73.07 KB, 下载次数: 0)
下载附件
39
2024-2-8 00:49 上传
我们直接扣下来即可
40.png (94.28 KB, 下载次数: 0)
下载附件
40
2024-2-8 00:49 上传
验证请求
这样,我们使用识别图片的轨迹加密生成en后去请求。
41.png (47.85 KB, 下载次数: 0)
下载附件
41
2024-2-8 00:49 上传
总结
在这个验证码飞速发展的时代,可能我今天的文章,明天就过期了。
所以,不要只一昧的CV,我们要学习新的思路,这样知识才是自己的。
如果文章有什么不足的欢迎各位大佬们补充。