前言: pe文件指在windows平台上的可执行文件(.exe,.dll,.com)了解他们的结构虽然对做题没什么用,但如果想开发新型外挂,防御新型外挂都是基于底层原理创新的
pe文件结构总览:
地址的基本概念
VA(Virtual Address):虚拟地址 PE 文件映射到内存空间时,数据在内存空间中对应的地址。
ImageBase:映射基址 PE 文件在内存空间中的映射起始位置,是个 VA 地址。
RVA(Relative Virtual Address):相对虚拟地址 PE 文件在内存中的 VA 相对于 ImageBase 的偏移量。
FOA(File Offset Address,FOA):文件偏移地址 PE 文件在磁盘上存放时,数据相对于文件开头位置的偏移量,文件偏移地址等于文件地址。
转换关系:
pe文件格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4 ]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10 ]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
这是dos头在微软的定义,我们只需要了解
(1)e_magic: DOS 映像文件格式标记,与 MS-DOS 兼容的 PE 文件都将该值设为 0x4D5A,对应的 ASCII 字符为:MZ。 (2)e_ip: DOS 代码的初始化指令入口。 (3)e_cs: DOS 代码的初始化代码段入口。 (4)e_lfanew:PE 文件头 _IMAGE_NT_HEADERS 结构的 FA 偏移地址,即指向 _IMAGE_NT_HEADERS 结构。
DOS_STUB 该结构未在 winnt.h 中定义,其内容随着链接时使用的链接器不同而不同,通常用于保存在 DOS 环境中的可执行代码。 例如:该结构中的代码用于显示字符串:“This program cannot run in DOS mode”。
位于 e_lfanew 处,结构
1 2 3 4 5 typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS;
1 2 3 4 5 6 7 8 9 10 typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
要掌握的只有:
(1)Machine:平台类型,映像文件只能在指定的平台或模拟指定平台的系统上运行。在 winnt.h 中定义的 Machine 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #define IMAGE_FILE_MACHINE_UNKNOWN 0 #define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 #define IMAGE_FILE_MACHINE_I386 0x014c #define IMAGE_FILE_MACHINE_R3000 0x0162 #define IMAGE_FILE_MACHINE_R4000 0x0166 #define IMAGE_FILE_MACHINE_R10000 0x0168 #define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 #define IMAGE_FILE_MACHINE_ALPHA 0x0184 #define IMAGE_FILE_MACHINE_SH3 0x01a2 #define IMAGE_FILE_MACHINE_SH3DSP 0x01a3 #define IMAGE_FILE_MACHINE_SH3E 0x01a4 #define IMAGE_FILE_MACHINE_SH4 0x01a6 #define IMAGE_FILE_MACHINE_SH5 0x01a8 #define IMAGE_FILE_MACHINE_ARM 0x01c0 #define IMAGE_FILE_MACHINE_THUMB 0x01c2 #define IMAGE_FILE_MACHINE_ARMNT 0x01c4 #define IMAGE_FILE_MACHINE_AM33 0x01d3 #define IMAGE_FILE_MACHINE_POWERPC 0x01F0 #define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1 #define IMAGE_FILE_MACHINE_IA64 0x0200 #define IMAGE_FILE_MACHINE_MIPS16 0x0266 #define IMAGE_FILE_MACHINE_ALPHA64 0x0284 #define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 #define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 #define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64 #define IMAGE_FILE_MACHINE_TRICORE 0x0520 #define IMAGE_FILE_MACHINE_CEF 0x0CEF #define IMAGE_FILE_MACHINE_EBC 0x0EBC #define IMAGE_FILE_MACHINE_AMD64 0x8664 #define IMAGE_FILE_MACHINE_M32R 0x9041 #define IMAGE_FILE_MACHINE_ARM64 0xAA64 #define IMAGE_FILE_MACHINE_CEE 0xC0EE
(2)NumberOfSections:Section 的数目,即 Section Table 数组的元素个数。Windows Loader 限制 Section 的数目为 96 。 (3)TimeDateStamp:文件创建的日期和时间。 (4)PointerToSymbolTable:PE符号表的 RVA 偏移量,如果 PE符号表不存在,则该值为 0 。 (5)NumberOfSymbols:PE符号表中的符号个数。 (6)SizeOfOptionalHeader:_IMAGE_OPTIONAL_HEADER 结构的大小,对于 obj 文件,该值为 0 。 (7)Characteristics:PE 文件的属性。在 winnt.h 中定义的 Characteristics 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define IMAGE_FILE_RELOCS_STRIPPED 0x0001 #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 #define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 #define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 #define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 #define IMAGE_FILE_32BIT_MACHINE 0x0100 #define IMAGE_FILE_DEBUG_STRIPPED 0x0200 #define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 #define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 #define IMAGE_FILE_SYSTEM 0x1000 #define IMAGE_FILE_DLL 0x2000 #define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64; #ifdef _WIN64 typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;#else typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER;#endif
需要记的:
在 IMAGE_NT_HEADERS 之后,紧跟 NumberOfSections 个 IMAGE_SECTION_HEADER。每个节描述文件和内存中的一个区域(例如 .text, .rdata, .data, .rsrc 等)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[8 ]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER;
常见节:
.text — 代码段(可执行、只读)。
.rdata — 只读数据(导出表、字符串、常量)。
.data — 已初始化的可读写数据。
.bss / uninitialized data — 在 PE 通常以 VirtualSize 指定但 SizeOfRawData 可能为 0。
.rsrc — 资源(图标、对话框、版本信息等)。
.reloc — 基址重定位表(如果启用了 ASLR 或 ImageBase 不是默认值时需要)。
.pdata / .xdata(x64 异常/函数表)等。
可选文件头中的数据目录表: Data Directory 位于 IMAGE_OPTIONAL_HEADER 内,是一个固定长度的数组(通常 16 项,IMAGE_NUMBEROF_DIRECTORY_ENTRIES)。每项结构如下:
1 2 3 4 5 typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY;
我们只需要记住里面的导出表,导入表,重定位表
导出表: 位于 数据目录表第 0 项 (IMAGE_DIRECTORY_ENTRY_EXPORT = 0)
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Name 导出表文件名首地址Base 导出函数起始序号NumberOfFunctions是dll文件中导出函数的个数:最大的序号-最小序号+1 NumberOfNames以名称导出函数的个数:即在dll文件中函数后面不加noname的数量
首先我们要知道dll文件通常怎么编写导出哪些函数,一般是用.def文件存储函数和序号,编译时指定这个def文件
1 2 3 4 5 LIBRARY mydll EXPORTS sum @2 Add @3 NONAME mul @7
上面这个例子里NumberOfFunctions就是6=7-2+1;
NumberOfNames就是2
在DLL文件中如何找到要用的函数呢? AddressOfNames存的是函数名称起始位置的偏移。 AddressOfNameOrdinals存的是序号,加上Base等于dll文件中函数后面的序号。 AddressOfFunctions存的是真正函数存储位置的偏移。
从右向左看
要找到MessageBoxW的函数地址,首先从AddressOfNames在AddressOfNameOrdinals中的索引找到MessageBoxW的序号,在AddressOfFunctions按序号找到地址。
导入表 一个文件只有一个导出表,有多个导入表
INT:导入名称表,无论在文件中还是在内存中都是指向函数的名称
IAT: 导入地址表,在文件中时,与INT是一样的指向函数名称,在内存中保存的是函数实际地址
1 2 3 4 5 6 7 8 9 10 11 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32;
1 2 3 4 5 typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1 ]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
这三个结构体之间的关系可以用图表示
_IMAGE_THUNK_DATA32四个字段的工作方式: u1.AddressOfData 按名称导入时的初始状态。这是最常见的导入方式。它是一个 RVA ,指向 IMAGE_IMPORT_BY_NAME 结构体。
Loader 执行时:
检查高位标志(是否为按序号导入)
如果按名称导入 → 从 AddressOfData 找到字符串 "MessageBoxA"
调用 GetProcAddress("MessageBoxA")
把查到的实际函数地址 写回到同一个 thunk 里 → 此时该 DWORD 的含义变成了 Function
u1.Function
阶段
字段含义
程序加载前
AddressOfData(指向函数名结构)
程序徐加载后
Function(函数实际地址)
所以你用 IDA 或 PE-Bear 打开导入表时, 可以看到一列是函数地址(已经被修正),那就是 u1.Function 的值。
u1.Ordinal 如果是 按序号导入 (而不是按名称), 那么 IMAGE_THUNK_DATA 的最高位会被置为 1,这个时候序号是ordinal的低16位
Loader 检查后,会直接按序号查找导出表中的对应函数。
u1.ForwarderString 具体工作流程
程序启动,系统加载器解析其导入表。
加载器看到需要从 Old.dll 导入一个函数。
加载器检查该函数对应的 IMAGE_THUNK_DATA 结构。
如果发现这个条目被标记为一个转发(Loader 看到字符串里有 .,就知道它是转发函数),那么 u1.ForwarderString 字段中存储的值就是一个 RVA。
这个 RVA 指向 PE 文件内部的一个字符串,这个字符串就是转发的目标,例如 "NewDLL.NewFunction"。
加载器于是会转而加载 NewDLL.dll,获取 NewFunction 的地址,并填充到程序的 IAT 中。
重定位表 为什么需要重定位表: 假设你编译了一个 DLL:
1 编译期设定的镜像基址 (ImageBase) = 0x10000000
代码里可能存在这样的指令:
1 mov eax, [0x10003000 ] ; 访问全局变量的绝对地址
但是当系统加载这个 DLL 时,如果地址 0x10000000 已经被别的模块占用, Windows 就会把它加载到另一个位置,比如 0x20000000。
那么所有访问 0x10003000 的指令都错了!
重定位表的任务: 告诉系统:“文件里哪些地方用了绝对地址”,好让 Loader 在加载时给它们加上偏移量修正
重定位表的结构层级: 整体结构是由若干个 重定位块 (Base Relocation Block) 组成。
1 2 3 4 5 6 typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION;
重定位项 (WORD) 结构
每个 WORD 包含两个部分(共 16 位):
位段
名称
含义
高 4 位
Type
重定位类型
低 12 位
Offset
该页内偏移
type类型在微软中的定义:
Type 值
名称
用途
0
IMAGE_REL_BASED_ABSOLUTE
无效项(跳过/对齐用)
1
IMAGE_REL_BASED_HIGH
高16位修正(16位系统遗留)
2
IMAGE_REL_BASED_LOW
低16位修正
3
IMAGE_REL_BASED_HIGHLOW
32位绝对地址修正(最常用)
10
IMAGE_REL_BASED_DIR64
64位绝对地址修正
在内存中的结构:
重定位表的工作原理(Windows Loader 处理流程) 1.系统加载映像文件:
2.计算偏移差:
1 Delta = LoadBase - ImageBase;
3.遍历每个重定位块:
4.按类型修正目标地址:
如果类型是 IMAGE_REL_BASED_HIGHLOW:
1 2 DWORD* pAddr = (DWORD*)(imageBase + VirtualAddress + Offset); *pAddr += Delta;
5.加载器修正完这些地址后:
阶段
内容
编译期
生成以固定 ImageBase 链接的可执行文件
加载期
如果装入地址 ≠ ImageBase,则触发重定位
.reloc
记录所有需要修改绝对地址的地方
Loader
根据差值修正每个位置的值
类型
HIGHLOW(32位) 或 DIR64(64位)
TLS表 什么是TLS?TLS是 Thread Local Storage的缩写线程局部存储。主要是为了解决多线程中变量同步的问题。
tls变量: TLS变量只需要定义一次,类似全局变量,但定义完后每一个线程都能获取TLS变量的副本,解决了不能同步访问TLS的问题。节约了时间和成本。
tls回调函数: 1 2 3 4 5 typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK) ( PVOID DllHandle, DWORD Reason, PVOID Reserved ) ;
它会在进程附加(1),线程附加(2),线程脱离(3),进程脱离(0)时调用(小括号内数字指这四个状态对应整数):
代码(包含tls变量和回调函数) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <Windows.h> #include <iostream> #pragma comment(linker,"/INCLUDE:__tls_used" ) _declspec(thread) int g_number = 100 ; HANDLE hEvent = NULL ; DWORD WINAPI threadProc1 (LPVOID lparam) { g_number = 200 ; printf ("threadProc1 g_number=%d\n" , g_number); SetEvent(hEvent); return 0 ; } DWORD WINAPI threadProc2 (LPVOID lparam) { WaitForSingleObject(hEvent, -1 ); printf ("threadProc2 g_number=%d\n" , g_number); return 0 ; } void NTAPI t_TlsCallBack_A (PVOID DllHandle, DWORD Reason, PVOID Reserved) { printf ("TLS函数执行了\n" ); } #pragma data_seg(".CRT$XLX" ) PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { t_TlsCallBack_A,0 }; #pragma data_seg() int main () { hEvent = CreateEventA(NULL , FALSE, FALSE, NULL ); HANDLE hThread1 = CreateThread(NULL , NULL , threadProc1, NULL , NULL , NULL ); HANDLE hThread2 = CreateThread(NULL , NULL , threadProc2, NULL , NULL , NULL ); WaitForSingleObject(hThread1,-1 ); WaitForSingleObject(hThread2,-1 ); CloseHandle(hEvent); system("pause" ); return 0 ; }
tls反调试: 既然我们知道了TLS是最先执行的,那么我们在TLS回调函数中加上判断是否被调试的API,若被调试直接在OEP之前终止程序,即可做到反调试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <Windows.h> #include <iostream> #pragma comment(linker,"/INCLUDE:__tls_used" ) void NTAPI TLS_CALLBACK1 (PVOID DllHandle, DWORD Reason, PVOID Reserved) { if (Reason == DLL_PROCESS_ATTACH) { BOOL result = FALSE; HANDLE hNewHandle = 0 ; DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hNewHandle, NULL , NULL , DUPLICATE_SAME_ACCESS); CheckRemoteDebuggerPresent(hNewHandle, &result); if (result) { MessageBoxA(0 , "程序被调试了!" , "警告" , MB_OK); ExitProcess(0 ); } } return ; } #pragma data_seg(".CRT$XLX" ) PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1,0 }; #pragma data_seg() int main () { printf ("main函数执行了" ); system("pause" ); return 0 ; }
tls表 1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_TLS_DIRECTORY { DWORD StartAddressOfRawData; DWORD EndAddressOfRawData; DWORD AddressOfIndex; DWORD AddressOfCallBacks; DWORD SizeOfZeroFill; DWORD Characteristics; } IMAGE_TLS_DIRECTORY32;
pe文件结构代码: 文件结构:
头文件:Main.cpp,CPeUtil.h
源文件:CPeUtil.cpp
CPeUtil.cpp: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 #include "CPeUtil.h" CPeUtil::CPeUtil() { FileBuff=NULL ; FileSize=0 ; pDosHeader = NULL ; pNtHeaders = NULL ; pFileHeader = NULL ; pOptionHeader = NULL ; } CPeUtil::~CPeUtil() { if (FileBuff) { delete[]FileBuff; FileBuff = NULL ; } } BOOL CPeUtil::loadFile (const char * patch) { HANDLE hFile = CreateFileA(patch, GENERIC_READ, 0 , NULL , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 ); if (hFile==0 ) { return FALSE; } FileSize = GetFileSize(hFile, 0 ); FileBuff = new char [FileSize]{0 }; DWORD realReadBytes = 0 ; BOOL readSuccess =ReadFile(hFile,FileBuff,FileSize,&realReadBytes,0 ); if (readSuccess==0 ) { return FALSE; } if (InitPeInfo()) { CloseHandle(hFile); return TRUE; } return FALSE; } BOOL CPeUtil::InitPeInfo () { pDosHeader = (PIMAGE_DOS_HEADER)FileBuff; if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { return FALSE; } pNtHeaders = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + FileBuff); if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) { return FALSE; } pFileHeader = &pNtHeaders->FileHeader; pOptionHeader = &pNtHeaders->OptionalHeader; return TRUE; } void CPeUtil::PrintSectionHeaders () { PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders); for (int i = 0 ; i < pFileHeader->NumberOfSections; i++) { char name[9 ]{ 0 }; memcpy_s(name, 9 , pSectionHeaders->Name, 8 ); printf ("区段名称:%s\n" , name); pSectionHeaders++; } } void CPeUtil::GetExportTable () { IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[0 ]; PIMAGE_EXPORT_DIRECTORY pexport = (PIMAGE_EXPORT_DIRECTORY)RvaToFoa(directory.VirtualAddress); char *dllName = RvaToFoa(pexport->Name)+FileBuff; printf ("文件名称:%s\n" , dllName); DWORD* funaddr = (DWORD*)(RvaToFoa(pexport->AddressOfFunctions) + FileBuff); WORD* peot = (WORD*)(RvaToFoa(pexport->AddressOfNameOrdinals) + FileBuff); DWORD* pent = (DWORD*)(RvaToFoa(pexport->AddressOfNames) + FileBuff); for (int i = 0 ; i < pexport->NumberOfFunctions; i++) { printf ("函数地址为:%x\n" ,*funaddr); for (int j = 0 ; j < pexport->NumberOfNames; j++) { if (peot[j]==i) { char * funName = RvaToFoa(pent[j])+FileBuff; printf ("函数名称为:%s\n" , funName); break ; } } funaddr++; } } void CPeUtil::GetImportTables () { IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[1 ]; PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(directory.VirtualAddress) + FileBuff); while (pImport->OriginalFirstThunk) { char * dllName = RvaToFoa(pImport->Name) + FileBuff; printf ("dll文件名称为:%s\n" , dllName); PIMAGE_THUNK_DATA pThunkData = (PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk) + FileBuff); while (pThunkData->u1.Function) { if (pThunkData->u1.Ordinal & 0x80000000 ) { printf ("按序号导入:%d\n" , pThunkData->u1.Ordinal & 0x7FFFFFFF ); } else { PIMAGE_IMPORT_BY_NAME importName = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pThunkData->u1.AddressOfData) + FileBuff); printf ("按名称导入:%s\n" , importName->Name); } pThunkData++; } pImport++; } } DWORD CPeUtil::RvaToFoa (DWORD rva) { PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders); for (int i = 0 ; i < pFileHeader->NumberOfSections; i++) { if (rva >= pSectionHeaders->VirtualAddress && rva < pSectionHeaders->VirtualAddress + pSectionHeaders->Misc.VirtualSize) { return rva - pSectionHeaders->VirtualAddress + pSectionHeaders->PointerToRawData; } pSectionHeaders++; } return 0 ; }
CPeUtil.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #pragma once #include <Windows.h> #include <iostream> class CPeUtil {public: CPeUtil(); ~CPeUtil(); BOOL loadFile (const char * patch) ; BOOL InitPeInfo () ; void PrintSectionHeaders () ; void GetExportTable () ; void GetImportTables () ; private: char * FileBuff; DWORD FileSize; PIMAGE_DOS_HEADER pDosHeader; PIMAGE_NT_HEADERS pNtHeaders; PIMAGE_FILE_HEADER pFileHeader; PIMAGE_OPTIONAL_HEADER pOptionHeader; DWORD RvaToFoa (DWORD rva) ; };
main.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include "CPeUtil.h" int main () { CPeUtil peUtil; BOOL ifSuccess = peUtil.loadFile("D:\\code\\VisualStudio2022\\FirstDLL\\Debug\\FirstDLL.dll" ); if (ifSuccess) { peUtil.GetImportTables(); return 0 ; } printf ("加载PE文件失败!\n" ); return 0 ; }
Reference: https://blog.csdn.net/weixin_44143678/article/details/120044602?spm=1001.2014.3001.5506
【【保姆级教程】16 节吃透 Windows PE 文件格式!从解析到 Hook 攻防全覆盖】https://www.bilibili.com/video/BV1cXT4z7Etf?p=8&vd_source=ef1be23ebedc3f547905767af45d9f93