【JS 逆向百例】webpack 改寫實戰,G 某遊戲 RSA 加密

K哥爬蟲發表於2021-11-03

關注微信公眾號:K哥爬蟲,QQ交流群:808574309,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!

宣告

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

逆向目標

  • 目標:G某遊戲登入
  • 主頁:aHR0cHM6Ly93d3cuZ205OS5jb20v
  • 介面:aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=
  • 逆向引數:
    Query String Parameters:

    password: kRtqfg41ogc8btwGlEw6nWLg8cHcCW6R8JaeM......

逆向過程

抓包分析

來到首頁,隨便輸入一個賬號密碼,點選登陸,抓包定位到登入介面為 aHR0cHM6Ly9wYXNzcG9ydC5nbTk5LmNvbS9sb2dpbi9sb2dpbjM=,GET 請求,Query String Parameters 裡,密碼 password 被加密處理了。

01.png

加密入口

直接搜尋關鍵字 password 會發現結果太多不好定位,使用 XHR 斷點比較容易定位到加密入口,有關 XHR 斷點除錯可以檢視 K 哥往期的教程:【JS 逆向百例】XHR 斷點除錯,Steam 登入逆向,如下圖所示,在 home.min.js 裡可以看到關鍵語句 a.encode(t.password, s)t.password 是明文密碼,s 是時間戳。

02.png

跟進 a.encode() 函式,此函式仍然在 home.min.js 裡,觀察這部分程式碼,可以發現使用了 JSEncrypt,並且有 setPublicKey 設定公鑰方法,由此可以看出應該是 RSA 加密,具體步驟是將明文密碼和時間戳組合成用 | 組合,經過 RSA 加密後再進行 URL 編碼得到最終結果,如下圖所示:

03.png

RSA 加密找到了公鑰,其實就可以直接使用 Python 的 Cryptodome 模組來實現加密過程了,程式碼如下所示:

import time
import base64
from urllib import parse
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5


password = "12345678"
timestamp = str(int(time.time() * 1000))
encrypted_object = timestamp + "|" + password
public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB"
rsa_key = RSA.import_key(base64.b64decode(public_key))  # 匯入讀取到的公鑰
cipher = PKCS1_v1_5.new(rsa_key)                        # 生成物件
encrypted_password = base64.b64encode(cipher.encrypt(encrypted_object.encode(encoding="utf-8")))
encrypted_password = parse.quote(encrypted_password)
print(encrypted_password)

即便是不使用 Python,我們同樣可以自己引用 JSEncrypt 模組來實現這個加密過程(該模組使用方法可參考 JSEncrypt GitHub),如下所示:

/*
引用 jsencrypt 加密模組,如果在 PyCharm 裡直接使用 require 引用最新版 jsencrypt,
執行可能會提示 jsencrypt.js 裡 window 未定義,直接在該檔案定義 var window = this; 即可,
也可以使用和網站用的一樣的 2.3.1 版本:https://npmcdn.com/jsencrypt@2.3.1/bin/jsencrypt.js
也可以將 jsencrypt.js 直接貼上到此指令碼中使用,如果提示未定義,直接在該指令碼中定義即可。
*/

JSEncrypt = require("jsencrypt")

function getEncryptedPassword(t, e) {
    var jsEncrypt = new JSEncrypt();
    jsEncrypt.setPublicKey('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB');
    var i = e ? e + "|" + t : t;
    return encodeURIComponent(jsEncrypt.encrypt(i));
}

var password = "12345678";
var timestamp = (new Date).getTime();
console.log(getEncryptedPassword(password, timestamp));

webpack 改寫

本文的標題是 webpack 改寫實戰,所以很顯然本文的目的是為了練習 JavaScript 模組化程式設計 webpack 程式碼的改寫,現在大多數站點都使用了這種寫法,然而並不是所有站點都像本文遇到的站點一樣,可以很容易使用其他方法來實現的,往往大多數站點需要你自己扒下他的原始碼來還原加密過程,有關 JavaScript 模組化程式設計,即 webpack,在 K 哥往期的文章中有過詳細的介紹:爬蟲逆向基礎,理解 JavaScript 模組化程式設計 webpack

一個標準的 webpack 整體是一個 IIFE 立即呼叫函式表示式,其中有一個模組載入器,也就是呼叫模組的函式,該函式中一般具有 function.call() 或者 function.apply() 方法,IIFE 傳遞的引數是一個列表或者字典,裡面是一些需要呼叫的模組,寫法類似於:

!function (allModule) {
    function useModule(whichModule) {
        allModule[whichModule].call(null, "hello world!");
    }
}([
    function module0(param) {console.log("module0: " + param)},
    function module1(param) {console.log("module1: " + param)},
    function module2(param) {console.log("module2: " + param)},
]);

觀察這次站點的加密程式碼,會發現所有加密方法都在 home.min.js 裡面,在此檔案開頭可以看到整個是一個 IIFE 立即呼叫函式表示式,function e 裡面有關鍵方法 .call(),由此可以判斷該函式為模組載入器,後面傳遞的引數是一個字典,裡面是一個個的物件方法,也就是需要呼叫的模組函式,這就是一個典型的 webpack 寫法,如下圖所示:

04.png

接下來我們通過 4 步完成對 webpack 程式碼的改寫,將原始程式碼扒下來實現加密的過程。

1、找到 IIFE

IIFE 立即呼叫函式表示式,也稱為立即執行函式,自執行函式,將原始碼中的 IIFE 框架摳出來,後續將有用的程式碼再往裡面放:

!function (t) {
    
}({
    
})

2、找到模組載入器

前面我們已經講過,帶有 function.call() 或者 function.apply() 方法的就是模組載入器,也就是呼叫模組的方法,在本例中,function e 就是模組載入器,將其摳下來即可,其他多餘的程式碼可以直接刪除,注意裡面用到了 i,所以定義 i 的語句也要摳下來:

!function (t) {
    function e(s) {
        if (i[s])
            return i[s].exports;
        var n = i[s] = {
            exports: {},
            id: s,
            loaded: !1
        };
        return t[s].call(n.exports, n, n.exports, e),
            n.loaded = !0,
            n.exports
    }
    var i = {};
}({
    
})

3、找到呼叫的模組

重新來到加密的地方,第一個模組是 3,n 裡面的 encode 方法最終返回的就是加密後的結果,如下圖所示:

05.png

第二個模組是 4,可以看到模組 3 裡面的 this.jsencrypt.encrypt(i) 方法實際上是呼叫的第 3340 行的方法,該方法在模組 4 裡面,這裡定位在模組 4 的方法,可以在瀏覽器開發者工具 source 頁面,將滑鼠游標放到該函式前面,一直往上滑動,直到模組開頭,也可以使用 VS Code 等編輯器,將整個 home.min.js 程式碼貼上過去,然後選擇摺疊所有程式碼,再搜尋這個函式,即可快速定位在哪個模組。

06.png

確定使用了 3 和 4 模組後,將這兩個模組的所有程式碼扣下來即可,大致程式碼架構如下(模組 4 具體的程式碼太長,已刪除):

!function (t) {
    function e(s) {
        if (i[s])
            return i[s].exports;
        var n = i[s] = {
            exports: {},
            id: s,
            loaded: !1
        };
        return t[s].call(n.exports, n, n.exports, e),
            n.loaded = !0,
            n.exports
    }
    var i = {};
}(
    {
        4: function (t, e, i) {},
        3: function (t, e, i) {
            var s;
            s = function (t, e, s) {
                function n() {
                    "undefined" != typeof r && (this.jsencrypt = new r.JSEncrypt,
                        this.jsencrypt.setPublicKey("-----BEGIN PUBLIC KEY-----略-----END PUBLIC KEY-----"))
                }

                var r = i(4);
                n.prototype.encode = function (t, e) {
                    var i = e ? e + "|" + t : t;
                    return encodeURIComponent(this.jsencrypt.encrypt(i))
                },
                    s.exports = n
            }.call(e, i, e, t),
                !(void 0 !== s && (t.exports = s))
        }
    }
)

這裡需要我們理解一個地方,那就是模組 3 的程式碼裡有一行 var r = i(4);,這裡的 i3: function (t, e, i) {},傳遞過來的 i,而模組 3 又是由模組載入器呼叫的,即 .call(n.exports, n, n.exports, e) 裡面的某個引數就是 i,前面在講解基礎的時候已經說過,.call 的第一個引數指定的是函式體內 this 物件的指向,並不代表真正引數,所以第一個 n.exports 並不是引數,從第二個引數即 n 開始算,那麼 i 其實就是 .call(n.exports, n, n.exports, e) 裡面的 e,所以 var r = i(4); 實際上就是模組載入器 function e 呼叫了模組 4,由於這裡模組 4 是個物件,所以這裡最好寫成 var r = i("4");,這裡是數字,所以可以成功執行,如果模組 4 名字變成 func4 或者其他名字,那麼呼叫時就必須要加引號了。

4、匯出加密函式

目前關鍵的加密程式碼已經剝離完畢了,最後一步就是需要把加密函式匯出來供我們呼叫了,首先定義一個全域性變數,如 eFunc,然後在模組載入器後面使用語句 eFunc = e,把模組載入器匯出來:

var eFunc;

!function (t) {
    function e(s) {
        if (i[s])
            return i[s].exports;
        var n = i[s] = {
            exports: {},
            id: s,
            loaded: !1
        };
        return t[s].call(n.exports, n, n.exports, e),
            n.loaded = !0,
            n.exports
    }
    var i = {};
    eFunc = e
}(
    {
        4: function (t, e, i) {},
        3: function (t, e, i) {}
    }
)

然後定義一個函式,傳入明文密碼,返回加密後的密碼:

function getEncryptedPassword(password) {
    var timestamp = (new Date).getTime();
    var encryptFunc = eFunc("3");
    var encrypt = new encryptFunc;
    return encrypt.encode(password, timestamp)
}

其中 timestamp 為時間戳,因為我們最終需要呼叫的是模組 3 裡面的 n.prototype.encode 這個方法,所以首先呼叫模組 3,返回的是模組 3 裡面的 n 函式(可以在瀏覽器執行程式碼,一步一步檢視結果),然後將其 new 出來,呼叫 n 的 encode 方法,返回加密後的結果。

自此,webpack 的加密程式碼就剝離完畢了,最後除錯會發現 navigator 和 window 未定義,定義一下即可:

var navigator = {};
var window = global;

這裡擴充套件一下,在瀏覽器裡面 window 其實就是 global,在 nodejs 裡沒有 window,但是有個 global,與瀏覽器的 window 物件型別相似,是全域性可訪問的物件,因此在 nodejs 環境中可以將 window 定義為 global,如果定義為空,可能會引起其他錯誤。

完整程式碼

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

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

JavaScript 加密關鍵程式碼架構

方法一:webpack 改寫原始碼實現 RSA 加密:

var navigator = {};
var window = global;
var eFunc;

!function (t) {
    function e(s) {
        if (i[s])
            return i[s].exports;
        var n = i[s] = {
            exports: {},
            id: s,
            loaded: !1
        };
        return t[s].call(n.exports, n, n.exports, e),
            n.loaded = !0,
            n.exports
    }

    var i = {};
    eFunc = e;
}(
    {
        4: function (t, e, i) {},
        3: function (t, e, i) {}
    }
)

function getEncryptedPassword(password) {
    var timestamp = (new Date).getTime();
    var encryptFunc = eFunc("3");
    var encrypt = new encryptFunc;
    return encrypt.encode(password, timestamp)
}

// 測試樣例
// console.log(getEncryptedPassword("12345678"))

方法二:直接使用 JSEncrypt 模組實現 RSA 加密:

/*
引用 jsencrypt 加密模組,此腳適合在 nodejs 環境下執行。
1、使用 require 語句引用,前提是使用 npm 安裝過;
2、將 jsencrypt.js 直接貼上到此指令碼中使用,同時要將結尾 exports.JSEncrypt = JSEncrypt; 改為 je = JSEncrypt 匯出方法。
PS:需要定義 var navigator = {}; var window = global;,否則提示未定義。
*/

// ========================= 1、require 方式引用 =========================
// var je = require("jsencrypt")

// =================== 2、直接將 jsencrypt.js 複製過來 ===================
/*! JSEncrypt v2.3.1 | https://npmcdn.com/jsencrypt@2.3.1/LICENSE.txt */
var navigator = {};
var window = global;

// 這裡是 jsencrypt.js 程式碼

function getEncryptedPassword(t) {
    var jsEncrypt = new je();
    jsEncrypt.setPublicKey('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq04c6My441Gj0UFKgrqUhAUg+kQZeUeWSPlAU9fr4HBPDldAeqzx1UR92KJHuQh/zs1HOamE2dgX9z/2oXcJaqoRIA/FXysx+z2YlJkSk8XQLcQ8EBOkp//MZrixam7lCYpNOjadQBb2Ot0U/Ky+jF2p+Ie8gSZ7/u+Wnr5grywIDAQAB');
    var e = (new Date).getTime();
    var i = e ? e + "|" + t : t;
    return encodeURIComponent(jsEncrypt.encrypt(i));
}

// 測試樣例
// console.log(getEncryptedPassword("12345678"));

Python 登入關鍵程式碼

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import re
import json
import time
import random
import base64
from urllib import parse

import execjs
import requests
from PIL import Image
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5

login_url = '脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler'
verify_image_url = '脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler'
check_code_url = '脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler'

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
session = requests.session()


def get_jquery():
    jsonp = ''
    for _ in range(21):
        jsonp += str(random.randint(0, 9))
    jquery = 'jQuery' + jsonp + '_'
    return jquery


def get_dict_from_jquery(text):
    result = re.findall(r'\((.*?)\)', text)[0]
    return json.loads(result)


def get_encrypted_password_by_javascript(password):
    # 兩個 JavaScript 指令碼,兩種方法均可
    with open('gm99_encrypt.js', 'r', encoding='utf-8') as f:
    # with open('gm99_encrypt_2.js', 'r', encoding='utf-8') as f:
        exec_js = f.read()
    encrypted_password = execjs.compile(exec_js).call('getEncryptedPassword', password)
    return encrypted_password


def get_encrypted_password_by_python(password):
    timestamp = str(int(time.time() * 1000))
    encrypted_object = timestamp + "|" + password
    public_key = "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler"
    rsa_key = RSA.import_key(base64.b64decode(public_key))  # 匯入讀取到的公鑰
    cipher = PKCS1_v1_5.new(rsa_key)                        # 生成物件
    encrypted_password = base64.b64encode(cipher.encrypt(encrypted_object.encode(encoding="utf-8")))
    encrypted_password = parse.quote(encrypted_password)
    return encrypted_password


def get_verify_code():
    response = session.get(url=verify_image_url, headers=headers)
    with open('code.png', 'wb') as f:
        f.write(response.content)
    image = Image.open('code.png')
    image.show()
    code = input('請輸入圖片驗證碼: ')
    return code


def check_code(code):
    timestamp = str(int(time.time() * 1000))
    params = {
        'callback': get_jquery() + timestamp,
        'ckcode': code,
        '_': timestamp,
    }
    response = session.get(url=check_code_url, params=params, headers=headers)
    result = get_dict_from_jquery(response.text)
    if result['result'] == 1:
        pass
    else:
        raise Exception('驗證碼輸入錯誤!')


def login(username, encrypted_password, code):
    timestamp = str(int(time.time() * 1000))
    params = {
        'callback': get_jquery() + timestamp,
        'encrypt': 1,
        'uname': username,
        'password': encrypted_password,
        'remember': 'checked',
        'ckcode': code,
        '_': timestamp
    }
    response = session.get(url=login_url, params=params, headers=headers)
    result = get_dict_from_jquery(response.text)
    print(result)


def main():
    # 測試賬號:15434947408,密碼:iXqC@aJt8fi@VwV
    username = input('請輸入登入賬號: ')
    password = input('請輸入登入密碼: ')

    # 獲取加密後的密碼,使用 Python 或者 JavaScript 實現均可
    encrypted_password = get_encrypted_password_by_javascript(password)
    # encrypted_password = get_encrypted_password_by_python(password)

    # 獲取驗證碼
    code = get_verify_code()

    # 校驗驗證碼
    check_code(code)

    # 登入
    login(username, encrypted_password, code)


if __name__ == '__main__':
    main()

相關文章