本文要分享的內容是去年爲了搶鞋而分析 極驗(GeeTest)反爬蟲防禦的筆記,因爲篇幅較長(爲了多混點CB)我會按照個人分析順序,分紅以下四個主題與你們分享:html
本文是第二篇《接口交互的解密方法》,書接上文,上一篇中,咱們遺留了兩個問題:前端
下面進入正文~python
geetest的js代碼不單是簡單的壓縮,應該是通過混淆加密的,下載一個geetest的js文件,格式化並還原編碼後,會發現js加載以後直接執行了幾個嵌套的大循環,猜想應該是經過decodeURI對關鍵信息進行解密,拼裝成一個大數組,這樣便達到隱藏關鍵代碼和信息目的,以下圖:json
如此,程序的代碼即可以寫成封裝成一個方法,經過數組下標來拼接程序代碼,達到隱藏關鍵信息,預防靜態分析的目的。根據個人經驗,目前市面上好多前端代碼都採用了這種代碼加密思路,好比 同花順數據中心的頁面數據,爲了防止接口被cors,構造了自定義的cookie信息,對應的js構造代碼也是這類加密思路。因爲還原此JS代碼並非本文的目的,因此咱們就直接說調試分析方法,對還原代碼有興趣的童鞋能夠本身再深刻分析。數組
如上說明,因爲官網加載的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進行覆蓋便可。cookie
瀏覽器中定位前端代碼,最有效的方法莫過於經過 瀏覽器事件 + 條件斷點的方式來定位了,可是因爲上面說的代碼加密的緣由,這種定位方法在這裏失效了,不論什麼事件最終都會進入到上面說的大數組解析的方法中去,以下圖:app
因此只能經過xhr請求入手,經過棧回溯的方法定位代碼。值得慶幸的是火狐瀏覽器原生就支持堆棧跟蹤,省了咱們很多時間,以下圖:cors
經過js下斷點不斷回溯,發現上圖中紅框部分是加密的代碼部分,下斷點,從新點擊登陸框,程序中斷,以下圖:ide
其中變量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
單步步入跟進變量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加密的方法以前,意外發現了上下文變量中的信息,以下圖:
至此,咱們知道上面遺留待分析的字符串: 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編碼處理。
至此,咱們知道,其大體的交互方式爲:
w參數
的格式爲: base64(aes(json))+rsa(aes的密鑰)
本章遺留了以下兩個問題:
限於篇幅,這兩個問題,待到下一篇《極驗反爬蟲防禦分析之接口交互的解密方法補遺》 中進行分析。