關注微信公眾號:K哥爬蟲,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!
宣告
本文章中所有內容僅供學習交流,抓包內容、敏感網址、資料介面均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關,若有侵權,請聯絡我立即刪除!
逆向目標
- 目標:某鵬教育登入介面加密,含有簡單的 JS 混淆
- 主頁:
aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi8=
- 介面:
aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi9BY2NvdW50L1VuaXRMb2dpbg==
- 逆向引數:Form Data:
black_box: eyJ2IjoiR01KM0VWWkVxMG0ydVh4WUd...
逆向過程
本次逆向的目標同樣是一個登入介面,其中的加密 JS 使用了簡單的混淆,可作為混淆還原的入門級教程,來到登入頁面,隨便輸入賬號密碼進行登入,其中登入的 POST 請求裡, Form Data 有個加密引數 black_box,也就是本次逆向的目標,抓包如下:
直接搜尋 black_box,在 login.js 裡可以很容易找到加密的地方,如下圖所示:
看一下 _fmOpt.getinfo()
這個方法,是呼叫了 fm.js 裡的 OO0O0()
方法,看這個又是 0 又是 O 的,多半是混淆了,如下圖所示:
點進去看一下,整個 fm.js 都是混淆程式碼,我們選中類似 OQoOo[251]
的程式碼,可以看到實際上是一個字串物件,也可以直接在 Console 裡輸出看到其實際值,這個 OO0O0
方法返回的 oOoo0[OQoOo[448]](JSON[OQoOo[35]](O0oOo[OQoOo[460]]))
,就是 black_box 的值,如下圖所示:
仔細觀察,可以發現 OQoOo
應該是一個類似陣列的東西,通過傳入元素下標來依次取其真實值,隨便搜尋一個值,可以在程式碼最後面找到一個陣列,這個陣列其實就是 OQoOo
,可以傳入下標來驗證一下,如下圖所示:
到這裡其實就知道了其大致混淆原理,我們可以把這個JS 拿下來,到本地寫個小指令碼,將這些值替換一下:
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-09
# @Author : 微信公眾號:K哥爬蟲
# @FileName: replace_js.py
# @Software: PyCharm
# @describe: 混淆還原小指令碼
# ==================================
# 待替換的值(太多了,僅列出少部分)
# 以實際列表為準,要和 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 需要根據你 fm_old.js 具體的字串進行替換
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)
使用此指令碼替換後,可能會發現 JS 會報錯,原因是一些換行符、斜槓解析錯誤,以及雙引號重複使用的問題,可以自己手動修改一下。
這裡需要注意的一點,fm.js 後面還有個字尾,類似 t=454594,t=454570 等,不同的字尾得到的 JS 內容也有差異,各種函式變數名和那個列表元素順序不同,實際上呼叫的方法是同一個,所以影響不大,只需要注意替換時列表內容、需要替換的那個字串和你下載的 JS 檔案裡的一致即可。
將 JS 還原後,我們可以將還原後的 JS 替換掉網站本身經過混淆後的 JS,這裡替換方法有很多,比如使用 Fiddler 等抓包工具替換響應、使用 ReRes 之類的外掛進行替換、使用瀏覽器開發者工具自帶的 Overrides 功能進行替換(Chrome 64 之後才有的功能)等,這裡我們使用 Fiddler 的 Autoresponder 功能來替換。
實測這個 fm.js 的字尾短時間內不會改變,所以可以直接複製其完整地址來替換,要嚴謹一點的話,我們可以用正規表示式來匹配這個 t 值,在 Fiddler 裡面選擇 AutoResponder,點選 Add Rule,新增替換規則,正規表示式的方法寫法如下:regex:https:\/\/static\.tongdun\.net\/v3\/fm\.js\?t=\d+
,注意 regex 字首必不可少,上方依次選中 Enable rules(應用規則)、Accept all CONNECTs(接受所有連線)、Unmatched requests passthrough(不匹配規則的就按照之前的請求地址傳送過去),Enable Latency 是設定延遲生效時間,不用勾選,如下圖所示:
替換後再次登入,下斷點,可以看到現在的 JS 已經清晰了不少,再看看這個函式最後的 return 語句,oQOQ0["blackBox"]
包含了 it
、os
、t
、v
三個引數,使用 JSON 的 stringify 方法將其轉換成字串,然後呼叫 QQo0
方法進行加密,如下圖所示:
我們先來看看 oQOQ0["blackBox"]
裡的四個引數,其中 it
、os
、v
三個引數在這個函式開始就已經有定義,v
就是 Q0oQQ["version"]
,是定值,直接搜尋可以發現這個值是在最開始的那個大列表裡,os
為定值,it
是兩個時間戳相減的值,O000o
這個方法就是兩個值進行相減,oQOQo
這個時間戳可以搜尋 var oQOQo
,是一開始載入就生成的時間戳,JS 一開始載入到點選登陸進入加密函式,也就一分鐘左右,所以這裡我們可以直接生成一個五位隨機數(一分鐘左右在毫秒上的差值在五位數左右)。
現在就剩下一個 t
引數了,往下看 t
其實就是 Q0oQQ["tokens"]
,中間經過了一個 if-else 語句,可以埋下斷點進行除錯,發現其實只執行了 else 語句,對 t
賦值也就這一句,所以剩下的程式碼其實在扣的時候都可以刪掉。
這個 tokens 多次測試發現是不變的,嘗試直接搜尋一下 token 關鍵字,可以發現其賦值的地方,對 id
按照 | 符號進行分割,取其第 1 個索引值就是 tokens,再看看 id
的值,並沒有找到明顯的生成邏輯,複製其值搜尋一下,發現是通過一個介面返回的,可以直接寫死,也可以自己先去請求一下這個介面,取其返回的值,如下圖所示:
自此所有引數都找完了,回到原來的 return 位置,還差一個加密函式,即 ooOoO["encode"]()
,直接跟進去,將這個方法扣下來即可,本地除錯缺啥補啥,將用到的函式補全就行了。
完整程式碼
GitHub 關注 K 哥爬蟲,持續分享爬蟲相關程式碼!歡迎 star !https://github.com/kgepachong/
以下只演示部分關鍵程式碼,不能直接執行! 完整程式碼倉庫地址:https://github.com/kgepachong...
JavaScript 加密關鍵程式碼架構
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"]));
}
// 測試樣例
console.log(OOoO0())
Python 登入關鍵程式碼
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-10
# @Author : 微信公眾號:K哥爬蟲
# @FileName: open_login.py
# @Software: PyCharm
# ==================================
import time
import execjs
import requests
login_url = "脫敏處理,完整程式碼關注 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("請輸入登入賬號: ")
password = input("請輸入登入密碼: ")
black_box = get_black_box()
login(black_box, username, password)
if __name__ == '__main__':
main()