Whoosy's Blog

藏巧于拙 用晦而明 寓清于浊 以屈为伸

0%

极验滑块验证码识别及破解

极验滑块验证码识别及破解

[success] 文档说明:

该文档主要介绍利用SeleniumPIL,和numpy等开源工具对滑块验证码的破解及识别

目标

通过程序识别并通过极验验证码的验证
  • 分析识别思路
  • 识别缺口位置
  • 生成滑动拖动路径
  • 模拟实现滑块拼合通过验证

识别思路

对于应用了极验验证码的网站,识别并不是没有办法。如果我们直接模拟表单提交的话,加密参数的构造是个问题,必须利用js逆向工程找出相关加密方式,如果参数构造有问题服务端就会校验失败,并且对方网站相关js文件更新也会对验证码识别造成影响,所以在这里我们采用直接模拟浏览器动作的方式来完成验证,在 Python 中我们就可以使用 Selenium 来通过完全模拟人的行为的方式来完成验证,此验证成本相对于直接去识别加密算法容易不少。

识别验证需要完成5步:

  • 模拟点击验证按钮
  • 抓取验证码背景图及缺口图
  • 识别滑动缺口的位置
  • 生成滑动轨迹数组
  • 模拟拖动滑动至缺口处

第一步操作就最简单的,可以直接使用selenium模拟点击出现滑块验证码
第二步抓取图片可以通过selenium执行js命令抓取相关图片
第三步操作识别缺口的位置比较关键,需要用到图像的相关处理方法,那缺口怎么找呢?首先来观察一下缺口的样子,如图所示:

node -v

可以看到缺口的四周边缘有明显的断裂边缘,而且边缘和边缘周围有明显的区别,我们可以实现一个边缘检测算法来找出缺口的位置。对于极验来说,我们可以利用和原图对比检测的方式来识别缺口的位置。
设定一个对比阈值,然后遍历两张图片找出相同位置像素 RGB 差距超过此阈值的像素点位置,那么此位置就是缺口的位置。

第四步操作看似简单,但是其中的坑比较多,极验验证码增加了机器轨迹识别,匀速移动、随机速度移动等方法都是不行的,只有完全模拟人的移动轨迹才可以通过验证,而人的移动轨迹一般是先加速后减速的,这又涉及到物理学中加速度的相关问题,但是这个也是有规律可循,用运动学知识也极易被破解。
滑块验证核心是后台验证轨迹参数,效验轨迹取点的分布,正常情况是如下图的离散分布

node -v
如果横坐标x是时间、纵坐标y是位移,那么每个点的切线就是加速度,会发现这样的一个规律,加速度由小变大,再又大变小,这是最主要的特征之一。

速度的变化率如下:

node -v

对速度而言肯定是先加速在减速,但加速度不会是固定的,应该是变变加速和变变加速,不过目前实际应用中的情况来说,是以速度变化情况为主要判别依据,因为在以恒加减速度生成的轨迹应用中不能通过检测,就说明目前极验已经对是否恒定加速度来鉴别机器和人工。

有了基本的思路之后就让我们用程序来实现一下它的识别过程吧。
知道上面的两点后我们就应该明白滑块验证的关键,并且可以预测它的下一步优化方式将是对加速度变化的验证,收集了两种轨迹生成方式:一种是以加减速为主的物理学生成方式(极容易被检测出来),另一种是根据轨迹离散分布生成的数学生成(基本不会被检测出来)。

数学方式是利用了离散数据中的4次曲线缓和方程模拟从零无规则加速在减速的过程。可参考http://robertpenner.com/scripts/easing_equations.txt

物理学方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def generate_tracks(S):
"""
:param S: 缺口距离Px
:return:
"""
S += 20
v = 0
t = 0.2
forward_tracks = []
current = 0
mid = S * 3 / 5 # 减速阀值
while current < S:
if current < mid:
a = 2 # 加速度为+2
else:
a = -3 # 加速度-3
s = v * t + 0.5 * a * (t ** 2)
v = v + a * t
current += s
forward_tracks.append(round(s))

back_tracks = [-3, -3, -2, -2, -2, -2, -2, -1, -1, -1]
return {'forward_tracks': forward_tracks, 'back_tracks': back_tracks}

数学生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

def ease_out_quart(x):
return 1 - pow(1 - x, 4)

def get_tracks(distance, seconds, ease_func):
distance += 20
tracks = [0]
offsets = [0]

for t in np.arange(0.0, seconds, 0.1):
ease = ease_func
offset = round(ease(t/seconds) * distance)
tracks.append(offset - offsets[-1])
offsets.append(offset)
tracks.extend([-3, -2, -3, -2, -2, -2, -2, -1, -0, -1, -1, -1]) # 正好抵消掉前面的加20

return offsets, tracks

最后我们只需要按照该运动轨迹拖动滑块即可,方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
def move_to_gap(self, track):
"""移动滑块到缺口处"""
slider = self.wait_browser.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_slider_button')))
ActionChains(self.browser).click_and_hold(slider).perform()

while track:
x = track.pop(0)
ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.02)

ActionChains(self.browser).release().perform()