博客配套代碼發布于github:喜馬拉雅登錄?(歡迎順手Star一下?)
相關知識點:webpack?補環境
相關爬蟲專欄:JS逆向爬蟲實戰??爬蟲知識點合集??爬蟲實戰案例?逆向知識點合集
此案例目標為逆向成功對應的參數,并成功返回對應請求。
一、爬取工作準備
進入網址:喜馬拉雅登錄,再點擊右上角那個小電腦,進入密碼登錄界面。
手機號輸入123456789,密碼輸入123456,點開F12,再點擊登錄,就可以開始我們的表演了。
一看就知道這個就是我們要的數據,再看載荷:
能看出我們大致要破解的四個參數:account,nonce,password,signature.
老規矩,直接復制這個數據包到curl to python,并將其復制到py運行查看結果(步驟在以往JS逆向案例有,不懂的直接看往期操作即可)
可以看到運行結果應是這個。如果并非這個結果說明參數傳遞錯誤。
二、逆向入口分析
直接encrypt搜,搜到七個,分析發現只有下面四個長得像,依次打上斷點。
只剩這倆地方斷的住。
試著從第二個開始找,我們直接選擇最下層的這個匿名,進去查看下:
巧了不是,account,password,nonce都看到了,
這兒還有這么完美的四大參數,不是這我吃。現在再挨個分析即可。
account:
account是n,n是→這里getEncryptPwd點進去的確可以進入到下層棧然后接著找,再補環境,但未免落了下成。往上再捋捋看看a的位置在哪。一直翻翻翻,終于看到:
看到a=r(19),webpack與加載器,看到這就很開心了,后面可以直接一套公式流webpack過掉,輕輕又松松啊。
password:
password是i,i是→跟上面的account近乎一個樣,也是webpack,處理近乎一致。
nonce:
nonce是t,t是→
呃,是傳入參數。而且這個t還是個一直變動的值。找不到上下層堆棧。
別急:當你看到檢測參數較多而且某個值似乎無法通過溯源找到適,可以看看其他的數據包:
果然,是從另一個數據包獲得的nonce,那我們只需要通過單獨向這個數據包發請求獲得nonce即可。
signature:
siganture是e,e是→可以將其簡化,即:
var e = a.getSignature({
? ? ? ? ? ? ? ? account: n,
? ? ? ? ? ? ? ? password: i,
? ? ? ? ? ? ? ? nonce: t
? ? ? ? ? ? })
乍一看,也是用了a加載器,直接像上面的賬密一樣...不行!
這里有個坑,而且非常隱蔽,筆者在這里就搗鼓了好久才知曉原因。
如果你用的是加載器的方式得到,它最后作為參數是錯誤的。原因極大概率是因為其獲取方式與常規加載器的邏輯不同。這里說一下此處的分析思路應如何。
正常來講是會去想到直接去用加載器,但用前一定先在控制臺打印觀察下。此處可以看到打印出來格式為哈希算法,則:盡可能別用加載器,加密算法的邏輯就單獨用算法方式解決,兩者獲取可能會沖突。
點進a.getSiganture看對應層邏輯:
?
這里選中的是e.toUpperCase()),能看到它是一個很長的由上方拼接獲取的字符
同時我們再把這一串打印進控制臺:
發現沒有:在同一時間線(同個ajax請求)下,獲得的兩個算法最終答案相近,但后者(加密算法)才對。
同時這里能通過分析哈希算法長度,即40位,基本確定其為sha1算法。
但還是有點不放心。我們這里是想直接用sha1的加密邏輯得到,但不確定它有沒有加鹽(魔改)算法,到SHA1加密網絡轉換器里,將e.toUpeerCase()的東西輸入進去看下(記得去掉引號)
與上述答案一致。那么就可以放心直接用算法了,至此這個邏輯基本完成。
綜上,所有參數分析完畢后可以開始我們的正式操作了。
三、破解逆向
1.補webpack
這段就老生常談的公式了,盡量簡化描述:
(如需更詳細的步驟請參考我往期寫的webpack處理公式)
選中加載器,刷新頁面并進去里面,全選復制粘貼過來并簡單處理掉里面的html元素,并在頂部寫個window=global。再把刷新進去位置的函數(function i(r)),設置個console.log('r:::',r)與window.loader=i
最后再將傳入代碼寫作:
一般如果webpack里是r('GSGX')這種還能依靠直接搜'GSGX'復制粘貼過來,但如果是像上圖這種r(19)的webpack,就得通過源函數來進入了:
選中r(19):進入里面這個棧并全局復制到mod01即可(對應的是數組,不用分析半天究竟是誰)
缺這玩意兒就全局搜并全部復制(注意這個搜索的是var xx,切記把這個var去掉不然沒法上升為全局變量),再給個mod02補上,并require過來(注意require的順序):
看到這里就能確定webpack環節結束,接下來到補環境階段。
2.補環境
卻啥補啥,補到此時:
有點怪,好像不太好補。那就把我們的監控器遞出來吧↓
(監控器具體用法在文章?[逆向知識] 補環境 -- 讓本地逆向如魚得水?處有講,建議在這里看一看)
function setProxy(proxyObjArr) {for (let i = 0; i < proxyObjArr.length; i++) {const handler = `{get: function(target, property, receiver) {console.log("方法:", "get ", "對象:", "${proxyObjArr[i]}", " 屬性:", property, " 屬性類型:", typeof property, ", 屬性值:", target[property], ", 屬性值類型:", typeof target[property]);return target[property];},set: function(target, property, value, receiver) {console.log("方法:", "set ", "對象:", "${proxyObjArr[i]}", " 屬性:", property, " 屬性類型:", typeof property, ", 屬性值:", value, ", 屬性值類型:", typeof target[property]);return Reflect.set(...arguments);}}`;eval(`try {${proxyObjArr[i]};${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});} catch (e) {${proxyObjArr[i]} = {};${proxyObjArr[i]} = new Proxy(${proxyObjArr[i]}, ${handler});}`);}
}setProxy(['window', 'document','canvas'])
document補全,看到:
很明顯知道canvas少了個toDataURL,加進去即可。
同樣,其他補環境邏輯也大致相同,陸續為setProxy加東西即可,只有一個點要注意下:
大大的userAgent,這玩意兒就不要再傳空值或空func,直接控制臺打印個丟過來不好嗎
這個點也是不太好處理,但只要全局搜就可以了,搜到發現隸屬于document對象,仿寫過來就行。同理,后面好幾個環境也可以靠這樣補。
最終成功補完沒再報錯,將setProxy的代理與之前console.log的r都注釋掉,
這個是window對象,也補上環境就消失了,
最終輸出:賬戶與密碼的邏輯搞定。
3.py與js代碼互傳
首先我們要確定py與js互傳的方式。分為execjs與subprocess。正好這里都會用到。execjs用于構建環境較簡單,沒有很多瀏覽器原生的bom/dom,api或更復雜的模擬環境這些;subprocess則用于如之前代碼中復雜的環境模擬,最終輸出。
(1) execjs
它的使用方式相較非常簡單,直接傳遞即可。這里我們以siganture的獲得舉例
cryptoJs = require("crypto-js")function y(t) {var e = "", r = Object.keys(t).sort((function (t, e) {return (t = t.charCodeAt(0)) - (e = e.charCodeAt(0))})), n = r.length;return r.forEach((function (r, o) {var i = t[r];e += "".concat(r, "=").concat(i),o < n - 1 && (e += "&")})),e
}function getSignature() {var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, e = y(t) + "&" + "WEB-V1-PRODUCT-E7768904917C4154A925FBE1A3848BC3E84E2C7770744E56AFBC9600C267891F";return e.toUpperCase(),cryptoJs.SHA1(e.toUpperCase()).toString()
}
這個signature在js中的構建邏輯,而在py中,我們只需要通過如下的代碼即可獲取對應js生成的代碼。
js_compile = execjs.compile(open('sign.js',encoding='utf8').read())
data = {'account': enc_account,'password': enc_password,'nonce': nonce_val}
signature = js_compile.call('getSignature',data)
其中data是傳遞進去的參數,'getSignature'則是用于選擇這個文件的這個函數。
(2) subprocess
其使用相較就復雜很多,這里也是分別放js與py文件對應代碼:
function get_all(account, password) {function get_account() {return (0, a.getEncryptPwd)(account)}function get_pwd() {return (0, a.getEncryptPwd)(password)}return {account:get_account(),pwd:get_pwd(),}
}act = process.argv[2]
pwd = process.argv[3]
const ret = get_all(act,pwd)
res = JSON.stringify(ret)
console.log(res)process.exit()
在js文件中,通過process.argv獲取傳遞過來的原始賬密并傳遞給get_all,再生成后打印到控制臺上。py文件再通過控制臺的消息對應接收過來。本質上可以理解為雙方是靠終端來接收各自的數據。
all_ = subprocess.check_output(f'node get_sign.js "{account}" "{password}"') # 分別獲得傳遞
all_ = all_.decode('utf8').strip()
# 通過json傳遞取出對應值
js_data = json.loads(all_)
account = js_data['account']
pwd = js_data['pwd']
注意:這里我用的是json數據來互傳。因為js傳遞過來的數據是個長字符串,用json格式才方便將其轉化為字典格式以提取。
四、完整邏輯構建
如上,我們基本已完成了相關具體邏輯與所有代碼的構建。
最終我們將其模塊化打包輸出:
if __name__ == '__main__':account = '123456789'password = '123456'cur_nonce = get_nonce()enc_account,enc_pwd = get_encrypt(account, password)signature = get_signature(enc_account,enc_pwd,cur_nonce)ret = login(enc_account,enc_pwd,cur_nonce,signature)print(ret)
答案獲取成功,逆向完成。
由于代碼量過大,本文并未將所有代碼都放上來。但可以看我的github開源代碼,來進一步全面分析。
📌 項目代碼 + 后續案例合集 全部發布在 GitHub 倉庫中,持續更新中,歡迎收藏!