[验证码逆向]某手势验证码逆向分析
本帖最后由 s1lencee 于 2024-2-8 15:56 编辑# [验证码逆向]某手势验证码逆向分析
> #### 本文章所有内容仅供学习和研究使用,本人不提供具体模型和源码。若有侵权,请联系我立即删除!维护网络安全,人人有责。
---
## 前言
手势验证码是一种绘制轨迹进行人机的验证码,我曾经遇到过几次。
查了查网上没有较好的教程,便打算自己写一篇文章。
目标网址:
```
aHR0cHM6Ly93d3cudmFwdGNoYS5jb20vI2RlbW8=
```
手势验证码长这样
**我将该验证码的协议逆向分析和验证码识别分为2篇文章,本文主要研究该验证码的逆向分析。**
## 目录
- ### 请求分析
- #### 准备工作
- #### 4种请求
- #### config请求
- #### get请求
- #### validate请求
- ### 获取验证码
- #### en参数分析
- #### fp指纹分析
- #### fp指纹分析2
- #### 构造en参数
- #### 获取数据
- ### 图片还原
- #### 还原代码分析
- #### 获取图片还原顺序
- #### python还原图片
- ### 图片识别
- #### 关于图片识别
- ### 构造请求
- #### 请求分析
- #### 轨迹分析
- #### 轨迹加密
- #### 请求验证
- ### 总结
---
## 请求分析
### 准备工作
首先,每个验证码平台都有唯一的业务标识,用于区分不同网站。我们应该养成看文档的好习惯,一般情况下直接在该验证码的官网找到Web端开发文档
可以看到可以看到验证码初始化的代码,`vid`就是唯一标识,然后在验证码出现的页面全局搜索`vaptcha(`
然后记住该`id`为`59b252ed57f5a2111xxxxxxx`(为了脱敏,部分真实数据我将用xxx代替)
### 4种请求
该验证码没有`debugger`反爬措施,直接打开浏览器开发者工具抓包即可。
可以看到一共有4种请求,其中`59b252ed57f5a2111xxxxxxx`接口就是用户的vid,用于获取验证服务器。
返回的`api`就是下面3个请求的域名。
### config请求
该请求是获取验证码配置的,即一些sdk地址和版本。
请求数据
响应数据
`vi`参数就是`vid`
其中我们只需要响应数据的`knock`值
**那么u值是怎么生成的呢?**
我们开始跟栈
`getConfig`这个函数名那么明显,我们进去看看
可以明显看到,u值的来源是`localStorage`中的`vaptchanu`,那么意味着可以是空值。
当把该页面的缓存清空后,可以发现u值为空,这里我就不放图了,有兴趣大家试试。
### get请求
顾名思义,该请求是获取验证码数据的。
请求数据
响应数据
可以看到`k`参数就是`config`接口返回的`knock`
而`en`参数包含一些环境检测,我将在下文分析
### validate请求
该请求用于验证是否正确。
请求数据
正确的响应数据
返回的`token`值就是我们的目标,我们携带该值请求需要验证的接口就可以返回数据了。
## 获取验证码
### en参数分析
我们进入post的前面的栈
由于`switch`的存在,代码是分步执行的,我们在`switch`打上记录点,记录执行顺序
可以看到,获取了一些`localStorage`的内容,
`hex_md5`其实就是md5加密 (严格来说不算加密,因为md5无法解密)
我们清空缓存后重新请求可以发现执行顺序。
```js
6
0
1
2
5
```
我们只需要把执行过的函数扣下来即可。
我只说几个重点
1. 获取了`localStorage`的变量都是三元运算符,当`localStorage`里的值为空时就会设置默认值
2. `_0x502684['GenerateFP']`和`_0x5d0164['GenerateFP']`都是浏览器指纹,但是后者是`Promise`异步任务,我接下来会分析这两个的区别。
3. ` _0x4f54b3['secretC']`是一个固定值。
4. `_0x4f54b3['globalMd5']`是`config`返回内容处理后的`md5`加密值。
将返回值的值相加即可得到该字符串。由于js的相加和python不太一样,我们可以使用以下代码在python中实现
```python
def sort_dict_by_key(input_dict):
"""字典按照键排序"""
sorted_keys = sorted(input_dict.keys())
sorted_dict = {key: input_dict 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 += ""
return result
```
### fp指纹分析
`_0x502684['GenerateFP']`函数主要通过canvas绘制特定字符再转换为base64来计算浏览器指纹。
传入一个参数,该参数将会绘制在canvas上。
该方法在不同浏览器上的结果不相同,这就意味着我们可以不用`canvas`来计算指纹,但是要求同一参数的计算值相同。
我们可以使用加盐的哈希算法来计算,在同一次请求中盐值应当相同(要注意CRC32校验,否则验证不通过)。
由于本文是教程,所以我就直接使用`canvas`来计算指纹(nodejs安装canvas库有些麻烦)
其他就是一些转换了,包括`CRC32`校验,直接扣下来即可。
### fp指纹分析2
`_0x5d0164['GenerateFP']`是个异步函数
我们跟进函数内查看,在返回部分打个断点重新请求。
可以看到,`_0x33ff5b`是浏览器环境,经过`hashComponents`后返回一个哈希值(我没见过这种加密,有懂的大佬可以说说)
查看代码得知该`GenerateFP`函数可以传入两个参数:
- 第一个参数将被设置进`_0x33ff5b`中,和浏览器环境一起加密
- 第二个参数是个布尔值,当为`true`时返回全部哈希值,`false`时截取前面8为返回
```js
// _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']()`可以获取该值
我们直接将`hashComponents`全部扣下来即可。
**关于浏览器环境**
就是检测一些字体、插件、设置等,完全可以弄成定值,同样的相同的参数返回的值应该相同。
### 构造en参数
`encryFunc`和`selectFrom`没有特殊含义,直接扣下来即可
最后,把扣下来的代码整合可以得到`en`值
### 获取数据
最后把`en`携带参数去请求服务器即可获得验证码数据
###
## 图片还原
### 还原代码分析
可以看到,接口返回的图片是乱序的。
我们可以在事件侦听器中勾选`canvas`创建,因为浏览器还原乱序图片肯定会用到`canvas`。
跳过几个检测指纹的`canvas`,我们成功断住了还原图片的地方
我们只需要注意传入的`_0x8782d1`即可,该变量就是图片顺序。
### 获取图片还原顺序
向前跟栈找到生成顺序的地方
大体就是一个计算得到的整数`_0x19be99`和接口返回的`img_order`经过一个`_0x2f94f6['Decrypt']`方法得到
和`en`一样,找到`switch`的执行顺序以后可以得到`0x19be99`的值
```js
_0x19be99 = hex2int(GenerateFP(ha)) + hex2int(hashComponents(hb)) + parseInt(secretC) + pow(r);
```
`r`、`ha`和`hb`就是`get`接口返回的内容
当有一项为空值时,就直接设为0.
`pow`为`work2.js`返回的内容,直接扣下来即可
整合后
```js
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还原图片
```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 < 5:
j = i
if int(order) < 5:
o = int(order)
drawImage(img, j * width, 0, width, height, o * width, 0, width, height)
else:
o = int(order) - 5
drawImage(img, j * width, 0, width, height, o * width, height, width, height)
else:
j = i - 5
if int(order) < 5:
o = int(order)
drawImage(img, j * width, height, width, height, o * width, 0, width, height)
else:
o = int(order) - 5
drawImage(img, j * width, height, width, height, o * width, height, width, height)
return new_img
```
传入2个参数即可还原,`img`是图片二进制数据,`order`为图片还原顺序
## 图片识别
### 关于图片识别
**图片识别我将在另一个专题详细讲**
**大家可以稍等**
## 构造请求
### 请求分析
和`get`请求一样,在`post`前面断住
我们可以看到,和`get`请求的`en`加密基本相同,只不过多了几个参数。
其中`_0x13cb8b`变量包含验证信息:
- `dt`: 用时,不到4位数用0补上
- `ch`: `canvas`的高
- `cw`: `canvas`的宽
- `v`:加密后的轨迹值
### 轨迹分析
而`_0x491ed2`就是轨迹信息。
可以看到这个轨迹有点迷糊,有很多小数。我们找到生成轨迹的地方。
可以看到,就是鼠标位置减去绘制的起始位置。
这里有个坑。
这里区域实际上比事件位置多出30px,(验证图片原高230px,宽400px)
所以我们在原来的位置加上30px即可。
**其他注意事项:**
当轨迹x或y间隔小于5时,不会加入轨迹列表
### 轨迹加密
`assemblyCoordData`就是轨迹加密函数
我们直接扣下来即可
### 验证请求
这样,我们使用识别图片的轨迹加密生成`en`后去请求。
## 总结
在这个验证码飞速发展的时代,可能我今天的文章,明天就过期了。
所以,不要只一昧的CV,我们要学习新的思路,这样知识才是自己的。
如果文章有什么不足的欢迎各位大佬们补充。 443566434 发表于 2024-2-14 17:15
请教一下,有些APP的比如某宝的滑动验证码,手动右滑就可以验证成功,但是用js代码的swipe滑 能右滑过去了 ...
大概率检测轨迹了,你的轨迹不像人滑的 最近想写一个程序,访问一个网页,但是卡在第一步
1、正常手动访问的话,需要经过滑动验证码进行验证;
2、但是我直接使用代码进行http请求的话,会返回错误状态码,信息提示需要经过验证码验证,请问有没有绕过的方案,或者是其他方案 我先在这提前祝大家新年快乐 凌晨发帖,分析得这么详细,学习了 进来学习下。 牛逼大佬支持一波
呵呵,正在学习中
进来学习下 进来学习下 前排膜拜一下大佬 这个帖子怎么消失了几天,8号上午还有,中午就没了