图片滑动验证码
# 图片滑动验证码
从用户体验来看,用户只需要使用鼠标拖动滑块到指定位置,如果位置正确,则通过验证。这是一种十分好用的验证方式,相比输入形式的验证码,省去了用户键盘的输入过程。
这类验证码的实现过程一般如下:
服务端准备图片若干个。
用户请求获取图片验证码,服务端随机选取一个图片作为验证码大图,复制指定形状的一小部分图片内容作为滑块。
记录复制的滑块位置,响应大图,滑块图给前端。
用户拖动滑块拼图,把滑动的位置信息发送给服务器。
服务器验证滑动位置是否和记录的相近,允许一定像素的误差,验证是否通过。
# 一个例子
以 https://beian.miit.gov.cn 网站为例
跳过前面的步骤, 我们请求验证返回数据剔除无用信息:
{
"code": 200,
"msg": "操作成功",
"params": {
"smallImage": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAA0JCgsKCxxxxxxxxxxxxxx",
"bigImage": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAA0JCgsKCA0lFAj/2Qxxxxxxxxxxx",
"uuid": "87db2dbb-4c1d-4633-a624-0045305e61e3",
"height": "81"
},
"success": true
}
2
3
4
5
6
7
8
9
10
11
bigImage smallImage 是 base64 编码后的图片,一个是大图,一个是用于滑动的方块,其中的 uuid 可以认为是图片 id,在图片验证时需要传给服务器。
如果我们拿 bigImage 内容进行 Base64 转图片,可以看到验证码大图。
验证码校验
我们滑动图片验证码到准确位置,这时会发起验证请求,我拷贝出了这条请求的详细信息。
curl 'https://hlwicpfwc.miit.gov.cn/icpproject_query/api/image/checkImage' \
--data-raw '{"key":"6af2c1bb-47a4-479d-a9f1-1ab17f86214e","value":"320"}' \
--compressed
2
3
从中可以发现,依旧在请求头中携带了 Token 字段。同时请求体携带了 key 和 value 两个字段,其中 key 就是上一步获取到的图片的 uuid,value 则是滑动验证码滑动的距离。320 也就是表示滑动了 320 个像素位置进行了拼图。
验证通过后得到响应信息,其中 params 是验证后得到的可以用于请求验证的值。
{
"code": 200,
"msg": "操作成功",
"params": "eyJ0eXBlIjozLCJleHREYXRhIjp7InZhZnljb2RlX2ltYWdlX2tleSI6IjZhZjJjMWJiLTQ3YTQtNDc5ZC1hOWYxLTFhYjE3Zjg2MjE0ZSJ9LCJlIjoxNjg5MDg4NDQ0MTM2fQ.cYPpQya1KiWZgC3T8ZNr9cWAM2aTHDtFUpzPIO-sMhQ",
"success": true
}
2
3
4
5
6
到这里 滑动验证码 验证过程完成
验证码校验这个阶段,因为要计算验证码的滑动距离,这里需要涉及到图像处理知识
,该怎么破解它?
# 验证码图片分析
我们获取几张验证图片观察,明显看到在要滑动的目标区域有被阴影覆盖,那么这一部分的 RGB 颜色就有所变化,一个正常的图片很少有这种矩形方块的颜色突变。
通过上面的分析,我们发现色彩其实并不重要,图片的灰度变化
(或者叫亮度变化
)比较重要,因为目标区域有被灰色覆盖。会产生一个矩形突变,那么我们可以把图片转换成灰度图片,这样更有利于观察。
准备验证图片一张
我们把这个图片进行解码,然后转换成灰度图片。
其中灰度值通过公式
R * 0.2126 + G * 0.7152 + B * 0.0722
计算得到。
以下为 JS 转换代码
function main(base64String) {
const decodedBytes = atob(base64String)
const byteNumbers = new Array(decodedBytes.length)
for (let i = 0; i < decodedBytes.length; i++) {
byteNumbers[i] = decodedBytes.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'image/png' })
const imageURL = URL.createObjectURL(blob)
const image = new Image()
image.onload = function () {
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const context = canvas.getContext('2d')
context.drawImage(image, 0, 0)
const imageData = context.getImageData(0, 0, canvas.width, canvas.height)
// 将图片像素点转换为黑白
for (let i = 0; i < imageData.data.length; i += 4) {
const red = imageData.data[i]
const green = imageData.data[i + 1]
const blue = imageData.data[i + 2]
const gray = Math.round(red * 0.2126 + green * 0.7152 + blue * 0.0722)
// 将像素点的颜色设置为非黑即白
imageData.data[i] = gray
imageData.data[i + 1] = gray
imageData.data[i + 2] = gray
}
context.putImageData(imageData, 0, 0)
// 保存黑白图片
const link = document.createElement('a')
link.href = canvas.toDataURL()
link.download = 'gray.jpg'
link.click()
URL.revokeObjectURL(imageURL)
console.log('验证码图片已保存')
}
image.src = imageURL
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
将图片转换成
其实这时,如果我们输出图片的每个像素点的灰度矩阵
,已经可以发现灰色方块处的灰度特点
了。可以很明显的看到灰色方块区域的灰度值变化。
例:
如果你检测这个灰度矩阵,你已经有办法找到灰色区域开始的位置了。
# 图片二值化
为了更方便的分析,我们可以对图片再次转化,我们可以选取一个阈值,超过指定阈值的像素点直接转换成白色,低于指定阈值的颜色转换成黑色。这样更方便后续的分析。
改造代码,添加亮度阈值判断,这里选择 100 作为阈值。
// 将像素点的颜色设置为非黑即白
if (gray > 100) {
gray = 255
} else {
gray = 0
}
2
3
4
5
6
转换后的图片变化如下:
如果我们打印这个二值化后的图片像素矩阵,可以更加清楚的看到灰色矩形区域几乎都是 0 值。
# 计算滑动距离
对二值化后的图片进行计算,可以非常方便的找到灰色矩形开始的位置,只需判断每个 (x,y) 像素值和前一个像素 (x-1,y)的值变化情况即可。如果某一列出现大量从 255 变化到 0 的像素,这里很大概率是达到了矩形的第一个边界,这列所在的 x 值就是要移动的距离。
// lightArray 存储了二值化后的图片像素信息
// 扫描发现 255->0 变化的最大数量
let maxChangeCount = 0
// maxChangeCount 对应的列值
let index = 0
for (let w = 1; w < width; w++) {
let changeCount = 0
for (let h = 0; h < height; h++) {
if (lightArray[h][w] === 0 && lightArray[h][w - 1] === 255) {
changeCount++
}
}
if (changeCount > maxChangeCount) {
maxChangeCount = changeCount
index = w
}
}
console.log(`分析得到验证码坐标值:${index}`)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
针对用于测试的熊猫验证码图片,计算得到滑动距离为 127,验证图片发现距离也大概是 127px 左右。
至此,图片滑动验证码自动化破解完成。
# 如果是守方, 如何防止攻击者破解验证码
可以加入拖动轨迹的验证,分析鼠标拖动的轨迹
来判断是否是人工拖动。
以下是个简单的例子
<div id="slider"></div>
<script>
var slider = document.getElementById('slider')
var isDragging = false
var trackData = [] // 轨迹数据
// 添加mousedown或touchstart事件监听
slider.addEventListener('mousedown', startDragging)
slider.addEventListener('touchstart', startDragging)
// 添加mousemove或touchmove事件监听
document.addEventListener('mousemove', drag)
document.addEventListener('touchmove', drag)
// 添加mouseup或touchend事件监听
document.addEventListener('mouseup', stopDragging)
document.addEventListener('touchend', stopDragging)
// 开始拖动
function startDragging(event) {
isDragging = true
trackData = [] // 重置轨迹数据
}
// 拖动中
function drag(event) {
if (isDragging) {
// 获取鼠标/触摸坐标
var x = event.clientX || event.touches[0].clientX
var y = event.clientY || event.touches[0].clientY
// 更新滑块的位置
slider.style.left = x + 'px'
slider.style.top = y + 'px'
// 添加当前坐标点到轨迹数据
trackData.push({ x: x, y: y })
}
}
// 停止拖动
function stopDragging(event) {
isDragging = false
// 在这里发送拖动轨迹数据 到服务端验证
requestValidateTrack(trackData)
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49