# GetProcAddress_C++_test **Repository Path**: cutecuteyu/get-proc-address-c-test ## Basic Information - **Project Name**: GetProcAddress_C++_test - **Description**: 纯C++实现GetProcAddress - **Primary Language**: C++ - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-02-03 - **Last Updated**: 2026-02-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 纯C++实现 GetProcAddress ## 项目介绍 本项目展示如何使用纯C++实现与Windows API中`GetProcAddress`完全相同的功能。通过手动解析PE文件格式的导出表来获取指定函数的地址。 ## 核心原理 ### PE文件格式结构 ``` DOS头 (IMAGE_DOS_HEADER) └── e_lfanew → PE头偏移 PE头 (IMAGE_NT_HEADERS) ├── Signature ("PE\0\0") ├── FileHeader (机器类型、段数量等) └── OptionalHeader (入口点、镜像基址等) └── DataDirectory[16] (数据目录表) └── [0] Export Directory (导出表) ``` ### 导出表结构 ``` IMAGE_EXPORT_DIRECTORY ├── Characteristics (保留字段) ├── TimeDateStamp (时间戳) ├── MajorVersion/MinorVersion (版本号) ├── Name (DLL名称的RVA) ├── Base (导出序号的起始值) ├── NumberOfFunctions (导出函数总数) ├── NumberOfNames (有名称的函数数量) ├── AddressOfFunctions (函数地址数组的RVA) ├── AddressOfNames (函数名称数组的RVA) └── AddressOfNameOrdinals (序号索引数组的RVA) ``` ### 三个关键数组的关系 ``` AddressOfNames[] → 指向函数名字符串 ↓ AddressOfNameOrdinals[] → 对应的序号索引 ↓ AddressOfFunctions[] → 实际的函数地址RVA ``` **查找示例:** ``` AddressOfNames[0] = "MessageBoxA" AddressOfNameOrdinals[0] = 5 AddressOfFunctions[5] = 0x1234 (MessageBoxA的RVA) ``` ## 详细代码实现 ### 第一步:验证DOS头 ```cpp IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModule; if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { // "MZ" = 0x5A4D return NULL; } ``` **原理:** 所有Windows PE文件都以DOS头开始,`e_magic`字段必须为`0x5A4D`("MZ"),这是DOS可执行文件的标识。 ### 第二步:定位PE头 ```cpp BYTE* pNtHeaders = (BYTE*)hModule + pDosHeader->e_lfanew; DWORD* pSignature = (DWORD*)pNtHeaders; if (*pSignature != IMAGE_NT_SIGNATURE) { // "PE\0\0" = 0x00004550 return NULL; } ``` **原理:** `e_lfanew`字段存储了PE头相对于文件开头的偏移量。跳转到该位置后,首先验证PE签名。 ### 第三步:解析FileHeader ```cpp IMAGE_FILE_HEADER* pFileHeader = (IMAGE_FILE_HEADER*)(pSignature + 1); BYTE* pOptionalHeader = (BYTE*)(pFileHeader + 1); ``` **原理:** PE签名(4字节)之后紧接着就是FileHeader(20字节),再后面是OptionalHeader。 **FileHeader结构:** - `Machine` (2字节) - 0x8664表示x64,0x014c表示x86 - `NumberOfSections` (2字节) - `TimeDateStamp` (4字节) - `PointerToSymbolTable` (4字节) - `NumberOfSymbols` (4字节) - `SizeOfOptionalHeader` (2字节) - OptionalHeader的大小 - `Characteristics` (2字节) ### 第四步:根据架构类型获取DataDirectory ```cpp if (pFileHeader->Machine == IMAGE_FILE_MACHINE_AMD64) { // 64位系统 IMAGE_OPTIONAL_HEADER64* pOptHeader64 = (IMAGE_OPTIONAL_HEADER64*)pOptionalHeader; pDataDirectory = pOptHeader64->DataDirectory; } else if (pFileHeader->Machine == IMAGE_FILE_MACHINE_I386) { // 32位系统 IMAGE_OPTIONAL_HEADER32* pOptHeader32 = (IMAGE_OPTIONAL_HEADER32*)pOptionalHeader; pDataDirectory = pOptHeader32->DataDirectory; } ``` **原理:** 32位和64位的OptionalHeader结构不同,但DataDirectory都在其末尾。 **DataDirectory数组:** 包含16个目录项,每个项8字节(VirtualAddress + Size) - [0] Export Directory - [1] Import Directory - [2] Resource Directory - [12] IAT (Import Address Table) ### 第五步:获取导出表 ```cpp DWORD ExportDirRVA = pDataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; IMAGE_EXPORT_DIRECTORY* pExportDir = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)hModule + ExportDirRVA); ``` **原理:** DataDirectory[0]存储导出表的RVA(相对虚拟地址),需要加上模块基址转换为实际地址。 ### 第六步:获取三个关键数组 ```cpp DWORD* pAddressOfFunctions = (DWORD*)((BYTE*)hModule + pExportDir->AddressOfFunctions); DWORD* pAddressOfNames = (DWORD*)((BYTE*)hModule + pExportDir->AddressOfNames); WORD* pAddressOfNameOrdinals = (WORD*)((BYTE*)hModule + pExportDir->AddressOfNameOrdinals); ``` **原理:** 这三个数组都是存储RVA,都需要转换为实际地址。 ### 第七步:遍历查找函数名 ```cpp for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) { char* CurrentFuncName = (char*)((BYTE*)hModule + pAddressOfNames[i]); if (strcmp(CurrentFuncName, lpProcName) == 0) { // 找到匹配 WORD OrdinalIndex = pAddressOfNameOrdinals[i]; DWORD FunctionRVA = pAddressOfFunctions[OrdinalIndex]; FARPROC pFunc = (FARPROC)((BYTE*)hModule + FunctionRVA); return pFunc; } } ``` **查找流程图:** ``` 开始遍历 AddressOfNames[i] ↓ 获取函数名字符串地址 ↓ strcmp() 比较函数名 ↓ 匹配成功? ↓ 是 获取 AddressOfNameOrdinals[i] 作为索引 ↓ 用索引查找 AddressOfFunctions[索引] ↓ 将RVA转换为实际地址返回 ``` ## 关键概念解释 ### RVA (Relative Virtual Address) RVA是相对于模块加载基址的偏移量。 ``` 实际地址 = 模块基址 + RVA ``` 例如: - 模块基址:`0x00007FFB1A400000` - 函数RVA:`0x1234` - 实际地址:`0x00007FFB1A401234` ### 为什么需要三个数组? 不是所有导出函数都有名字(可以通过序号导出),所以需要: 1. `AddressOfNames` - 存储有名字的函数名称指针 2. `AddressOfNameOrdinals` - 存储名称对应的序号索引 3. `AddressOfFunctions` - 存储所有函数的地址(包括无名函数) 通过序号索引连接名称和地址。 ### 32位与64位的区别 | 特性 | x86 (32位) | x64 (64位) | |------|-----------|-----------| | OptionalHeader大小 | 224字节 | 240字节 | | 指针大小 | 4字节 | 8字节 | | Machine值 | 0x014c | 0x8664 | | 函数地址数组 | DWORD (4字节) | DWORD (4字节) - 存储RVA | **注意:** 即使在64位系统中,导出表中存储的仍然是32位RVA,因为RVA不会超过4GB。 ## 编译方法 右键点击 `build.ps1` 选择"使用PowerShell运行"或在PowerShell中执行: ```powershell .\build.ps1 ``` 或直接手动执行编译命令: ```powershell g++.exe -std=c++17 -o MyGetProcAddress.exe MyGetProcAddress.cpp -luser32 ``` ## 运行效果 ``` ================================================== MyGetProcAddress - Pure C++ Implementation ================================================== [+] user32.dll module base: 0x00007FFB1A400000 [*] Searching for MessageBoxA via MyGetProcAddress... [DEBUG] Machine: 0x8664 [DEBUG] ExportDirRVA: 0x36000 [DEBUG] NumberOfNames: 850 [DEBUG] NumberOfFunctions: 1200 [DEBUG] Base: 1 [DEBUG] Searching... 0/850 [DEBUG] Searching... 100/850 [DEBUG] Searching... 200/850 [+] Found MessageBoxA at: 0x00007FFB1A567890 [*] Calling MessageBoxA... [+] MessageBoxA called successfully! ``` ## 代码结构 ```cpp // 使用Windows官方PE结构体定义 #include // 核心函数 FARPROC MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName); ``` ## 应用场景 - **安全研究** - 理解Windows PE加载机制 - **恶意代码分析** - 分析shellcode如何解析API - **反调试/反检测** - 实现自定义API解析以躲避Hook - **教育目的** - 学习Windows内部机制 ## 注意事项 > **警告**: 本代码仅供教育和安全研究目的。在生产环境中应始终使用系统提供的`GetProcAddress`函数,因为: > - 系统函数经过了充分测试和优化 > - 系统函数处理了各种边界情况 > - 自实现可能存在安全漏洞 ## 许可证 MIT License