1. 程式人生 > 其它 >寫一個frida通殺指令碼

寫一個frida通殺指令碼

1. 前言

過年對我來說和平常沒什麼區別,該幹什麼幹什麼。

之前沒接觸過 frida 這個工具,前幾天用了一些時間學習了一下,相比於 xposed hook 框架,frida 相對於除錯方面真的很方便。現在網上也有一些 frida 通殺指令碼(也有叫自吐演算法指令碼的),但是一般都是在 iv向量構造,key 構造分別進行 hook ,這樣就導致 最後輸出結果不是一個整體,加密和解密的資料,iv向量,key,輸出不在同一塊。我也不想從網上拿來就用(總感覺自己寫一遍用起來才舒服,畢竟這個不算太複雜,還能熟悉一下 frida),所以我想製作一個輸出以上資訊在同一塊演算法通殺指令碼,後面也用C++ Qt寫了一個軟體用來檢視記錄的資料。

2. 什麼是hook?

這個問題讓我想起我在大學期間,當時我用 Linux mint 系統,linux 系統上沒有 QQ,所以我用 deepin-wine封裝的QQ軟體。但是使用過程中我發現有一個bug,就是不能開啟接收到檔案或資料夾,我當時猜測這個問題是 mint 沒有 對應的檔案管理器導致的,因為我用的是mint, 而軟體使用的系統是在 deepin 上使用的,所以我在 mint系統上建立了一個與 deepin系統上的檔案管理器同名的命令指令碼,然後這個命令指令碼去呼叫 mint 本地檔案管理器去開啟 對應的資料夾或檔案,這樣問題就解決了。

當年文章

這個原理就類似 hook,只不過我沒有攔截訊息的傳遞(因為壓根就沒有接收訊息的命令)。通俗來講就是 攔截住訊息傳遞,然後再去處理這個訊息。當時我解決 deepin-wine QQ上這個bug,感覺自己這個操作太秀了,現在想想不過是當時自己瞭解的東西太少,是一種無知的體現。

3. 通殺演算法(自吐演算法)指令碼原理

安卓上呼叫 AES 加密解密, MD5 摘要演算法時,都是需要呼叫基礎的一個類,所以只要 hook 這個基礎類,那麼無論在什麼時候什麼地方呼叫到 演算法,都會執行到基礎類。除非是app 裡面自己實現的加密演算法,那就只能分析 app 內部的程式碼了。

例如一個AES加密的呼叫示例:

public static byte[] aes_enc(byte[] bytesContent, String key) throws Exception
{
    byte[] raw = key.getBytes("utf-8");
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    byte[] enc = cipher.doFinal(bytesContent);
    return enc;
}

可以看到這裡有一個關鍵類 Cipher ,只需 hook doFinal()這個函式,就可以獲得密文和明文,而祕鑰 可以通過 hook SecretKeySpec() 這個類建構函式來獲得。

這些類的實現都在 jce.jar 中實現,在 JDK中有。

Cipher 類:

SecretKeySpec

4. 分析

在 hook 這些類的建構函式或普通函式的時候,遇到了很多過載,下面簡單理出這些過載的呼叫關係。

Cipher 類

// getInstance 函式
// 2.overload('java.lang.String', 'java.lang.String') -> 3
// 1.overload('java.lang.String') |
// 3.overload('java.lang.String', 'java.security.Provider') |


// init 函式
// 1.overload('int', 'java.security.Key') -> 4
// 2.overload('int', 'java.security.cert.Certificate') -> 6
// 3.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters') -> 7
// 5.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec') ->8
// 6.overload('int', 'java.security.cert.Certificate', 'java.security.SecureRandom') |
// 4.overload('int', 'java.security.Key', 'java.security.SecureRandom') |
// 7.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters', 'java.security.SecureRandom') |
// 8.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec', 'java.security.SecureRandom') |

// doFinal 函式
// 1.overload() |
// 2.overload('[B') | 
// 3.overload('java.nio.ByteBuffer', 'java.nio.ByteBuffer') | 
// 4.overload('[B', 'int') |
// 5.overload('[B', 'int', 'int') |
// 6.overload('[B', 'int', 'int', '[B') |
// 7.overload('[B', 'int', 'int', '[B', 'int') |

這裡的數字表示過載函式的編號條目, -> 表示呼叫,| 表示不呼叫其它過載函數了。通過這種寫法,可以清晰的看出 過載函式之間的呼叫關係。

所以為什麼需要理清過載關係? 因為這樣可以知道 hook 哪些 函式是必要的。如果我們不知道過載函式之間呼叫的關係,直接hook 一個函式的所有過載:

var cipher = Java.use("javax.crypto.Cipher");
    // 加密型別
    // 2.overload('java.lang.String', 'java.lang.String') -> 3
    // 1.overload('java.lang.String') |
    // 3.overload('java.lang.String', 'java.security.Provider') |
    for (let index = 0; index < cipher.getInstance.overloads.length; index++) {
        cipher.getInstance.overloads[index].implementation = function () {
            console.log("型別:" + JSON.stringify(arguments[0]));
            console.log(JSON.stringify(this));
            return this.getInstance.apply(this, arguments);
        }
    }

這樣,你可能會發現,輸出 型別: 兩次, 因為 1,2,3 函式都被 hook了,app 可能只是呼叫了 2 ,而 2 本身有呼叫了 3。所以就會輸出兩次,所以針對這個 getInstance 函式,只需要 hook 1,3 過載函式就可以實現對所有呼叫的監聽。

5. 深度分析

上述內容,可以實現對呼叫函式引數的監聽,並且減少了 不必要 函式的 hook。但是還是不能實現 輸出資料 在一整塊地方,加密型別輸出 和 明文,密文 可能是分散輸出的。而要實現我 前言 中所述 的功能,就必須通過 hook 一個函式來實現,在一個函式內 獲取 祕鑰iv 向量明文密文模式加密or解密,這些資訊,然後輸出,其實這種肯定是要利用物件本身來傳遞的,就是檢視物件屬性上的繫結。


模式 : 這裡所說的模式,指的是 "AES/ECB/PKCS5Padding" 字串,所以從 getInstance 函式開始分析,當然這裡只需要分析 肯定會被呼叫到的函式過載,也就是結尾帶|的函式:

可以看到,getInstance 這個函式就是返回了一個 cipher 類的例項化物件,並且這個字串傳遞過去,所以進一步跟蹤分析其建構函式:

可以看到這幾個建構函式,將 paramString 引數都專遞給了 this.transformation,那麼這個 模式 就可以通過 this來獲取到了


加密or解密: 加密或解密,是 init 函式的第一個引數,所以這裡重 init 函式開始分析引數的傳遞狀態,同樣也只需要分析肯定能被呼叫到的函式就可以:

這4個過載函式都是 paramInt函式都是 傳遞給 this.opmode


IV向量, 這個有點難找,具體過程不多說了,可以通過 this.spi.engineGetIV() 或者 this.getIV() 獲得,其實this.getIV() 也是呼叫 this.spi.engineGetIV()獲得的。


密文和明文:因為我要hook 一個函式,所以我想hook的就是最後的 doFinal 函式,這樣就可以獲取到 密文和明文了,然後再通過 this ,獲取到上述所說的 模式加密或解密iv向量


祕鑰: 只有這個引數是我沒有通過 this 獲取到,所以我 hook 了init 函式,其第二個引數就是祕鑰。


6. 問題及解決

因為除了祕鑰我都可以 通過 this 來獲取到,所以祕鑰獲得後我在 JS 用一個全域性變數來儲存,然後在我hook 的 doFinal 函式 中進行輸出,但是這裡就遇到一個問題,多個執行緒可能同時進行加密解密,key 可能不是當時 物件 doFinal 函式使用的 key,那麼這裡我需要對例項化物件的唯一ID 與 key 進行一個繫結,然後 doFinal 函式裡通過物件唯一ID 來獲取key ,進行輸出。

這裡的解決方案是我製作了一個字典(python 叫字典,js叫啥我忘了),其中鍵為物件的唯一ID,值為 祕鑰。這就能保證其對應關係的準確性了。

7. python 呼叫及資料儲存

hookCalc.js

var allKeys = {};

Java.perform(function () {
    var cipher = Java.use("javax.crypto.Cipher");

    for (let index = 0; index < cipher.init.overloads.length; index++) {
        cipher.init.overloads[index].implementation = function () {
            allKeys[this.toString()] = arguments[1].getEncoded();
            this.init.apply(this, arguments);
        }
    }

    for (let index = 0; index < cipher.doFinal.overloads.length; index++) {
        cipher.doFinal.overloads[index].implementation = function () {
            var dict = {};
            dict["EorD"] = this.opmode.value; //模式 加密解密
            dict["method"] = this.transformation.value; //加密型別
            var iv =  this.spi.value.engineGetIV();
            if (iv){
                dict["iv"] = iv;
            }else{
                dict["iv"] = "";
            }
            if (allKeys[this.toString()]){
                dict["password"] = allKeys[this.toString()]
            }else{
                dict["password"] = "";
            }
            var retVal = this.doFinal.apply(this, arguments);
            dict["receData"] = "";
            dict["resData"] = "";
            if (arguments.length >= 1 && arguments[0].$className != "java.nio.ByteBuffer") {
                dict['receData'] = arguments[0];
                dict["resData"] = retVal;
            }
            send(dict);
            return retVal;
        }
    }
})

main.py

import frida
import sys
import sqlite3 
import hashlib

 
index = 0
db = "me.db"

def md5(data):
    hl = hashlib.md5()
    hl.update(data)
    return hl.hexdigest()

def createDB():
    sql = '''
    CREATE TABLE IF NOT EXISTS "record" (
        "id"    TEXT NOT NULL,
        "method"    INTEGER,
        "EorD"  TEXT,
        "password"  BLOB,
        "iv"    BLOB,
        "receData"  BLOB,
        "resData"   BLOB,
        PRIMARY KEY("id")
    );
    '''
    conn = sqlite3.connect(db)
    cursor = conn.cursor()
    cursor.execute(sql)
    conn.commit()
    conn.close()

def message(message,arg2):
    try:
        global index
        conn = sqlite3.connect(db)
        cursor = conn.cursor()

        if message['type'] == "send":
            data = message['payload']
            method = data["method"]
            EorD = data["EorD"]
            password = bytes([i if i>=0 else 256+i for i in  data["password"]])
            iv =  bytes([i if i>=0 else 256+i for i in   data["iv"]])
            receData =  bytes([i if i>=0 else 256+i for i in  data["receData"]])
            resData =  bytes([i if i>=0 else 256+i for i in  data["resData"]])
            id_md5 = md5((method+str(EorD)).encode()+password+iv+receData+resData)
            
            sql = "insert into record(id,method,EorD,password,iv,receData,resData) values (?,?,?,?,?,?,?)"
            cursor.execute(sql,(id_md5,method,EorD,sqlite3.Binary(password),sqlite3.Binary(iv),sqlite3.Binary(receData),sqlite3.Binary(resData)))
            conn.commit()
            print(index)
            index += 1
    except Exception as e:
        print(e)
        pass

with open("hookCalc.js",encoding='utf8') as f:
    js = f.read()

process = frida.get_remote_device().attach("APP名字,非包名")
script = process.create_script(js)
script.on("message",message)
script.load()
createDB()
print(process)
sys.stdin.read()

資料不便於輸出預覽,因為量大且不可輸出(二進位制資料輸出亂碼),JS 雖然有 轉換編碼及資料格式的函式,但是我還是習慣用 python 來處理。我將其儲存到一個 sqlite3 資料庫,這裡我做了md5,保證資料儲存的唯一性,然後我用 C++ QT 寫個簡單軟體預覽這些資料。

C++預覽資料軟體

軟體讀取同級目錄的 me.db 資料庫。

其它

指令碼主要針對 AES 演算法,也應該可以捕獲的RSA,至於md5可以自行拓展,因為主要就是 AES演算法 引數較多,會有輸出會這一塊那一塊的問題,md5 根本不會有這樣問題。

軟體及指令碼

本文來自部落格園,作者:Hello_wshuo,轉載請註明原文連結:https://www.cnblogs.com/Hellowshuo/p/15856211.html