美文网首页Pythoner集中营pyqt学习笔记数据科学
利用二分法+opencv识别网易易盾滑动验证码的位移值

利用二分法+opencv识别网易易盾滑动验证码的位移值

作者: 疯智子 | 来源:发表于2018-04-11 13:55 被阅读342次

    在做毕设的时候接了一个小项目,破解一个网站的登录验证,研究了一下发现该网站采用的网易易盾的验证码,下面翻出网址就是一顿开撸!http://dun.163.com/trial/jigsaw

    image

    先在网上查了下资料,发现知乎下刚好有个帖子,滑块验证码(滑动验证码)相比图形验证码,破解难度如何?,有个高票回答为了用机器学习的方式来破解网易易盾的滑动验证码,请了几个小学生来标注数据,但我身边不认识小学生啊,总不能也去网吧遛一圈吧,我怕我去了就专心地吃起了鸡,而把正事忘干净了,无奈之下,只能再想想别的办法。。。

    能不能单纯通过算法而不借助数据就把这个验证码破解了呢?

    上面的帖子里还出现了极验验证码的破解方式,因为它每次会出两张图,一张带缺口的,一张不带缺口的,于是可以通过图片对比找到位移。但网易易盾自始至终只有一张图片,这可咋对比哦,没办法,还是先研究一下验证码长啥样吧。

    image

    用开发者工具看了一下,发现它的验证码也是两部分,一部分是jpg格式的目标图片,一部分是png格式的带缺口的滑块。于是很自然地想到要是能直接把缺口给抠下来,然后在整个目标图片查找一下,最后输出匹配位置的坐标岂不是就搞定了吗。

    搜索了一下opencv的图像匹配,发现刚好有个模板匹配的功能,于是第一个问题就解决了。再来看看python可以怎么抠图,然而找了半天最多都只有替换背景,虽然理论上稍微研究一下应该是能够把前景图片单独保存下来的,但那会儿下午刚好有点困,不想动太多脑子,哈哈哈。

    紧接着就想到,png格式的图片,背景是透明的,那模板匹配的时候是不是直接就能匹配上呢?有了这个想法的我瞬间懊恼不已,觉得自己之前一心想抠图真的是太蠢了,不管了,先试试能不能匹配上吧。把目标图片一换,果然是有结果的,但结果好像有点多啊。没关系,一定是阈值定得太低了,调了几次阈值后找到一个合适的,结果完美。只是我对自己之前的愚蠢做法更懊恼了。。。

    image

    接下来试试换一张图片能不能跑通吧,熟练地右键下载图片并覆盖,按下F5运行,得到了这样的图片:

    image

    woc??这是什么鬼,怎么又是这么多结果,,看来每个图片的阈值是不一样的啊。我没去细究其中的原理,不过猜想一下感觉还是有道理的,不同颜色的图片,明暗不一样,缺口的位置不一样,缺口的颜色就会不一样,所以阈值一定是有区别的。

    然而,道理是想明白了,我TM怎么能自动找准阈值呢?难不成最后还是得祭出机器学习的大招?

    就在万念俱灰的时候,突然想到测试时调阈值的情况,阈值设置得太大就没有结果,设置得太小就有N个结果,这不就是高中还是初中数学学的二分法的应用题吗。想到之后觉得此题已经被我拿下了,于是马上上手撸代码。阈值的范围区间是[0, 1],分别设置成左端L和右端R(如此佛系的变量命名估计也只有我想得出来了,233333333333),算法如下:

    • 阈值始终为区间左端和右端的均值,即 threshhold = (R+L)/2;
    • 如果当前阈值查找结果数量大于1,则说明阈值太小,需要往右端靠近,即左端就增大,即L += (R - L) / 2;
    • 如果结果数量为0,则说明阈值太大,右端应该减小,即R -= (R - L) / 2;
    • 当结果数量为1时,说明阈值刚好

    下面是模板匹配的函数,可以显示匹配结果

        def match(self, target, template):
            img_rgb = cv2.imread(target)
            img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
            template = cv2.imread(template,0)
            run = 1
            w, h = template.shape[::-1]
            print(w, h)
            res = cv2.matchTemplate(img_gray,template,cv2.TM_CCOEFF_NORMED) 
            
            # 使用二分法查找阈值的精确值 
            L = 0
            R = 1
            while run < 20:
                run += 1
                threshold = (R + L) / 2
                print(threshold)
                if threshold < 0:
                    print('Error')
                    return None
                loc = np.where( res >= threshold)
                print(len(loc[1]))
                if len(loc[1]) > 1:
                    L += (R - L) / 2
                elif len(loc[1]) == 1:
                    print('目标区域起点x坐标为:%d' % loc[1][0])
                    break
                elif len(loc[1]) < 1:
                    R -= (R - L) / 2
    
            for pt in zip(*loc[::-1]):
                cv2.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (7, 279, 151), 2)
            cv2.imshow('Dectected', img_rgb)
            cv2.waitKey(0)
            cv2.destroyAllWindows()
            return loc[1][0]
    

    让我们来看看用上面的代码跑下来会发生什么吧

    image

    上图中这些小数就是在自动使用二分法寻找阈值时的阈值记录,用7次就找出结果了,效率还是蛮高的。实际测试中,我最多遇到有14次才找准阈值的精确解的,也是很煞费苦心了。

    到这里整个程序的核心就已经解决了,但还有一点小问题,这个代码在我自己电脑上运行的时候没问题,但到了别人的电脑上就不好使了,因为屏幕分辨率不一样,所以最终操纵浏览器要移动的位移是需要缩放的。要拿到这个缩放系数,可以将目标文件在网页上的大小和下载下来后实际的大小做比对,即可拿到。我们用一个get_pic函数来解决所有与图片相关的问题。

        def get_pic(self):
            time.sleep(2) # 有的网站可能加载比较慢,会导致网页上看到的图片和下载下来的图片不一样,等待个1~2秒就好了
            target = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'yidun_bg-img')))
            template = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'yidun_jigsaw')))
            target_link = target.get_attribute('src')
            template_link = template.get_attribute('src')
            target_img = Image.open(BytesIO(requests.get(target_link).content))
            template_img = Image.open(BytesIO(requests.get(template_link).content))
            target_img.save('target.jpg') # 将目标图片下载到本地
            template_img.save('template.png')
            size_orign = target.size
            local_img = Image.open('target.jpg') # 打开本地保存的目标图片,获取图片宽度
            size_loc = local_img.size
            self.zoom = 320 / int(size_loc[0]) # 缩放系数为320除以本地的宽度,320为目标图片在网页上的宽度
            print(self.zoom)
    

    这个缩放系数乘以match函数拿到的缺口水平坐标就是我们要移动的距离,当然最后还是需要一定的微调的。效果长这样,第一排的0.66666666就是缩放系数,最下面红圈里的的数值就是实际需要移动的位移。于是这个代码即使更换了屏幕的分辨率,也能完美运行。

    image

    下面贴上完整代码,删除了一些debug的东西,轨迹的计算参考了其他大神的代码

    from PIL import Image, ImageEnhance
    from selenium import webdriver  
    from selenium.webdriver import ActionChains  
    from selenium.webdriver.common.by import By  
    from selenium.webdriver.common.keys import Keys  
    from selenium.webdriver.support import expected_conditions as EC  
    from selenium.webdriver.support.wait import WebDriverWait   
    import cv2
    import numpy as np
    from io import BytesIO
    import time, requests
    
    class CrackSlider():
        """
        通过浏览器截图,识别验证码中缺口位置,获取需要滑动距离,并模仿人类行为破解滑动验证码
        """
        def __init__(self):
            super(CrackSlider, self).__init__()
            # 实际地址
            self.url = 'http://dun.163.com/trial/jigsaw'
            self.driver = webdriver.Chrome()
            self.wait = WebDriverWait(self.driver, 20)
            self.zoom = 1
    
        def open(self):
            self.driver.get(self.url)
    
        def get_pic(self):
            time.sleep(2)
            target = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'yidun_bg-img')))
            template = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'yidun_jigsaw')))
            target_link = target.get_attribute('src')
            template_link = template.get_attribute('src')
            target_img = Image.open(BytesIO(requests.get(target_link).content))
            template_img = Image.open(BytesIO(requests.get(template_link).content))
            target_img.save('target.jpg')
            template_img.save('template.png')
            size_orign = target.size
            local_img = Image.open('target.jpg')
            size_loc = local_img.size
            self.zoom = 320 / int(size_loc[0])
    
        def get_tracks(self, distance):
            print(distance)
            distance += 20
            v = 0
            t = 0.2
            forward_tracks = []
            current = 0
            mid = distance * 3/5
            while current < distance:
                if current < mid:
                    a = 2
                else:
                    a = -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}
    
        def match(self, target, template):
            img_rgb = cv2.imread(target)
            img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
            template = cv2.imread(template,0)
            run = 1
            w, h = template.shape[::-1]
            print(w, h)
            res = cv2.matchTemplate(img_gray,template,cv2.TM_CCOEFF_NORMED) 
            
            # 使用二分法查找阈值的精确值 
            L = 0
            R = 1
            while run < 20:
                run += 1
                threshold = (R + L) / 2
                print(threshold)
                if threshold < 0:
                    print('Error')
                    return None
                loc = np.where( res >= threshold)
                print(len(loc[1]))
                if len(loc[1]) > 1:
                    L += (R - L) / 2
                elif len(loc[1]) == 1:
                    print('目标区域起点x坐标为:%d' % loc[1][0])
                    break
                elif len(loc[1]) < 1:
                    R -= (R - L) / 2
    
            return loc[1][0]
    
        def crack_slider(self):
            self.open()
            target = 'target.jpg'
            template = 'template.png'
            self.get_pic()
            distance = self.match(target, template)
            tracks = self.get_tracks((distance + 7 )*self.zoom) # 对位移的缩放计算
            print(tracks)
            slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'yidun_slider')))
            ActionChains(self.driver).click_and_hold(slider).perform()
    
            for track in tracks['forward_tracks']:
                ActionChains(self.driver).move_by_offset(xoffset=track, yoffset=0).perform()
    
            time.sleep(0.5)
            for back_tracks in tracks['back_tracks']:
                ActionChains(self.driver).move_by_offset(xoffset=back_tracks, yoffset=0).perform()
    
            ActionChains(self.driver).move_by_offset(xoffset=-3, yoffset=0).perform()
            ActionChains(self.driver).move_by_offset(xoffset=3, yoffset=0).perform()
            time.sleep(0.5)
            ActionChains(self.driver).release().perform()
            try:
                failure = self.wait.until(EC.text_to_be_present_in_element((By.CLASS_NAME, 'yidun_tips__text'), '向右滑动滑块填充拼图'))
                print(failure)
            except:
                print('验证成功')
                return None
    
            if failure:
                self.crack_slider()
    
    if __name__ == '__main__':
        c = CrackSlider()
        c.crack_slider()
    
    

    实际中测试发现,用此方法检测缺口的正确率能达到95%以上(19/20),不正确的时候直接重试即可,轻松简洁。祝大家玩得愉快~

    相关文章

      网友评论

      • 88e151590758:作者可以说的上是 破解滑块验证码的第二人! nb 佩服 。第一种方法 都是 一张完整图片和带有缺口的图片去比较 得到两个缺口的位移。网上大都就是这一种办法。 我百度了14天 才看到作者的这种办法 高明 。但是 还有个很大的实际问题 。你这里需要的两张图片 第一张只带有一个缺口的验证码图片 第二张是纯粹的缺口图片 现在的情况是 用你这种办法依然破解不了滑块验证码(开始的第一种办法也破解不了了) 因为 现在滑块验证码升级了 你可以看看。。路人 急求 有空 望回复 qq2310867630 求解
      • b食猫噶魚:试了一下,网易是可以的,然后我试了一下这个不行https://passport.jd.com/new/login.aspx,不知道是什么原因,请大神指教一下,谢谢
      • f93610ea5478:http://www.sf-express.com/cn/sc/dynamic_function/waybill/#search/bill-number/456389047823
        sf的这个template图片好像匹配不了
        smalldu:顺丰那个是暗色的块,例子中是偏白色的块 我也遇到暗色的块匹配很不准
        楼主可否告知 怎么解决
        楼主可以尝试下顺丰的这个 改下element的定位
        f93610ea5478:@疯智子 我试了下例子中的 图片 模板匹配, 匹配度还蛮高的, 但是顺丰这个 匹配度比较差
        疯智子:可以详细描述下问题么 我简单看了一下 顺丰这个用的不是网易易盾的 所以webdriver在做元素定位那部分的代码需要改一下
      • IT人故事会:总结可以

      本文标题:利用二分法+opencv识别网易易盾滑动验证码的位移值

      本文链接:https://www.haomeiwen.com/subject/yxgahftx.html