聲明
本文章中所有內(nèi)容僅供學(xué)習(xí)交流,抓包內(nèi)容昆稿、敏感網(wǎng)址纺座、數(shù)據(jù)接口均已做脫敏處理,嚴(yán)禁用于商業(yè)用途和非法用途溉潭,否則由此產(chǎn)生的一切后果均與作者無(wú)關(guān)净响,若有侵權(quán),請(qǐng)聯(lián)系我立即刪除喳瓣!
逆向目標(biāo)
- 目標(biāo):某鵬教育登錄接口加密馋贤,含有簡(jiǎn)單的 JS 混淆
- 主頁(yè):
aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi8=
- 接口:
aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi9BY2NvdW50L1VuaXRMb2dpbg==
- 逆向參數(shù):Form Data:
black_box: eyJ2IjoiR01KM0VWWkVxMG0ydVh4WUd...
逆向過(guò)程
本次逆向的目標(biāo)同樣是一個(gè)登錄接口,其中的加密 JS 使用了簡(jiǎn)單的混淆畏陕,可作為混淆還原的入門(mén)級(jí)教程配乓,來(lái)到登錄頁(yè)面,隨便輸入賬號(hào)密碼進(jìn)行登錄惠毁,其中登錄的 POST 請(qǐng)求里扰付, Form Data 有個(gè)加密參數(shù) black_box,也就是本次逆向的目標(biāo)仁讨,抓包如下:
直接搜索 black_box羽莺,在 login.js 里可以很容易找到加密的地方,如下圖所示:
看一下 _fmOpt.getinfo()
這個(gè)方法洞豁,是調(diào)用了 fm.js 里的 OO0O0()
方法盐固,看這個(gè)又是 0 又是 O 的,多半是混淆了丈挟,如下圖所示:
點(diǎn)進(jìn)去看一下刁卜,整個(gè) fm.js 都是混淆代碼,我們選中類似 OQoOo[251]
的代碼曙咽,可以看到實(shí)際上是一個(gè)字符串對(duì)象蛔趴,也可以直接在 Console 里輸出看到其實(shí)際值,這個(gè) OO0O0
方法返回的 oOoo0[OQoOo[448]](JSON[OQoOo[35]](O0oOo[OQoOo[460]]))
例朱,就是 black_box 的值孝情,如下圖所示:
仔細(xì)觀察,可以發(fā)現(xiàn) OQoOo
應(yīng)該是一個(gè)類似數(shù)組的東西洒嗤,通過(guò)傳入元素下標(biāo)來(lái)依次取其真實(shí)值箫荡,隨便搜索一個(gè)值,可以在代碼最后面找到一個(gè)數(shù)組渔隶,這個(gè)數(shù)組其實(shí)就是 OQoOo
羔挡,可以傳入下標(biāo)來(lái)驗(yàn)證一下洁奈,如下圖所示:
到這里其實(shí)就知道了其大致混淆原理,我們可以把這個(gè)JS 拿下來(lái)绞灼,到本地寫(xiě)個(gè)小腳本利术,將這些值替換一下:
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-09
# @Author : 微信公眾號(hào):K哥爬蟲(chóng)
# @FileName: replace_js.py
# @Software: PyCharm
# @describe: 混淆還原小腳本
# ==================================
# 待替換的值(太多了,僅列出少部分)
# 以實(shí)際列表為準(zhǔn)低矮,要和 fm_old.js 里的列表一致
item = ['referrer', 'absolute', 'replace',...]
# 混淆后的 JS
with open("fm_old.js", "r", encoding="utf-8") as f:
js_lines = f.readlines()
js = ""
for j in js_lines:
js += j
for i in item:
# Qo00o 需要根據(jù)你 fm_old.js 具體的字符串進(jìn)行替換
str_old = "Qo00o[{}]".format(item.index(i))
js = js.replace(str_old, '"' + i + '"')
# 還原后的 JS
with open("fm_new.js", "w", encoding="utf-8") as f:
f.write(js)
使用此腳本替換后印叁,可能會(huì)發(fā)現(xiàn) JS 會(huì)報(bào)錯(cuò),原因是一些換行符商佛、斜杠解析錯(cuò)誤喉钢,以及雙引號(hào)重復(fù)使用的問(wèn)題,可以自己手動(dòng)修改一下良姆。
這里需要注意的一點(diǎn)肠虽,fm.js 后面還有個(gè)后綴,類似 t=454594玛追,t=454570 等税课,不同的后綴得到的 JS 內(nèi)容也有差異,各種函數(shù)變量名和那個(gè)列表元素順序不同痊剖,實(shí)際上調(diào)用的方法是同一個(gè)韩玩,所以影響不大,只需要注意替換時(shí)列表內(nèi)容陆馁、需要替換的那個(gè)字符串和你下載的 JS 文件里的一致即可找颓。
將 JS 還原后,我們可以將還原后的 JS 替換掉網(wǎng)站本身經(jīng)過(guò)混淆后的 JS叮贩,這里替換方法有很多击狮,比如使用 Fiddler 等抓包工具替換響應(yīng)、使用 ReRes 之類的插件進(jìn)行替換益老、使用瀏覽器開(kāi)發(fā)者工具自帶的 Overrides 功能進(jìn)行替換(Chrome 64 之后才有的功能)等彪蓬,這里我們使用 Fiddler 的 Autoresponder 功能來(lái)替換。
實(shí)測(cè)這個(gè) fm.js 的后綴短時(shí)間內(nèi)不會(huì)改變捺萌,所以可以直接復(fù)制其完整地址來(lái)替換档冬,要嚴(yán)謹(jǐn)一點(diǎn)的話,我們可以用正則表達(dá)式來(lái)匹配這個(gè) t 值桃纯,在 Fiddler 里面選擇 AutoResponder酷誓,點(diǎn)擊 Add Rule,添加替換規(guī)則慈参,正則表達(dá)式的方法寫(xiě)法如下:regex:https:\/\/static\.tongdun\.net\/v3\/fm\.js\?t=\d+
呛牲,注意 regex 前綴必不可少,上方依次選中 Enable rules(應(yīng)用規(guī)則)驮配、Accept all CONNECTs(接受所有連接)、Unmatched requests passthrough(不匹配規(guī)則的就按照之前的請(qǐng)求地址發(fā)送過(guò)去),Enable Latency 是設(shè)置延遲生效時(shí)間壮锻,不用勾選琐旁,如下圖所示:
替換后再次登錄,下斷點(diǎn)猜绣,可以看到現(xiàn)在的 JS 已經(jīng)清晰了不少灰殴,再看看這個(gè)函數(shù)最后的 return 語(yǔ)句,oQOQ0["blackBox"]
包含了 it
掰邢、os
牺陶、t
、v
三個(gè)參數(shù)辣之,使用 JSON 的 stringify 方法將其轉(zhuǎn)換成字符串掰伸,然后調(diào)用 QQo0
方法進(jìn)行加密,如下圖所示:
我們先來(lái)看看 oQOQ0["blackBox"]
里的四個(gè)參數(shù)怀估,其中 it
狮鸭、os
、v
三個(gè)參數(shù)在這個(gè)函數(shù)開(kāi)始就已經(jīng)有定義多搀,v
就是 Q0oQQ["version"]
歧蕉,是定值,直接搜索可以發(fā)現(xiàn)這個(gè)值是在最開(kāi)始的那個(gè)大列表里康铭,os
為定值惯退,it
是兩個(gè)時(shí)間戳相減的值,O000o
這個(gè)方法就是兩個(gè)值進(jìn)行相減从藤,oQOQo
這個(gè)時(shí)間戳可以搜索 var oQOQo
催跪,是一開(kāi)始加載就生成的時(shí)間戳,JS 一開(kāi)始加載到點(diǎn)擊登陸進(jìn)入加密函數(shù)呛哟,也就一分鐘左右叠荠,所以這里我們可以直接生成一個(gè)五位隨機(jī)數(shù)(一分鐘左右在毫秒上的差值在五位數(shù)左右)。
現(xiàn)在就剩下一個(gè) t
參數(shù)了扫责,往下看 t
其實(shí)就是 Q0oQQ["tokens"]
榛鼎,中間經(jīng)過(guò)了一個(gè) if-else 語(yǔ)句,可以埋下斷點(diǎn)進(jìn)行調(diào)試鳖孤,發(fā)現(xiàn)其實(shí)只執(zhí)行了 else 語(yǔ)句者娱,對(duì) t
賦值也就這一句,所以剩下的代碼其實(shí)在扣的時(shí)候都可以刪掉苏揣。
這個(gè) tokens 多次測(cè)試發(fā)現(xiàn)是不變的黄鳍,嘗試直接搜索一下 token 關(guān)鍵字,可以發(fā)現(xiàn)其賦值的地方平匈,對(duì) id
按照 | 符號(hào)進(jìn)行分割框沟,取其第 1 個(gè)索引值就是 tokens藏古,再看看 id
的值,并沒(méi)有找到明顯的生成邏輯忍燥,復(fù)制其值搜索一下拧晕,發(fā)現(xiàn)是通過(guò)一個(gè)接口返回的,可以直接寫(xiě)死梅垄,也可以自己先去請(qǐng)求一下這個(gè)接口厂捞,取其返回的值,如下圖所示:
自此所有參數(shù)都找完了队丝,回到原來(lái)的 return 位置靡馁,還差一個(gè)加密函數(shù),即 ooOoO["encode"]()
机久,直接跟進(jìn)去臭墨,將這個(gè)方法扣下來(lái)即可,本地調(diào)試缺啥補(bǔ)啥吞加,將用到的函數(shù)補(bǔ)全就行了裙犹。
完整代碼
GitHub 關(guān)注 K 哥爬蟲(chóng),持續(xù)分享爬蟲(chóng)相關(guān)代碼衔憨!歡迎 star 叶圃!https://github.com/kgepachong/
以下只演示部分關(guān)鍵代碼,不能直接運(yùn)行践图! 完整代碼倉(cāng)庫(kù)地址:https://github.com/kgepachong/crawler/
JavaScript 加密關(guān)鍵代碼架構(gòu)
function oQ0OQ(Q0o0, o0OQ) {
return Q0o0 < o0OQ;
}
function O000O(Q0o0, o0OQ) {
return Q0o0 >> o0OQ;
}
function Qo0oo(Q0o0, o0OQ) {
return Q0o0 | o0OQ;
}
function OOO0Q(Q0o0, o0OQ) {
return Q0o0 << o0OQ;
}
function OooQo(Q0o0, o0OQ) {
return Q0o0 & o0OQ;
}
function Oo0OO(Q0o0, o0OQ) {
return Q0o0 + o0OQ;
}
var oQoo0 = {};
oQoo0["_keyStr"] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
oQoo0["encode"] = function QQQ0(Q0o0) {
var o0OQ = 62;
while (o0OQ) {
switch (o0OQ) {
case 116 + 13 - 65: {}
case 118 + 8 - 63: {}
case 94 + 8 - 40: {}
case 122 + 6 - 63: {}
}
}
};
oQoo0["_utf8_encode"] = function oOQ0(Q0o0) {}
function OOoO0() {
var tokens = "e0ia+fB5zvGuTjFDgcKahQwg2UEH8b0k7EK/Ukt4KwzyCbpm11jjy8Au64MC6s7HvLRacUxd7ka4AdDidJmYAA==";
var version = "+X+3JWoUVBc12xtmgMpwzjAone3cp6/4QuFj7oWKNk+C4tqy4un/e29cODlhRmDy";
var Oo0O0 = {};
Oo0O0["blackBox"] = {};
Oo0O0["blackBox"]["v"] = version;
Oo0O0["blackBox"]["os"] = "web";
Oo0O0["blackBox"]["it"] = parseInt(Math.random() * 100000);
Oo0O0["blackBox"]["t"] = tokens;
return oQoo0["encode"](JSON.stringify(Oo0O0["blackBox"]));
}
// 測(cè)試樣例
console.log(OOoO0())
Python 登錄關(guān)鍵代碼
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-10
# @Author : 微信公眾號(hào):K哥爬蟲(chóng)
# @FileName: open_login.py
# @Software: PyCharm
# ==================================
import time
import execjs
import requests
login_url = "脫敏處理掺冠,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler"
def get_black_box():
with open('get_black_box.js', 'r', encoding='utf-8') as f:
exec_js = f.read()
black_box = execjs.compile(exec_js).call('OOoO0')
return black_box
def login(black_box, username, password):
params = {"bust": str(int(time.time() * 1000))}
data = {
"loginName": username,
"passWord": password,
"validateNum": "",
"black_box": black_box
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
}
response = requests.post(url=login_url, params=params, data=data, headers=headers)
print(response.json())
def main():
username = input("請(qǐng)輸入登錄賬號(hào): ")
password = input("請(qǐng)輸入登錄密碼: ")
black_box = get_black_box()
login(black_box, username, password)
if __name__ == '__main__':
main()