【JS 逆向百例】反混淆入門,某鵬教育 JS 混淆還原

K哥爬蟲發表於2021-12-02
關注微信公眾號:K哥爬蟲,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!

宣告

本文章中所有內容僅供學習交流,抓包內容、敏感網址、資料介面均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關,若有侵權,請聯絡我立即刪除!

逆向目標

  • 目標:某鵬教育登入介面加密,含有簡單的 JS 混淆
  • 主頁:aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi8=
  • 介面:aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi9BY2NvdW50L1VuaXRMb2dpbg==
  • 逆向引數:Form Data:black_box: eyJ2IjoiR01KM0VWWkVxMG0ydVh4WUd...

逆向過程

本次逆向的目標同樣是一個登入介面,其中的加密 JS 使用了簡單的混淆,可作為混淆還原的入門級教程,來到登入頁面,隨便輸入賬號密碼進行登入,其中登入的 POST 請求裡, Form Data 有個加密引數 black_box,也就是本次逆向的目標,抓包如下:

01.png

直接搜尋 black_box,在 login.js 裡可以很容易找到加密的地方,如下圖所示:

02.png

看一下 _fmOpt.getinfo() 這個方法,是呼叫了 fm.js 裡的 OO0O0() 方法,看這個又是 0 又是 O 的,多半是混淆了,如下圖所示:

03.png

點進去看一下,整個 fm.js 都是混淆程式碼,我們選中類似 OQoOo[251] 的程式碼,可以看到實際上是一個字串物件,也可以直接在 Console 裡輸出看到其實際值,這個 OO0O0 方法返回的 oOoo0[OQoOo[448]](JSON[OQoOo[35]](O0oOo[OQoOo[460]])),就是 black_box 的值,如下圖所示:

04.png

仔細觀察,可以發現 OQoOo 應該是一個類似陣列的東西,通過傳入元素下標來依次取其真實值,隨便搜尋一個值,可以在程式碼最後面找到一個陣列,這個陣列其實就是 OQoOo,可以傳入下標來驗證一下,如下圖所示:

05.png

到這裡其實就知道了其大致混淆原理,我們可以把這個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 是設定延遲生效時間,不用勾選,如下圖所示:

06.png

替換後再次登入,下斷點,可以看到現在的 JS 已經清晰了不少,再看看這個函式最後的 return 語句,oQOQ0["blackBox"] 包含了 itostv 三個引數,使用 JSON 的 stringify 方法將其轉換成字串,然後呼叫 QQo0 方法進行加密,如下圖所示:

07.png

我們先來看看 oQOQ0["blackBox"] 裡的四個引數,其中 itosv 三個引數在這個函式開始就已經有定義,v 就是 Q0oQQ["version"],是定值,直接搜尋可以發現這個值是在最開始的那個大列表裡,os 為定值,it 是兩個時間戳相減的值,O000o 這個方法就是兩個值進行相減,oQOQo 這個時間戳可以搜尋 var oQOQo,是一開始載入就生成的時間戳,JS 一開始載入到點選登陸進入加密函式,也就一分鐘左右,所以這裡我們可以直接生成一個五位隨機數(一分鐘左右在毫秒上的差值在五位數左右)。

08.png

現在就剩下一個 t 引數了,往下看 t 其實就是 Q0oQQ["tokens"],中間經過了一個 if-else 語句,可以埋下斷點進行除錯,發現其實只執行了 else 語句,對 t 賦值也就這一句,所以剩下的程式碼其實在扣的時候都可以刪掉。

09.png

這個 tokens 多次測試發現是不變的,嘗試直接搜尋一下 token 關鍵字,可以發現其賦值的地方,對 id 按照 | 符號進行分割,取其第 1 個索引值就是 tokens,再看看 id 的值,並沒有找到明顯的生成邏輯,複製其值搜尋一下,發現是通過一個介面返回的,可以直接寫死,也可以自己先去請求一下這個介面,取其返回的值,如下圖所示:

10.png

11.png

自此所有引數都找完了,回到原來的 return 位置,還差一個加密函式,即 ooOoO["encode"](),直接跟進去,將這個方法扣下來即可,本地除錯缺啥補啥,將用到的函式補全就行了。

12.png

完整程式碼

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()

相關文章