極驗反爬蟲防護分析之接口交互的解密方法

本文要分享的內容是去年爲了搶鞋而分析 極驗(GeeTest)反爬蟲防護的筆記,由於篇幅較長(爲了多混點CB)我會按照我的分析順序,分成如下四個主題與大家分享:

  1. 極驗反爬蟲防護分析之交互流程分析
  2. 極驗反爬蟲防護分析之接口交互的解密方法
  3. 極驗反爬蟲防護分析之接口交互的解密方法補遺
  4. 極驗反爬蟲防護分析之slide驗證方式下圖片的處理及滑動軌跡的生成思路

 

本文是第二篇《接口交互的解密方法》,書接上文,上一篇中,我們遺留了兩個問題:

  1. 所有用於驗證的代碼都是混淆之後的,如何進行代碼還原或者調試分析。
  2. 請求與返回的關鍵數據都是加密的後的字符串如上述3、4、6等接口中的W參數,如何解密。

下面進入正文~


JS代碼的還原與調試分析

geetest的js代碼不單是簡單的壓縮,應該是經過混淆加密的,下載一個geetest的js文件,格式化並還原編碼後,會發現js加載之後直接執行了幾個嵌套的大循環,猜測應該是通過decodeURI對關鍵信息進行解密,拼裝成一個大數組,這樣便達到隱藏關鍵代碼和信息目的,如下圖:

 

 

 

如此,程序的代碼便可以寫成封裝成一個方法,通過數組下標來拼接程序代碼,達到隱藏關鍵信息,預防靜態分析的目的。根據我的經驗,目前市面上好多前端代碼都採用了這種代碼加密思路,比如 同花順數據中心的頁面數據,爲了防止接口被cors,構造了自定義的cookie信息,對應的js構造代碼也是這類加密思路。由於還原此JS代碼並不是本文的目的,所以我們就直接說調試分析方法,對還原代碼有興趣的童鞋可以自己再深入分析。

代碼Hook及覆蓋時機

如上說明,由於官網加載的Geetest腳本都是壓縮加密混淆之後的代碼,並不方便分析和調試。受X86年代更換目標dll路徑就可以達到dll劫持思路的啓發,我們將腳本下載下來格式化、替換轉義字符之後,通過瀏覽器控制檯加載修改後的腳本覆蓋原js代碼來達到同樣的效果(因證書驗證的問題,將代碼放在了我自己的空間裏):

var script = document.createElement('script');
script.src = "https://xxx/static/js/fullpage.8.8.4.js";
document.getElementsByTagName('head')[0].appendChild(script);

var script = document.createElement('script');
script.src = "https://xxxx/static/js/slide.7.6.0.js";
document.getElementsByTagName('head')[0].appendChild(script);

經過分析,代碼在執行完上一篇文章的第三步之後開始加載對應的代碼,第四步的請求參數中的內容纔開始加密的,因此要分析加密代碼可以再加載了相應js之後再用我們的js進行覆蓋即可。

加密代碼的定位方法

瀏覽器中定位前端代碼,最有效的方法莫過於通過 瀏覽器事件 + 條件斷點的方式來定位了,但是由於上面說的代碼加密的原因,這種定位方法在這裏失效了,不論什麼事件最終都會進入到上面說的大數組解析的方法中去,如下圖:

 

 

 所以只能通過xhr請求入手,通過棧回溯的方法定位代碼。值得慶幸的是火狐瀏覽器原生就支持堆棧跟蹤,省了我們不少時間,如下圖:

 

 

 通過js下斷點不斷回溯,發現上圖中紅框部分是加密的代碼部分,下斷點,重新點擊登錄框,程序中斷,如下圖:

其中變量e的值記錄一下:

46207!!219363!!CSS1Compat!!334!!-1!!-1!!-1!!-1!!1!!-1!!-1!!9!!60!!45!!9!!15!!-1!!-1!!-1!!-1!!-1!!1!!-1!!-1!!231!!2!!-1!!-1!!-1!!157!!23!!44!!23!!1396!!279!!1396!!877!!zh-CN!!zh-CN,zh,zh-TW,zh-HK,en-US,en!!-1!!1!!24!!Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0!!1!!1!!1680!!1050!!1636!!1027!!1!!1!!1!!-1!!MacIntel!!1!!-8!!805a6cdeadd4f48ade985597f74928cb!!cc03697d39800df1ef0d2229132a62e8!!!!0!!-1!!0!!4!!AndaleMono,Arial,ArialBlack,ArialNarrow,ArialRoundedMTBold,ArialUnicodeMS,ComicSansMS,Courier,CourierNew,Geneva,Georgia,Helvetica,HelveticaNeue,Impact,LUCIDAGRANDE,MicrosoftSansSerif,Monaco,Palatino,Tahoma,Times,TimesNewRoman,TrebuchetMS,Verdana!!1568168747074!!-1,-1,-15,0,0,0,0,97,230,2,134245,8,7,441,444,992,3151782,3151782,3151970,-1!!-1!!-1!!577!!75!!49!!222!!75!!false!!false

RSA加的分析

單步步入跟進變量r的計算過程,由於方法是通過數組轉義的,所以需要步入多次才能進入到正確的變量中,如下圖:

 

 

 

到這裏,需要分兩步分析:
i. 待加密的內容92c689c0f4282f4e是什麼,怎麼得到的。
i. 跟進繼續分析,看是否能得到RSA加密的公鑰

這裏備份一下加密的結果:

031d0a23604ba7778905403f8403e780224cdfa6854551f55d2efd84436434df3579139c391d0b34d2ff91cbb29bf24902cf2dc1b03e165db3601d5f6cdbb6a1f1ef81f03b8085c5606671b50f22db362f8ddfec89551f163f96e84b1e22387b6e229fe1ab2ab76f8dcc2a8a15b840ebad8c75b7afbf126f2b6f33f478774e8d

待加密的那串字符串已經執行過了,需要再重新啓動調試分析,所以這裏不打斷本次調試,繼續分析RSA的公鑰,如下圖:

 

 

 得到的RSA的公鑰信息爲:

publicExponent = 10001
publicKey = 00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81

至此,RSA加密需要的信息我們分析完畢,模擬RSA加密的python代碼如下:

import rsa
from binascii import b2a_hex

e = '010001'
e = int(e, 16)

n = '00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81'
n = int(n, 16)

pub_key = rsa.PublicKey(e=e, n=n)
print(b2a_hex(rsa.encrypt(b"0fe524023c414bb5", pub_key)))

AES加密的分析

繼續單步步入AES加密的方法之前,意外發現了上下文變量中的信息,如下圖:

 

 至此,我們知道上面遺留待分析的字符串: 92c689c0f4282f4e, 是AES加密用的Key。一會兒代碼中證實一下。

 

 由上圖可知,
a. key=n: 密鑰key,對應的值: 959603510..., 轉換爲十六進制爲: 0x39326336...,對應的ascii爲: 92c6..., 說明上面的字符串確實是AES加密用的key。
b. iv=a[$_BBIFN(344)]; 補位值, 對應的值是: 808464432..., 轉換爲十六進制爲: 0x30303030...,說明補位以字符0000000000000000補齊
c. mode=a[$_BBIFN(344)]; 加密模式,分析可知是CBC模式的加密
d. blockSize=4; 切分區塊大小爲4字節
e. ciphertext=i; 序列化後的代加密字符,原文是t,下面記錄下代加密的原文內容:

{
        "gt": "2328764cdf162e8e60cc0b04383fef81",
        "challenge": "960780255cdadcbdebde1fb646d5cb77",
        "offline": false,
        "product": "float",
        "width": "100%",
        "lang": "zh-hk",
        "protocol": "https://",
        "fullpage": "/static/js/fullpage.8.8.4.js",
        "beeline": "/static/js/beeline.1.0.1.js",
        "static_servers": ["static.geetest.com/", "dn-staticdown.qbox.me/"],
        "slide": "/static/js/slide.7.6.3.js",
        "maze": "/static/js/maze.1.0.1.js",
        "aspect_radio": {
                "click": 128,
                "pencil": 128,
                "beeline": 50,
                "voice": 128,
                "slide": 103
        },
        "voice": "/static/js/voice.1.2.0.js",
        "pencil": "/static/js/pencil.1.0.3.js",
        "type": "fullpage",
        "click": "/static/js/click.2.8.5.js",
        "geetest": "/static/js/geetest.6.0.9.js",
        "cc": 4,
        "ww": true,
        "i":     "46207!!219363!!CSS1Compat!!334!!-1!!-1!!-1!!-1!!1!!-1!!-1!!9!!60!!45!!9!!15!!-1!!-1!!-1!!-1!!-1!!1!!-1!!-1!!231!!2!!-1!!-1!!-1!!157!!23!!44!!23!!1396!!279!!1396!!877!!zh-CN!!zh-CN,zh,zh-TW,zh-HK,en-US,en!!-1!!1!!24!!Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0!!1!!1!!1680!!1050!!1636!!1027!!1!!1!!1!!-1!!MacIntel!!1!!-8!!805a6cdeadd4f48ade985597f74928cb!!cc03697d39800df1ef0d2229132a62e8!!!!0!!-1!!0!!4!!AndaleMono,Arial,ArialBlack,ArialNarrow,ArialRoundedMTBold,ArialUnicodeMS,ComicSansMS,Courier,CourierNew,Geneva,Georgia,Helvetica,HelveticaNeue,Impact,LUCIDAGRANDE,MicrosoftSansSerif,Monaco,Palatino,Tahoma,Times,TimesNewRoman,TrebuchetMS,Verdana!!1568168747074!!-1,-1,-15,0,0,0,0,97,230,2,134245,8,7,441,444,992,3151782,3151782,3151970,-1!!-1!!-1!!577!!75!!49!!222!!75!!false!!false"
}

至此,AES加密分析結束,整理的python代碼如下:

import base64
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex

class PrpCrypt(object):
    def __init__(self, key):
        self.key = key.encode('utf-8')
        self.mode = AES.MODE_CBC

    # 加密函數,如果text不足16位就用空格補足爲16位,
    # 如果大於16當時不是16的倍數,那就補足爲16的倍數。
    def encrypt(self, text):
        text = text.encode('utf-8')
        cryptor = AES.new(self.key, self.mode, b'0000000000000000')
        # 這裏密鑰key 長度必須爲16(AES-128),
        # 24(AES-192),或者32 (AES-256)Bytes 長度
        # 目前AES-128 足夠目前使用
        length = 16
        count = len(text)
        if count < length:
            add = (length - count)
            # \0 backspace
            # text = text + ('\0' * add)
            text = text + ('0' * add).encode('utf-8')
        elif count > length:
            add = (length - (count % length))
            # text = text + ('\0' * add)
            text = text + ('0' * add).encode('utf-8')
        self.ciphertext = cryptor.encrypt(text)
        # 因爲AES加密時候得到的字符串不一定是ascii字符集的,輸出到終端或者保存時候可能存在問題
        # 所以這裏統一把加密後的字符串轉化爲16進制字符串
        return b2a_hex(self.ciphertext)

    # 解密後,去掉補足的空格用strip() 去掉
    def decrypt(self, text):
        cryptor = AES.new(self.key, self.mode, b'0000000000000000')
        plain_text = cryptor.decrypt(a2b_hex(text))
        # return plain_text.rstrip('\0')
        return bytes.decode(plain_text).rstrip('\0')

if __name__ == '__main__':
    txt = "{\"gt\":\"2328764cdf162e8e60cc0b04383fef81\",...}"
    pc = PrpCrypt('92c689c0f4282f4e')  # 初始化密鑰
    e = pc.encrypt(txt)  # 加密
    d = pc.decrypt(e)  # 解密
    print("加密:", e)
    print("解密:", d)

後記

對比發現,之前記錄的e的值就是這裏待加密內容中 i的值gt 和 challenge可以從上一章的分析中得到,AES加密之後,將加密的結果base64編碼處理。

至此,我們知道,其大致的交互方式爲:

  1. 用時間戳生成AES的密鑰,並將密鑰通過RSA加密。
  2. 通過AES+密鑰將要提交的內容加密。
  3. 將提交內容的密文與密鑰的密文拼接成w參數進行提交,w參數的格式爲: base64(aes(json))+rsa(aes的密鑰)

本章遺留了如下兩個問題:

  1. AES的密鑰如何生成。
  2. e的值是什麼內容,如何計算得到。

限於篇幅,這兩個問題,待到下一篇《極驗反爬蟲防護分析之接口交互的解密方法補遺》 中進行分析。

 

轉載:https://www.52pojie.cn/thread-1162893-1-1.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章