破解極驗(geetest)驗證碼

  破解極驗(geetest)驗證碼

這是兩年前的帖子: http://www.v2ex.com/t/138479 一個月前的破解程序,我沒用過 asp.net ,不知道是否是真的破解了, demo 不能換 id 和 網址: https://github.com/wsguest/geetest 請高手鑑定一下。javascript

 

最近在搞爬蟲的時候在好幾個網站都碰到了一種叫作geetest的滑動條驗證碼,一直沒有太好的辦法只能在觸發這個驗證碼後發個報警去手動處理一下。http://www.geetest.com/exp_embed是他們官網的樣例。 


 


後來研究了下以爲要破解這個驗證碼有這麼幾個問題:html

  1. 沒法直接經過發送url請求來實現鼠標拖動的動做;
  2. 實際的背景圖片是亂的,並非咱們實際肉眼看到的圖像,以下圖;
  3. 「開創行爲判別算法,利用數據挖掘和機器學習,提取超過200多個行爲判別特徵,創建堅若磐石的多維驗證防護體系。」這是官網的描述,聽上去就已經很高大上,查了些資料也都說拖動軌跡的識別是geetest的核心內容而無過多的表述,那麼這也應該是主要的難點了。 


    這裏背景圖片是須要從新組織的 


    後面我也就基於了以上的問題去一步一步研究如何實現模擬這一操做: 

一.安裝配置geetest的樣例

首先本身安裝配置一份geetest的樣例。雖然geetest官網上有樣例,但有時候反應比較慢,並且後面研究拖動軌跡的時候還須要對樣例作必定的改動。編程語言我使用的是python2.7,因此這裏選擇的也是Python版本的。java

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


安裝Gitpython

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

在github中clone出最新Demo項目:mysql

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

安裝GeetestSDK:linux

[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:git

[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

後面就能夠直接運行了:github

[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就能夠了。web

如今在瀏覽器裏打開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能夠從字符界面切換到圖形界面。

相關文章
相關標籤/搜索