
离线 TA的专栏
- 打卡等级:热心大叔
- 打卡总天数:227
- 打卡月天数:0
- 打卡总奖励:3343
- 最近打卡:2025-04-13 11:14:19
|
目录
- 定义头文件
- 进程与线程
- PE文件操作
- 反汇编与扫描
进程钩子扫描是一种安全技术和分析方法,用于检测和分析进程内的指令是否被篡改或注入了恶意功能。钩子(Hook)技术允许开发人员在执行特定系统调用或函数时插入自定义代码。虽然进程钩子在调试和软件功能扩展中发挥了重要作用,但该技术也可以被恶意软件用来拦截和修改程序行为,从而隐藏其活动或进行其他恶意操作。本章将通过Capstone引擎实现64位进程钩子的扫描,读者可使用此段代码检测目标进程内是否被挂了钩子。
通过进程钩子扫描,安全研究人员和开发人员可以检测进程中是否存在未授权的钩子,并分析这些钩子的行为。这有助于识别和防止恶意软件的活动,确保系统和应用程序的完整性和安全性。
在编写代码之前,读者需要自行下载并配置 反汇编引擎,配置参数如下所示;
在之前的 命令行解析工具中笔者介绍了如何扫描32位进程内的钩子,由于32位进程需要重定位所以在扫描时需要考虑到对内存地址的修正,而64位进程则无需考虑重定位的问题,其钩子扫描原理与32位保持一致,均通过将磁盘和内存中的代码段进行反汇编,并逐条比较它们的机器码和反汇编结果。如果存在差异,则表示该代码段在内存中被篡改或挂钩。
定义头文件
首先引入 头文件,并引用 静态库,通过定义 来存储每个PE文件中节的文件偏移及大小信息,通过 用于存放进程内的模块信息,而 则用来存放反汇编信息,底部则定义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;
- // 存放PE信息段
- 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;
- // 全局PE结构
- IMAGE_DOS_HEADER* dosHeader; // DOS头
- IMAGE_NT_HEADERS* ntHeader; // NT头
- IMAGE_FILE_HEADER* fileHeader; // 标准PE头
- IMAGE_OPTIONAL_HEADER64* optionalHeader; // 可选PE头
- IMAGE_SECTION_HEADER* sectionHeader; // 节表
复制代码 进程与线程
在进程与线程处理模块中,我们定义了三个函数: 、 和 - GetModuleInfoByProcessName
复制代码。 函数接收一个进程名并返回该进程的句柄,方便后续的进程操作; 函数通过进程名获取其对应的PID(进程标识符),用于标识特定进程; - GetModuleInfoByProcessName
复制代码函数接收一个进程名并返回该进程内所有模块的信息,包括模块路径、模块名和模块基址,便于对进程内的模块进行分析和处理。 - // -----------------------------------------------------------------------------------
- // 进程线程部分
- // -----------------------------------------------------------------------------------
- // 通过进程名获取进程句柄
- // 参数:
- // 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)
- {
- // 关闭快照句柄,避免内存泄漏
- CloseHandle(processSnap);
- // 返回句柄
- return OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
- }
- } while (Process32Next(processSnap, &processEntry));
- // 关闭快照句柄,避免内存泄漏
- CloseHandle(processSnap);
- return (HANDLE)NULL;
- }
- // 根据进程名获取PID
- // 参数:
- // 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;
- }
- // 获取进程内所有模块信息
- // 参数:
- // processName - 进程名
- // 返回值:
- // 包含模块信息的向量
- 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);
- // 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转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;
- // 放入容器内
- moduleInfos.push_back(moduleInfo);
- hasMoreModules = Module32Next(moduleSnap, &moduleEntry);
- }
- CloseHandle(moduleSnap);
- return moduleInfos;
- }
复制代码 PE文件操作
如下代码实现了PE(Portable Executable)文件的读取、解析和扩展功能。我们定义了三个主要函数: 用于从磁盘读取PE文件数据, 用于解析PE文件的头信息, 用于将PE文件扩展为内存中加载后的形式,并复制文件中的各个节(section)到内存中。最后, 函数获取了PE文件中代码段的起始地址和大小信息。 - // -----------------------------------------------------------------------------------
- // PE文件读写部分
- // -----------------------------------------------------------------------------------
- // 读取硬盘PE文件数据
- // 参数:
- // filePath - 文件路径
- // fileBuffer - 文件缓冲区指针
- // 返回值:
- // 文件大小
- 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;
- }
- // 读取PE头信息
- // 参数:
- // fileBuffer - 文件缓冲区指针
- // 返回值:
- // 成功返回1,失败返回0
- DWORD64 ParsePEHeaders(LPVOID fileBuffer)
- {
- if (fileBuffer == NULL)
- {
- // 缓冲区指针无效
- return 0;
- }
- // 判断是否是有效的MZ标记
- if (*((PWORD)fileBuffer) != IMAGE_DOS_SIGNATURE)
- {
- return 0;
- }
- dosHeader = (IMAGE_DOS_HEADER*)fileBuffer;
- // 判断是否是有效的pe标志
- 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); // 标准PE头赋值
- optionalHeader = (IMAGE_OPTIONAL_HEADER64*)((DWORD64)fileHeader + IMAGE_SIZEOF_FILE_HEADER); // 可选PE头赋值,标准PE头地址+标准PE头大小
- sectionHeader = (IMAGE_SECTION_HEADER*)((DWORD64)optionalHeader + fileHeader->SizeOfOptionalHeader); // 第一个节表 可选PE头地址+可选PE头大小
- return 1;
- }
- // 拉伸PE结构
- // 参数:
- // fileBuffer - 硬盘状态的PE数据指针
- // imageBuffer - 用来存放拉伸后的PE数据的指针
- // 返回值:
- // PE镜像大小
- DWORD64 ExpandPEImageBuffer(LPVOID fileBuffer, LPVOID* imageBuffer)
- {
- if (fileBuffer == NULL)
- {
- return 0;
- }
- // 申请ImageBuffer所需的内存空间
- LPVOID buffer = malloc(sizeof(char) * optionalHeader->SizeOfImage);
- if (buffer == NULL)
- {
- return 0;
- }
- memset(buffer, 0, optionalHeader->SizeOfImage); // 将空间初始化为0
- 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;
- }
- // 获取本程序代码段在内存中的起始地址和大小
- // 参数:
- // textInfo - 存放代码段信息的结构体指针
- // 返回值:
- // 代码段的数量
- 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;
- }
复制代码 反汇编与扫描
反汇编部分通过定义 函数,该函数接收一个起始地址及代码长度,当执行结束后会将反汇编结果放入到 容器内返回给用户,具体的反汇编实现细节可自行参考代码学习。 - // -----------------------------------------------------------------------------------
- // 反汇编部分
- // -----------------------------------------------------------------------------------
- // 反汇编字符串
- // 参数:
- // 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开始,返回总条数
- 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;
- }
复制代码最后我们在主函数中来实现反汇编比对逻辑,首先我们分别指定一个磁盘文件路径并将其放入到 变量内,然后通过 - GetModuleInfoByProcessName
复制代码得到进程内的所有加载模块信息,并对比进程内模块是否为 也就是进程自身,当然此处也可被替换为例如 等模块,当磁盘与内存被读入后,通过 解析PE头信息,并将PE文件通过 拉伸到内存中模拟加载后的状态。
随后,通过 获取代码节的地址和大小,将磁盘和内存中的代码段数据分别读取到缓冲区中。最后,通过 反汇编库对磁盘和内存中的代码段进行反汇编,并逐条 对比反汇编指令,以检测代码是否被篡改。整个过程包括文件读取、内存解析、反汇编和数据对比,最后输出检测结果并释放分配的内存资源。 - 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);
- // 读取磁盘PE文件
- fileSize = ReadPEFile(moduleInfos[i].modulePath, &fileBuffer);
- // 解析PE头
- DWORD64 ref = ParsePEHeaders(fileBuffer);
- // 拉伸PE
- LPVOID imageBuffer = NULL;
- DWORD64 sizeOfImage = ExpandPEImageBuffer(fileBuffer, &imageBuffer);
- // 获取.text节地址
- 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位应用程序,此处为 进程,通过 附加,并跳转到 的程序领空,如下图所示;
此时我们随意找一处位置,这里就选择 处,并将其原始代码由 修改为 长度为6字节,如下图所示;
至此,我们编译并运行 程序,此时则可输出 进程中的第一个模块也就是 的挂钩情况,输出效果如下图所示;
到此这篇关于运用Capstone实现64位进程钩子扫描的文章就介绍到这了,更多相关Capstone 64位钩子扫描内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:  进行删除处理。
4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
|