天天看點

破解極驗(geetest)驗證碼

破解極驗(geetest)驗證碼

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

破解極驗(geetest)驗證碼

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

  1. 無法直接通過發送url請求來實作滑鼠拖動的動作;
  2. 實際的背景圖檔是亂的,并不是我們實際肉眼看到的圖像,如下圖;
  3. “開創行為判别算法,利用資料挖掘和機器學習,提取超過200多個行為判别特征,建立堅若磐石的多元驗證防禦體系。”這是官網的描述,聽上去就已經很高大上,查了些資料也都說拖動軌迹的識别是geetest的核心内容而無過多的表述,那麼這也應該是主要的難點了。 
    破解極驗(geetest)驗證碼
    後面我也就基于了以上的問題去一步一步研究如何實作模拟這一操作: 

一.安裝配置geetest的樣例

首先自己安裝配置一份geetest的樣例。雖然geetest官網上有樣例,但有時候反應比較慢,而且後面研究拖動軌迹的時候還需要對樣例做一定的改動。程式設計語言我使用的是python2.7,是以這裡選擇的也是python版本的。

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

安裝git:

[[email protected] ~]# yum install git
           

在github中clone出最新Demo項目:

[[email protected] ~]# git clone https://github.com/GeeTeam/gt-python-sdk.git
           

安裝GeetestSDK:

[[email protected] ~]# cd gt-python-sdk/
[[email protected] gt-python-sdk]# python setup.py install3
           

安裝Django,要注意的是最新的Django-1.10.1和目前的GeetestSDK是有相容性問題的,要用Django-1.8.14:

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

後面就可以直接運作了:

[[email protected] ~]# cd gt-python-sdk/demo/django_demo/
[[email protected] 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。

破解極驗(geetest)驗證碼

二.在浏覽器上模拟滑鼠拖動的操作

參考内容: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()
           
破解極驗(geetest)驗證碼

三.計算圖檔中缺口的偏移量

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

上面的移動位置我寫了一個固定的值,實際情況這個值是不固定的,需要根據背景圖檔的缺口來算出這個偏移量。然而要計算缺口的偏移量還要先還原圖檔。 

1.還原圖檔

如上圖,原始的圖檔是亂的,但是我們可以在html裡面可以看到把同一個圖檔的位置進行重新組合就可以看到還原後的圖檔了: 

破解極驗(geetest)驗證碼

代碼如下:

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,我們可以把它複制出來加以改造。首先找個工具把原代碼格式化一下,然後再加入以下的内容:

破解極驗(geetest)驗證碼

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是要用我剛才改造過的那個。

破解極驗(geetest)驗證碼

static.geetest.com裡面隻要static目錄裡的内容即可,pictures裡面的圖檔找不到會自動指向到他們備用的網站的。我用的簡易web server是HTTP File Server,可以在下載下傳。

如此一來,我們每次滑動滑鼠包括代碼實作的滑動操作在浏覽器裡都能顯示出滑動的軌迹: 

破解極驗(geetest)驗證碼

2.模拟人的行為

有了軌迹的資料,我們就可以進行對比分析了。上圖的是我手動滑動的軌迹,而下圖的是我通過代碼拖動的軌迹,其實根本就不需要涉及到什麼複雜的資料挖掘機器學習的算法,兩眼一看就能識别出不同來: 

破解極驗(geetest)驗證碼

這裡我總結了一下差别(一個{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()
           

運作結果:

破解極驗(geetest)驗證碼

五.浏覽器的相容問題

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方法如下:

[[email protected] ~]# yum groupinstall "X Window System" -y
[[email protected] ~]# yum groupinstall "Desktop" -y
[[email protected] ~]# yum install firefox -y
           

注意不要純shell環境下運作,要在圖形界面的運作。運作init 5可以從字元界面切換到圖形界面。

繼續閱讀