如何運用Capstone實現(xiàn)64位進程鉤子掃描
進程鉤子掃描是一種安全技術(shù)和分析方法,用于檢測和分析進程內(nèi)的指令是否被篡改或注入了惡意功能。鉤子(Hook)技術(shù)允許開發(fā)人員在執(zhí)行特定系統(tǒng)調(diào)用或函數(shù)時插入自定義代碼。雖然進程鉤子在調(diào)試和軟件功能擴展中發(fā)揮了重要作用,但該技術(shù)也可以被惡意軟件用來攔截和修改程序行為,從而隱藏其活動或進行其他惡意操作。本章將通過Capstone引擎實現(xiàn)64位進程鉤子的掃描,讀者可使用此段代碼檢測目標(biāo)進程內(nèi)是否被掛了鉤子。
通過進程鉤子掃描,安全研究人員和開發(fā)人員可以檢測進程中是否存在未授權(quán)的鉤子,并分析這些鉤子的行為。這有助于識別和防止惡意軟件的活動,確保系統(tǒng)和應(yīng)用程序的完整性和安全性。
在編寫代碼之前,讀者需要自行下載并配置Capstone反匯編引擎,配置參數(shù)如下所示;

在之前的PeView命令行解析工具中筆者介紹了如何掃描32位進程內(nèi)的鉤子,由于32位進程需要重定位所以在掃描時需要考慮到對內(nèi)存地址的修正,而64位進程則無需考慮重定位的問題,其鉤子掃描原理與32位保持一致,均通過將磁盤和內(nèi)存中的代碼段進行反匯編,并逐條比較它們的機器碼和反匯編結(jié)果。如果存在差異,則表示該代碼段在內(nèi)存中被篡改或掛鉤。
定義頭文件
首先引入capstone.h頭文件,并引用capstone64.lib靜態(tài)庫,通過定義PeTextInfo來存儲每個PE文件中節(jié)的文件偏移及大小信息,通過ModuleInfo用于存放進程內(nèi)的模塊信息,而DisassemblyInfo則用來存放反匯編信息,底部則定義PE結(jié)構(gòu)的全局變量用于存儲頭指針。
#include <windows.h>
#include <TlHelp32.h>
#include <tchar.h>
#include <iostream>
#include <atlconv.h>
#include <vector>
#include <inttypes.h>
#include <capstone/capstone.h>
#pragma comment(lib,"capstone64.lib")
using namespace std;
// 存放PE信息段
struct PeTextInfo
{
DWORD64 virtualAddress; // 節(jié)區(qū)在內(nèi)存的偏移
DWORD64 pointerToRawData; // 節(jié)區(qū)在文件中的偏移
DWORD64 size; // 大小
};
// 存放進程內(nèi)所有模塊信息
typedef struct
{
char modulePath[256]; // 模塊路徑
char moduleName[128]; // 模塊名
long long moduleBase; // 模塊基址
}ModuleInfo;
// 存放反匯編數(shù)據(jù)
typedef struct
{
int opCodeSize; // 機器碼長度
int opStringSize; // 反匯編長度
unsigned long long address;// 相對地址
unsigned char opCode[16]; // 機器碼
char opString[256]; // 反匯編
}DisassemblyInfo;
// 全局PE結(jié)構(gòu)
IMAGE_DOS_HEADER* dosHeader; // DOS頭
IMAGE_NT_HEADERS* ntHeader; // NT頭
IMAGE_FILE_HEADER* fileHeader; // 標(biāo)準(zhǔn)PE頭
IMAGE_OPTIONAL_HEADER64* optionalHeader; // 可選PE頭
IMAGE_SECTION_HEADER* sectionHeader; // 節(jié)表進程與線程
在進程與線程處理模塊中,我們定義了三個函數(shù):GetProcessHandleByName、GetProcessIDByName和GetModuleInfoByProcessName。GetProcessHandleByName函數(shù)接收一個進程名并返回該進程的句柄,方便后續(xù)的進程操作;GetProcessIDByName函數(shù)通過進程名獲取其對應(yīng)的PID(進程標(biāo)識符),用于標(biāo)識特定進程;GetModuleInfoByProcessName函數(shù)接收一個進程名并返回該進程內(nèi)所有模塊的信息,包括模塊路徑、模塊名和模塊基址,便于對進程內(nèi)的模塊進行分析和處理。
// -----------------------------------------------------------------------------------
// 進程線程部分
// -----------------------------------------------------------------------------------
// 通過進程名獲取進程句柄
// 參數(shù):
// processName - 進程名
// 返回值:
// 進程句柄
HANDLE GetProcessHandleByName(PCHAR processName)
{
// 初始化進程快照
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
// 獲得快照句柄
HANDLE processSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// 獲取第一個進程
Process32First(processSnap, &processEntry);
do
{
USES_CONVERSION;
if (strcmp(processName, W2A(processEntry.szExeFile)) == 0)
{
// 關(guān)閉快照句柄,避免內(nèi)存泄漏
CloseHandle(processSnap);
// 返回句柄
return OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
}
} while (Process32Next(processSnap, &processEntry));
// 關(guān)閉快照句柄,避免內(nèi)存泄漏
CloseHandle(processSnap);
return (HANDLE)NULL;
}
// 根據(jù)進程名獲取PID
// 參數(shù):
// processName - 進程名
// 返回值:
// 進程ID
DWORD64 GetProcessIDByName(LPCTSTR processName)
{
DWORD64 processID = 0xFFFFFFFF;
HANDLE snapshot = INVALID_HANDLE_VALUE;
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
Process32First(snapshot, &processEntry);
do
{
if (!_tcsicmp(processName, (LPCTSTR)processEntry.szExeFile))
{
processID = processEntry.th32ProcessID;
break;
}
} while (Process32Next(snapshot, &processEntry));
CloseHandle(snapshot);
return processID;
}
// 獲取進程內(nèi)所有模塊信息
// 參數(shù):
// processName - 進程名
// 返回值:
// 包含模塊信息的向量
std::vector<ModuleInfo> GetModuleInfoByProcessName(CHAR* processName)
{
// 讀取進程中的模塊信息
MODULEENTRY32 moduleEntry;
USES_CONVERSION;
DWORD64 processID = GetProcessIDByName(A2W(processName));
// 存放模塊路徑
std::vector<ModuleInfo> moduleInfos = {};
// 在使用這個結(jié)構(gòu)前,先設(shè)置它的大小
moduleEntry.dwSize = sizeof(MODULEENTRY32);
// 獲取模塊快照
HANDLE moduleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, processID);
// INVALID_HANDLE_VALUE表示無效的句柄
if (moduleSnap == INVALID_HANDLE_VALUE)
{
return{};
}
BOOL hasMoreModules = Module32First(moduleSnap, &moduleEntry); // 獲取第一個模塊信息
char* modulePath = NULL; // 模塊路徑
char* moduleName = NULL; // 模塊名
DWORD64 moduleBase = NULL; // 模塊基址
while (hasMoreModules)
{
ModuleInfo moduleInfo;
USES_CONVERSION;
// W2A 將wchar轉(zhuǎn)ascii
modulePath = W2A(moduleEntry.szExePath);
moduleBase = (DWORD64)moduleEntry.modBaseAddr;
moduleName = W2A(moduleEntry.szModule);
// printf("模塊路徑: %s -> 模塊基地址: %x -> 模塊名: %s \n", ModulePath, ModuleBase, ModuleName);
strcpy_s(moduleInfo.modulePath, modulePath);
strcpy_s(moduleInfo.moduleName, moduleName);
moduleInfo.moduleBase = moduleBase;
// 放入容器內(nèi)
moduleInfos.push_back(moduleInfo);
hasMoreModules = Module32Next(moduleSnap, &moduleEntry);
}
CloseHandle(moduleSnap);
return moduleInfos;
}PE文件操作
如下代碼實現(xiàn)了PE(Portable Executable)文件的讀取、解析和擴展功能。我們定義了三個主要函數(shù):ReadPEFile用于從磁盤讀取PE文件數(shù)據(jù),ParsePEHeaders用于解析PE文件的頭信息,ExpandPEImageBuffer用于將PE文件擴展為內(nèi)存中加載后的形式,并復(fù)制文件中的各個節(jié)(section)到內(nèi)存中。最后,GetCodeSectionInfo函數(shù)獲取了PE文件中代碼段的起始地址和大小信息。
// -----------------------------------------------------------------------------------
// PE文件讀寫部分
// -----------------------------------------------------------------------------------
// 讀取硬盤PE文件數(shù)據(jù)
// 參數(shù):
// filePath - 文件路徑
// fileBuffer - 文件緩沖區(qū)指針
// 返回值:
// 文件大小
DWORD64 ReadPEFile(LPSTR filePath, LPVOID* fileBuffer)
{
FILE* file = NULL;
fopen_s(&file, filePath, "rb");
if (file == NULL)
{
return 0;
}
else
{
// 計算文件大小
fseek(file, 0, SEEK_END);
long long fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
// 開辟指定大小的內(nèi)存
LPVOID buffer = malloc(sizeof(char) * fileSize);
if (buffer == NULL)
{
fclose(file);
return 0;
}
// 將文件數(shù)據(jù)拷貝到緩沖區(qū)
size_t bytesRead = fread(buffer, sizeof(char), fileSize, file);
if (!bytesRead)
{
free(buffer);
fclose(file);
return 0;
}
*fileBuffer = buffer;
buffer = NULL;
fclose(file);
return fileSize;
}
return 0;
}
// 讀取PE頭信息
// 參數(shù):
// fileBuffer - 文件緩沖區(qū)指針
// 返回值:
// 成功返回1,失敗返回0
DWORD64 ParsePEHeaders(LPVOID fileBuffer)
{
if (fileBuffer == NULL)
{
// 緩沖區(qū)指針無效
return 0;
}
// 判斷是否是有效的MZ標(biāo)記
if (*((PWORD)fileBuffer) != IMAGE_DOS_SIGNATURE)
{
return 0;
}
dosHeader = (IMAGE_DOS_HEADER*)fileBuffer;
// 判斷是否是有效的pe標(biāo)志
if (*((PDWORD)((DWORD64)fileBuffer + dosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)
{
return 0;
}
ntHeader = (IMAGE_NT_HEADERS*)((DWORD64)fileBuffer + dosHeader->e_lfanew); // NT頭賦值
fileHeader = (IMAGE_FILE_HEADER*)((DWORD64)ntHeader + 4); // 標(biāo)準(zhǔn)PE頭賦值
optionalHeader = (IMAGE_OPTIONAL_HEADER64*)((DWORD64)fileHeader + IMAGE_SIZEOF_FILE_HEADER); // 可選PE頭賦值,標(biāo)準(zhǔn)PE頭地址+標(biāo)準(zhǔn)PE頭大小
sectionHeader = (IMAGE_SECTION_HEADER*)((DWORD64)optionalHeader + fileHeader->SizeOfOptionalHeader); // 第一個節(jié)表 可選PE頭地址+可選PE頭大小
return 1;
}
// 拉伸PE結(jié)構(gòu)
// 參數(shù):
// fileBuffer - 硬盤狀態(tài)的PE數(shù)據(jù)指針
// imageBuffer - 用來存放拉伸后的PE數(shù)據(jù)的指針
// 返回值:
// PE鏡像大小
DWORD64 ExpandPEImageBuffer(LPVOID fileBuffer, LPVOID* imageBuffer)
{
if (fileBuffer == NULL)
{
return 0;
}
// 申請ImageBuffer所需的內(nèi)存空間
LPVOID buffer = malloc(sizeof(char) * optionalHeader->SizeOfImage);
if (buffer == NULL)
{
return 0;
}
memset(buffer, 0, optionalHeader->SizeOfImage); // 將空間初始化為0
memcpy(buffer, fileBuffer, optionalHeader->SizeOfHeaders); // 把頭+節(jié)表+對齊的內(nèi)存復(fù)制過去
// 復(fù)制節(jié)
for (int i = 0; i < fileHeader->NumberOfSections; i++)
{
buffer = (LPVOID)((DWORD64)buffer + (sectionHeader + i)->VirtualAddress); // 定位這個節(jié)內(nèi)存中的偏移
fileBuffer = (LPVOID)((DWORD64)fileBuffer + (sectionHeader + i)->PointerToRawData); // 定位這個節(jié)在文件中的偏移
memcpy(buffer, fileBuffer, (sectionHeader + i)->SizeOfRawData); // 復(fù)制節(jié)在文件中所占的內(nèi)存過去
buffer = (LPVOID)((DWORD64)buffer - (sectionHeader + i)->VirtualAddress); // 恢復(fù)到起始位置
fileBuffer = (LPVOID)((DWORD64)fileBuffer - (sectionHeader + i)->PointerToRawData); // 恢復(fù)到起始位置
}
*imageBuffer = buffer;
buffer = NULL;
return optionalHeader->SizeOfImage;
}
// 獲取本程序代碼段在內(nèi)存中的起始地址和大小
// 參數(shù):
// textInfo - 存放代碼段信息的結(jié)構(gòu)體指針
// 返回值:
// 代碼段的數(shù)量
DWORD64 GetCodeSectionInfo(PeTextInfo* textInfo)
{
int length = 0;
for (int i = 0; i < fileHeader->NumberOfSections; i++)
{
// 判斷是否是可執(zhí)行的代碼
if (((sectionHeader + i)->Characteristics & 0x20000000) == 0x20000000)
{
(textInfo + length)->virtualAddress = (sectionHeader + i)->VirtualAddress;
(textInfo + length)->pointerToRawData = (sectionHeader + i)->PointerToRawData;
(textInfo + length)->size = (sectionHeader + i)->SizeOfRawData;
length++;
}
}
return length;
}反匯編與掃描
反匯編部分通過定義DisassembleCode函數(shù),該函數(shù)接收一個起始地址及代碼長度,當(dāng)執(zhí)行結(jié)束后會將反匯編結(jié)果放入到DisassemblyInfo容器內(nèi)返回給用戶,具體的反匯編實現(xiàn)細節(jié)可自行參考代碼學(xué)習(xí)。
// -----------------------------------------------------------------------------------
// 反匯編部分
// -----------------------------------------------------------------------------------
// 反匯編字符串
// 參數(shù):
// startOffset - 起始地址
// size - 代碼大小
// 返回值:
// 包含反匯編信息的向量
std::vector<DisassemblyInfo> DisassembleCode(unsigned char *startOffset, int size)
{
std::vector<DisassemblyInfo> disassemblyInfos = {};
csh handle;
cs_insn *insn;
size_t count;
// 打開句柄
if (cs_open(CS_ARCH_X86, CS_MODE_64, &handle) != CS_ERR_OK)
{
return{};
}
// 反匯編代碼,地址從0x0開始,返回總條數(shù)
count = cs_disasm(handle, (unsigned char *)startOffset, size, 0x0, 0, &insn);
if (count > 0)
{
DWORD index;
// 循環(huán)反匯編代碼
for (index = 0; index < count; index++)
{
// 清空
DisassemblyInfo disasmInfo;
memset(&disasmInfo, 0, sizeof(DisassemblyInfo));
// 循環(huán)拷貝機器碼
for (int x = 0; x < insn[index].size; x++)
{
disasmInfo.opCode[x] = insn[index].bytes[x];
}
// 拷貝地址長度
disasmInfo.address = insn[index].address;
disasmInfo.opCodeSize = insn[index].size;
// 拷貝反匯編指令
strcpy_s(disasmInfo.opString, insn[index].mnemonic);
strcat_s(disasmInfo.opString, " ");
strcat_s(disasmInfo.opString, insn[index].op_str);
// 得到反匯編長度
disasmInfo.opStringSize = (int)strlen(disasmInfo.opString);
disassemblyInfos.push_back(disasmInfo);
}
cs_free(insn, count);
}
else
{
return{};
}
cs_close(&handle);
return disassemblyInfos;
}最后我們在主函數(shù)中來實現(xiàn)反匯編比對邏輯,首先我們分別指定一個磁盤文件路徑并將其放入到fullPath變量內(nèi),然后通過GetModuleInfoByProcessName得到進程內(nèi)的所有加載模塊信息,并對比進程內(nèi)模塊是否為Win32Project.exe也就是進程自身,當(dāng)然此處也可被替換為例如user32.dll等模塊,當(dāng)磁盤與內(nèi)存被讀入后,通過ParsePEHeaders解析PE頭信息,并將PE文件通過ExpandPEImageBuffer拉伸到內(nèi)存中模擬加載后的狀態(tài)。
隨后,通過GetCodeSectionInfo獲取代碼節(jié)的地址和大小,將磁盤和內(nèi)存中的代碼段數(shù)據(jù)分別讀取到緩沖區(qū)中。最后,通過Capstone反匯編庫對磁盤和內(nèi)存中的代碼段進行反匯編,并逐條memcmp對比反匯編指令,以檢測代碼是否被篡改。整個過程包括文件讀取、內(nèi)存解析、反匯編和數(shù)據(jù)對比,最后輸出檢測結(jié)果并釋放分配的內(nèi)存資源。
int main(int argc, char *argv[])
{
DWORD64 fileSize = 0;
LPVOID fileBuffer = NULL;
// 從完整路徑中獲取文件名
CHAR fullPath[256] = { 0 };
CHAR fileName[64] = { 0 }, *p = NULL;
strcpy_s(fullPath, "d:\\Win32Project.exe");
strcpy_s(fileName, (p = strrchr(fullPath, '\\')) ? p + 1 : fullPath);
// 打開進程
HANDLE processHandle = GetProcessHandleByName(fileName);
// 循環(huán)輸出所有模塊信息
std::vector<ModuleInfo> moduleInfos = GetModuleInfoByProcessName(fileName);
for (int i = 0; i < moduleInfos.size(); i++)
{
if (strcmp(moduleInfos[i].moduleName, "Win32Project.exe") == 0)
{
printf("[*] 模塊基地址: 0x%I64X | 模塊路徑: %s \n", moduleInfos[i].moduleBase, moduleInfos[i].modulePath);
// 讀取磁盤PE文件
fileSize = ReadPEFile(moduleInfos[i].modulePath, &fileBuffer);
// 解析PE頭
DWORD64 ref = ParsePEHeaders(fileBuffer);
// 拉伸PE
LPVOID imageBuffer = NULL;
DWORD64 sizeOfImage = ExpandPEImageBuffer(fileBuffer, &imageBuffer);
// 獲取.text節(jié)地址
PeTextInfo textInfo;
DWORD64 textSectionCount = GetCodeSectionInfo(&textInfo);
// 讀入磁盤數(shù)據(jù)
unsigned char *fileTextBuffer = NULL;
fileTextBuffer = (unsigned char *)malloc((textInfo.size));
memcpy(fileTextBuffer, (unsigned char *)((DWORD64)imageBuffer + textInfo.virtualAddress), textInfo.size);
// 讀入內(nèi)存數(shù)據(jù)
unsigned char *memoryTextBuffer = NULL;
DWORD64 protectTemp = NULL;
DWORD64 moduleBase = moduleInfos[i].moduleBase;
memoryTextBuffer = (unsigned char *)malloc(textInfo.size);
for (int j = 0; j < textInfo.size; j++)
{
ReadProcessMemory(processHandle, (LPVOID)(moduleBase + textInfo.virtualAddress), memoryTextBuffer, sizeof(char) * textInfo.size, NULL);
}
// 開始反匯編
std::vector<DisassemblyInfo> fileDisassembly = DisassembleCode(fileTextBuffer, textInfo.size);
std::vector<DisassemblyInfo> memoryDisassembly = DisassembleCode(memoryTextBuffer, textInfo.size);
for (int k = 0; k < fileDisassembly.size(); k++)
{
printf("0x%I64X | ", moduleBase + memoryDisassembly[k].address);
printf("文件匯編: %-45s | ", fileDisassembly[k].opString);
printf("內(nèi)存匯編: %-45s | ", memoryDisassembly[k].opString);
// 開始對比
if (memcmp(fileDisassembly[k].opCode, memoryDisassembly[k].opCode, fileDisassembly[k].opCodeSize) != 0)
{
// 被掛鉤
printf("文件=> ");
for (int l = 0; l < fileDisassembly[k].opCodeSize; l++)
{
printf("0x%02X ", fileDisassembly[k].opCode[l]);
}
printf(" 內(nèi)存=> ");
for (int m = 0; m < memoryDisassembly[k].opCodeSize; m++)
{
printf("0x%02X ", memoryDisassembly[k].opCode[m]);
}
}
printf("\n");
}
// 釋放
imageBuffer = NULL;
free(fileBuffer);
free(fileTextBuffer);
free(memoryTextBuffer);
}
}
system("pause");
return 0;
}為了測試掃描效果,我們可以啟動一個64位應(yīng)用程序,此處為Win32Project.exe進程,通過x64dbg附加,并跳轉(zhuǎn)到Win32Project.exe的程序領(lǐng)空,如下圖所示;

此時我們隨意找一處位置,這里就選擇00007FF6973110E6處,并將其原始代碼由int3修改為nop長度為6字節(jié),如下圖所示;

至此,我們編譯并運行lyshark.exe程序,此時則可輸出Win32Project.exe進程中的第一個模塊也就是Win32project.exe的掛鉤情況,輸出效果如下圖所示;

到此這篇關(guān)于運用Capstone實現(xiàn)64位進程鉤子掃描的文章就介紹到這了,更多相關(guān)Capstone 64位鉤子掃描內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言實現(xiàn)學(xué)生消費管理系統(tǒng)
這篇文章主要為大家詳細介紹了C語言實現(xiàn)學(xué)生消費管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08
C++使用模板實現(xiàn)單鏈表(類外實現(xiàn))
這篇文章主要為大家詳細介紹了C++使用模板實現(xiàn)單鏈表的相關(guān)資料,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-12-12
C語言動態(tài)內(nèi)存管理的實現(xiàn)示例
動態(tài)內(nèi)存管理是一種允許程序在運行時根據(jù)需要動態(tài)申請和回收內(nèi)存的策略,它提供了四種重要的函數(shù),本文就來介紹一下,感興趣的可以了解一下2024-11-11

