某圖片站反爬加密字段x-api-key破解


前言

此次逆向的是某“你們都懂”領域的圖片站,目前此站限制注冊,非會員無法訪問;前兩天偶然搞到了份邀請碼,進入后發現質量還可以,於是嘗試爬取,在爬蟲編寫過程中發現此站點采用了不少手段來阻止自動化腳本(或者重放攻擊),可以作為一個比較有代表性的爬蟲逆向案例,故記錄於此。

分析過程

登錄進來后,發現頁面顯示了一段Loading動畫,然后才自上而下加載了出來,右鍵查看主頁源代碼

<!DOCTYPE html> <html lang="zh-Hans"> <head> <title>Loading... - Poi</title> <meta charset="utf-8">.......

  

這里基本就可以確定,是異步加載類的資源站,而且在代碼底部還有vendor.js,大概是用vue開發的,傳統的頁面元素定位法在這里不適用,應該是要找接口了。由於我希望整合進系統(見此前的E站爬蟲文章)的爬蟲入口是畫冊頁的鏈接,所以暫時不需要對index頁進行分析,開Charles,隨便點開一本,找到鏈接對應的條目,重點關注headers和cookies

 

 首先嘗試ctrl c+v大法,直接復制headers和cookies構造一模一樣的請求,這招在不少登錄驗證網站都是有用的,但在此網站並未起效:網站返回了一個友好錯誤頁面,並提示不要搞事情,顯然cookies或headers里有某些時變或由算法在本地動態生成的字段。事實上,cookie里的st很明顯就是一個時間戳,而其余幾個字段也基本都是口令或id的意思,想要了解這些字段的產生,或許得從登錄開始分析。 

從抓包結果來看,登錄分為兩個過程,https://xxxx.com/auth/login先GET,然后POST,其中的POST提交為json序列化后的用戶名密碼數據,GET中的response有set cookie操作,為st和poi_session賦了值,而POST時request攜帶的cookie依舊是這兩個(還有三個谷歌統計的cookies),所以cookies不需太關注。但headers卻增加了一個關鍵字段:

 

字符串里兩個連等號,這基本就是base64編碼的標志,但等號卻出現在了前面,應該是做了一次逆序,逆序后解碼

 

依舊沒什么規律,大概率是用js動態生成的,那么想解析就需要找到生成函數和函數傳參。回到Charles,在login頁面的GET方法時序后,POST前,有三個js文件和一個/env目錄的頁面被請求了,三個js文件分別是manifest.js, vendor.js和app.js,實錘是拿webpack打包的了,里面都是好幾千行,先放在一邊;/env請求時發現請求頭沒有異常的字段,說明x-api-key生成很可能在它之后,此頁面返回了一個json, 其中比較引人注意的一個字段是client_secret

  

哎咋這么眼熟......跟上面我們解碼出的base64字符串相比,雖然順序不一樣,但基礎字符似乎是一致的,兩者之間肯定存在某種聯系。

(筆者其實一開始並沒有發現這點,到后面找到生成函數才反應過來的)

/env到此已經沒什么線索了,login頁面本身的<script>標簽內也沒有什么信息,x-api-key的生成函數只可能位於三個js文件內,app.js是程序入口文件,從這里開始分析是比較合理的,而且這種明顯是自創的加密字段也不太可能是第三方庫。首先想到的是打斷點,但這不是點擊事件,單步調試又基本沒有可操作性,於是嘗試通過關鍵字定位函數;app.js中搜索x-api-key,然而......並沒有,但搜索authtoken是能找到相關函數定義和多出調用的,像這類功能相近的函數沒理由分散在不同的文件里。

從另一個角度思考,js語句要在headers里增加一項,除了字段名外,語句里也會出現"headers"字樣,那搜索"headers"呢?出現的地方並不多,結果在約3/4的位置找到了這么一段

(function(e){return e.headers.common[atob(atob("V0MxaFVHa3RTMFY1")).toUpperCase()]=t.e()

atob是base64解碼函數,把"V0MxaFVHa3RTMFY1"解碼兩次看看

 

發現了,這段話就是x-api-key計算的核心語句!接下來看看t和e都是什么。

往上找,跟t最近相關的是這樣一段

function() {
    var t = this, e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0; this.initUserState() .then((function() { return t.initialized = !0 })) .catch((function() { e < 2 && setTimeout((function() { return t.initUser(++e) }), 2e3) })) }

t = this,所以重點還在e上。本段提到的e顯然是一個數值類型,不是方法,繼續尋找

 

由於app.js內大量的變量名重用,通過調用關系定位e()很困難,但根據js內的函數定義風格,e的定義一定是這樣的

e:function(){.....

果然查找到了

e: function() {
    var t = this.env.client_secret, e = this.$moment() .unix() + this.serverTimeOffset, n = (Math.pow(e, 2) + Math.pow(navigator.userAgent.length, 2)) .toString() .split("") .map((function(e) { return t[e] })) .join(""); return btoa(n) .split("") .reverse() .join("") }

看到這里涉及到了取當前時間戳,瀏覽器頭"user-agent"長度,平方運算,最后把得到的整數分割成單個數字,map取到client_secret的值,而client_secret之前已經獲取到了,還差一個serverTimeOffset,搜索后找到它的定義函數

setServerTimeOffset: function() {
    var t = Math.floor((window.performance.timing.responseEnd - window.performance .timing.responseStart) / 1e3) || 0; t = t >= 0 ? t : 0, this.serverTimeOffset = Number(cookies.get("st")) + t - this.$moment() .unix() }

t由請求報文的時延決定,幾百毫秒的延時,運算結果認為是0即可(不嚴謹,但大多數時候沒問題),所以serverTimeOffset就是cookies的st值減去當前時間,到此x-api-key的所有運算參數都獲得了,用Python寫就是

client_secret = self.env.get("client_secret")
serverTimeOffset=int(self.session.cookies.get("st"))+0-int(time.time()) e = int(time.time())+serverTimeOffset n = "".join(map(lambda x: client_secret[int(x)], str(pow(e,2)+pow(len(head['user-agent']),2)))) x_api_key = str(base64.b64encode(n.encode("utf-8")), "utf-8")[::-1]

至此x-api-key的構造分析完畢,接下來進入畫冊詳情頁的分析。


 

詳情頁的headers和cookies未有特別之處,sentinel和auth_token分別在login的POST和GET index頁時由set cookie添加。

詳情頁同樣是異步加載,內容的接口如下圖,用GET方法獲取。

 

 

 

 headers部分除了x-api-key外,多了authorization,值就是"Bearer "+auth_token,很簡單,但它返回json里的數據有些不是明文

  

 等號在前,果斷逆序解碼,獲得標題。如果沒想到逆序的話,在app.js里搜索"encrypt"或"title",也能搜到加解密函數的定義,思想與上面其實是一致的。

圖片資源列表也在此json中,以明文儲存,雖然不能直接用所給的地址下載圖片,但用正則提取出特征碼后,即可拼接出真正的圖片地址。

 

最后一個坑在心跳包上,因為筆者發現此網站的每個頁面都會隔120s往/heartbeat發一個心跳包,一開始並沒在意,后來才發現,heartbeat會更新cookies里的st字段值,x-api-key是用st值算出來的,而每個帶x-api-key字段的請求發生時,x-api-key要重新運算更新!如果st的值小於當前時間120秒,那算出來的x-api-key就會非法!表現為在下載完一本漫書(通常耗時超兩分鍾)后,訪問新頁面就會401,解決的話倒也不用真2分鍾發一次,只需要在請求新頁面前幾秒發一個心跳包,令st得到更新即可。


 2020/03/04更新:/env的返回值里還有一個expired字段,當時間超過expired所指定的時間戳后,auth_token值就會失效,需要再任意請求站內一個頁面,來更新auth_token值。

總結

逆向此網站花了一天時間,非專業人員,手法比較生疏,如果說有一些感受,那就是對前后端分離設計的網站,抓包時注意包的時序;定位js函數時,功能相近的很多時候也會寫在一起;有些字段找不到時,編碼成base64再試試,以及細心觀察。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM