使用Frida在Windows中攔截C++函數
1.摘要
Frida是一款基于Python+javascript的Hook框架, 可運行在Windows、Android、iOS、Linux、MacOS全平臺系統中,主要使用了動態二進制插樁技術。插樁技術是指將額外的代碼注入到目標程序中, 以實現收集目標運行時信息, 插樁技術主要分為兩種:源代碼插樁和二進制插樁, 源代碼插樁是將額外代碼注入到程序源代碼中,二進制插樁是將額外代碼注入到二進制可執行文件中。
使用Frida可以訪問目標進程的內存空間,在目標程序運行時可以覆蓋一些功能,從導入的類中調用函數,在堆上可以查找對象實例并使用這些對象實例,并可以Hook、跟蹤和攔截函數等等。
2.Frida的能力
Frida是一個非常強大的動態instrumentation框架,其主要能力包括:
- 攔截(Hook)系統API調用:可以hook目標程序的各種系統API調用,如文件、網絡、進程、加密等,攔截和修改參數或返回值。
- 注入JS代碼:可以實時向目標程序注入JavaScript代碼,調用其函數接口或修改程序運行邏輯。
- 動態調試:可以動態地設置斷點、Dump內存、遍歷對象等,無需重啟程序。
- 反混淆和脫殼:可以通過dump內存或hook相關函數的方式對加殼/混淆的目標程序進行反混淆和脫殼。
- 適用范圍廣:支持Windows、Linux、macOS、iOS、Android等主流平臺。
- 多語言綁定:提供Java、Python、C#等語言的綁定庫, 可以使用腳本語言進行交互。
- 插件機制:支持擴展自己的插件,實現自定義功能。
- 開源免費:Frida是完全開源免費的,社區活躍。
3.Frida常用語法
Frida在實戰使用過程中,經常使用的功能語法主要包括以下這些:
導入frida模塊:
const frida = require('frida');
附加/注入進程:
// 附加
const session = await frida.attach(pid);
// 注入
const session = await frida.spawn([path], options);
創建/加載/卸載腳本實例:
# 創建
const script = await session.createScript(source);
# 加載
await script.load();
# 卸載
await script.unload();
導出函數:
rpc.exports = {
func1: (args) => {
// ...
}
}
Hook函數:
Interceptor.attach(target, {
onEnter: function(args) {
},
onLeave: function(retval) {
}
});
讀寫內存:
let buf = Memory.readByteArray(addr, len);
Memory.writeByteArray(addr, [1, 2, 3]);
枚舉/搜索模塊:
// 枚舉
Process.enumerateModules()
// 搜索
Process.findModuleByName()
枚舉/搜索導出函數:
// 枚舉
Module.enumerateExports()
// 搜索
Module.findExportByName()
調用函數:
let retval = Module.getExportByName()(args);
4.Frida安裝
這里以Windows10環境為基礎進行實驗, 首先在Windows搜索框中搜索:PowerShell, 以管理員權限打開, 并執行以下命令:
pip install frida-tools
注意:這里一定要以管理員權限打開PowerShell,否則可能會安裝失敗。
安裝成功后如圖所示:
圖片
輸入命令frida --version 查看Frida的版本號,如果正常顯示版本號,則說明安裝成功, 如圖:
圖片
5.編寫測試程序
編寫測試程序的目的是要驗證Frida能否成功Hook測試程序中的指定函數, 并將函數的每個參數內容進行打印。
我在這里的測試程序使用C++編寫, 主要完成2個函數, AES加密和解密算法, 設想的步驟是將AES加密算法和解密算法的兩個函數編譯成Dll,并將兩個函數導出, 然后再寫一個客戶端程序加載Dll并調用導出函數。
AES加密算法的C++代碼如下:
extern "C" __declspec(dllexport) void AesEncrypt(unsigned char* plaintext, int plaintext_len, unsigned char* key, unsigned char* iv,
unsigned char* ciphertext) {
EVP_CIPHER_CTX* ctx;
int len;
int ciphertext_len;
printf("AesEncrypt is at %p\n", AesEncrypt);
ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len);
ciphertext_len = len;
EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);
ciphertext_len += len;
EVP_CIPHER_CTX_free(ctx);
}
AES加密算法函數AesEncrypt包含了四個參數,分別為: 明文字符串、明文字符串長度、Key、iv向量。
AES解密算法C++代碼如下:
// AES解密
extern "C" __declspec(dllexport) void AesDecrypt(unsigned char* ciphertext, int ciphertext_len, unsigned char* key, unsigned char* iv,
unsigned char* plaintext) {
EVP_CIPHER_CTX* ctx;
int len;
int plaintext_len;
printf("AesDecrypt is at %p\n", AesDecrypt);
ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv);
EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len);
plaintext_len = len;
EVP_DecryptFinal_ex(ctx, plaintext + len, &len);
plaintext_len += len;
EVP_CIPHER_CTX_free(ctx);
}
同樣,AES解密算法函數AesDecrypt也提供了四個參數,分別為:密文文本、密文文本長度、Key、iv向量。
重新建立一個新的C++工程, 這里模擬了真實程序的業務場景,對明文字符串使用AES算法加密,為了防止程序運行太快退出,這里將主要程序邏輯放到一個while循環中,并使用暫停功能進行控制,方便后面的函數Hook實驗。為了方便操作, 我在中間插入了pause暫停, 方便后面手動控制函數的調用時機。
應用代碼如下:
typedef void(__stdcall* AES_ENCRYPT_TYPE)(unsigned char*, int, unsigned char*, unsigned char*,
unsigned char*);
typedef void(__stdcall* AES_DECRYPT_TYPE)(unsigned char*, int, unsigned char*, unsigned char*,
unsigned char*);
int main()
{
while (1) {
//std::cout << "Hello World!\n";
//原文
unsigned char plaintext[] = "This is a plaintext message";
// 密鑰
unsigned char key[32] = "suntiger20232021sdvdiuyt657uhjg";
// 初始化向量
unsigned char iv[16] = "9876kvdfdkkdfdf";
// 密文緩沖區
unsigned char ciphertext[128];
memset(ciphertext, 0, 128);
// 加密
AesEncrypt(plaintext, strlen((char*)plaintext), key, iv, ciphertext);
system("pause");
unsigned char decryptedtext[128];
memset(decryptedtext, 0, 128);
AesDecrypt(ciphertext, strlen((char*)ciphertext), key, iv, decryptedtext);
printf("%s\n", decryptedtext);
system("pause");
Sleep(1);
}
}
將以上代碼編譯執行后,每按一次空格鍵, 程序便會依次向下執行, 如圖:
圖片
6.編寫插樁程序
插樁程序代碼由Python和Javascript語言組合而成,功能非常強大, 以下代碼是我自己實現的對動態鏈接庫中的導出函數AesEncrypt進行Hook,建立一個Python工程,代碼如下:
import frida
import sys
def on_message(message, data):
print(message)
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
local = frida.get_local_device()
// 附加進程
session = local.attach("test.exe")
script = session.create_script("""
var baseAddr = Module.findBaseAddress('aes.dll');
// 查找函數模塊
var aesEncryptAddr = Module.findExportByName("aes.dll", "AesEncrypt");
Interceptor.attach(aesEncryptAddr, {
onEnter: function(args) {
send(args[0]);
console.log('');
console.log('[+]plaintext: ' + Memory.readUtf8String(args[0]));
console.log('[+]plaintext len: ' + args[1].toInt32());
console.log('[+]key:' + Memory.readUtf8String(args[2]));
console.log('[+]iv:' + Memory.readUtf8String(args[3]));
}
});
""")
script.on('message', on_message)
script.load()
sys.stdin.read()
session.detach()
在上面的Hook代碼中,我主要完成了以下操作:
- 通過frida在本地查找進程test.exe,也就是我上面實現的客戶端測試程序進程。
- 通過Javascript腳本在進程中查找模塊aes.dll, 這個動態鏈接庫是我上面實現的AES加解密測試程序。
- 在動態鏈接庫中查找導出函數AES加密函數:AesEncrypt,并附加到該函數地址進行參數監控。
- 打印每個參數的內容。
以下是我執行插樁程序后,獲取到的參數內容,如圖:
圖片
可以看到,已經成功把解密函數的四個參數全部打印出來, C++函數Hook成功。