ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

第三章:PE文件导出表结构与ROR13哈希算法

第三章:PE文件导出表结构与ROR13哈希算法 大家好你们可以叫我凌。今天我们来讲讲 PE文件导出表结构 以及 ROR13哈希算法。本文对应完整笔记Github笔记仓库入口https://github.com/LingBale/My-Learning-Journey/tree/114455cdac3e272f8605ed6a362fb8bc523710e9/Windows%20%E7%BA%A2%E9%98%9F%E5%AE%9E%E6%88%98/%E7%AC%AC3%E8%AF%BE.PE%E6%96%87%E4%BB%B6%E5%AF%BC%E5%87%BA%E8%A1%A8%E7%BB%93%E6%9E%84%E4%B8%8EROR13%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95为什么要学习导出表结构及ROR13通常我们写注入器时会出现这样的代码HMODULE hKernel32 GetModuleHandleA(kernel32.dll); FARPROC funcAddr GetProcAddress(hKernel32, CreateRemoteThread);虽很代码很简洁但有个致命的缺陷在二进制文件里会明文出现CreateRemoteThread字符串。杀毒软件和 EDR 在做静态扫描时会直接搜索二进制文件中的敏感字符串。一旦发现 CreateRemoteThread、WriteProcessMemory、VirtualAllocEx 这类字符串同时出现就基本可以断定这是个注入工具直接拉黑。所以我们需要一种方法不写出函数名的明文也能找到它的地址。这就是用哈希值代替明文函数名手工遍历DLL的导出表找到目标函数。导出表是什么回顾及深入一个DLL比如 kernel32.dll本身是个 PE 文件。PE文件里有段区域专门记录了“这个 DLL 对外提供了哪些函数”这段区域就叫导出表Export Table。导出表里最核心的是三张“清单”清单名字它存了什么名字数组(AddressOfNames)每个导出函数的名字(如 CreateFile、ReadFile)序号数组(AddressOfNameOrdinals)每个函数名对应的序号(用来和下面那张表对应)地址数组(AddressOfFunctions)每个导出函数的真实地址偏移量(RVA)它们的关系是这样的名字数组[0] CreateFile → 序号数组[0] 5 → 地址数组[5] 函数地址的RVA名字数组[1] ReadFile → 序号数组[1] 8 → 地址数组[8] 函数地址的RVA物理意义你拿个函数名先在“名字数组”里找到它的位置索引 i然后用索引 i 去“序号数组”里取出一个数字最后用这个数字去“地址数组”里取出真正的函数地址。 这就是 GetProcAddress 内部在做的事。我们现在要手工把它复现遍。ROR13 哈希讲解如果我们不想在代码里写明文 CreateRemoteThread我们需要一种方法把字符串转换 成一个数字然后只存这个数字。这个“把字符串变成数字”的过程就叫哈希。哈希算法有很多种这里用最简单的一种ROR13。ROR13 的物理动作ROR 是Rotate Right循环右移的缩写。➢ 普通右移把二进制位往右移右边移出去的位直接丢掉。➢ 循环右移把二进制位往右移右边移出去的位补到左边来。比如一个 32 位整数 0x00000001➢ 普通右移 1 位0x00000000丢掉了 1➢ 循环右移 1 位0x800000001 从右边跑到左边去了ROR13 的意思就是循环右移 13 位。说再多也不如代码来的清楚以下为 ROR13 的完整流程DWORD Ror13Hash(const char* str){ DWORD hash{0}; while(*str){ hash (hash 13) | (hash (32-13)); hash (unsigned char)*str; str; } return hash; }注意这个过程是不可逆的。你只能从字符串算出哈希值不能从哈希值反推出字符串。 但因为它对不同的字符串产生的哈希值几乎不可能相同碰撞概率低所以我们可以用哈希值来唯一标识一个函数名。当然目前安全厂商可能已经对 Ror13 做了防护因为太过经典我们也可以靠自己或AI来自定义一套自己的Hash算法来避免杀软对 Ror13 的查杀。涉及的核心结构体IMAGE_DOS_HEADERDOS头PE文件开头typedef struct _IMAGE_DOS_HEADER { WORD e_magic; // 0x00 ← MZ0x5A4D校验是否为有效DOS文件 WORD e_cblp; // 0x02 WORD e_cp; // 0x04 WORD e_crlc; // 0x06 WORD e_cparhdr; // 0x08 WORD e_minalloc; // 0x0A WORD e_maxalloc; // 0x0C WORD e_ss; // 0x0E WORD e_sp; // 0x10 WORD e_csum; // 0x12 WORD e_ip; // 0x14 WORD e_cs; // 0x16 WORD e_lfarlc; // 0x18 WORD e_ovno; // 0x1A WORD e_res[4]; // 0x1C WORD e_oemid; // 0x24 WORD e_oeminfo; // 0x26 WORD e_res2[10]; // 0x28 LONG e_lfanew; // 0x3C ← 指向NT头的文件偏移量核心字段 } IMAGE_DOS_HEADER;IMAGE_FILE_HEADERPE文件头typedef struct _IMAGE_FILE_HEADER { WORD Machine; // 0x00 CPU架构0x8664x640x14Cx86 WORD NumberOfSections; // 0x02 节区数量 DWORD TimeDateStamp; // 0x04 编译时间戳 DWORD PointerToSymbolTable; // 0x08 DWORD NumberOfSymbols; // 0x0C WORD SizeOfOptionalHeader; // 0x10 可选头大小 WORD Characteristics; // 0x12 文件属性 } IMAGE_FILE_HEADER;IMAGE_OPTIONAL_HEADER6464位可选头(关键)typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; // 0x00 0x20BPE32 BYTE MajorLinkerVersion; // 0x02 BYTE MinorLinkerVersion; // 0x03 DWORD SizeOfCode; // 0x04 DWORD SizeOfInitializedData; // 0x08 DWORD SizeOfUninitializedData; // 0x0C DWORD AddressOfEntryPoint; // 0x10 入口点RVA DWORD BaseOfCode; // 0x14 ULONGLONG ImageBase; // 0x18 默认加载基址 DWORD SectionAlignment; // 0x20 节对齐 DWORD FileAlignment; // 0x24 文件对齐 // ... 省略中间字段 DWORD NumberOfRvaAndSizes; // 0x6C 数据目录数量 IMAGE_DATA_DIRECTORY DataDirectory[16]; // 0x70 ← 数据目录数组核心 } IMAGE_OPTIONAL_HEADER64;关键偏移DataDirectory[0]导出表目录位于可选头偏移 0x70 0x08 0x78 处实际计算DataDirectory在结构体中的偏移是0x70DataDirectory[0]的VirtualAddress 在 0x70Size在0x74。但ntHeaders 0x88定位到的是 DataDirectory[0].VirtualAddress这 是因为ntHeaders指向的是 IMAGE_NT_HEADERS 的起始Signature 处。所以 0x88 0x04Signature 0x14FileHeader 0x70DataDirectory偏移。当然这个偏移量稍微看下就行了因为写代码的时候通常还是使用结构体来进行快速定位的。IMAGE_DATA_DIRECTORY数据目录项typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // 0x00 数据目录的RVA相对虚拟地址 DWORD Size; // 0x04 数据目录的大小 } IMAGE_DATA_DIRECTORY;索引含义DataDirectory[0] 导出表DataDirectory[1] 导入表DataDirectory[5] 重 定位表。IMAGE_EXPORT_DIRECTORY导出表核心结构typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; // 0x00 未使用恒为0 DWORD TimeDateStamp; // 0x04 时间戳 WORD MajorVersion; // 0x08 WORD MinorVersion; // 0x0A DWORD Name; // 0x0C 模块名称的RVA如kernel32.dll DWORD Base; // 0x10 起始序号 DWORD NumberOfFunctions; // 0x14 导出函数总数含无名函数 DWORD NumberOfNames; // 0x18 ← 按名称导出的函数数量 DWORD AddressOfFunctions; // 0x1C ← 函数地址表RVA DWORD AddressOfNames; // 0x20 ← 函数名称表RVA DWORD AddressOfNameOrdinals; // 0x24 ← 函数序号表RVA } IMAGE_EXPORT_DIRECTORY;关键字段NumberOfNames、AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals 四个字段是遍历导出表的核心。IMAGE_NT_HEADERS64NT头完整结构typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; // 0x00 PE\0\00x00004550 IMAGE_FILE_HEADER FileHeader; // 0x04 IMAGE_OPTIONAL_HEADER64 OptionalHeader; // 0x18 } IMAGE_NT_HEADERS64;涉及的关键宏与常量IMAGE_DOS_SIGNATURE0x5A4D➢ 含义DOS头的有效标识ASCII字符为 MZ。➢ 用途校验文件是否为有效的PE文件。IMAGE_NT_SIGNATURE0x00004550➢ 含义NT头的有效标识ASCII字符为 PE\0\0。➢ 用途在DOS头之后校验确认是Windows PE可执行文件。IMAGE_DIRECTORY_ENTRY_EXPORT值为 0➢ 含义数据目录数组中的导出表索引。➢ 用途DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] 即为导出表目录项。IMAGE_SIZEOF_SHORT_NAME值为 8➢ 含义PE导出/导入函数名的最大长度。➢ 用途某些API名称超过8字节需特别注意。完整利用链第 1 步从模块基址出发HMODULE hModule → BYTE* base模块加载基址│▼第 2 步解析DOS头拿到NT头偏移e_lfanew *(DWORD*)(base 0x3C)│└── ntHeaders base e_lfanew│▼第 3 步从NT头定位导出表数据目录DataDirectory[0].VirtualAddress *(DWORD*)(ntHeaders 0x88)│└── 这个RVA就是导出表相对于模块基址的偏移│▼第 4 步定位导出表结构ExportTable (IMAGE_EXPORT_DIRECTORY*)(base exportRVA)│▼第 5 步从导出表读取四个关键字段┌─────────────────────────────────────────────┐│ NumberOfNames ExportTable 0x18 ││ AddressOfFunctions ExportTable 0x1C ││ AddressOfNames ExportTable 0x20 ││ AddressOfNameOrdinals ExportTable 0x24 │└─────────────────────────────────────────────┘│▼第 6 步构建三个数组指针RVA → 实际地址nameArray (DWORD*)(base AddressOfNames)ordinalArray (WORD*)(base AddressOfNameOrdinals)funcArray (DWORD*)(base AddressOfFunctions)│▼第 7 步遍历名称数组ROR13哈希比对for (i 0; i NumberOfNames; i) {funcName (char*)(base nameArray[i]);if (Ror13Hash(funcName) targetHash) {ordinal ordinalArray[i]; // 拿到序号索引funcRVA funcArray[ordinal]; // 通过序号拿函数RVAreturn base funcRVA; // 返回函数绝对地址}}文字总结➢ DOS头 → 读 e_lfanew偏移0x3C→ 跳转到NT头。➢ NT头 → 读 DataDirectory[0].VirtualAddress偏移0x88→ 拿到导出表RVA。➢ 导出表 → 读四个关键字段NumberOfNames、AddressOfFunctions、 AddressOfNames、AddressOfNameOrdinals。➢ 三数组 → 名称数组、序号数组、地址数组。➢ 遍历匹配 → 对每个函数名算ROR13哈希与目标哈希比对。➢ 命中返回 → 通过序号数组找到函数RVA加上基址返回绝对地址。导出表三数组结构详解三数组的关系AddressOfNames名字数组├─ [0] → RVA(CreateRemoteThread) ─┐├─ [1] → RVA(VirtualAllocEx) ─┤└─ [2] → RVA(WriteProcessMemory) ─┤│AddressOfNameOrdinals序号数组 │ 一一对应├─ [0] → 5 ───────────────────┤├─ [1] → 12 ───────────────────┤└─ [2] → 8 ───────────────────┘│AddressOfFunctions地址数组 │ 通过序号索引├─ [5] → RVA(CreateRemoteThread函数) ←┘├─ [8] → RVA(WriteProcessMemory函数)└─ [12] → RVA(VirtualAllocEx函数)三数组的职责数组类型作用AddressOfNamesDWORD*存储函数名字符串的RVAAddressOfNameOrdinalsWORD*存储函数在地址数组中的索引AddressOfFunctionsDWORD*存储函数地址的RVA遍历逻辑for (DWORD i 0; i NumberOfNames; i) { char* funcName (char*)(base nameArray[i]); // 第i个函数名 if (Ror13Hash(funcName) targetHash) { WORD ordinal ordinalArray[i]; // 该函数在地址数组中的索引 DWORD funcRVA funcArray[ordinal]; // 函数RVA return base funcRVA; // 绝对地址 } }代码示例以下为相关代码示例1基于结构体#include windows.h #include iostream #include iomanip #include string #ifdef _WIN64 // 导出表 struct MY_IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // 数据目录的 RVA DWORD Size; // 数据目录的大小 }; struct MY_IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; // 未使用恒为0 DWORD TimeDateStamp; // 时间戳 WORD MajorVersion; // 主版本号 WORD MinorVersion; // 次版本号 DWORD Name; // 模块名称的RVA DWORD Base; // 起始序号 DWORD NumberOfFunctions; // 导出函数的总数 DWORD NumberOfNames; // 按名称导出的函数数量 DWORD AddressOfFunctions; // 导出函数地址表的RVA DWORD AddressOfNames; // 导出函数名称表的RVA DWORD AddressOfNameOrdinals; // 导出函数序号表的RVA }; struct MY_IMAGE_NT_HEADERS64 { DWORD Signature; // PE 签名0x00004550即 PE\0\0 IMAGE_FILE_HEADER FileHeader; // PE 文件头 IMAGE_OPTIONAL_HEADER64 OptionalHeader; // 64 位可选头 }; // ROR13 哈希计算函数 DWORD Ror13Hash(const char* str){ DWORD hash{0}; while(*str){ hash (hash 13) | (hash (32-13)); hash (unsigned char)*str; str; } return hash; } void* FindFuncByHash(HMODULE hModule, DWORD targetHash){ BYTE* base (BYTE*)hModule; DWORD e_lfanew *(DWORD*)(base 0x3c); BYTE* ntHeaders base e_lfanew; MY_IMAGE_DATA_DIRECTORY* pDataDirectory_0 (MY_IMAGE_DATA_DIRECTORY*)((MY_IMAGE_NT_HEADERS64*)ntHeaders)-OptionalHeader.DataDirectory[0]; MY_IMAGE_EXPORT_DIRECTORY* ExportTable (MY_IMAGE_EXPORT_DIRECTORY*)(base pDataDirectory_0-VirtualAddress); DWORD numberOfName ExportTable-NumberOfNames; DWORD* funcArray (DWORD*)(base ExportTable-AddressOfFunctions); // 地址 DWORD* nameArray (DWORD*)(base ExportTable-AddressOfNames); // 名字 WORD* ordinalArray (WORD*)(base ExportTable-AddressOfNameOrdinals); // 序号 for(DWORD i{0}; i numberOfName; i){ char* funcName (char*)(base nameArray[i]); if(Ror13Hash(funcName) targetHash){ WORD ordinal ordinalArray[i]; DWORD funcRVA funcArray[ordinal]; return base funcRVA; } } return nullptr; } int main(){ DWORD targetHash Ror13Hash(CreateRemoteThread); std::cout [*] 目标哈希值: 0x std::hex std::setw(8) targetHash std::dec std::endl; HMODULE hKernel32 GetModuleHandleA(kernel32.dll); std::cout [*] kernel32.dll 基址: 0x std::hex (uintptr_t)hKernel32 std::dec std::endl; void* addr FindFuncByHash(hKernel32, targetHash); if (addr) { std::cout [] 找到 CreateRemoteThread 地址: 0x std::hex (uintptr_t)addr std::dec std::endl; // 用 GetProcAddress 对比验证 FARPROC check GetProcAddress(hKernel32, CreateRemoteThread); std::cout [*] GetProcAddress 拿到的地址: 0x std::hex (uintptr_t)check std::dec std::endl; if (addr (void*)check) { std::cout [] 两个地址一致验证成功 std::endl; } } else { std::cout [-] 未找到函数 std::endl; } } #endif #ifndef _WIN64 int main() { std::cout 此程序仅支持 64 位系统 std::endl; return 0; } #endif2基于偏移量#include windows.h #include iostream #include iomanip #include string // ROR13 哈希函数 DWORD Ror13Hash(const char* str) { DWORD hash 0; while (*str) { // 循环右移 13 位 hash (hash 13) | (hash (32 - 13)); // 加上当前字符 hash (unsigned char)*str; str; } return hash; } void* FindFuncByHash(HMODULE hModule, DWORD targetHash) { BYTE* base (BYTE*)hModule; // 第一步读 e_lfanew跳到 NT 头 DWORD e_lfanew *(DWORD*)(base 0x3C); BYTE* ntHeaders base e_lfanew; // 第二步从 DataDirectory[0] 拿到导出表 RVA DWORD exportRVA *(DWORD*)(ntHeaders 0x88); // 第三步跳到导出表 BYTE* exportDir base exportRVA; // 第四步从导出表里读出四个关键字段 DWORD numberOfNames *(DWORD*)(exportDir 0x18); // 名字总数 DWORD addressOfFunctions *(DWORD*)(exportDir 0x1C); // 地址数组 DWORD addressOfNames *(DWORD*)(exportDir 0x20); // 名字数组 DWORD addressOfNameOrdinals *(DWORD*)(exportDir 0x24); // 序号数组 // 第五步把偏移量转成真正的指针 DWORD* nameArray (DWORD*)(base addressOfNames); WORD* ordinalArray (WORD*)(base addressOfNameOrdinals); DWORD* funcArray (DWORD*)(base addressOfFunctions); // 第六步遍历名字数组算哈希比对 for (DWORD i 0; i numberOfNames; i) { char* funcName (char*)(base nameArray[i]); if (Ror13Hash(funcName) targetHash) { WORD ordinal ordinalArray[i]; DWORD funcRVA funcArray[ordinal]; return base funcRVA; } } return nullptr; } int main() { // 预计算 CreateRemoteThread 的哈希值 DWORD targetHash Ror13Hash(CreateRemoteThread); std::cout [*] 目标哈希值: 0x std::hex std::setw(8) std::setfill(0) targetHash std::dec std::endl; // 拿 kernel32.dll 基址 HMODULE hKernel32 GetModuleHandleA(kernel32.dll); std::cout [*] kernel32.dll 基址: 0x std::hex (uintptr_t)hKernel32 std::dec std::endl; // 用哈希查找函数 void* addr FindFuncByHash(hKernel32, targetHash); if (addr) { std::cout [] 找到 CreateRemoteThread 地址: 0x std::hex (uintptr_t)addr std::dec std::endl; // 用 GetProcAddress 对比验证 FARPROC check GetProcAddress(hKernel32, CreateRemoteThread); std::cout [*] GetProcAddress 拿到的地址: 0x std::hex (uintptr_t)check std::dec std::endl; if (addr (void*)check) { std::cout [] 两个地址一致验证成功 std::endl; } } else { std::cout [-] 未找到函数 std::endl; } system(pause); }那么本章节就到此为止了我们下章见
返回列表