极验验证的滑动验证码破解

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介:

最近在搞爬虫的时候在好几个网站都碰到了一种叫做geetest的滑动条验证码,一直没有太好的办法只能在触发这个验证码后发个报警去手动处理一下。http://www.geetest.com/exp_embed是他们官网的样例。 


 


后来研究了下觉得要破解这个验证码有这么几个问题:

  1. 无法直接通过发送url请求来实现鼠标拖动的动作;

  2. 实际的背景图片是乱的,并不是我们实际肉眼看到的图像,如下图;

  3. “开创行为判别算法,利用数据挖掘和机器学习,提取超过200多个行为判别特征,建立坚若磐石的多维验证防御体系。”这是官网的描述,听上去就已经很高大上,查了些资料也都说拖动轨迹的识别是geetest的核心内容而无过多的表述,那么这也应该是主要的难点了。 


    这里背景图片是需要重新组织的 


    后面我也就基于了以上的问题去一步一步研究如何实现模拟这一操作: 

一.安装配置geetest的样例

首先自己安装配置一份geetest的样例。虽然geetest官网上有样例,但有时候反应比较慢,而且后面研究拖动轨迹的时候还需要对样例做一定的改动。编程语言我使用的是python2.7,所以这里选择的也是Python版本的。

参考内容:http://www.geetest.com/install/sections/idx-server-sdk.html#python 


安装Git

[root@mysql-test1 ~]# yum install git

在github中clone出最新Demo项目:

[root@mysql-test1 ~]# git clone https://github.com/GeeTeam/gt-python-sdk.git

安装GeetestSDK:

[root@mysql-test1 ~]# cd gt-python-sdk/
[root@mysql-test1 gt-python-sdk]# python setup.py install

安装Django,要注意的是最新的Django-1.10.1和当前的GeetestSDK是有兼容性问题的,要用Django-1.8.14:

[root@mysql-test1 ~]# wget --no-check-certificate  https://www.djangoproject.com/download/1.8.14/tarball/
[root@mysql-test1 ~]# tar zxvf Django-1.8.14.tar.gz
[root@mysql-test1 ~]# cd Django-1.8.14
[root@mysql-test1 Django-1.8.14]# python setup.py install

后面就可以直接运行了:

[root@mysql-test1 ~]# cd gt-python-sdk/demo/django_demo/
[root@mysql-test1 django_demo]# python manage.py runserver 0.0.0.0:8000



另外如果安装启动的时候报sqlite相关的错误,那就要安装Linux的sqlite-devel包,然后再编译安装python就可以了。

现在在浏览器里打开http://192.168.161.51:8000/就可以看到安装的geetest样例了。

另外还可以把gt-python-sdk/demo/django_demo/static/index.html里面41-61行注释掉,只保留嵌入式的Demo。



二.在浏览器上模拟鼠标拖动的操作

参考内容:http://www.cnblogs.com/wangly/p/5630069.html

这里要实现鼠标拖动的动作靠直接发送url请求是无法实现的,需要有个真的浏览器再去模拟鼠标拖动的动作。根据参考的内容使用了Selenium(也有python版本的)可以实现这一操作。

通过python的pip可以直接安装,我这里显示的版本是selenium-2.53。除此之外还需要根据浏览器下载webdriver。我使用的是chrome,驱动在http://download.csdn.net/detail/paololiu/9620177有下载,下载完后解压放到chrome的安装目录即可。另外还要注意chrome的版本,我这里使用的是52.0.2743.116。

#!/usr/local/bin/python
# -*- coding: utf8 -*-

'''
Created on 2016年9月2日

@author: PaoloLiu
'''

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains
import time

def main():

#     这里的文件路径是webdriver的文件路径
    driver = webdriver.Chrome(executable_path=r"C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe")

#     打开网页
    driver.get("http://192.168.161.51:8000/")

#     等待页面的上元素刷新出来
    WebDriverWait(driver, 30).until(lambda the_driver: the_driver.find_element_by_xpath("//div[@class='gt_slider_knob gt_show']").is_displayed())
    WebDriverWait(driver, 30).until(lambda the_driver: the_driver.find_element_by_xpath("//div[@class='gt_cut_bg gt_show']").is_displayed())
    WebDriverWait(driver, 30).until(lambda the_driver: the_driver.find_element_by_xpath("//div[@class='gt_cut_fullbg gt_show']").is_displayed())

#     找到滑动的圆球
    element=driver.find_element_by_xpath("//div[@class='gt_slider_knob gt_show']")

#     鼠标点击元素并按住不放
    print "第一步,点击元素"
    ActionChains(driver).click_and_hold(on_element=element).perform()
    time.sleep(1)

    print "第二步,拖动元素"
#     拖动鼠标到指定的位置,注意这里位置是相对于元素左上角的相对值
    ActionChains(driver).move_to_element_with_offset(to_element=element, xoffset=200, yoffset=50).perform()
    time.sleep(1)

    print "第三步,释放鼠标"
#     释放鼠标
    ActionChains(driver).release(on_element=element).perform()

    time.sleep(3)

if __name__ == '__main__':
    pass

    main()



三.计算图片中缺口的偏移量

参考内容:http://www.cnblogs.com/yuananyun/p/5655019.html

上面的移动位置我写了一个固定的值,实际情况这个值是不固定的,需要根据背景图片的缺口来算出这个偏移量。然而要计算缺口的偏移量还要先还原图片。 

1.还原图片

如上图,原始的图片是乱的,但是我们可以在html里面可以看到把同一个图片的位置进行重新组合就可以看到还原后的图片了: 

代码如下:

import PIL.Image as image
import PIL.ImageChops as imagechops
import time,re,cStringIO,urllib2,random

def get_merge_image(filename,location_list):
    '''
    根据位置对图片进行合并还原
    :filename:图片
    :location_list:图片位置
    '''
    pass

    im = image.open(filename)

    new_im = image.new('RGB', (260,116))

    im_list_upper=[]
    im_list_down=[]

    for location in location_list:

        if location['y']==-58:
            pass
            im_list_upper.append(im.crop((abs(location['x']),58,abs(location['x'])+10,166)))
        if location['y']==0:
            pass

            im_list_down.append(im.crop((abs(location['x']),0,abs(location['x'])+10,58)))

    new_im = image.new('RGB', (260,116))

    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(driver,div):
    '''
    下载并还原图片
    :driver:webdriver
    :div:图片的div
    '''
    pass

    #找到图片所在的div
    background_images=driver.find_elements_by_xpath(div)

    location_list=[]

    imageurl=''

    for background_image in background_images:
        location={}

        #在html里面解析出小图片的url地址,还有长高的数值
        location['x']=int(re.findall("background-image: url\(\"(.*)\"\); background-position: (.*)px (.*)px;",background_image.get_attribute('style'))[0][1])
        location['y']=int(re.findall("background-image: url\(\"(.*)\"\); background-position: (.*)px (.*)px;",background_image.get_attribute('style'))[0][2])
        imageurl=re.findall("background-image: url\(\"(.*)\"\); background-position: (.*)px (.*)px;",background_image.get_attribute('style'))[0][0]

        location_list.append(location)

    imageurl=imageurl.replace("webp","jpg")

    jpgfile=cStringIO.StringIO(urllib2.urlopen(imageurl).read())

    #重新合并图片 
    image=get_merge_image(jpgfile,location_list )

    return image


2.计算缺口位置

通过python的PIL.ImageChops可以计算出两个图片不同地方的位置,方法如下:

import PIL.ImageChops as imagechops
diff=imagechops.difference(image1, image2)
diff.show()
print diff.getbbox()

但是这在我们这里并不适用。因为我们得到的两个图片是通过拼接而成的,并且两张原图在背景上也还是稍有区别的,而difference方法计算得过于精确,所以这里得到的位置并不会是我们要的缺口的位置。这里我借用的参考内容的方法:两张原始图的大小都是相同的260*116,那就通过两个for循环依次对比每个像素点的RGB值,如果相差超过50则就认为找到了缺口的位置:

def is_similar(image1,image2,x,y):
    '''
    对比RGB值
    '''
    pass

    pixel1=image1.getpixel((x,y))
    pixel2=image2.getpixel((x,y))

    for i in range(0,3):
        if abs(pixel1[i]-pixel2[i])>=50:
            return False

    return True

def get_diff_location(image1,image2):
    '''
    计算缺口的位置
    '''

    i=0

    for i in range(0,260):
        for j in range(0,116):
            if is_similar(image1,image2,i,j)==False:
                return  i


四.鼠标拖动的轨迹

1.输出鼠标滑动轨迹

参考内容:http://blog.csdn.net/ieternite/article/details/51483491

如果我们直接把上面算出来的缺口位置放到前面脚本里,你会发现即使移动的位置正确了,提示却是“怪物吃了饼图”,验证不通过。很显然,geetest识别出了这个动作并不是人的行为。这我们就需要去查看自然人滑动鼠标和我们代码实现的滑动在轨迹上有什么不同。 


geetest目前版本客户端最核心的是geetest.5.5.36.js,我们可以把它复制出来加以改造。首先找个工具把原代码格式化一下,然后再加入以下的内容:

index.html页面的上直接调用的是gt.js,再由gt.js去调用geetest.5.5.36.js。我用的土办法是自己搭建一个简易的web server,并在host里面把static.geetest.com域名指向到我自己的web server,然后再把页面上要调用的static.geetest.com里的内容都放到我自己搭建的web server上,当然geetest.5.5.36.js是要用我刚才改造过的那个。

static.geetest.com里面只要static目录里的内容即可,pictures里面的图片找不到会自动指向到他们备用的网站的。我用的简易web server是HTTP File Server,可以在下载。

如此一来,我们每次滑动鼠标包括代码实现的滑动操作在浏览器里都能显示出滑动的轨迹: 
 

2.模拟人的行为

有了轨迹的数据,我们就可以进行对比分析了。上图的是我手动滑动的轨迹,而下图的是我通过代码拖动的轨迹,其实根本就不需要涉及到什么复杂的数据挖掘机器学习算法,两眼一看就能识别出不同来: 

这里我总结了一下差别(一个{x,y,z}是一个轨迹记录点,x代表x轴,y代表y轴,z代表累计时间毫秒): 
1.时间不宜太长又或者太短,最好能控制在1-5秒之内,另外两个相邻的记录点的时间也最好能控制在50ms以内,并且间隔的时间也不宜相同; 
2.乡邻的x值差值也不宜太大,最好控制在以5内,并且差值也不要是一层不变的; 
3.geetest虽然是横向拖动的,不会涉及到纵向移动,所以这部分很容易是被忽略的:y轴的值要控制在[-5,5]范围内,不能过大。而且上下抖动的频率不能高,要平缓一点。我试下来最好的办法就是平稳固定的0上,也不要上下抖动了。

完整代码如下:

#!/usr/local/bin/python
# -*- coding: utf8 -*-

'''
Created on 2016年9月2日

@author: PaoloLiu
'''

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains
import PIL.Image as image
import time,re,cStringIO,urllib2,random

def get_merge_image(filename,location_list):
    '''
    根据位置对图片进行合并还原
    :filename:图片
    :location_list:图片位置
    '''
    pass

    im = image.open(filename)

    new_im = image.new('RGB', (260,116))

    im_list_upper=[]
    im_list_down=[]

    for location in location_list:

        if location['y']==-58:
            pass
            im_list_upper.append(im.crop((abs(location['x']),58,abs(location['x'])+10,166)))
        if location['y']==0:
            pass

            im_list_down.append(im.crop((abs(location['x']),0,abs(location['x'])+10,58)))

    new_im = image.new('RGB', (260,116))

    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(driver,div):
    '''
    下载并还原图片
    :driver:webdriver
    :div:图片的div
    '''
    pass

    #找到图片所在的div
    background_images=driver.find_elements_by_xpath(div)

    location_list=[]

    imageurl=''

    for background_image in background_images:
        location={}

        #在html里面解析出小图片的url地址,还有长高的数值
        location['x']=int(re.findall("background-image: url\(\"(.*)\"\); background-position: (.*)px (.*)px;",background_image.get_attribute('style'))[0][1])
        location['y']=int(re.findall("background-image: url\(\"(.*)\"\); background-position: (.*)px (.*)px;",background_image.get_attribute('style'))[0][2])
        imageurl=re.findall("background-image: url\(\"(.*)\"\); background-position: (.*)px (.*)px;",background_image.get_attribute('style'))[0][0]

        location_list.append(location)

    imageurl=imageurl.replace("webp","jpg")

    jpgfile=cStringIO.StringIO(urllib2.urlopen(imageurl).read())

    #重新合并图片 
    image=get_merge_image(jpgfile,location_list )

    return image

def is_similar(image1,image2,x,y):
    '''
    对比RGB值
    '''
    pass

    pixel1=image1.getpixel((x,y))
    pixel2=image2.getpixel((x,y))

    for i in range(0,3):
        if abs(pixel1[i]-pixel2[i])>=50:
            return False

    return True

def get_diff_location(image1,image2):
    '''
    计算缺口的位置
    '''

    i=0

    for i in range(0,260):
        for j in range(0,116):
            if is_similar(image1,image2,i,j)==False:
                return  i

def get_track(length):
    '''
    根据缺口的位置模拟x轴移动的轨迹
    '''
    pass

    list=[]

#     间隔通过随机范围函数来获得
    x=random.randint(1,3)

    while length-x>=5:
        list.append(x)

        length=length-x
        x=random.randint(1,3)

    for i in xrange(length):
        list.append(1)

    return list

def main():

#     这里的文件路径是webdriver的文件路径
    driver = webdriver.Chrome(executable_path=r"C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe")
#     driver = webdriver.Firefox()

#     打开网页
    driver.get("http://172.16.2.7:8000/")

#     等待页面的上元素刷新出来
    WebDriverWait(driver, 30).until(lambda the_driver: the_driver.find_element_by_xpath("//div[@class='gt_slider_knob gt_show']").is_displayed())
    WebDriverWait(driver, 30).until(lambda the_driver: the_driver.find_element_by_xpath("//div[@class='gt_cut_bg gt_show']").is_displayed())
    WebDriverWait(driver, 30).until(lambda the_driver: the_driver.find_element_by_xpath("//div[@class='gt_cut_fullbg gt_show']").is_displayed())

#     下载图片
    image1=get_image(driver, "//div[@class='gt_cut_bg gt_show']/div")
    image2=get_image(driver, "//div[@class='gt_cut_fullbg gt_show']/div")

#     计算缺口位置
    loc=get_diff_location(image1, image2)

#     生成x的移动轨迹点
    track_list=get_track(loc)

#     找到滑动的圆球
    element=driver.find_element_by_xpath("//div[@class='gt_slider_knob gt_show']")
    location=element.location
#     获得滑动圆球的高度
    y=location['y']

#     鼠标点击元素并按住不放
    print "第一步,点击元素"
    ActionChains(driver).click_and_hold(on_element=element).perform()
    time.sleep(0.15)

    print "第二步,拖动元素"
    track_string = ""
    for track in track_list:
        track_string = track_string + "{%d,%d}," % (track, y - 445)
#         xoffset=track+22:这里的移动位置的值是相对于滑动圆球左上角的相对值,而轨迹变量里的是圆球的中心点,所以要加上圆球长度的一半。
#         yoffset=y-445:这里也是一样的。不过要注意的是不同的浏览器渲染出来的结果是不一样的,要保证最终的计算后的值是22,也就是圆球高度的一半
        ActionChains(driver).move_to_element_with_offset(to_element=element, xoffset=track+22, yoffset=y-445).perform()
#         间隔时间也通过随机函数来获得
        time.sleep(random.randint(10,50)/100)
    print track_string
#     xoffset=21,本质就是向后退一格。这里退了5格是因为圆球的位置和滑动条的左边缘有5格的距离
    ActionChains(driver).move_to_element_with_offset(to_element=element, xoffset=21, yoffset=y-445).perform()
    time.sleep(0.1)
    ActionChains(driver).move_to_element_with_offset(to_element=element, xoffset=21, yoffset=y-445).perform()
    time.sleep(0.1)
    ActionChains(driver).move_to_element_with_offset(to_element=element, xoffset=21, yoffset=y-445).perform()
    time.sleep(0.1)
    ActionChains(driver).move_to_element_with_offset(to_element=element, xoffset=21, yoffset=y-445).perform()
    time.sleep(0.1)
    ActionChains(driver).move_to_element_with_offset(to_element=element, xoffset=21, yoffset=y-445).perform()

    print "第三步,释放鼠标"
#     释放鼠标
    ActionChains(driver).release(on_element=element).perform()

    time.sleep(3)

#     点击验证
    submit=driver.find_element_by_xpath("//input[@id='embed-submit']")
    ActionChains(driver).click(on_element=submit).perform()

    time.sleep(5)

driver.quit()

if __name__ == '__main__':
    pass

    main()

运行结果:



五.浏览器的兼容问题

1.最为重要的就是代码注释里说的y轴的高度问题,我试了PhantomJS,Chrome和Firefox三个浏览器,每一种渲染出来的高度都是不一样的,一定要保证最终的结果是拖动球高度的一半(一般都是22); 


2.版权兼容性(以下是我验证过可行的): 
selenium (2.53.6)===>PhantomJS 2.1 
selenium (2.53.6)===>Chrome 52 
selenium (2.53.6)===>Firefox 45(注意不要用48,有兼容问题) 


3.webdriver的cookie问题: 
有的时候我们需要带入cookie进行验证,那就有了cookie的问题了。Chrome和Firefox都可以通过webdriver.add_cookie来实现,但是经我试下来这个方法和PhantomJS有兼容性问题,我是这样解决的:

    def save_cookies(self, driver, file_path, inputcookie):
#         LINE = "document.cookie = '{name}={value}; path={path}; domain={domain}; expires={expires}';\n"

        dict_cookie = {}

        for item in inputcookie.split(";"):
            dict_cookie[item.split("=")[0].strip()] = item.split("=")[1].strip()

#         logging.info(dict_cookie)

        with open(file_path, 'w') as file :
            for cookie in driver.get_cookies() :
#                 logging.info(cookie)
                if u'expires' in cookie:
                    if cookie['name'] in dict_cookie:
                        line = "document.cookie = '%s=%s; path=%s; domain=%s; expires=%s';\n" % (cookie['name'], dict_cookie[cookie['name']], cookie['path'], cookie['domain'], cookie['expires'])
                    else:
                        line = "document.cookie = '%s=%s; path=%s; domain=%s; expires=%s';\n" % (cookie['name'], cookie['value'], cookie['path'], cookie['domain'], cookie['expires'])
                else:
                    if cookie['name'] in dict_cookie:
                        line = "document.cookie = '%s=%s; path=%s; domain=%s';\n" % (cookie['name'], dict_cookie[cookie['name']], cookie['path'], cookie['domain'])
                    else:
                        line = "document.cookie = '%s=%s; path=%s; domain=%s';\n" % (cookie['name'], cookie['value'], cookie['path'], cookie['domain'])
#                 logging.info(line)
                file.write(line.encode("utf8"))

    def load_cookies(self, driver, file_path):
        with open(file_path, 'r') as file:
            driver.execute_script(file.read())

再如此调用就可以解决cookie的兼容性问题了:

driver.get(url)

# save the cookies to a file
self.save_cookies(driver, r"cookies.js", cookies)

# delete all the cookies
driver.delete_all_cookies()

# load the cookies from the file
self.load_cookies(driver, r"cookies.js")

# reopen url
driver.get(url)



4.PhantomJS浏览器解析出来的图片url是不带引号的,而Firefox和Chrome解析出来的是带引号的,这里正则过滤的时候要注意一下的。 


我最终使用的是selenium+Firefox。我实际运行的环境是centos,PhantomJS确实是个不错的选择,直接在shell里运行就可以了,不需要配置图形界面。但是使用下来破解的成功率不高,因为没有界面,也看不出运行的情况。Chrome在centos6.5里面没有现成的安装包,安装使用比较复杂。最终也就只有Firefox了。

centos配置firefox方法如下:

[root@db2-test1 ~]# yum groupinstall "X Window System" -y
[root@db2-test1 ~]# yum groupinstall "Desktop" -y
[root@db2-test1 ~]# yum install firefox -y

注意不要纯shell环境下运行,要在图形界面的运行。运行init 5可以从字符界面切换到图形界面。










本文转自 yntmdr 51CTO博客,原文链接:http://blog.51cto.com/yntmdr/1905966,如需转载请自行联系原作者
相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
数据采集 前端开发 开发者
滑动拼图验证码的原理和破解方法~
滑动拼图验证码的原理和破解方法~
2152 0
滑动拼图验证码的原理和破解方法~
|
8月前
|
安全 开发工具 Python
滑动拼图验证,摆脱烦人的验证码输入
你最近是否遇到过令人头疼的验证码?为何不让滑动拼图成为你的新选择呢?通过完成一个有趣的滑动拼图来验证你的身份,既能锻炼大脑,又能保护你的隐私。
滑动拼图验证,摆脱烦人的验证码输入
|
8月前
|
JavaScript 前端开发
记录2分钟破解知识星球验证弹窗
先声明我是这个网站的会员我可以进去
782 0
|
安全 开发工具
KgCaptcha 行为验证码安全策略设置
在验证码项目中,都会遇到验证码被恶意大量高频的调用,给服务造成很多无效的注册或登录,占用大量的系统资源。
KgCaptcha 行为验证码安全策略设置
|
安全 网络安全 UED
要想破解验证码,就得从了解开始!
最近在研究验证码安全,本文就来介绍四种流行的验证码(滑动拼图、文字点选、语序点选、空间推理),写这篇文章的出发点并非是想绕过或破解验证码,而是根据自身业务情况来选择对应的验证码类型。
要想破解验证码,就得从了解开始!
|
JavaScript API 数据安全/隐私保护
数美滑块验证码分析
本文以官网的滑块验证码为例,分析验证过程,完成模拟验证。
1272 0
数美滑块验证码分析
|
数据采集 小程序 Python
论如何破解腾讯滑块验证码
论如何破解腾讯滑块验证码
1925 0
|
JavaScript Java
实现登录时进行校验验证码的功能
JavaEE中,实现登录时进行校验验证码的功能(图文并茂!!!)
实现登录时进行校验验证码的功能
|
数据采集 文字识别
文字点选验证码的破解方法~
大家好,我是志斌~ 志斌之前一直在写反爬虫系列的文章,但是因为自身水平有限,所以一直没更验证码反爬虫之文字点选验证码反爬虫的解决方式,这次专门为大家找了一个大佬——张老师,来跟大家分享一下他解决文字点选验证码的方法~
1381 0