滑動驗證流行於現在各大網站,破解難度也變得沒那麼容易了。破解一般有兩種思路,一是對請求進行api以及js進行分析,模擬軌跡引數來獲取請求引數,二是利用自動化工具模擬滑鼠移動來進行擬人化操作,分析api要比後者難得多,並且就維護而言,官方一旦改變加密演算法,就要重新分析,本文將介紹後者,下面直接開始。

我們以geetest的樣例為例,用selenium來一步步對其進行模擬操作,sdk地址:

從上述連結下載SDK並按教程安裝。

滑動驗證碼模擬操作

demo目錄下有3個執行包,這裡我選擇flask,在flask_demo目錄下執行

python run start

在瀏覽器中開啟

http://

localhost:5000/

就能訪問

滑動驗證碼模擬操作

分別有彈出式、嵌入式和移動端手動實現彈出式三種,為了避免干擾,我們只保留嵌入式demo,在

滑動驗證碼模擬操作

logiin。html裡面把另外2個demo註釋掉,重新整理瀏覽器如下,

滑動驗證碼模擬操作

接下來要利用selenium在瀏覽器模擬操作,程式碼如下

from selenium import webdriver

from selenium。webdriver。chrome。options import Options

from selenium。webdriver。support import expected_conditions as EC

from selenium。webdriver。support。ui import WebDriverWait

from selenium。webdriver。common。action_chains import ActionChains

from selenium。webdriver。common。by import By

import time

chrome_options = Options()

chrome_options。add_experimental_option(“debuggerAddress”, “127。0。0。1:9222”)

class Crack():

def __init__(self):

self。url = ‘http://localhost:5000/’

self。driver = webdriver。Chrome(options=chrome_options)

self。wait = WebDriverWait(self。driver, 10)

self。driver。get(self。url)

def get_slider(self):

“”“

獲取滑塊

:return: 滑塊物件

”“”

while True:

try:

slider = self。driver。find_element_by_xpath(“//div[@class=‘gt_slider_knob gt_show’]”)

break

except:

time。sleep(0。5)

return slider

def slide(self):

self。driver。get(“http://localhost:5000/”)

# 找到滑動的圓球

slide = self。get_slider()

# 第一步,點選元素並按住不放

ActionChains(self。driver)。click_and_hold(slide)。perform()

time。sleep(1)

# 第二步,拖動元素

# 拖動滑鼠到指定的位置,注意這裡位置是相對於元素左上角的相對值

ActionChains(self。driver)。move_to_element_with_offset(slide,xoffset=100, yoffset=50)。perform()

time。sleep(1)

# 釋放滑鼠

ActionChains(self。driver)。release(on_element=slide)。perform()

if __name__ == ‘__main__’:

crack = Crack()

crack。slide()

在這我簡單得對滑塊進行一個定點距離的滑動,那麼我們知道,如果滑塊可以成功拖動到缺口處我們的任務就成功一大半了,怎麼識別缺口呢?來分析下html

滑動驗證碼模擬操作

滑動驗證碼模擬操作

我們發現目標圖片是由1張圖片根據不一樣的位置擺放來拼合而成的,而且每張拼合的小圖片是10*58畫素,一共2排,先來看看這張被拼接的背景圖片是怎麼樣的,

http://

static。geetest。com/pict

ures/gt/579066de6/579066de6。webp

滑動驗證碼模擬操作

我去,被打亂了,我們看到的圖是這樣的

滑動驗證碼模擬操作

也就是說我們要先把打亂的這張圖變成下面這張正常的圖,實際上,另外一張圖才是我們需要的,就是帶缺口的原圖,因為缺口可以讓我們後期求出滑塊應該滑動的距離

滑動驗證碼模擬操作

# 帶缺口的背景圖

crack。wait。until(lambda the_driver: the_driver。find_element_by_xpath(“//div[@class=‘gt_cut_bg gt_show’]”)。is_displayed())

img_target = crack。get_image(“//div[@class=‘gt_cut_bg gt_show’]/div[@class=‘gt_cut_bg_slice’]”)

img_target。save(‘target_bg。jpg’)

這裡要等待圖片載入以後才操作,要注意的是這裡是2張圖片,一張是原圖,一張是帶缺口的原圖,我們需要的是帶缺口的原圖,xpath對應的class也要對應上,成功的話我們將圖片儲存下來,圖片長這個樣子

滑動驗證碼模擬操作

具體演算法先上程式碼

def get_img_item(self,item):

# 在html裡面解析出小圖片的url地址,還有長高的數值

attr = item。get_attribute(‘style’)

pattern = “background-image: url\(\”(。*)\“\); background-position: (。*)px (。*)px;”

res = re。findall(pattern,attr)

imageurl = res[0][0]

return {

‘x’: int(res[0][1]),

‘y’: int(res[0][2]),

‘imageurl’: imageurl

}

def get_merge_image(self,filename,location_list):

‘’‘

根據位置對圖片進行合併還原

:filename:圖片

:location_list:圖片位置

’‘’

# 二進位制讀取方式

im = Image。open(filename)

# im。show()

im_list_upper=[]

im_list_down=[]

for item in location_list:

# y只有2個值 因為圖片是由 2排圖片組合成的

‘’‘

x的序列 1 13 25 37 49 。。301

y -58

’‘’

if item[‘y’]==-58:

crop_item = im。crop((abs(item[‘x’]),58,abs(item[‘x’])+10,116))

im_list_upper。append(crop_item)

if item[‘y’]==0:

crop_item = im。crop((abs(item[‘x’]),0,abs(item[‘x’])+10,58))

im_list_down。append(crop_item)

# 正確拼圖大小為260*116

new_im = Image。new(‘RGB’, (260,116))

# 從0開始往右拼圖

x_offset = 0

for im in im_list_upper:

new_im。paste(im, (x_offset,0))

x_offset += im。size[0]

x_offset = 0

for im in im_list_down:

new_im。paste(im, (x_offset,58))

x_offset += im。size[0]

return new_im

def get_image(self,xpath_select_str):

# 找到圖片所在的div

background_images = self。driver。find_elements_by_xpath(xpath_select_str)

location_list=[]

location_list = [self。get_img_item(item) for item in background_images]

imageurl = location_list[0][‘imageurl’]。replace(“webp”,“jpg”)

# 向記憶體中寫入圖片二進位制流

jpgfile = io。BytesIO(urllib。request。urlopen(imageurl)。read())

# 重新合併圖片

image = self。get_merge_image(jpgfile,location_list)

return image

來看看思路

1 找到圖片以後將其改成jpg格式的地址,然後寫進記憶體中,並且解析其拆分圖片的位置

2 將組成原圖的圖片分為上下2組進行裁剪,裁剪的位置即是第一步獲取的位置資訊

3 將2組裁剪的圖全部依次複製到一張大圖上,拼完圖即是我們想要的圖片

這裡考察的是對圖片庫PIL的一些操作,主要是裁剪跟貼上。帶缺口的原圖我們獲取到了,下面我們還需要拿到滑塊的圖片,滑塊圖片獲取就簡單得多,獲取對應屬性進行儲存即可

def save_slide_ball(self):

# 找到圖片所在的div

background_images = self。driver。find_elements_by_xpath(“//div[@class=‘gt_slice gt_show’]”)

if len(background_images) > 0:

attr = background_images[0]。get_attribute(‘style’)

pattern = “background-image: url\(\”(。*)\“\);”

res = re。findall(pattern,attr)

pngfile = io。BytesIO(urllib。request。urlopen(res[0])。read())

im = Image。open(pngfile)

im。save(‘ball。png’)

return True

return False

滑動驗證碼模擬操作

有了帶缺口的原圖和缺塊圖片,我們怎麼知道缺塊在原圖的位置呢?這裡就要用到opencv了,處理如下

def find_pic_loc(self, target, template):

‘’‘

找出影象中最佳匹配位置

target: 目標即背景圖

template: 模板即需要找到的圖

:return: 返回最佳匹配及其最差匹配和對應的座標

使用cv2庫,先讀取背景圖,然後夜視化處理(消除噪點),然後讀取模板圖片,

使用cv2自帶圖片識別找到模板在背景圖中的位置,使用minMaxLoc提取出最佳匹配的最大值和最小值,

返回一個數組形如(-0。3,0。95,(121,54),(45,543))元組四個元素,分別是最小匹配機率、最大匹配機率,

最小匹配機率對應座標,最大匹配機率對應座標。

我們需要的是最大匹配機率座標,對應的分別是x和y座標,但是這個不一定,有些時候可能是最小匹配機率座標,

最好是根據機率的絕對值大小來比較。

’‘’

target_rgb = cv2。imread(target)

target_gray = cv2。cvtColor(target_rgb, cv2。COLOR_BGR2GRAY)

template_rgb = cv2。imread(template, 0)

res = cv2。matchTemplate(target_gray, template_rgb, cv2。TM_CCOEFF_NORMED)

value = cv2。minMaxLoc(res)

# 這裡測試最準確的值是第[2]個的x座標

return value[2][0]

# 獲取缺塊在原圖中的位置

pic_loc = crack。find_pic_loc(‘target_bg。jpg’,‘miss_block。png’)

因為我對opencv並沒有深入瞭解,這段程式碼對我如同黑箱一般,等我學了opencv再來解釋吧,感謝

提供的程式碼。這裡貼一下opencv的安裝方法,如果用的是anaconda,可以按下面的安裝

conda install -c conda-forge opencv

conda install -c conda-forge/label/broken opencv

pip install opencv-python

我們根據返回左邊來進行移動,我自己測試的結果成功率相當高,但是還差半個缺塊,這是為什麼呢? 因為返回的是缺口最左邊的座標,需要多滑半個缺塊的距離,大約22左右 。 大家可以試試。測試程式碼如下,結合上文所有為止。

from selenium import webdriver

from selenium。webdriver。chrome。options import Options

from selenium。webdriver。support import expected_conditions as EC

from selenium。webdriver。support。ui import WebDriverWait

from selenium。webdriver。common。action_chains import ActionChains

from selenium。webdriver。common。by import By

import time

import re

import io

import urllib。request

import cv2

import os

from PIL import Image

chrome_options = Options()

chrome_options。add_experimental_option(“debuggerAddress”, “127。0。0。1:9222”)

class Crack():

def __init__(self):

self。url = ‘http://localhost:5000/’

self。driver = webdriver。Chrome(options=chrome_options)

self。wait = WebDriverWait(self。driver, 10)

self。driver。get(self。url)

def get_slider(self):

“”“

獲取滑塊

:return: 滑塊物件

”“”

while True:

try:

slider = self。driver。find_element_by_xpath(“//div[@class=‘gt_slider_knob gt_show’]”)

break

except:

time。sleep(0。5)

return slider

def get_img_item(self,item):

# 在html裡面解析出小圖片的url地址,還有長高的數值

attr = item。get_attribute(‘style’)

pattern = “background-image: url\(\”(。*)\“\); background-position: (。*)px (。*)px;”

res = re。findall(pattern,attr)

imageurl = res[0][0]

return {

‘x’: int(res[0][1]),

‘y’: int(res[0][2]),

‘imageurl’: imageurl

}

def get_merge_image(self,filename,location_list):

‘’‘

根據位置對圖片進行合併還原

:filename:圖片

:location_list:圖片位置

’‘’

# 二進位制讀取方式

im = Image。open(filename)

# im。show()

im_list_upper=[]

im_list_down=[]

for item in location_list:

# y只有2個值 因為圖片是由 2排圖片組合成的

‘’‘

x的序列 1 13 25 37 49 。。301

y -58

’‘’

if item[‘y’]==-58:

crop_item = im。crop((abs(item[‘x’]),58,abs(item[‘x’])+10,116))

im_list_upper。append(crop_item)

if item[‘y’]==0:

crop_item = im。crop((abs(item[‘x’]),0,abs(item[‘x’])+10,58))

im_list_down。append(crop_item)

# 正確拼圖大小為260*116

new_im = Image。new(‘RGB’, (260,116))

# 從0開始往右拼圖

x_offset = 0

for im in im_list_upper:

new_im。paste(im, (x_offset,0))

x_offset += im。size[0]

x_offset = 0

for im in im_list_down:

new_im。paste(im, (x_offset,58))

x_offset += im。size[0]

return new_im

def get_image(self,xpath_select_str):

# 找到圖片所在的div

background_images = self。driver。find_elements_by_xpath(xpath_select_str)

location_list=[]

location_list = [self。get_img_item(item) for item in background_images]

imageurl = location_list[0][‘imageurl’]。replace(“webp”,“jpg”)

# 向記憶體中寫入圖片二進位制流

jpgfile = io。BytesIO(urllib。request。urlopen(imageurl)。read())

# 重新合併圖片

image = self。get_merge_image(jpgfile,location_list)

return image

def slide(self, xoffset):

# 找到滑動的圓球

slide = self。get_slider()

# 第一步,點選元素並按住不放

ActionChains(self。driver)。click_and_hold(slide)。perform()

time。sleep(1)

# 第二步,拖動元素

# 拖動滑鼠到指定的位置,注意這裡位置是相對於元素左上角的相對值

ActionChains(self。driver)。move_to_element_with_offset(slide,xoffset=xoffset, yoffset=50)。perform()

time。sleep(1)

# 釋放滑鼠

ActionChains(self。driver)。release(on_element=slide)。perform()

def save_miss_block(self):

# 找到圖片所在的div

background_images = self。driver。find_elements_by_xpath(“//div[@class=‘gt_slice gt_show’]”)

if len(background_images) > 0:

attr = background_images[0]。get_attribute(‘style’)

pattern = “background-image: url\(\”(。*)\“\);”

res = re。findall(pattern,attr)

pngfile = io。BytesIO(urllib。request。urlopen(res[0])。read())

im = Image。open(pngfile)

im。save(‘miss_block。png’)

return True

return False

def find_pic_loc(self, target, template):

‘’‘

找出影象中最佳匹配位置

target: 目標即背景圖

template: 模板即需要找到的圖

:return: 返回最佳匹配及其最差匹配和對應的座標

使用cv2庫,先讀取背景圖,然後夜視化處理(消除噪點),然後讀取模板圖片,

使用cv2自帶圖片識別找到模板在背景圖中的位置,使用minMaxLoc提取出最佳匹配的最大值和最小值,

返回一個數組形如(-0。3,0。95,(121,54),(45,543))元組四個元素,分別是最小匹配機率、最大匹配機率,

最小匹配機率對應座標,最大匹配機率對應座標。

我們需要的是最大匹配機率座標,對應的分別是x和y座標,但是這個不一定,有些時候可能是最小匹配機率座標,

最好是根據機率的絕對值大小來比較。

滑塊驗證較為核心的兩步,第一步是找出缺口距離,第二步是生成軌跡並滑動,較為複雜的情況下還要考慮初始模板圖片在背景圖中的座標,以及模板圖片透明邊緣的寬度,這些都是影響軌跡的因素。

’‘’

target_rgb = cv2。imread(target)

target_gray = cv2。cvtColor(target_rgb, cv2。COLOR_BGR2GRAY)

template_rgb = cv2。imread(template, 0)

res = cv2。matchTemplate(target_gray, template_rgb, cv2。TM_CCOEFF_NORMED)

value = cv2。minMaxLoc(res)

# 這裡測試最準確的值是第[2]個的x座標

return value[2][0]

if __name__ == ‘__main__’:

crack = Crack()

# 帶缺口的背景圖

crack。wait。until(lambda the_driver: the_driver。find_element_by_xpath(“//div[@class=‘gt_cut_bg gt_show’]”)。is_displayed())

img_target = crack。get_image(“//div[@class=‘gt_cut_bg gt_show’]/div[@class=‘gt_cut_bg_slice’]”)

img_target。save(‘target_bg。jpg’)

# 儲存缺塊圖片

crack。save_miss_block()

# 獲取缺塊在原圖中的位置

pic_loc = crack。find_pic_loc(‘target_bg。jpg’,‘miss_block。png’)

crack。slide(pic_loc+22)

# 刪除圖片

os。remove(‘miss_block。png’)

os。remove(‘target_bg。jpg’)

以上成功執行我們會看到滑塊能夠正確的得移動到缺塊的位置,結果是被吃掉了,當然了,一瞬間到達目標,我們需要更擬人化的操作,一般人滑動的速度是先快後慢,也就是勻加速接著快到達目標勻減速

def get_track(self, distance):

“”“

拿到移動軌跡,模仿人的滑動行為,先勻加速後均減速

勻變速運動基本公式:

①:v=v0+at

②:s=v0t+½at²

③:v²-v0²=2as

根據偏移量獲取移動軌跡

:param distance: 偏移量

:return: 移動軌跡

”“”

# 移動軌跡

track = []

# 當前位移

current = 0

# 減速閾值

mid = distance * 4 / 5

# 計算間隔

t = 0。2

# 初速度

v = 0

while current < distance:

if current < mid:

# 加速度為正

a = 6

else:

# 加速度為負

a = -7

# 初速度v0

v0 = v

# 當前速度v = v0 + at

v = v0 + a * t

# 移動距離x = v0t + 1/2 * a * t^2

move = v0 * t + 1 / 2 * a * t * t

# 當前位移

current += move

# 加入軌跡

track。append(round(move))

return track

以此得到運動軌跡序列,

def move_to_gap(self, slider, track, distance):

“”“

拖動滑塊到缺口處

:param slider: 滑塊

:param track: 軌跡

:return:

”“”

# 找到滑動的圓球 點選元素

ActionChains(self。driver)。click_and_hold(slider)。perform()

left_step = reduce(lambda x,y:x+y, track) - distance

while track:

x = random。choice(track)

ActionChains(self。driver)。move_by_offset(xoffset=x, yoffset=0)。perform()

track。remove(x)

time。sleep(random。randint(6, 10) / 2000)

time。sleep(0。5)

‘’‘

序列完了之後可能還會多出幾步,我們把多出的幾步往回走

’‘’

if left_step >0:

for i in range(1,left_step+1):

ActionChains(self。driver)。move_by_offset(xoffset=-1, yoffset=0)。perform()

time。sleep(random。randint(6, 10) / 100)

ActionChains(self。driver)。release()。perform()

將運動軌跡隨機打在移動操作上,完了還加上隨機的停頓時間。筆者親自測試,成功率嘛,55開,並不是很高,加速度為7的時候成功率相對比較高。等有空了,再來最佳化這個軌跡演算法。參考程式碼