乱序拼图验证的识别并还原-puzzle-captcha

乱序拼图验证的识别并还原-puzzle-captcha

前言

乱序拼图验证是一种较少见的验证码防御市面上更多的是拖动滑块被完美攻克的有不少都在行为轨迹上下足了功夫本文不讨论轨迹模拟范畴就只针对拼图还原进行研究

找一个市面比较普及的顶像乱序拼图进行验证它号称的防御能力 4 用户体验 3 通过研究发现它的还原程度相当高思路也很简单下面一步步的讲解还原过程

环境准备

1. 依赖

  • 采集模拟 selenium
  • 特征匹配 python+opencv

2. 安装环境

!pip install setuptools!pip install selenium!pip install numpy Matplotlib!pip install opencv-python

3.chormedriver 下载

找到对应浏览器版本+系统平台 driver macOS 建议存放到 /usr/local/bin

!wget https://npm.taobao.org/mirrors/chromedriver/95.0.4638.69/chromedriver_mac64.zip

采集样本

引入依赖库使用 webdriver 打开官方网站的产品演示页面

import osimport cv2import timeimport urllib.requestimport matplotlib.pyplot as pltimport numpy as npfrom PIL import Imagefrom selenium import webdriverfrom selenium.webdriver import ActionChainsfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWait

创建下载样本的代码主要流程是打开官网 demo 页后截图保存

# 采集代码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"]) # 设置开发者模式        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)        #设置显示等待时间        self.wait = WebDriverWait(self.browser, 20)        self.browser.get(self.url)    # 打开验证码 demo 页面,并强制元素在浏览器可视区域    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.webp" # 样本文件        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

开始采集

crack = CrackPuzzleCaptcha()crack.init()crack.openTest()
browser2 = crack.download()

已保存 test.webp

调研结果

  • 关键 1: 显示的拼图的原图就是已经乱序的状态
  • 关键 2: 原图是一个整体那么获取原图切割并编号能得到与拼图过程一致的结果
  • 关键 3: 拼图只需要做 1 次换位即可2x2 的矩阵可以对 [1,2,3,4] 进行排列组合得到所有的拼接结果

分析过程

1. 辅助函数

定义辅助函数方便获取参数

# 显示图形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.webp'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 个小块的 W/H/C 信息,并四图编号:左上=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 个小块的 W/H/C 信息,并四图编号:左上=1,右上=2,左下=3,右下=4(75, 150, 3) (75, 150, 3) (75, 150, 3) (75, 150, 3)

原图

切割图

完成切割后还需要重组合并 4 个图像用于匹配最佳结果

3. 图像拼接

# 拼接函数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

从编号上来看应该将 [1,2,3,4] 还原成 [4,2,3,1] 就是正确的图测试下还原效果

# 还原图 f = merge(sum_rows,sum_cols,channels,part4,part2,part3,part1)show_images([f],' 还原图 [4,2,3,1]')

还原图 [4,2,3,1]

4. 排列组合

已知 python 实现排列组合非常方便测试代码如下

import itertools# 对应拼图的 4 个块的编号puzzle_list = [    "1: 左上 ","2: 右下 ",    "3: 左下 ","4: 右下 "]result = itertools.permutations(puzzle_list,4)cnt=0for 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 函数对切割的小图进行组合还原转换灰度图提取轮廓

# 还原图 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]

灰度

提取轮廓

再测试一种新的组合看看轮廓特征 [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],' 正确的-提取轮廓 ')

提取轮廓

提取轮廓

正确的-提取轮廓

通过提取轮廓可以看到拼接结果的明显的线条错误的图至少存在一条 x 轴或 y 轴的线而拼接成功的基本没有线段位置或长度及线条数量可以决定正确率,需要多调整参数并筛选

这是因为原图有明显的过渡色它是为了用户体验而设计方便人们使用它的时候能够 容易 的区分并找出正确的拼图位置

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. 特征匹配

特征已知后现在剩下的就是对特征进行检测可以计算 x/2,y/2 十字架的色差也可以用 opencv 的直线提取测试代码如下

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(' 没找到线条 ')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(' 没找到线条 ')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])

提取轮廓

没找到线条

7. 匹配过程

import itertoolsprint(' 原图顺序 ')print(1,2)print(3,4)show_images([img])# 按编号,将切割的图放入 list 做排列组合list1 = [    [1,part1],    [2,part2],    [3,part3],    [4,part4]]result = itertools.permutations(list1,4)idx =1finded = FalsefinalResult = []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]] #获取最终排列正确的结果        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+=1print(' 测试次数 ',idx,' 最终状态 ',finded,finalResult)
原图顺序1 23 4

1 排列: 1 2 3 4 线: 42 排列: 1 2 4 3 线: 53 排列: 1 3 2 4 线: 44 排列: 1 3 4 2 线: 25 排列: 1 4 2 3 线: 36 排列: 1 4 3 2 线: 47 排列: 2 1 3 4 线: 38 排列: 2 1 4 3 线: 59 排列: 2 3 1 4 线: 310 排列: 2 3 4 1 线: 311 排列: 2 4 1 3 线: 112 排列: 2 4 3 1 线: 113 排列: 3 1 2 4 线: 214 排列: 3 1 4 2 线: 215 排列: 3 2 1 4 线: 316 排列: 3 2 4 1 线: 317 排列: 3 4 1 2 线: 518 排列: 3 4 2 1 线: 319 排列: 4 1 2 3 线: 420 排列: 4 1 3 2 线: 321 排列: 4 2 1 3 线: 2

还原图像


正确顺序4 23 1

完成!
测试次数 22 最终状态 True [4, 2, 3, 1]

8. 提取结果

再看看如何这种拼图如果要交换位置的组合有 12

list1 = [1,2,3,4]result = itertools.permutations(list1,2)idx=0for 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)
#交换函数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 diffsab = 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]

交换的位置 换算成小图中心偏移坐标采用查表法

#大图尺寸pwidth = 150 pheight = 75 #小图 xy 中心点 = 大图 wh 1/4px = round(pwidth/2)py = round(pheight/2)#创建坐标表offset_points = [    [px,py],[px+pwidth,py],    [px,py+pheight],[px+pwidth,py+pheight]]print(offset_points)print(ab)#通过结果作为索引,拿到坐标表索引的坐标drag_start = offset_points[ ab[0] -1 ]drag_end = offset_points[ ab[1] -1 ]print(' 起点偏移坐标 ',drag_start,' 终点偏移坐标 ',drag_end)
[[75, 38], [225, 38], [75, 113], [225, 113]][4, 1]起点偏移坐标 [225, 113] 终点偏移坐标 [75, 38]

9. 模拟操作

至此已经完成了拼图还原的分析所有过程下面采用另一种简单的方法move_to_element 方法内置的拖动 dom-a 到 dom-b 位置测试下结果

# 模拟聚焦按钮,让拼图显示出来onebtn = browser2.find_element_by_css_selector('#dx_captcha_oneclick_bar-logo_2 > span')ActionChains(browser2).move_to_element(onebtn).perform() time.sleep(1)

获取最终结果

ab = change_check([1,2,3,4],finalResult)print(ab)
[4, 1]

找到网页拼图的 dom 元素存储下来用于操作并交换拼图

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]
<ipython-input-22-61fb3f895e04>:1: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() insteadd1 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-left_3 > div')<ipython-input-22-61fb3f895e04>:2: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() insteadd2 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-top-right_3 > div')<ipython-input-22-61fb3f895e04>:3: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() insteadd3 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-left_3 > div')<ipython-input-22-61fb3f895e04>:4: DeprecationWarning: find_element_by_* commands are deprecated. Please use find_element() insteadd4 = browser2.find_element_by_css_selector('#dx_captcha_jigsaw_fragment-bottom-right_3 > div')

找出要拖动的 2 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")>
ActionChains(browser2).drag_and_drop(drag_start,drag_end).perform()# browser2.close()

简单拖一下目标网站认可了但它判定是有问题的又弹出一种新的验证码出来看来仅仅能够识别还原正确拼图还只是开端如何伪造一个让其认可的运行环境又是一个新的技术研究领域值得与各位共同学习分享交流

边学边做如有错误之处敬请指出谢谢

项目地址https://github.com/suifei/puzzle-captcha

免责声明:本网信息来自于互联网,目的在于传递更多信息,并不代表本网赞同其观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,并请自行核实相关内容。本站不承担此类作品侵权行为的直接责任及连带责任。如若本网有任何内容侵犯您的权益,请及时联系我们,本站将会在24小时内处理完毕。
相关文章
返回顶部