滑動驗證碼模擬操作
滑動驗證流行於現在各大網站,破解難度也變得沒那麼容易了。破解一般有兩種思路,一是對請求進行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的時候成功率相對比較高。等有空了,再來最佳化這個軌跡演算法。參考程式碼