JavaScript 逆向爬取實戰(zhàn)(下)
“
閱讀本文大概需要 20?分鐘。
這一篇是 JavaScript 逆向爬取的第二篇。那么接下來我為大家縷順一下學(xué)習(xí)順序。
系列文章的第一篇啟于總結(jié)一些網(wǎng)站加密和混淆技術(shù),這篇文章我們介紹了網(wǎng)頁防護(hù)技術(shù),包括接口加密和 JavaScript 壓縮、加密和混淆。能夠為學(xué)習(xí) JavaScript 逆向爬取奠定堅實的基礎(chǔ)。
接下來就是 JavaScript 逆向爬取的第一篇JavaScript 逆向爬取實戰(zhàn)。分為上下章發(fā)出是因為確實寫得太長了(手動狗頭)。
那么話不多說,我們開始今天的學(xué)習(xí)吧~
詳情頁加密 id 入口的尋找
好,那么我們觀察下上一步的輸出結(jié)果,我們把結(jié)果格式化一下,看看部分結(jié)果:
{'count':100,'results':[{'id':1,'name':'霸王別姬','alias':'Farewell My Concubine','cover':'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c','categories':['劇情','愛情'],'published_at':'1993-07-26','minute':171,'score':9.5,'regions':['中國大陸','中國香港']},...]}
這里我們看到有個 id 是 1,另外還有一些其他的字段如電影名稱、封面、類別等等,那么這里面一定有什么信息是用來唯一區(qū)分某個電影的。
但是呢,這里我們點擊下第一個部電影的信息,可以看到它跳轉(zhuǎn)到了 URL 為 https://dynamic6.scrape.cuiqingcai.com/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx 的頁面,可以看到這里 URL 里面有一個加密 id 為 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx,那么這個和電影的這些信息有什么關(guān)系呢?
這里,如果你仔細(xì)觀察規(guī)律其實是可以比較容易地找出規(guī)律來的,但是這總歸是觀察出來的,如果遇到一些觀察不出規(guī)律的那就歇菜了。所以還是需要靠技巧去找到它真正加密的位置。
這時候我們該怎么辦呢?
分析一下,這個加密 id 到底是什么生成的。
我們在點擊詳情頁的時候就看到它訪問的 URL 里面就帶上了 ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx 這個加密 id 了,而且不同的詳情頁的加密 id 是不同的,這說明這個加密 id 的構(gòu)造依賴于列表頁 Ajax 的返回結(jié)果,所以可以確定這個加密 id 的生成是發(fā)生在 Ajax 請求完成后或者點擊詳情頁的一瞬間。
那為了進(jìn)一步確定是發(fā)生在何時,我們看看頁面源碼,可以看到在沒有點擊之前,詳情頁鏈接的 href 里面就已經(jīng)帶有加密 id 了,如圖所示。

由此我們可以確定,這個加密 id 是在 Ajax 請求完成之后生成的,而且肯定也是由 JavaScript 生成的了。
那怎么再去找 Ajax 完成之后的事件呢?是否應(yīng)該去找 Ajax 完成之后的事件呢?
可以是可以,可以試試,我們可以在 Sources 面板的右側(cè),有一個 Event Listener Breakpoints,這里有一個 XHR 的監(jiān)聽,包括發(fā)起時、成功后、發(fā)生錯誤時的一些監(jiān)聽,這里我們勾選上 readystatechange 事件,代表 Ajax 得到響應(yīng)時的事件,其他的斷點可以都刪除了,然后刷新下頁面看下,如圖所示。

這里我們可以看到就停在了 Ajax 得到響應(yīng)時的位置了。
那這里我們怎么再去找這個 id 怎么加密的呢?這里可以選擇一點點斷點找下去,但估計找的過程會崩潰掉,因為這里可能會會逐漸調(diào)用到頁面 UI 渲染的一些底層實現(xiàn),甚至可能找著找著都不知道找到哪里去了。
那怎么辦呢?這里我們再介紹一種定位的方法,那就是 Hook。
Hook 技術(shù)中文又叫做鉤子技術(shù),它就是在程序運(yùn)行的過程中,對其中的某個方法進(jìn)行重寫,在原先的方法前后加入我們自定義的代碼。相當(dāng)于在系統(tǒng)沒有調(diào)用該函數(shù)之前,鉤子程序就先捕獲該消息,鉤子函數(shù)先得到控制權(quán),這時鉤子函數(shù)既可以加工處理(改變)該函數(shù)的執(zhí)行行為。
通俗點來說呢,比如我要 Hook 一個方法 a,我可以先臨時用一個變量存一下,把它存成 _a,然后呢,我再重新聲明一個方法 a,里面加點自己的邏輯,比如加點調(diào)試語句、輸出語句等等,然后再調(diào)用下 _a,這里調(diào)用的 _a 就是之前的 a。那這樣就相當(dāng)于新的方法 a 里面混入了我們自己定義的邏輯,同時又把原來的方法 a 也執(zhí)行了一遍。所以這不會影響原有的執(zhí)行邏輯和運(yùn)行效果,但是我們通過這種改寫就順利在原來的 a 方法前后加上了我們自己的邏輯,這就是 Hook。
那么,我們這里怎么用 Hook 的方式來找到加密 id 的加密入口點呢?
想一下,這個加密 id 是一個 Base64 編碼的字符串,那么生成過程中想必就調(diào)用了 JavaScript 的 Base64 編碼的方法,這個方法名叫做 btoa,這個 btoa 方法可以將參數(shù)轉(zhuǎn)化成 Base64 編碼。當(dāng)然 Base64 也有其他的實現(xiàn)方式,比如利用 crypto-js 這個庫實現(xiàn)的,這個可能底層調(diào)用的就不是 btoa 方法了。
所以,我們其實現(xiàn)在并不確定是不是調(diào)用的 btoa 方法實現(xiàn)的 Base64 編碼,那就先試試吧。
要實現(xiàn) Hook,其實關(guān)鍵在于將原來的方法改寫,這里我們其實就是 Hook btoa 這個方法了,btoa 這個方法屬于 window 對象,我們將 window 對象的 btoa 方法進(jìn)行改寫即可。
改寫的邏輯如下:
(function(){'use strict'function hook(object, attr){var func =object[attr]object[attr]=function(){console.log('hooked',object, attr, arguments)var ret = func.apply(object, arguments)debuggerconsole.log('result', ret)return ret}}hook(window,'btoa')})()
我們定義了一個 hook 方法,傳入 object 和 attr 參數(shù),意思就是 Hook object 對象的 attr 參數(shù)。例如我們?nèi)绻?Hook 一個 alert 方法,那就把 object 設(shè)置為 window,把 attr 設(shè)置為 alert 字符串。這里我們想要 Hook Base64 的編碼方法,那么這里我們就只需要 Hook window 對象的 btoa 方法就好了。
我們來看下,首先一句?var func = object[attr],相當(dāng)于我們先把它賦值為一個變量,我們調(diào)用 func 方法就可以實現(xiàn)和原來相同的功能。接著,我們再直接改寫這個方法的定義,直接改寫?object[attr],將其改寫成一個新的方法,在新的方法中,通過?func.apply?方法又重新調(diào)用了原來的方法。這樣我們就可以保證,前后方法的執(zhí)行效果是不受什么影響的,之前這個方法該干啥就還是干啥的。但是和之前不同的是,我們自定義方法之后,現(xiàn)在可以在?func?方法執(zhí)行的前后,再加入自己的代碼,如?console.log?將信息輸出到控制臺,如?debugger?進(jìn)入斷點等等。這個過程中,我們先臨時保存下來了?func?方法,然后定義一個新的方法,接管程序控制權(quán),在其中自定義我們想要的實現(xiàn),同時在新的方法里面再重新調(diào)回?func?方法,保證前后結(jié)果是不受影響的。所以,我們達(dá)到了在不影響原有方法效果的前提下,可以實現(xiàn)在方法的前后實現(xiàn)自定義的功能,就是 Hook 的完整實現(xiàn)過程。
最后,我們調(diào)用 hook 方法,傳入 window 對象和 btoa 字符串即可。
那這樣,怎么去注入這個代碼呢呢?這里我們介紹三種注入方法。
?直接控制臺注入?復(fù)寫 JavaScript 代碼?Tampermonkey 注入
控制臺注入
對于我們這個場景,控制臺注入其實就夠了,我們先來介紹這個方法。
這個其實很簡單了,就是直接在控制臺輸入這行代碼運(yùn)行,如圖所示。

執(zhí)行完這段代碼之后,相當(dāng)于我們就已經(jīng)把 window 的 btoa 方法改寫了,可以控制臺調(diào)用下 btoa 方法試試,如:
btoa('germey')回車之后就可以看到它進(jìn)入了我們自定義的 debugger 的位置停下了,如圖所示。

我們把斷點向下執(zhí)行,點擊 Resume 按鈕,然后看看控制臺的輸出,可以看到也輸出了一些對應(yīng)的結(jié)果,如被 Hook 的對象,Hook 的屬性,調(diào)用的參數(shù),調(diào)用后的結(jié)果等,如圖所示。

那這里我們就可以看到,我們通過 Hook 的方式改寫了 btoa 方法,使其每次在調(diào)用的時候都能停到一個斷點,同時還能輸出對應(yīng)的結(jié)果。
好,那接下來怎么用 Hook 找到對應(yīng)的加密 id 的加密入口呢?
由于此時我們是在控制臺直接輸入的 Hook 代碼,所以頁面一旦刷新就無效了,但由于我們這個網(wǎng)站是 SPA 式的頁面,所以在點擊詳情頁的時候頁面是不會整個刷新的,所以這段代碼依然還會生效。但是如果不是 SPA 式的頁面,即每次訪問都需要刷新頁面的網(wǎng)站,這種注入方式就不生效了。
好,那我們的目的是為了 Hook 列表頁 Ajax 加載完成后的的加密 id 的 Base64 編碼的過程,那怎么在不刷新頁面的情況下再次復(fù)現(xiàn)這個操作呢?很簡單,點下一頁就好了。
這時候我們可以點擊第 2 頁的按鈕,這時候可以看到它確實再次停到了 Hook 方法的 debugger 處,由于列表頁的 Ajax 和加密 id 都會帶有 Base64 編碼的操作,因此它每一個都能 Hook 到,通過觀察對應(yīng)的 Arguments 或當(dāng)前網(wǎng)站的行為或者觀察棧信息,我們就能大體知道現(xiàn)在走到了哪個位置了,從而進(jìn)一步通過棧的調(diào)用信息找到調(diào)用 Base64 編碼的位置。
我們可以根據(jù)調(diào)用棧的信息來觀察這些變量在哪一層發(fā)生變化的,比如最后的這一層,我們可以很明顯看到它執(zhí)行了 Base64 編碼,編碼前的結(jié)果是:
ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1編碼后的結(jié)果是:
ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx如圖所示。

這里很明顯。
那么核心問題就來了,編碼前的結(jié)果?ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1又是怎么來的呢?我們展開棧的調(diào)用信息,一層層看看這個字符串的變化情況。如果不變那就看下一層,如果變了那就停下來仔細(xì)看看。
最后我們可以在第五層找到它的變化過程,如圖所示。

那這里我們就一目了然了,看到了?_0x135c4d?是一個寫死的字符串?ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb,然后和傳入的這個?_0x565f18?拼接起來就形成了最后的字符串。
那這個?_0x565f18?又是怎么來的呢?再往下追一層,那就一目了然了,其實就是 Ajax 返回結(jié)果的單個電影信息的 id。
所以,這個加密邏輯的就清楚了,其實非常非常簡單,就是?ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb1?加上電影 id,然后 Base64 編碼即可。
到此,我們就成功用 Hook 的方式找到加密的 id 生成邏輯了。
但是想想有什么不太科學(xué)的地方嗎?剛才其實也說了,我們的 Hook 代碼是在控制臺手動輸入的,一旦刷新頁面就不生效了,這的確是個問題。而且它必須是在頁面加載完了才注入的,所以它并不能在一開始就生效。
下面我們再介紹幾種 Hook 注入方式
重寫 JavaScript
我們可以借助于 Chrome 瀏覽器的 Overrides 功能實現(xiàn)某些 JavaScript 文件的重寫和和保存,它會在本地生成一個 JavaScript 文件副本,以后每次刷新的時候會使用副本的內(nèi)容。
這里我們需要切換到 Sources 選項卡的 Overrides 選項卡,然后選擇一個文件夾,比如這里我自定了一個文件夾名字叫做 modify,如圖所示。

然后我們隨便選一個 JavaScript 腳本,后面貼上這段注入腳本,如圖所示。

保存文件。
此時可能提示頁面崩潰,但是不用擔(dān)心,重新刷新頁面就好了,這時候我們就發(fā)現(xiàn)現(xiàn)在瀏覽器加載的 JavaScript 文件就是我們修改過后的了,文件的下方會有一個標(biāo)識符,如圖所示。

同時我們還注意到這時候它就直接進(jìn)入了斷點模式,成功 Hook 到了 btoa 這個方法了。
其實這個 Overrides 這個功能非常有用,有了它我們可以持久化保存我們?nèi)我庑薷牡?JavaScript 代碼,所以我們想在哪里改都可以了,甚至可以直接修改 JavaScript 的原始執(zhí)行邏輯也都是可以的。
Tampermonkey 注入
如果我們不想用 Overrides 的方式改寫 JavaScript 的方式注入的話,還可以借助于瀏覽器插件來實現(xiàn)注入,這里推薦的瀏覽器插件叫做 Tampermonkey,中文叫做油猴。它是一款瀏覽器插件,支持 Chrome。利用它我們可以在瀏覽器加載頁面時自動執(zhí)行某些 JavaScript 腳本。由于執(zhí)行的是 JavaScript,所以我們幾乎可以在網(wǎng)頁中完成任何我們想實現(xiàn)的效果,如自動爬蟲、自動修改頁面、自動響應(yīng)事件等等。
首先我們需要安裝 Tampermonkey,這里我們使用的瀏覽器是 Chrome。直接在 Chrome 應(yīng)用商店或者在 Tampermonkey 的官網(wǎng) https://www.tampermonkey.net/ 下載安裝即可。
安裝完成之后,在 Chrome 瀏覽器的右上角會出現(xiàn) Tampermonkey 的圖標(biāo),這就代表安裝成功了。

我們也可以自己編寫腳本來實現(xiàn)想要的功能。編寫腳本難不難呢?其實就是寫 JavaScript 代碼,只要懂一些 JavaScript 的語法就好了。另外除了懂 JavaScript 語法,我們還需要遵循腳本的一些寫作規(guī)范,這其中就包括一些參數(shù)的設(shè)置。
下面我們就簡單實現(xiàn)一個小的腳本,實現(xiàn)某個功能。
首先我們可以點擊 Tampermonkey 插件圖標(biāo),點擊「管理面板」按鈕,打開腳本管理頁面。

界面類似顯示如下圖所示。

在這里顯示了我們已經(jīng)有的一些 Tampermonkey 腳本,包括我們自行創(chuàng)建的,也包括從第三方網(wǎng)站下載安裝的。
另外這里也提供了編輯、調(diào)試、刪除等管理功能,我們可以方便地對腳本進(jìn)行管理。
接下來我們來創(chuàng)建一個新的腳本來試試,點擊左側(cè)的「+」號,會顯示如圖所示的頁面。

初始化的代碼如下:
// ==UserScript==// @name New Userscript// @namespace http://tampermonkey.net/// @version 0.1// @description try to take over the world!// @author You// @match https://www.tampermonkey.net/documentation.php?ext=dhdg// @grant none// ==/UserScript==(function(){'use strict';// Your code here...})();
這里最上面是一些注釋,但這些注釋是非常有用的,這部分內(nèi)容叫做?UserScript Header?,我們可以在里面配置一些腳本的信息,如名稱、版本、描述、生效站點等等。
在?UserScript Header?下方是 JavaScript 函數(shù)和調(diào)用的代碼,其中?'use strict'?標(biāo)明代碼使用 JavaScript 的嚴(yán)格模式,在嚴(yán)格模式下可以消除 Javascript 語法的一些不合理、不嚴(yán)謹(jǐn)之處,減少一些怪異行為,如不能直接使用未聲明的變量,這樣可以保證代碼的運(yùn)行安全,同時提高編譯器的效率,提高運(yùn)行速度。在下方?// Your code here...?這里我們就可以編寫自己的代碼了。
我們可以將腳本改寫為如下內(nèi)容:
// ==UserScript==// @name HookBase64// @namespace https://scrape.cuiqingcai.com/// @version 0.1// @description Hook Base64 encode function// @author Germey// @match https://dynamic6.scrape.cuiqingcai.com/// @grant none// @run-at document-start// ==/UserScript==(function(){'use strict'function hook(object, attr){var func =object[attr]console.log('func', func)object[attr]=function(){console.log('hooked',object, attr)var ret = func.apply(object, arguments)debuggerreturn ret}}hook(window,'btoa')})()
這時候啟動腳本,重新刷新頁面,可以發(fā)現(xiàn)也可以成功 Hook 住 btoa 方法,如圖所示。

然后我們再順著找調(diào)用邏輯就好啦。
以上,我們就成功通過 Hook 的方式找到加密 id 的實現(xiàn)了。
詳情頁 Ajax 的 token 尋找
現(xiàn)在我們已經(jīng)找到詳情頁的加密 id 了,但是還差一步,其 Ajax 請求也有一個 token,如圖所示。

其實這個 token 和詳情頁的 token 構(gòu)造邏輯是一樣的了。
這里就不再展開說了,可以運(yùn)用上文的幾種找入口的方法來找到對應(yīng)的加密邏輯。
Python 實現(xiàn)詳情頁爬取
現(xiàn)在我們已經(jīng)成功把詳情頁的加密 id 和 Ajax 請求的 token 找出來了,下一步就能使用 Python 完成爬取了,這里我就只實現(xiàn)第一頁的爬取了,代碼示例如下:
import hashlibimport timeimport base64from typing importList,Anyimport requestsINDEX_URL ='https://dynamic6.scrape.cuiqingcai.com/api/movie?limit={limit}&offset={offset}&token={token}'DETAIL_URL ='https://dynamic6.scrape.cuiqingcai.com/api/movie/{id}?token={token}'LIMIT =10OFFSET =0SECRET ='ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb'def get_token(args:List[Any]):timestamp = str(int(time.time()))args.append(timestamp)sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest()return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8')args =['/api/movie']token = get_token(args=args)index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token)response = requests.get(index_url)print('response', response.json())result = response.json()for item in result['results']:id = item['id']encrypt_id = base64.b64encode((SECRET + str(id)).encode('utf-8')).decode('utf-8')args =[f'/api/movie/{encrypt_id}']token = get_token(args=args)detail_url = DETAIL_URL.format(id=encrypt_id, token=token)response = requests.get(detail_url)print('response', response.json())
這里模擬了詳情頁的加密 id 和 token 的構(gòu)造過程,然后請求了詳情頁的 Ajax 接口,這樣我們就可以爬取到詳情頁的內(nèi)容了。
總結(jié)
本節(jié)內(nèi)容很多,一步步介紹了整個網(wǎng)站的 JavaScript 逆向過程,其中的技巧有:
?全局搜索查找入口?代碼格式化?XHR 斷點?變量監(jiān)聽?斷點設(shè)置和跳過?棧查看?Hook 原理?Hook 注入?Overrides 功能?Tampermonkey 插件?Python 模擬實現(xiàn)
掌握了這些技巧我們就能更加得心應(yīng)手地實現(xiàn) JavaScript 逆向分析。
本節(jié)代碼:https://github.com/Python3WebSpider/ScrapeDynamic6
