进程钩子扫描是一种安全技术和分析方法,用于检测和分析进程内的指令是否被篡改或注入了恶意功能。钩子(Hook)技术允许开发人员在执行特定系统调用或函数时插入自定义代码。虽然进程钩子在调试和软件功能扩展中发挥了重要作用,但该技术也可以被恶意软件用来拦截和修改程序行为,从而隐藏其活动或进行其他恶意操作。本章将通过Capstone引擎实现64位进程钩子的扫描,读者可使用此段代码检测目标进程内是否被挂了钩子。
通过进程钩子扫描,安全研究人员和开发人员可以检测进程中是否存在未授权的钩子,并分析这些钩子的行为。这有助于识别和防止恶意软件的活动,确保系统和应用程序的完整性和安全性。
在编写代码之前,读者需要自行下载并配置Capstone
反汇编引擎,配置参数如下所示;
在之前的PeView
命令行解析工具中笔者介绍了如何扫描32位进程内的钩子,由于32位进程需要重定位所以在扫描时需要考虑到对内存地址的修正,而64位进程则无需考虑重定位的问题,其钩子扫描原理与32位保持一致,均通过将磁盘和内存中的代码段进行反汇编,并逐条比较它们的机器码和反汇编结果。如果存在差异,则表示该代码段在内存中被篡改或挂钩。
定义头文件
首先引入capstone.h
头文件,并引用capstone64.lib
静态库,通过定义PeTextInfo
来存储每个PE文件中节的文件偏移及大小信息,通过ModuleInfo
用于存放进程内的模块信息,而DisassemblyInfo
则用来存放反汇编信息,底部则定义PE结构的全局变量用于存储头指针。
#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;
struct PeTextInfo { DWORD64 virtualAddress; DWORD64 pointerToRawData; DWORD64 size; };
typedef struct { char modulePath[256]; char moduleName[128]; long long moduleBase; }ModuleInfo;
typedef struct { int opCodeSize; int opStringSize; unsigned long long address; unsigned char opCode[16]; char opString[256]; }DisassemblyInfo;
IMAGE_DOS_HEADER* dosHeader; IMAGE_NT_HEADERS* ntHeader; IMAGE_FILE_HEADER* fileHeader; IMAGE_OPTIONAL_HEADER64* optionalHeader; IMAGE_SECTION_HEADER* sectionHeader;
|
进程与线程
在进程与线程处理模块中,我们定义了三个函数:GetProcessHandleByName
、GetProcessIDByName
和GetModuleInfoByProcessName
。GetProcessHandleByName
函数接收一个进程名并返回该进程的句柄,方便后续的进程操作;GetProcessIDByName
函数通过进程名获取其对应的PID(进程标识符),用于标识特定进程;GetModuleInfoByProcessName
函数接收一个进程名并返回该进程内所有模块的信息,包括模块路径、模块名和模块基址,便于对进程内的模块进行分析和处理。
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) { CloseHandle(processSnap);
return OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID); } } while (Process32Next(processSnap, &processEntry));
CloseHandle(processSnap); return (HANDLE)NULL; }
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; }
std::vector<ModuleInfo> GetModuleInfoByProcessName(CHAR* processName) { MODULEENTRY32 moduleEntry; USES_CONVERSION; DWORD64 processID = GetProcessIDByName(A2W(processName));
std::vector<ModuleInfo> moduleInfos = {};
moduleEntry.dwSize = sizeof(MODULEENTRY32);
HANDLE moduleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, processID);
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;
modulePath = W2A(moduleEntry.szExePath); moduleBase = (DWORD64)moduleEntry.modBaseAddr; moduleName = W2A(moduleEntry.szModule);
strcpy_s(moduleInfo.modulePath, modulePath); strcpy_s(moduleInfo.moduleName, moduleName); moduleInfo.moduleBase = moduleBase;
moduleInfos.push_back(moduleInfo); hasMoreModules = Module32Next(moduleSnap, &moduleEntry); }
CloseHandle(moduleSnap); return moduleInfos; }
|
PE文件操作
如下代码实现了PE(Portable Executable)文件的读取、解析和扩展功能。我们定义了三个主要函数:ReadPEFile
用于从磁盘读取PE文件数据,ParsePEHeaders
用于解析PE文件的头信息,ExpandPEImageBuffer
用于将PE文件扩展为内存中加载后的形式,并复制文件中的各个节(section)到内存中。最后,GetCodeSectionInfo
函数获取了PE文件中代码段的起始地址和大小信息。
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);
LPVOID buffer = malloc(sizeof(char) * fileSize); if (buffer == NULL) { fclose(file); return 0; } 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; }
DWORD64 ParsePEHeaders(LPVOID fileBuffer) { if (fileBuffer == NULL) { return 0; }
if (*((PWORD)fileBuffer) != IMAGE_DOS_SIGNATURE) { return 0; }
dosHeader = (IMAGE_DOS_HEADER*)fileBuffer;
if (*((PDWORD)((DWORD64)fileBuffer + dosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE) { return 0; }
ntHeader = (IMAGE_NT_HEADERS*)((DWORD64)fileBuffer + dosHeader->e_lfanew); fileHeader = (IMAGE_FILE_HEADER*)((DWORD64)ntHeader + 4); optionalHeader = (IMAGE_OPTIONAL_HEADER64*)((DWORD64)fileHeader + IMAGE_SIZEOF_FILE_HEADER); sectionHeader = (IMAGE_SECTION_HEADER*)((DWORD64)optionalHeader + fileHeader->SizeOfOptionalHeader); return 1; }
DWORD64 ExpandPEImageBuffer(LPVOID fileBuffer, LPVOID* imageBuffer) { if (fileBuffer == NULL) { return 0; }
LPVOID buffer = malloc(sizeof(char) * optionalHeader->SizeOfImage); if (buffer == NULL) { return 0; }
memset(buffer, 0, optionalHeader->SizeOfImage); memcpy(buffer, fileBuffer, optionalHeader->SizeOfHeaders);
for (int i = 0; i < fileHeader->NumberOfSections; i++) { buffer = (LPVOID)((DWORD64)buffer + (sectionHeader + i)->VirtualAddress); fileBuffer = (LPVOID)((DWORD64)fileBuffer + (sectionHeader + i)->PointerToRawData); memcpy(buffer, fileBuffer, (sectionHeader + i)->SizeOfRawData); buffer = (LPVOID)((DWORD64)buffer - (sectionHeader + i)->VirtualAddress); fileBuffer = (LPVOID)((DWORD64)fileBuffer - (sectionHeader + i)->PointerToRawData); }
*imageBuffer = buffer; buffer = NULL; return optionalHeader->SizeOfImage; }
DWORD64 GetCodeSectionInfo(PeTextInfo* textInfo) { int length = 0; for (int i = 0; i < fileHeader->NumberOfSections; i++) { 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
函数,该函数接收一个起始地址及代码长度,当执行结束后会将反汇编结果放入到DisassemblyInfo
容器内返回给用户,具体的反汇编实现细节可自行参考代码学习。
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{}; }
count = cs_disasm(handle, (unsigned char *)startOffset, size, 0x0, 0, &insn);
if (count > 0) { DWORD index;
for (index = 0; index < count; index++) { DisassemblyInfo disasmInfo; memset(&disasmInfo, 0, sizeof(DisassemblyInfo));
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; }
|
最后我们在主函数中来实现反汇编比对逻辑,首先我们分别指定一个磁盘文件路径并将其放入到fullPath
变量内,然后通过GetModuleInfoByProcessName
得到进程内的所有加载模块信息,并对比进程内模块是否为Win32Project.exe
也就是进程自身,当然此处也可被替换为例如user32.dll
等模块,当磁盘与内存被读入后,通过ParsePEHeaders
解析PE头信息,并将PE文件通过ExpandPEImageBuffer
拉伸到内存中模拟加载后的状态。
随后,通过GetCodeSectionInfo
获取代码节的地址和大小,将磁盘和内存中的代码段数据分别读取到缓冲区中。最后,通过Capstone
反汇编库对磁盘和内存中的代码段进行反汇编,并逐条memcmp
对比反汇编指令,以检测代码是否被篡改。整个过程包括文件读取、内存解析、反汇编和数据对比,最后输出检测结果并释放分配的内存资源。
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);
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);
fileSize = ReadPEFile(moduleInfos[i].modulePath, &fileBuffer);
DWORD64 ref = ParsePEHeaders(fileBuffer);
LPVOID imageBuffer = NULL; DWORD64 sizeOfImage = ExpandPEImageBuffer(fileBuffer, &imageBuffer);
PeTextInfo textInfo; DWORD64 textSectionCount = GetCodeSectionInfo(&textInfo);
unsigned char *fileTextBuffer = NULL; fileTextBuffer = (unsigned char *)malloc((textInfo.size)); memcpy(fileTextBuffer, (unsigned char *)((DWORD64)imageBuffer + textInfo.virtualAddress), textInfo.size);
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("内存汇编: %-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(" 内存=> "); 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位应用程序,此处为Win32Project.exe
进程,通过x64dbg
附加,并跳转到Win32Project.exe
的程序领空,如下图所示;
此时我们随意找一处位置,这里就选择00007FF6973110E6
处,并将其原始代码由int3
修改为nop
长度为6字节,如下图所示;
至此,我们编译并运行lyshark.exe
程序,此时则可输出Win32Project.exe
进程中的第一个模块也就是Win32project.exe
的挂钩情况,输出效果如下图所示;