文 | 崔慶才
出品 | 進(jìn)擊的Coder(ID:FightingCoder)
已獲得原公眾號的授權(quán)轉(zhuǎn)載
“注:本文來自《Python3網(wǎng)絡(luò)爬蟲開發(fā)實(shí)戰(zhàn)(第二版)》一書。
”
現(xiàn)在越來越多的網(wǎng)站也已經(jīng)應(yīng)用了這些技術(shù)對其數(shù)據(jù)接口進(jìn)行了保護(hù),在做爬蟲時(shí)如果我們遇到了這種情況,我們可能就不得不硬著頭皮來去想方設(shè)法找出其中隱含的關(guān)鍵邏輯了,這個(gè)過程我們可以稱之為 JavaScript 逆向。
既然我們要做 JavaScript 逆向,那少不了要用到瀏覽器的開發(fā)者工具,因?yàn)榫W(wǎng)頁是在瀏覽器中加載的,所以多數(shù)的調(diào)試過程也是在瀏覽器中完成的。
工欲善其事,必先利其器。本節(jié)我們先來基于 Chrome 瀏覽器介紹一下瀏覽器開發(fā)者工具的使用。但由于開發(fā)者工具功能十分復(fù)雜,本節(jié)主要介紹對 JavaScript 逆向有一些幫助的功能,學(xué)會了這些,我們在做 JavaScript 逆向調(diào)試的過程會更加得心應(yīng)手。
本節(jié)我們以一個(gè)示例網(wǎng)站 https://spa2.scrape.center/ 來做演示,用這個(gè)示例來介紹瀏覽器開發(fā)者工具各個(gè)面版的用法。
首先我們用 Chrome 瀏覽器打開示例網(wǎng)站,頁面如圖所示:
接下來打開開發(fā)者工具,我們會看到類似圖 xx 所示的結(jié)果。
這里可以看到多個(gè)面板標(biāo)簽,如 Elements、Console、Sources 等,這就是開發(fā)者工具的一個(gè)個(gè)面板,功能豐富而又強(qiáng)大,先對面板作下簡單的介紹:
了解了這些面板之后,我們來深入了解幾個(gè)面板中對 JavaScript 調(diào)試很有幫助的功能。
之前我們是用 Elements 面板來審查頁面的節(jié)點(diǎn)信息的,我們可以查看當(dāng)前頁面的 HTML 源代碼及其在網(wǎng)頁中對應(yīng)的位置,查看某個(gè)條目的標(biāo)題對應(yīng)的頁面源代碼,如圖所示。
點(diǎn)擊右側(cè)的 Styles 選項(xiàng)卡,可以看到對應(yīng)節(jié)點(diǎn)的 CSS 樣式,我們可以自行在這里增刪樣式,實(shí)時(shí)預(yù)覽效果,這對網(wǎng)頁開發(fā)十分有幫助。
在 Computed 選項(xiàng)卡中還可以看到當(dāng)前節(jié)點(diǎn)的盒子模型,比如外邊距、內(nèi)邊距等,還可以看到當(dāng)前節(jié)點(diǎn)最終計(jì)算出的 CSS 的樣式,如圖所示。
接下來切換到右側(cè)的 Event Listeners 選項(xiàng)卡,這里可以顯示各個(gè)節(jié)點(diǎn)當(dāng)前已經(jīng)綁定的事件,都是 JavaScript 原生支持的,下面簡單列舉幾個(gè)事件。
change
:HTML 元素改變時(shí)會觸發(fā)的事件。click
:用戶點(diǎn)擊 HTML 元素時(shí)會觸發(fā)的事件。mouseover
:用戶在一個(gè) HTML 元素上移動鼠標(biāo)會觸發(fā)的事件。mouseout
:用戶從一個(gè) HTML 元素上移開鼠標(biāo)會觸發(fā)的事件。keydown
:用戶按下鍵盤按鍵會觸發(fā)的事件。load
:瀏覽器完成頁面加載時(shí)會觸發(fā)的事件。通常,我們會給按鈕綁定一個(gè)點(diǎn)擊事件,它的處理邏輯一般是由 JavaScript 定義的,這樣在我們點(diǎn)擊按鈕的時(shí)候,對應(yīng)的 JavaScript 代碼便會執(zhí)行。比如在圖 xx 中,我們選中切換到第 2 頁的節(jié)點(diǎn),右側(cè) Event Listeners 選項(xiàng)卡下會看到它綁定的事件。
這里有對應(yīng)事件的代碼位置,內(nèi)容為一個(gè) JavaScript 文件名稱 chunk-vendors.77daf991.js
,然后緊跟一個(gè)冒號,然后再跟了一個(gè)數(shù)字 7。所以對應(yīng)的事件處理函數(shù)是定義在 chunk-vendors.77daf991.js
這個(gè)文件的第 7 行。點(diǎn)擊這個(gè)代碼位置,便會自動跳轉(zhuǎn) Sources 面板,打開對應(yīng)的 chunk-vendors.77daf991.js
文件并跳轉(zhuǎn)到對應(yīng)的位置,如圖所示。
所以,利用好 Event Listeners,我們可以輕松地找到各個(gè)節(jié)點(diǎn)綁定事件的處理方法所在的位置,幫我們在 JavaScript 逆向過程中找到一些突破口。
剛才我們已經(jīng)通過 Event Listeners 找到了對應(yīng)的事件處理方法所在的位置并成功跳轉(zhuǎn)到了代碼所在的位置。
但是,這部分代碼似乎被壓縮過了,可讀性很差,根本沒法閱讀,這時(shí)候應(yīng)該怎么辦呢?
不用擔(dān)心,Sources 面板提供了一個(gè)便捷好用的代碼美化功能。我們點(diǎn)擊代碼面板左下角的格式化按鈕,代碼就會變成如圖所示的樣子。
此時(shí)會新出現(xiàn)一個(gè)叫作 chunk-vendors.77daf991.js:formatted
的選項(xiàng)卡,文件名后面加了 formatted 標(biāo)識,代表這是被格式化的結(jié)果。我們會發(fā)現(xiàn),原來代碼在第 7 行,現(xiàn)在自動對應(yīng)到了第 4445 行,而且對應(yīng)的代碼位置會高亮顯示,代碼可讀性大大增強(qiáng)!
這個(gè)功能在調(diào)試過程中非常常用,用好這個(gè)功能會給我們的 JavaScript 調(diào)試過程帶來極大的便利。
接下來介紹一個(gè)非常重要的功能——斷點(diǎn)調(diào)試。在調(diào)試代碼的時(shí)候,我們可以在需要的位置上打斷點(diǎn),當(dāng)對應(yīng)事件觸發(fā)時(shí),瀏覽器就會自動停在斷點(diǎn)的位置等待調(diào)試,此時(shí)我們可以選擇單步調(diào)試,在面板中觀察調(diào)用棧、變量值,以更好地追蹤對應(yīng)位置的執(zhí)行邏輯。
那么斷點(diǎn)怎么打呢?我們接著以上面的例子來說。首先單擊如圖所示的代碼行號。
這時(shí)候行號處就出現(xiàn)了一個(gè)藍(lán)色的箭頭,這就證明斷點(diǎn)已經(jīng)添加好了,同時(shí)在右側(cè)的 Breakpoints 選項(xiàng)卡下會出現(xiàn)我們添加的斷點(diǎn)的列表。
由于我們知道這個(gè)斷點(diǎn)是用來處理翻頁按鈕的點(diǎn)擊事件的,所以可以在網(wǎng)頁里面點(diǎn)擊按鈕試一下,比如點(diǎn)擊第 2 頁的按鈕,這時(shí)候就會發(fā)現(xiàn)斷點(diǎn)被觸發(fā)了,如圖所示。
這時(shí)候我們可以看到頁面中顯示了一個(gè)叫作 Paused in debugger
的提示,這說明瀏覽器執(zhí)行到剛才我們設(shè)置斷點(diǎn)的位置處就不再繼續(xù)執(zhí)行了,等待我們發(fā)號施令執(zhí)行調(diào)試。
此時(shí)代碼停在了第 4446 行,回調(diào)參數(shù) e
就是對應(yīng)的點(diǎn)擊事件 MouseEvent
。在右側(cè)的 Scope 面板處,可以觀察到各個(gè)變量的值,比如在 Local
域下有當(dāng)前方法的局部變量,我們可以在這里看到 MouseEvent
的各個(gè)屬性,如圖所示。
另外我們關(guān)注到有一個(gè)方法 o
,它在 Jr
方法下面,所以切換到 Closure(Jr)
域可以查看它的定義及其接收的參數(shù),如圖所示。
我們可以看到,FunctionLocation
又指向了方法 o
,點(diǎn)擊之后便又可以跳到指定位置,用同樣的方式進(jìn)行斷點(diǎn)調(diào)試即可。
在 Scope 面板還有多個(gè)域,這里就不再展開介紹了??傊?,通過 Scope 面板,我們可以看到當(dāng)前執(zhí)行環(huán)境下的變量的值和方法的定義,知道當(dāng)前代碼究竟執(zhí)行了怎樣的邏輯。
接下來切換到 Watch 面板,在這里可以自行添加想要查看的變量和方法,點(diǎn)擊右上角的 + 號按鈕,我們可以任意添加想要監(jiān)聽的對象,如圖所示。
比如這里我們比較關(guān)注 o.apply
是一個(gè)怎樣的方法,于是點(diǎn)擊添加 o.apply
,這里就會把對應(yīng)的方法定義呈現(xiàn)出來,展開之后可以再點(diǎn)擊 FunctionLocation
定位其源碼位置。
我們還可以切換到 Console 面板,輸入任意的 JavaScript 代碼,便會執(zhí)行、輸出對應(yīng)的結(jié)果,如圖所示。
如果我們想看看變量 arguments
的第一個(gè)元素是什么,那么可以直接敲入 arguments[0]
,便會輸出對應(yīng)的結(jié)果 MouseEvent
,只要在當(dāng)前上下文能訪問到的變量都可以直接引用并輸出。
此時(shí)我們還可以選擇單步調(diào)試,這里有 3 個(gè)重要的按鈕,如圖所示。
這 3 個(gè)按鈕都可以做單步調(diào)試,但功能不同。
用得較多的是第一個(gè),相當(dāng)于逐行調(diào)試,比如點(diǎn)擊 Step Over Next Function Call 這個(gè)按鈕,就運(yùn)行到了 4447 行,高亮的位置就變成了這一行,如圖所示。
在調(diào)試的過程中,我們可能會跳到一個(gè)新的位置,比如點(diǎn)擊上述 Step Over Next Function Call 幾下,可能會跳到一個(gè)叫作 ct
的方法中,這時(shí)候我們也不知道發(fā)生了什么,如圖所示。
那究竟是怎么跳過來的呢?我們可以觀察一下右側(cè)的 Call Stack 面板,就可以看到全部的調(diào)用過程了。比如它的上一步是 ot
方法,再上一步是 pt
方法,點(diǎn)擊對應(yīng)的位置也可以跳轉(zhuǎn)到對應(yīng)的代碼位置,如圖所示。
有時(shí)候調(diào)用棧是非常有用的,利用它我們可以回溯某個(gè)邏輯的執(zhí)行流程,從而快速找到突破口。
在調(diào)試過程中,如果想快速跳到下一個(gè)斷點(diǎn)或者讓 JavaScript 代碼運(yùn)行下去,可以點(diǎn)擊 Resume script execution 按鈕,如圖所示。
這時(shí)瀏覽器會直接執(zhí)行到下一個(gè)斷點(diǎn)的位置,從而避免陷入無窮無盡的調(diào)試中。
當(dāng)然,如果沒有其他斷點(diǎn)了,瀏覽器就會恢復(fù)正常狀態(tài)。比如這里我們就沒有再設(shè)置其他斷點(diǎn)了,瀏覽器直接運(yùn)行并加載了下一頁的數(shù)據(jù),同時(shí)頁面恢復(fù)正常,如圖所示。
上面我們介紹了一些 DOM 節(jié)點(diǎn)的 Listener,通過 Listener 我們可以手動設(shè)置斷點(diǎn)并進(jìn)行調(diào)試。但其實(shí)針對這個(gè)例子,通過翻頁的點(diǎn)擊事件 Listener 是不太容易找到突破口的。
接下來我們再介紹一個(gè)方法—— Ajax 斷點(diǎn),它可以在發(fā)生 Ajax 請求的時(shí)候觸發(fā)斷點(diǎn)。對于這個(gè)例子,我們的目標(biāo)其實(shí)就是找到 Ajax 請求的那一部分邏輯,找出加密參數(shù)是怎么構(gòu)造的。可以想到,通過 Ajax 斷點(diǎn),使頁面在獲取數(shù)據(jù)的時(shí)候停下來,我們就可以順著找到構(gòu)造 Ajax 請求的邏輯了。
怎么設(shè)置呢?
我們把之前的斷點(diǎn)全部取消,切換到 Sources 面板下,然后展開 XHR/fetch Breakpoints,這里就可以設(shè)置 Ajax 斷點(diǎn),如圖所示。
要設(shè)置斷點(diǎn),就要先觀察 Ajax 請求。和之前一樣,我們點(diǎn)擊翻頁按鈕 2,在 Network 面板里面觀察 Ajax 請求是怎樣的,請求的 URL 如圖所示。
可以看到 URL 里面包含 /api/movie
這樣的內(nèi)容,所以我們可以在剛才的 XHR/fetch Breakpoints 面板中添加攔截規(guī)則。點(diǎn)擊 + 號,可以看到一行 Break when URL contains:
的提示,意思是當(dāng) Ajax 請求的 URL 包含填寫的內(nèi)容時(shí),會進(jìn)入斷點(diǎn)停止,這里可以填寫 /api/movie
,如圖所示。
這時(shí)候我們再點(diǎn)擊翻頁按鈕 3,觸發(fā)第 3 頁的 Ajax 請求。會發(fā)現(xiàn)點(diǎn)擊之后頁面走到斷點(diǎn)停下來了,如圖所示。
格式化代碼看一下,發(fā)現(xiàn)它停到了 Ajax 最后發(fā)送的那個(gè)時(shí)候,即底層的 XMLHttpRequest
的 send
方法,可是似乎還是找不到 Ajax 請求是怎么構(gòu)造的。前面我們講過調(diào)用棧 Call Stack,通過調(diào)用棧是可以順著找到前序調(diào)用邏輯的,所以順著調(diào)用棧一層層找,也可以找到構(gòu)造 Ajax 請求的邏輯,最后會找到一個(gè)叫作 onFetchData
的方法,如圖所示。
接下來切換到 onFetchData
方法并將代碼格式化,可以看到如圖所示的調(diào)用方法。
可以發(fā)現(xiàn),可能使用了 axios
庫發(fā)起了一個(gè) Ajax 請求,還有 limit
、offset
、token
這 3 個(gè)參數(shù),基本就能確定了,順利找到了突破口!我們就不在此展開分析了,后文會有完整的分析實(shí)戰(zhàn)。
因此在某些情況下,我們可以在比較容易地通過 Ajax 斷點(diǎn)找到分析的突破口,這是一個(gè)常見的尋找 JavaScript 逆向突破口的方法。
要取消斷點(diǎn)也很簡單,只需要在 XHR/fetch Breakpoints 面板取消勾選即可,如圖所示。
我們知道,一個(gè)網(wǎng)頁里面的 JavaScript 是從對應(yīng)服務(wù)器上下載下來并在瀏覽器執(zhí)行的。有時(shí)候,我們可能想要在調(diào)試的過程中對 JavaScript 做一些更改,比如說有以下需求:
發(fā)現(xiàn) JavaScript 文件中包含很多阻撓調(diào)試的代碼或者無效代碼、干擾代碼,想要將其刪除。
調(diào)試到某處,想要加一行 console.log
輸出一些內(nèi)容,以便觀察某個(gè)變量或方法在頁面加載過程中的調(diào)用情況。在某些情況下,這種方法比打斷點(diǎn)調(diào)試更方便。
調(diào)試過程遇到某個(gè)局部變量或方法,想要把它賦值給 window
對象以便全局可以訪問或調(diào)用。
在調(diào)試的時(shí)候,得到的某個(gè)變量中可能包含一些關(guān)鍵的結(jié)果,想要加一些邏輯將這些結(jié)果轉(zhuǎn)發(fā)到對應(yīng)的目標(biāo)服務(wù)器。
這時(shí)候我們可以試著在 Sources 面板中對 JavaScript 進(jìn)行更改,但這種更改并不能長久生效,一旦刷新頁面,更改就全都沒有了。比如我們在 JavaScript 文件中寫入一行 JavaScript 代碼,然后保存,如圖所示。
這時(shí)候可以發(fā)現(xiàn) JavaScript 文件上出現(xiàn)了一個(gè)感嘆號標(biāo)志,提示我們做的更改是不會保存的。這時(shí)候重新刷新頁面,再看一下更改的這個(gè)文件,如圖所示。
有什么方法可以修改呢?其實(shí)有一些瀏覽器插件可以實(shí)現(xiàn),比如 ReRes。在插件中,我們可以添加自定義的 JavaScript 文件,并配置 URL 映射規(guī)則,這樣瀏覽器在加載某個(gè)在線 JavaScript 文件的時(shí)候就可以將內(nèi)容替換成自定義的 JavaScript 文件了。另外,還有一些代理服務(wù)器也可以實(shí)現(xiàn),比如 Charles、Fiddler,借助它們可以在加載 JavaScript 文件時(shí)修改對應(yīng) URL 的響應(yīng)內(nèi)容,以實(shí)現(xiàn)對 JavaScript 文件的修改。
其實(shí)瀏覽器的開發(fā)者工具已經(jīng)原生支持這個(gè)功能了,即瀏覽器的 Overrides 功能,它在 Sources 面板左側(cè),如圖所示。
我們可以在 Overrides 面板上選定一個(gè)本地的文件夾,用于保存需要更改的 JavaScript 文件,我們來實(shí)際操作一下。
首先,根據(jù)上文設(shè)置 Ajax 斷點(diǎn)的方法,找到對應(yīng)的構(gòu)造 Ajax 請求的位置,根據(jù)一些網(wǎng)頁開發(fā)知識,我們可以大體判斷出 then
后面的回調(diào)方法接收的參數(shù) a
中就包含了 Ajax 請求的結(jié)果,如圖所示。
我們打算在 Ajax 請求成功獲得 Response 的時(shí)候,在控制臺輸出 Response 的結(jié)果,也就是通過 console.log
輸出變量 a
。
再切回 Overrides 面板,點(diǎn)擊 + 按鈕,這時(shí)候?yàn)g覽器會提示我們選擇一個(gè)本地文件夾,用于存儲要替換的 JavaScript 文件。這里我選定了一個(gè)我任意新建的文件夾 ChromeOverrides,注意,這時(shí)候可能會遇到如圖所示的提示,如果沒有問題,直接點(diǎn)擊“允許”即可。
這時(shí),在 Overrides 面板下就多了一個(gè) ChromeOverrides 文件夾,用于存儲所有我們想要更改的 JavaScript 文件,如圖所示。
我們可以看到,現(xiàn)在所在的 JavaScript 選項(xiàng)卡是 chunk-19c920f8.012555a2.js:formatted
,代碼已經(jīng)被格式化了。因?yàn)楦袷交蟮拇a是無法直接在瀏覽器中修改的,所以為了方便,我們可以將格式化后的文件復(fù)制到文本編輯器中,然后添加一行代碼,修改如下:
...
}).then((function(a) {
console.log('response', a) // 添加一行代碼
var e = a.data
, s = e.results
, n = e.count;
t.loading = !1,
...
接著把修改后的內(nèi)容替換到原來的 JavaScript 文件中。這里要注意,切換到 chunk-19c920f8.012555a2.js
文件才能修改,直接替換 JavaScript 文件的所有內(nèi)容即可,如圖所示。
替換完畢之后保存,這時(shí)候再切換回 Overrides 面板,就可以發(fā)現(xiàn)成功生成了新的 JavaScript 文件,它用于替換原有的 JavaScript 文件,如圖所示。
好,此時(shí)我們?nèi)∠袛帱c(diǎn),然后刷新頁面,就可以在控制臺看到輸出的 Reponse 結(jié)果了,如圖所示。
正如我們所料,我們成功將變量 a
輸出,其中的 data
字段就是 Ajax 的 Response 結(jié)果,證明改寫 JavaScript 成功!而且刷新頁面也不會丟失了。
我們還可以增加一些 JavaScript 邏輯,比如直接將變量 a
的結(jié)果通過 API 發(fā)送到遠(yuǎn)程服務(wù)器,并通過服務(wù)器將數(shù)據(jù)保存下來,也就完成了直接攔截 Ajax 請求并保存數(shù)據(jù)的過程了。
修改 JavaScript 文件有很多用途,此方案可以為我們進(jìn)行 JavaScript 的逆向帶來極大的便利。
本節(jié)總結(jié)了一些瀏覽器開發(fā)者工具中對 JavaScript 逆向非常有幫助的功能,熟練掌握了這些功能會對后續(xù) JavaScript 逆向分析打下堅(jiān)實(shí)的基礎(chǔ),請大家好好研究。