一雏节、前言
亂序拼圖驗(yàn)證是一種較少見(jiàn)的驗(yàn)證碼防御醇坝,市面上更多的是拖動(dòng)滑塊邑跪,被完美攻克的有不少,都在行為軌跡上下足了功夫呼猪,本文不討論軌跡模擬范疇画畅,就只針對(duì)拼圖還原進(jìn)行研究。
找一個(gè)市面比較普及的頂像亂序拼圖進(jìn)行驗(yàn)證宋距,它號(hào)稱的防御能力4星轴踱,用戶體驗(yàn)3星,通過(guò)研究發(fā)現(xiàn)谚赎,它的還原程度相當(dāng)高淫僻,思路也很簡(jiǎn)單,下面一步步的講解還原過(guò)程壶唤。
二雳灵、環(huán)境準(zhǔn)備
1.依賴
- 采集模擬 selenium
- 特征匹配 python+opencv
2.安裝環(huán)境
pip install setuptools
pip install selenium
pip install numpy Matplotlib
pip install opencv-python
3.chormedriver 下載
找到對(duì)應(yīng)瀏覽器版本+系統(tǒng)平臺(tái)的driver后,macOS 建議存放到** /usr/local/bin**
wget https://npm.taobao.org/mirrors/chromedriver/95.0.4638.69/chromedriver_mac64.zip
三闸盔、采集樣本
引入依賴庫(kù)悯辙,使用 webdriver 打開(kāi)官方網(wǎng)站的產(chǎn)品演示頁(yè)面
import os
import cv2
import time
import urllib.request
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
創(chuàng)建下載樣本的代碼,主要流程是打開(kāi)官網(wǎng)的demo頁(yè)后迎吵,截圖并保存
# 采集代碼
class CrackPuzzleCaptcha():
# 初始化webdriver
def init(self):
self.url = 'https://www.dingxiang-inc.com/business/captcha'
chrome_options = webdriver.ChromeOptions()
# chrome_options.add_argument("--start-maximized")
chrome_options.add_experimental_option("excludeSwitches", ["ignore-certificate-errors","enable-automation"]) # 設(shè)置為開(kāi)發(fā)者模式
path = r'/usr/local/bin/chromedriver' #macOS
# path = r'D:\Anaconda3\chromedriver.exe' #windows
self.browser = webdriver.Chrome(executable_path=path,chrome_options=chrome_options)
#設(shè)置顯示等待時(shí)間
self.wait = WebDriverWait(self.browser, 20)
self.browser.get(self.url)
# 打開(kāi)驗(yàn)證碼demo頁(yè)面躲撰,并強(qiáng)制元素在瀏覽器可視區(qū)域
def openTest(self):
time.sleep(1)
self.browser.execute_script('setTimeout(function(){document.querySelector("body > div.wrapper-main > div.wrapper.wrapper-content > div > div.captcha-intro > div.captcha-intro-header > div > div > ul > li.item-8").click();},0)')
self.browser.execute_script('setTimeout(function(){document.querySelector("body > div.wrapper-main > div.wrapper.wrapper-content > div > div.captcha-intro > div.captcha-intro-body > div > div.captcha-intro-demo").scrollIntoView();},0)')
time.sleep(1)
# 找到原圖,webp格式钓觉,直接下載保存
def download(self):
onebtn = self.browser.find_element_by_css_selector('#dx_captcha_oneclick_bar-logo_2 > span')
ActionChains(self.browser).move_to_element(onebtn).perform()
time.sleep(1)
#下載webp
img_url = self.browser.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-left_3 > img').get_attribute("src")
img_address = "test.png" # 樣本文件
response = urllib.request.urlopen(img_url)
img = response.read()
with open(img_address, 'wb') as f:
f.write(img)
print('已保存', img_address)
return self.browser
def crack(self):
pass
開(kāi)始采集
crack = CrackPuzzleCaptcha()
crack.init()
crack.openTest()
browser2 = crack.download()
已保存 test.png
四茴肥、調(diào)研結(jié)果
- 關(guān)鍵1:顯示的拼圖的原圖就是已經(jīng)亂序的狀態(tài)
- 關(guān)鍵2:原圖是一個(gè)整體,那么獲取原圖切割并編號(hào)荡灾,能得到與拼圖過(guò)程一致的結(jié)果
- 關(guān)鍵3:拼圖只需要做1次換位即可,2x2的矩陣,可以對(duì)[1,2,3,4]進(jìn)行排列組合批幌,得到所有的拼接結(jié)果
五础锐、分析過(guò)程
1.輔助函數(shù)
定義輔助函數(shù),方便獲取參數(shù)
# 顯示圖形
def show_images(images: list , title = '') -> None:
if title!='':
print(title)
n: int = len(images)
f = plt.figure()
for i in range(n):
f.add_subplot(1, n, i + 1)
plt.imshow(images[I])
plt.show(block=True)
# 獲取圖像的基本信息
def getSize(p):
sum_rows = p.shape[0]
sum_cols = p.shape[1]
channels = p.shape[2]
return sum_rows,sum_cols,channels
2.圖像切割
# 輸入樣本
file = 'test.png'
img = cv2.imread(file)
sum_rows,sum_cols,channels = getSize(img)
part_rows,part_cols = round(sum_rows/2),round(sum_cols/2)
print('樣本圖 高度荧缘、寬度皆警、通道',sum_rows,sum_cols,channels)
print('四圖切分,求原圖中心位置',part_rows,part_cols)
part1 = img[0:part_rows, 0:part_cols]
part2 = img[0:part_rows, part_cols:sum_cols]
part3 = img[part_rows:sum_rows, 0:part_cols]
part4 = img[part_rows:sum_rows, part_cols:sum_cols]
print('切割為4個(gè)小塊的 W/H/C 信息截粗,并四圖編號(hào):左上=1信姓,右上=2,左下=3绸罗,右下=4\n',getSize(part1),getSize(part2),getSize(part3),getSize(part4))
show_images([img],'原圖')
show_images([part1,part2],'切割圖')
show_images([part3,part4])
輸出:
樣本圖 高度意推、寬度、通道 150 300 3
四圖切分珊蟀,求原圖中心位置 75 150
切割為4個(gè)小塊的 W/H/C 信息菊值,并四圖編號(hào):左上=1,右上=2育灸,左下=3腻窒,右下=4
(75, 150, 3) (75, 150, 3) (75, 150, 3) (75, 150, 3)
原圖
切割圖
完成切割后,還需要重組合并4個(gè)圖像磅崭,用于匹配最佳結(jié)果
3.圖像拼接
# 拼接函數(shù)
def merge(sum_rows,sum_cols,channels,p1,p2,p3,p4):
final_matrix = np.zeros((sum_rows, sum_cols,channels), np.uint8)
part_rows,part_cols = round(sum_rows/2),round(sum_cols/2)
final_matrix[0:part_rows, 0:part_cols] = p1
final_matrix[0:part_rows, part_cols:sum_cols] = p2
final_matrix[part_rows:sum_rows, 0:part_cols] = p3
final_matrix[part_rows:sum_rows, part_cols:sum_cols] = p4
return final_matrix
從編號(hào)上來(lái)看儿子,應(yīng)該將 [1,2,3,4] 還原成 [4,2,3,1] 就是正確的圖,測(cè)試下還原效果
# 還原圖
f = merge(sum_rows,sum_cols,channels,part4,part2,part3,part1)
show_images([f],'還原圖 [4,2,3,1]')
還原圖 [4,2,3,1]
4.排列組合
已知 python 實(shí)現(xiàn)排列組合非常方便砸喻,測(cè)試代碼如下
import itertools
# 對(duì)應(yīng)拼圖的4個(gè)塊的編號(hào)
puzzle_list = [
"1:左上","2:右下",
"3:左下","4:右下"
]
result = itertools.permutations(puzzle_list,4)
cnt=0
for x in result:
cnt+=1
print(x)
print('共',cnt,'種組合')
輸出:
('1:左上', '2:右下', '3:左下', '4:右下')
('1:左上', '2:右下', '4:右下', '3:左下')
('1:左上', '3:左下', '2:右下', '4:右下')
('1:左上', '3:左下', '4:右下', '2:右下')
('1:左上', '4:右下', '2:右下', '3:左下')
('1:左上', '4:右下', '3:左下', '2:右下')
('2:右下', '1:左上', '3:左下', '4:右下')
('2:右下', '1:左上', '4:右下', '3:左下')
('2:右下', '3:左下', '1:左上', '4:右下')
('2:右下', '3:左下', '4:右下', '1:左上')
('2:右下', '4:右下', '1:左上', '3:左下')
('2:右下', '4:右下', '3:左下', '1:左上')
('3:左下', '1:左上', '2:右下', '4:右下')
('3:左下', '1:左上', '4:右下', '2:右下')
('3:左下', '2:右下', '1:左上', '4:右下')
('3:左下', '2:右下', '4:右下', '1:左上')
('3:左下', '4:右下', '1:左上', '2:右下')
('3:左下', '4:右下', '2:右下', '1:左上')
('4:右下', '1:左上', '2:右下', '3:左下')
('4:右下', '1:左上', '3:左下', '2:右下')
('4:右下', '2:右下', '1:左上', '3:左下')
('4:右下', '2:右下', '3:左下', '1:左上')
('4:右下', '3:左下', '1:左上', '2:右下')
('4:右下', '3:左下', '2:右下', '1:左上')
共 24 種組合
5.特征提取
采用 merge 函數(shù)柔逼,對(duì)切割的小圖進(jìn)行組合還原后,轉(zhuǎn)換為灰度圖并提取輪廓恩够。
# 還原圖
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
show_images([f],'還原圖[1,2,3,4]')
# 灰度
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
show_images([gray],'灰度')
# 提取輪廓
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取輪廓')
還原圖[1,2,3,4]
灰度
提取輪廓
再測(cè)試一種新的組合卒落,對(duì)比輪廓特征[1,3,2,4]和原始的輪廓特征[4,2,3,1]
f = merge(sum_rows,sum_cols,channels,part1,part3,part2,part4)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取輪廓')
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取輪廓')
# 正確的
f = merge(sum_rows,sum_cols,channels,part4,part2,part3,part1)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'正確的-提取輪廓')
提取輪廓
提取輪廓
正確的-提取輪廓
通過(guò)提取輪廓,可以看到拼接結(jié)果的明顯的線條蜂桶,錯(cuò)誤的圖至少存在一條x軸或y軸的線儡毕,而拼接成功的基本沒(méi)有(線段位置或長(zhǎng)度及線條數(shù)量可以決定正確率,需要多調(diào)整參數(shù)并篩選)扑媚。
這是因?yàn)樵瓐D有明顯的過(guò)渡色腰湾,它是為了用戶體驗(yàn)而設(shè)計(jì),方便人們使用它的時(shí)候疆股,能夠‘容易’的區(qū)分费坊,并找出正確的拼圖位置。
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
show_images([f],'背景漸變色')
show_images([part3,part2,part1,part4],'切割后')
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
lf = f.copy()
cv2.line(lf, (0, 75), (300, 75), (0, 0, 255), 2)
cv2.line(lf, (150, 0), (150, 150), (0, 0, 255), 2)
show_images([lf],'亂序旬痹,漸變色成為了‘十字’特征線')
背景漸變色
切割后
亂序附井,漸變色成為了‘十字’特征線
6.特征匹配
特征已知后讨越,現(xiàn)在剩下的就是對(duì)特征進(jìn)行檢測(cè),可以計(jì)算 x/2,y/2 十字架的色差永毅,也可以用 opencv 的直線提取把跨,測(cè)試代碼如下:
f = merge(sum_rows,sum_cols,channels,part1,part2,part3,part4)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取輪廓')
lines = cv2.HoughLinesP(edges,0.01,np.pi/360,60,minLineLength=50,maxLineGap=10)
if lines is None:
print('沒(méi)找到線條')
else:
lf = f.copy()
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(lf, (x1, y1), (x2, y2), (0, 0, 255), 2)
show_images([lf])
提取輪廓
嘗試正確的組合 [4,2,3,1]
f = merge(sum_rows,sum_cols,channels,part4,part2,part3,part1)
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
show_images([edges],'提取輪廓')
lines = cv2.HoughLinesP(edges,0.01,np.pi/360,60,minLineLength=50,maxLineGap=10)
if lines is None:
print('沒(méi)找到線條')
else:
lf = f.copy()
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(lf, (x1, y1), (x2, y2), (0, 0, 255), 2)
show_images([lf])
提取輪廓
沒(méi)找到線條
7.匹配過(guò)程
import itertools
print('原圖順序')
print(1,2)
print(3,4)
show_images([img])
# 按編號(hào),將切割的圖放入list做排列組合
list1 = [
[1,part1],
[2,part2],
[3,part3],
[4,part4]
]
result = itertools.permutations(list1,4)
idx =1
finded = False
finalResult = []
for x in result:
# 排列組合合并圖像
f = merge(sum_rows,sum_cols,channels,x[0][1],x[1][1],x[2][1],x[3][1])
# 圖像特征提取
gray = cv2.cvtColor(f, cv2.COLOR_BGRA2GRAY)
edges = cv2.Canny(gray, 35, 80, apertureSize=3)
# 直線匹配
lines = cv2.HoughLinesP(edges,0.01,np.pi/360,60,minLineLength=50,maxLineGap=10)
if lines is None:
print('還原圖像')
show_images([f])
show_images([gray])
show_images([edges])
print('正確順序')
print(x[0][0],x[1][0])
print(x[2][0],x[3][0])
print('完成!!')
finded = True
finalResult =[x[0][0],x[1][0],x[2][0],x[3][0]] #獲取最終排列正確的結(jié)果
break
else:
print(idx, '排列:' , x[0][0],x[1][0],x[2][0],x[3][0] , '線:', len(lines))
lf = f.copy()
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(lf, (x1, y1), (x2, y2), (0, 0, 255), 2)
# show_images([lf])
pass
idx+=1
print('測(cè)試次數(shù)',idx,'最終狀態(tài)',finded,finalResult)
輸出:
原圖順序
1 2
3 4
1 排列: 1 2 3 4 線: 4
2 排列: 1 2 4 3 線: 5
3 排列: 1 3 2 4 線: 4
4 排列: 1 3 4 2 線: 2
5 排列: 1 4 2 3 線: 3
6 排列: 1 4 3 2 線: 4
7 排列: 2 1 3 4 線: 3
8 排列: 2 1 4 3 線: 5
9 排列: 2 3 1 4 線: 3
10 排列: 2 3 4 1 線: 3
11 排列: 2 4 1 3 線: 1
12 排列: 2 4 3 1 線: 1
13 排列: 3 1 2 4 線: 2
14 排列: 3 1 4 2 線: 2
15 排列: 3 2 1 4 線: 3
16 排列: 3 2 4 1 線: 3
17 排列: 3 4 1 2 線: 5
18 排列: 3 4 2 1 線: 3
19 排列: 4 1 2 3 線: 4
20 排列: 4 1 3 2 線: 3
21 排列: 4 2 1 3 線: 2
還原圖像
正確順序
4 2
3 1
完成!
測(cè)試次數(shù) 22 最終狀態(tài) True [4, 2, 3, 1]
8.提取結(jié)果
再看看如何這種拼圖沼死,如果要交換位置的組合有12種
list1 = [1,2,3,4]
result = itertools.permutations(list1,2)
idx=0
for x in result:
idx+=1
print(idx,x)
輸出:
1 (1, 2)
2 (1, 3)
3 (1, 4)
4 (2, 1)
5 (2, 3)
6 (2, 4)
7 (3, 1)
8 (3, 2)
9 (3, 4)
10 (4, 1)
11 (4, 2)
12 (4, 3)
交換函數(shù)
#交換函數(shù)
def change_check(a,b):
diffs = []
if len(a)!=len(b):
return diffs
for i in range(len(a)):
if a[i]!=b[I]:
diffs.append(b[I])
return diffs
ab = change_check([1,2,3,4],finalResult)
print('原始',[1,2,3,4])
print('最終',finalResult)
print('要交換的位置',ab)
輸出:
原始 [1, 2, 3, 4]
最終 [4, 2, 3, 1]
要交換的位置 [4, 1]
將‘交換的位置’換算成小圖中心的偏移坐標(biāo)着逐,采用查表法
#大圖尺寸
pwidth = 150
pheight = 75
#小圖xy中心點(diǎn) = 大圖wh 1/4
px = round(pwidth/2)
py = round(pheight/2)
#創(chuàng)建坐標(biāo)表
offset_points = [
[px,py],[px+pwidth,py],
[px,py+pheight],[px+pwidth,py+pheight]
]
print(offset_points)
print(ab)
#通過(guò)結(jié)果作為索引,拿到坐標(biāo)表索引的坐標(biāo)
drag_start = offset_points[ ab[0] -1 ]
drag_end = offset_points[ ab[1] -1 ]
print('起點(diǎn)偏移坐標(biāo)',drag_start,'終點(diǎn)偏移坐標(biāo)',drag_end)
輸出:
[[75, 38], [225, 38], [75, 113], [225, 113]]
[4, 1]
起點(diǎn)偏移坐標(biāo) [225, 113] 終點(diǎn)偏移坐標(biāo) [75, 38]
9.模擬操作
至此意蛀,已經(jīng)完成了拼圖還原的分析所有過(guò)程耸别,下面采用另一種簡(jiǎn)單的方法,move_to_element 方法县钥,內(nèi)置的拖動(dòng) dom-a 到 dom-b 位置秀姐,測(cè)試下結(jié)果
# 模擬聚焦按鈕,讓拼圖顯示出來(lái)
onebtn = browser2.find_element_by_css_selector('#dx_captcha_oneclick_bar-logo_2 > span')
ActionChains(browser2).move_to_element(onebtn).perform()
time.sleep(1)
獲取最終結(jié)果
ab = change_check([1,2,3,4],finalResult)
print(ab)
輸出
[4, 1]
找到網(wǎng)頁(yè)拼圖的dom元素魁蒜,存儲(chǔ)下來(lái)用于操作并交換拼圖
d1 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-left_3 > div')
d2 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-right_3 > div')
d3 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-left_3 > div')
d4 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-right_3 > div')
drag_elements = [d1,d2,d3,d4]
找出要拖動(dòng)的2個(gè)dom囊扳,并交付給 webdriver
drag_start = drag_elements[ ab[0] -1 ]
drag_end = drag_elements[ ab[1] -1 ]
print('drag_start',drag_start, 'drag_end',drag_end)
輸出
drag_start <selenium.webdriver.remote.webelement.WebElement (session="1d7d691bd509cd03cd8b1483da2056ea", element="8439005e-eb70-4b02-856e-eebbe2526d6d")>
drag_end <selenium.webdriver.remote.webelement.WebElement (session="1d7d691bd509cd03cd8b1483da2056ea", element="f9239df5-9aa3-43ae-a6af-afacf81eb670")>
執(zhí)行動(dòng)作
ActionChains(browser2).drag_and_drop(drag_start,drag_end).perform()
# browser2.close()
簡(jiǎn)單拖一下,目標(biāo)網(wǎng)站認(rèn)可了兜看,但它判定是有問(wèn)題的锥咸,又彈出一種新的驗(yàn)證碼出來(lái),看來(lái)僅僅能夠識(shí)別還原正確拼圖還只是開(kāi)端细移,如何偽造一個(gè)讓其認(rèn)可的運(yùn)行環(huán)境搏予,又是一個(gè)新的技術(shù)研究領(lǐng)域,值得與各位共同學(xué)習(xí)與分享交流弧轧。
六雪侥、終
邊學(xué)邊做,如有錯(cuò)誤之處敬請(qǐng)指出精绎,謝謝速缨!
項(xiàng)目地址:https://github.com/suifei/puzzle-captcha