記一次酣暢淋漓的js逆向
摘要
本文記錄了對一個混淆后的js腳本的逆向過程,并介紹了過程中遇到的兩種js混淆策略與應對方式;與此同時,本文還記錄了對于禁止F12調試的站點的破解方法;最后,本文對js逆向與這過程中的AI工具使用情況進行了一些感受分享與討論。
背景介紹
今天在寫爬蟲的練習題時遇到了這樣一個難題:目標資源是一個圖片的url,但是不同于以往的情況,我在http響應記錄里搜索這個圖片的url,發現并不能搜到。從邏輯上來講,這個url被展示到瀏覽器上了,那么只可能是之前的請求中帶有和這個url相關的信息,如果這個信息不是直接給出的url,那么只可能是編碼或者加密過的url。
順著這個思路,我繼續翻響應記錄,果然,在一個html的script標簽內發現了一個對象,里面就包含使用base64編碼的url。但是嘗試解密后發現解密出來是亂碼,并且對象里還有個名為key的字符串,如下所示
{
"url": "HZM84hjXwDewS4cjMZ4w/bSUfXugYnglcxAKE2sCrsAf4J6oQ......",
"key": "110f8cf1f11e48c13dab209976c1821d"
}
那么很顯然,這里是進行了加密的。再考慮到base64解碼后的數據長度為64,且key也是32個字符,所以可以很合理的懷疑這里使用了aes加密,但是不知道具體的填充方式和iv,在嘗試了一些組合之后還是無果,于是只能從解密的js代碼入手。
一路追蹤這個對象的使用情況,最后定位到了一個名為crypto.js的腳本,其中有一個名為decrypt的函數,可以確認url就是在這里完成解密的,但是線上的js腳本一般都是經過了混淆的,這次的也不例外,經過加密的js完全就是天書,根本不可讀。于是便有了本文的重頭戲:對經過混淆的js的還原??紤]到還原js是個很無聊的工作,所以這里就不提及具體的細節了,只記錄一下大致的方法,以及這次遇到的混淆手段。
js逆向
嘗試偷懶:fail
這次是我第一次接觸混淆后的js逆向,所以我先在網上查了一下,看看有沒有可以自動解混淆的方法,不過我也清楚這些工具也只能夠幫部分的忙,畢竟混淆時已經把變量名等信息都丟掉了,要把這些還原出來是不可能的。在搜索結果中我找到了 https://deobfuscate.io/ 這個網站,把js丟進去之后,輸出了一份看起來整潔了許多的代碼,但是decrypt部分仍然是天書,但是至少也聊勝于無了。
峰回路轉:字符串解密函數的破解
字符串解密函數的破解是整個逆向過程的核心。這里首先說明一下js逆向的難點在哪:js逆向的難度來自于js語言自身,js是使用解釋型語言,這一特性使得js可以在運行時對一個對象做很多調整,例如動態的添加和刪除對象的屬性,動態的訪問一個對象的屬性和方法等。
一個在js混淆里經常用的的trick就是把函數調用變成獲取函數的方法+調用方法的方式,即如下所示:
CryptoJS.AES.enc(data, key)
// 上述代碼等價于
CryptoJS["AES"]["enc"](data, key)
其中AES和enc兩個字符串是可以通過其他函數來獲得。所以,在混淆后的js代碼里,我們經常會見到這種寫法
_0x3c22fb = CryptoJS_0x154728[_0x4328d(0x216, ']2%*'](0x10))
對于java和binary,雖然看不懂函數名,但是至少可以整理出明確的調用鏈;而反觀js,如果不能破解0x4328d這個字符串解密函數,我們甚至都無法指出這是在調用哪個函數。
要通過靜態分析摸清楚字符串解密函數的算法是很麻煩的,這里我們可以取個巧,把js放到本地運行起來,然后在某個位置調用0x4328d這個字符串解密函數,例如,如果我們想要知道_0x4328d(0x226, 'xbaC')代表什么,我們只需要執行一次這個函數即可。
這里可能會有小伙伴有疑問:為什么不直接執行到decrypt呢?這樣就可以直接打斷點讀出具體的值了。這里不這么做的原因是:本地運行時發現,js腳本執行中會報錯,無法執行到decrypt的位置,所以我只能在報錯點之前調用字符串解密函數來完成間接的解密。
當然,也可以直接在線上的站點調試js,但是站點本身還做了防調試的處理,這個的破解方法我會在文末介紹一下。
攻堅奪旗:完成js逆向目標
在能夠理清楚每一行代碼是在調用什么之后,就可以開始正是破解decrypt函數了。具體的破解過程并沒有什么值得說的地方,就只是使用字符串解密函數不停的解密字符串而已。
這其中遇到了js加密的另一個比較常見的trick:即使用一個對象來繼續混淆函數調用,例子如下所示:
const _0xacf5f1 = {
'Y2aK': _0x4328d(0x213, 'oXQv'),
'JaxMs': function (_0xaabd93, _0x57327b) {
return _0xaabd93 == _0x57327b;
},
'e33Ahs': function (_0x26743c, _0x1d7c34) {
return _0x26743c === _0x1d7c34;
},
'woszh': 'JaxMs',
'vYmge': _0x4328d(0x15f, 'by3O')
};
這個對象里面封裝了2個字符串和2個函數,這兩個函數都只是起到判斷相等的作用。在需要混淆判斷相等的操作時,可以把等號換成在這個對象里面取某個元素并進行調用,之后再用字符串加密的方式把key混淆掉,就能進一步加大破解難度。
最終在完成了破解之后,發現腳本使用的是cbc模式,pkcs7填充。特別的,密鑰是之前的key取sha256之后的結果,也難怪之前一直都沒嘗試出來。有了這些信息之后,我讓AI基于這個算法用python寫了一個解密腳本,經測試,腳本能夠成果解密url。
至此,這個js逆向任務完成。
基于在線調試的方法
這次的目標站點使用了禁止調試的技術,具體表現是,頁面加載完成后按F12沒有反應,如果在頁面加載前就按F12喚起了調試菜單,那么調試會斷在一個代碼為debugger的地方,并且頁面上會顯示Paused in debugger。
禁止調試的破解方法我參考了這篇文章 https://blog.csdn.net/shisanxiang_/article/details/143328204 ,只需要添加日志點,并將其設為false就能夠破解。


在完成禁止調試的破解之后,只需要在左側Page里找到crypto.js,然后打斷點,就能夠摸清楚decrypt的執行情況了。這一點是我在事后才摸索出來的,如果一開始就用這個方法的話,應該是能夠節省不少時間的。

一些主觀感受
關于js逆向
雖然這篇文章里提到的點并不多,但是這個逆向破解確實是實打實的卡了我三個多小時。之前我也做過一些java和純binary的逆向,從這次經歷來看,js逆向是稍微簡單一些的。
這么說主要是因為:js雖然經過了混淆,但是還是保留了完整的函數結構,使得我們能夠很方便的在運行時調用某個函數。例如,能夠在js運行時打斷點,然后調用字符串解密函數直接獲取解密后的結果。這一點是純binary和java都比較難實現的(真要實現肯定也能行,就是會復雜很多)。
從整個流程的感覺上來看,字符串解密函數的破解就像是橫亙在我和目標之間的唯一一座大山:在想辦法破解了字符串解密函數之后,一切都豁然開朗了,剩下的部分就只需要按圖索驥慢慢摸索,最終就能還原出這個函數出來。
AI工具使用感受
AI是個非常好用的搜索引擎和助手,尤其是在今天這種需要快速編寫原型代碼來驗證猜想的場景。例如,我對CryptoJS這個庫并不是很熟悉,如果按照傳統的方法,我需要查官方文檔,這會耗費大量的時間;但是在使用AI之后,我可以直接讓AI生成與之等價的python代碼,并在我熟悉的環境里完成對這個想法的驗證。
同時,對于某些我不是很確定的語法,例如substring只傳一個參數代表什么,也可以直接通過AI獲取答案,這會比傳統的搜索引擎要快上很多。
最最重要的是,這種方法并不會打斷我們工作時的思路。在過去,如果我對某個庫的用法不了解,只能去查文檔,這會打斷我原有的思路;但在使用AI之后,我可以像隨口問小助手一樣,把問題描述清楚之后就能直接得到回答,而不需要打斷思路去學習其他東西。
可以想象這會是一個怎樣災難的畫面:在逆向這種本身就很消耗精力和耐心的場景,如果要驗證一個猜想還得去從頭開始學CryptoJS這個庫的用法,以及python里AES加解密庫的用法??梢哉f,這一次我是真真切切地感受到了AI帶來的工作效率提升。

浙公網安備 33010602011771號