【JS 逆向百例】PEDATA 加密資訊以及 zlib.gunzipSync() 的應用

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

宣告

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

逆向目標

  • 目標:某投資領域 SAAS 系統 PEDATA MAX 資訊,返回結果加密
  • 主頁:aHR0cHM6Ly9tYXgucGVkYXRhLmNuL2NsaWVudC9uZXdzL25ld3NmbGFzaA==
  • 介面:aHR0cHM6Ly9tYXgucGVkYXRhLmNuL2FwaS9xNHgvbmV3c2ZsYXNoL2xpc3Q=
  • 逆向引數:請求返回的加密結果,data: "L+o+YmIyNDE..."

抓包分析

我們在首頁,點選檢視全部24小時資訊,往下拉,資訊是以 Ajax 形式載入的,我們選中開發者工具 XHR 進行篩選,很容易找到一個 list 請求,其返回值 data 是一串經過加密後的字串,exor 不知道是啥,但是後面可能有用,ts 是時間戳,如下圖所示:

01.png

Payload 裡的引數沒有什麼特別的,就是一些翻頁資訊,我們再看看請求 header,這裡注意 CookieHTTP-X-TOKEN 兩個引數,訪問這個頁面需要登入賬號,一般來說,Cookie 是用來標識不同使用者的,但經過 K 哥測試發現,此案例中,這個 HTTP-X-TOKEN 引數才是用來識別使用者的,所以不需要 Cookie 也行,隨便提一嘴,Cookie 中我們經常看到有 Hm_lvt_xxxHm_lpvt_xxx 是用於百度聯盟廣告的資料統計的,與爬蟲無關。

02.png

加密逆向

我們注意到返回的是一個字典,在獲取到加密資料後,肯定會有一個取值的過程,所以我們直接搜尋鍵,搜尋 exor 結果只有一個:

03.png

這裡 e.data 就是返回的字典,e.data.datae.data.exor 依次取加密值和 exor,這裡就可以猜測是將加密值取出來進行解密操作了,我們在此函式結尾處也打個斷點,看看這段程式碼執行完畢後,data 的值是否變成了明文:

04.png

不出所料,Object(p["y"])(e.data.data, e.data.exor) 這段程式碼就是解密函式了,Object(p["y"]) 其實是呼叫了 M 方法,跟進去看看:

05.png

傳入的 t 和 n 分別是加密值和 exor,最後返回的 JSON.parse(c) 就是解密結果:

06.png

關鍵程式碼:

function M(t, n) {
    var a = L(Object(s["a"])(), n)
    , r = Y(B(t), a)
    , c = o.a.gunzipSync(e.from(r)).toString("utf-8");
    return JSON.parse(c)
}

挨個函式扣下來,簡單的就不講了, 其中 Object(s["a"]),選中它,其實是呼叫了 c 方法,跟進 c 方法,實際上是取了 loginToken,這個 loginToken 就是我們前面分析的請求頭中的 HTTP-X-TOKEN,包含了你的登入資訊。

擴充知識:window.localStorage 屬性用於在瀏覽器中儲存鍵值對形式的資料,localStoragesessionStorage 類似,區別在於:localStorage 中的資料可以長期保留,沒有過期時間,直到被手動刪除。sessionStorage 的資料僅儲存在當前會話中,在關閉視窗或標籤頁之後將會刪除這些資料。

07.png

再往下看,有個 o.a.gunzipSync(),先放一下,先看看傳入的引數 e.from(r),跟進看可能看不出來什麼,直接對比 re.from(r),會發現都是 Uint8Array 的資料,一模一樣的,如下圖所示:

08.png

再來看看 o.a.gunzipSync(),實際上呼叫的是 chunk-vendors.js 裡的匿名函式,不知道這個 JS 不要緊,我們注意到 chunk-vendors.js 裡面的程式碼有超過14萬行,再加上這個奇怪的名字,什麼模組供應商,不難想到這是一個系統或者第三方生成的 JS,事實上它是 vue 應用程式構建過程中建立的檔案,對於我們爬蟲工程師來講,粗暴的將其理解為類似 jquery.js 一樣的東西也行,我們一般是不會去扣 jquery.js 裡面的程式碼的,同樣這個 chunk-vendors.js 也不可能傻傻的去扣。

09.png

我們重點看看這個函式名,gunzipSync,其他不認識,但認識 zip 吧,可以聯想到應該與壓縮有關,不瞭解同樣不要緊,直接使出百度大法:

10.png

這直接給出了 nodejs 裡面的實現方法,用的是 zlib 模組,隨便找個示例看看用法:

var zlib = require('zlib');
var input = "Nidhi";
var gzi = zlib.gzipSync(input);
var decom = zlib.gunzipSync(new Buffer.from(gzi)).toString();

console.log(decom);

進一步學習,我們可以知道 zlib.gunzipSync() 方法是 zlib 模組的內建應用程式程式設計介面,用於使用 Gunzip 解壓資料塊。傳入的資料可以是 Buffer、TypedArray、DataView、ArrayBuffer、string 型別,在官方文件中我們可以看到更新歷史裡面,在 v8.0.0 以後,傳入的資料就支援 Uint8Array 了:

11.png

結合前面我們對 r 值的分析,所以在 nodejs 裡,直接把 r 值傳入到 zlib.gunzipSync() 方法裡就可以了,將用到的 L、V、B 三個方法扣出來,然後配合 zlib 庫,改寫一下就能拿到解壓後的資料了:

function getDecryptedData(encryptedData, exor, loginToken) {
    var a = L(loginToken, exor);
    var r = Y(B(encryptedData), a)
    var decryptedData = zlib.gunzipSync(r).toString();
    return decryptedData
}

12.png

完整程式碼

GitHub 關注 K 哥爬蟲,持續分享爬蟲相關程式碼!歡迎 star !https://github.com/kgepachong/

以下只演示部分關鍵程式碼,不能直接執行! 完整程式碼倉庫地址:https://github.com/kgepachong...

JavaScript 加密程式碼

/* ==================================
# @Time    : 2021-12-31
# @Author  : 微信公眾號:K哥爬蟲
# @FileName: main.js
# @Software: PyCharm
# ================================== */

var zlib = require('zlib');

function L(e, t) {
    if ("1" == t)
        return [7, 65, 75, 31, 71, 101, 57, 0];
    for (var n = [], a = 0, r = t.length; a < r; a += 2)
        n.push(e.substr(1 * t.substr(a, 2), 1).charCodeAt());
    return n
}

function Y(e, t) {
    for (var n, a = new Uint8Array(e.length), r = 0, c = e.length; r < c; r++)
        n = t[r % t.length],
            a[r] = e[r].charCodeAt() ^ n;
    return a
}

function B(e) {
    var t, n, a, r, c, u, i, o = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", s = "", f = 0;
    e = e.replace(/[^A-Za-z0-9\+\/\=]/g, "");
    while (f < e.length)
        r = o.indexOf(e.charAt(f++)),
            c = o.indexOf(e.charAt(f++)),
            u = o.indexOf(e.charAt(f++)),
            i = o.indexOf(e.charAt(f++)),
            t = r << 2 | c >> 4,
            n = (15 & c) << 4 | u >> 2,
            a = (3 & u) << 6 | i,
            s += String.fromCharCode(t),
        64 != u && (s += String.fromCharCode(n)),
        64 != i && (s += String.fromCharCode(a));
    return s
}

function getDecryptedData(encryptedData, exor, loginToken) {
    var a = L(loginToken, exor);
    var r = Y(B(encryptedData), a)
    var decryptedData = zlib.gunzipSync(r).toString();
    return decryptedData
}

Python 示例程式碼

# ==================================
# --*-- coding: utf-8 --*--
# @Time    : 2021-12-31
# @Author  : 微信公眾號:K哥爬蟲
# @FileName: main.py
# @Software: PyCharm
# ==================================


import execjs
import requests

news_est_url = "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler"
login_token = "token 換成你自己的!"
headers = {
    "Accept": "application/json, text/plain, */*",
    "Content-Type": "application/json",
    "Host": "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler",
    "HTTP-X-TOKEN": login_token,
    "Origin": "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler",
    "Referer": "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"
}


def get_decrypted_data(encrypted_data, exor):
    with open('pedata_decrypt.js', 'r', encoding='utf-8') as f:
        pedata_js = f.read()
    decrypted_data = execjs.compile(pedata_js).call('getDecryptedData', encrypted_data, exor, login_token)
    return decrypted_data


def get_encrypted_data():
    data = {
        "type": "",
        "module": "LP",
        "page":
            {
                "currentPage": 1,
                "pageSize": 10
            }
    }
    response = requests.post(url=news_est_url, headers=headers, json=data).json()
    encrypted_data, exor = response["data"], response["exor"]
    return encrypted_data, exor


def main():
    encrypted_data, exor = get_encrypted_data()
    decrypted_data = get_decrypted_data(encrypted_data, exor)
    print(decrypted_data)


if __name__ == '__main__':
    main()

相關文章