PE:PE结构

mac2022-06-30  32

文章目录

一、名词解析二、PE头结构2.0 概览2.1 DOS头 (IMAGE_DOS_HEADER)2.2 NT头(IMAGE_NT_HEADERS)(1) NT头之PE签名(IMAGE_NT_SIGNATURE)(2) NT头之文件映像头(IMAGE_FILE_HEADER)(3) NT头之可选映像头(IMAGE_OPTIONAL_HEADER)(4) NT头之16个数据目录(IMAGE_DATA_DIRECTORY) 2.3 节头(IMAGE_SECTION_HEADERS) 三、PE数据表3.1 导出表3.2 导入表(1) 导入函数的调用机制(2) 导入表的结构(内存加载前)(3) 导入表的结构(内存加载后)(4) 手动模拟修复IAT表 3.3 基址重定位表(1) 什么是重定位表?在何处需要使用?(2) 重定位表的设计思路分析(3) 重定位表的结构(4) 更改基址时,根据重定位表修正数据 3.4 绑定导入表 参考资料

一、名词解析

名词含义ModulePE文件被windows加载器载入虚拟内存后,在虚拟内存中称为模块(Module)hModule模块(Module)的起始地址称为模块句柄(hModule)ImageBase模块句柄(hModule)也称为基地址(ImageBase)VA虚拟地址(Virtual Address),即某个数据在虚拟内存中的地址值。是被拉伸后(内存对齐后)的地址值。RVA相对虚拟地址(Relative Virtual Address),即某个数据在内存中相对于模块载入地址或基地址(hModule或ImageBase)的偏移量。是被拉伸后(内存对齐)后的偏移量。FOA文件偏移地址(File Offset Address),即某个数据在文件系统中相对文件起始地址的偏移量

二、PE头结构

2.0 概览

PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。PE文件的结构都定义在"winnt.h"头文件中的"Image Format"节。32位和64位的PE结构略有区别,32位版本称为’PE32’,64位称为’PE32+’PE头 = DOS头(MS-DOS头 + DOS stub) + NT头( PE签名+文件映像头+可选映像头(包含16个数据目录))+ 若干个“节头”。PE结构如下图:PE头的大小(文件对齐后)= DOS头+NT头+16个节表+节表后的未知数据 = NT头的SizeOfHeaders字段。向后舍入为文件对齐值FileAlignment的倍数。

2.1 DOS头 (IMAGE_DOS_HEADER)

DOS头由MS-DOS头和DOS stub组成。

DOS stub存放了提示字符串,例如’This program cannot be run in MS-DOS mdoe’.在MS-DOS头中,有用的属性如下: FOA(相对DOS头起始位置)字节长度属性名描述+0h2e_magic'MZ’标记,即0x4D5A+3Ch4e_lfanewRVA值,指向NT头中’PE签名’

e_lfanew是NT头起始位置的RVA(或FOA,因为在内存中PE头不会被拉伸,所以RVA=FOA)

2.2 NT头(IMAGE_NT_HEADERS)

紧跟着DOS stub的是NT映像头(IMAGE_NT_HEADERS),有PE(32位)和PE+(64位)两种版本的NT映像头,他们略有区别。

(1) NT头之PE签名(IMAGE_NT_SIGNATURE)

在一个有效的PE文件里,NT头的前4字节是‘PE签名’,在winnt.h头文件中定义了这个值 :

#define IMAGE_NT_SIGNATURE 0x00004550 // PE00 FOA(相对NT头起始位置)字节宽度(32bit or 64bit)属性名描述+0h4IMAGE_NT_SIGNATUREPE签名,值为0x00004550(‘PE\0\0’)

(2) NT头之文件映像头(IMAGE_FILE_HEADER)

由于文件映像头(IMAGE_FILE_HEADER)的结构在COFF格式的OBJ文件的开始处也能被找到,所以又称文件映像头为"COFF File Header"。文件映像头(IMAGE_FILE_HEADER)的所有属性如下表 FOA(相对NT头起始位置)字节宽度(32bit or 64bit)属性名描述+04h2Machine镜像文件能运行的处理器平台+06h2NumberOfSections节数量+08h4TimeDateStamp文件创建时间(从UTC时间1970年1月1日00:00起的总秒数的低32位)+0Ch4PointerToSymbolTable(已废除)+10h4NumberOfSymbols(已废除)+14h2SizeOfOptionalHeader可选映像头(IMAGE_OPTIONAL_HEADER)的大小。这个大小在32位和64位文件中是不同的。对于32位文件来说,它是224(0xE0);对于64位文件来说,它是240(0xF0)。+16h2Characteristics文件属性,每位有不同的含义 文件映像头(IMAGE_FILE_HEADER)的Characteristics属性的十六进制各位的含义定义在"winnt.h"头文件的IMAGE_FILE_XXX宏。例如,当Characteristics=0x0201时,表示文件的调试信息被移除 && 不存在重定位表 宏定义十六进制描述#define IMAGE_FILE_RELOCS_STRIPPED0x0001Relocation info stripped from file.此文件不包含重定位信息。即PE文件必须被加载器加载到其指定的基地址上,否则加载器会报错。#define IMAGE_FILE_EXECUTABLE_IMAGE0x0002File is executable (i.e. no unresolved external references).文件是可执行(文件)#define IMAGE_FILE_LINE_NUMS_STRIPPED0x0004Line nunbers stripped from file.行号信息被移去#define IMAGE_FILE_LOCAL_SYMS_STRIPPED0x0008Local symbols stripped from file.符号信息被移去#define IMAGE_FILE_AGGRESIVE_WS_TRIM0x0010Aggressively trim working set#define IMAGE_FILE_LARGE_ADDRESS_AWARE0x0020App can handle >2gb addresses应用程序可以处理超过2GB的地址值#define IMAGE_FILE_BYTES_REVERSED_LO0x0080Bytes of machine word are reversed.处理机的低位字节是相反的#define IMAGE_FILE_32BIT_MACHINE0x010032 bit word machine.目标平台是32位机器#define IMAGE_FILE_DEBUG_STRIPPED0x0200Debugging info stripped from file in .DBG file .DBG文件的调试信息被移除#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP0x0400If Image is on removable media, copy and run from the swap file.#define IMAGE_FILE_NET_RUN_FROM_SWAP0x0800If Image is on Net, copy and run from the swap file.#define IMAGE_FILE_SYSTEM0x1000System File.系统文件#define IMAGE_FILE_DLL0x2000File is a DLL.文件是DLL文件#define IMAGE_FILE_UP_SYSTEM_ONLY0x4000File should only be run on a UP machine#define IMAGE_FILE_BYTES_REVERSED_HI0x8000Bytes of machine word are reversed.处理机的高位字节是相反的

(3) NT头之可选映像头(IMAGE_OPTIONAL_HEADER)

64位的可选映像头(IMAGE_OPTIONAL_HEADER)比起32位有些许变化:

64位的可选映像头的BaseOfData被移除了,此处被并入紧随其后的ImageBase中。64位的可选映像头的ImageBase宽度由4字节变为8字节。64位的可选映像头的SizeOfStackReserve宽度由4字节变为8字节。64位的可选映像头的SizeOfStackCommit宽度由4字节变为8字节。64位的可选映像头的SizeOfHeapReserve宽度由4字节变为8字节。64位的可选映像头的SizeOfHeapCommit宽度由4字节变为8字节。 FOA(相对NT头起始位置)字节宽度(32bit or 64bit)属性名描述+18h2Magic0x107表明这是一个ROM镜像文件。0x10B表明这是一个32位PE文件。0x20B表明这是一个64位PE文件。+1Ah1MajorLinkerVersion+1Bh1MinorLinkerVersion+1Ch4SizeOfCode+20h4SizeOfInitializedData+24h4SizeOfUninitializedData+28h4AddressOfEntryPoint可执行文件的入口点地址相对于基地址(ImageBase)的RVA。对于一般程序镜像来说,它就是启动地址。当其为0时,则表示从ImageBase开始执行。对于dll文件是可选的。+2Ch4BaseOfCode拉伸后(内存对齐后)的代码节的RVA。必须是SectionAlignment的整数倍。+30h or null4 or 0BaseOfData拉伸后(内存对齐后)的数据节的RVA。必须是SectionAlignment的整数倍。(在64位文件中此处被并入紧随其后的ImageBase中。)+34h or +30h4 or 8ImageBase静态基地址,PE文件被加载时首选的载入地址。如果有可能(即目前没有其他文件占据这块地址),加载器就会在这个地址载入模块,并且将跳过应用基址重定位这一步骤。+38h4SectionAlignment节对齐值(内存对齐值),它必须≥文件对齐值(FileAlignment)。每个节的载入地址必须是内存对齐值(SectionAlignment)的整数倍。默认的内存对齐值是目标CPU的页尺寸,对于windows系统的最小内存对齐值是1000h(4KB),在IA-64上,内存对齐是8KB。+3Ch4FileAlignment文件对齐值+40h2MajorOperatingSystemVersion+42h2MinorOperatingSystemVersion+44h2MajorImageVersion+46h2MinorImageVersion+48h2MajorSubsystemVersion+4Ah2MinorSubsystemVersion+4Ch4Win32VersionValue+50h4SizeOfImage文件被拉伸加载至内存后的镜像大小(内存对齐后)。向后舍入为内存对齐值SectionAlignment的倍数。(例如原数据是300字节,但文件以400字节对齐,那么Size of Headers = 400)+54h4SizeOfHeaders所有的头大小(文件对齐后)= DOS头+NT头+16个节表+节表后的未知数据。向后舍入为文件对齐值FileAlignment的倍数。+58h4CheckSum+5Ch2Subsystem+5Eh2DllCharacteristicsDLL特征,见后表。+60h4 or 8SizeOfStackReserve+64h or +68h4 or 8SizeOfStackCommit+68h or +70h4 or 8SizeOfHeapReserve+6Ch or +78h4 or 8SizeOfHeapCommit+70h or +80h4LoaderFlags+74h or +84h4NumberOfRvaAndSizes数据目录的个数。由于以前发行的Windows NT的原因,它的值只能为16。 DllCharacteristics属性的含义在“winnt.h”的“DllCharacteristics Entries”部分进行了定义,这个属性似乎决定了是否使用动态基址。使用StudyPE.exe可以切换是否动态基址,从而验证此属性。DllCharacteristics的16进制含义如下: 宏定义十六进制描述#define IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA0x0020Image can handle a high entropy 64-bit virtual address space.#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE0x0040DLL can move.DLL可以在加载时被重定位。#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY0x0080Code Integrity Image#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT0x0100Image is NX compatible#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION0x0200Image understands isolation and doesn’t want it#define IMAGE_DLLCHARACTERISTICS_NO_SEH0x0400Image does not use SEH. No SE handler may reside in this image#define IMAGE_DLLCHARACTERISTICS_NO_BIND0x0800Do not bind this image.#define IMAGE_DLLCHARACTERISTICS_APPCONTAINER0x1000Image should execute in an AppContainer#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER0x2000Driver uses WDM model 是WDM驱动程序。#define IMAGE_DLLCHARACTERISTICS_GUARD_CF0x4000Image supports Control Flow Guard.#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE0x8000

(4) NT头之16个数据目录(IMAGE_DATA_DIRECTORY)

数据目录的个数为16,由16个IMAGE_DATA_DIRECTORY结构体组成,"winnt.h"中给出了结构体的定义:

// // Directory format. // typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //该数据目录相对基地址的RVA DWORD Size; //数据目录的大小,这个值可以不准确,因为有数据目录有自己的大小声明方式 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 16个数据目录的FOA如下表 序号FOA(相对NT头起始位置)字节宽度(32bit or 64bit)描述0+78h or +88h8Export Directory 导出表的RVA和大小1+80h or +90h8Import Directory 导入表的RVA和大小2+88h or +98h8资源表的RVA和大小3+90h or +A0h8异常表的RVA和大小4+98h or +A8h8属性证书表的RVA和大小5+A0h or +B0h8Base Relocation Directory 基址重定位表的RVA和大小6+A8h or +B8h8调试数据的起始地址RVA和大小7+B0h or +C0h8保留,必须为08+B8h or +C8h8将被存储在全局指针寄存器中的一个值的RVA。这个结构的Size域必须为09+C0h or +D0h8线程局部存储(TLS)表的RVA和大小。10+C8h or +D8h8加载配置表的RVA和大小。11+D0h or +E0h8Bound Import In Headers 绑定导入表的RVA和大小。12+D8h or +E8h8Import Address Table 导入地址表IAT的RVA和大小。13+E0h or +F0h8延迟导入描述符的RVA和大小。14+E8h or +F8h8COM运行时头部的RVA和大小。(已废除)15+F0h or +100h8保留,必须为0

2.3 节头(IMAGE_SECTION_HEADERS)

紧跟着NT头的是节表或节头(IMAGE_SECTION_HEADERS),这是由若干个IMAGE_SECTION_HEADER结构体组成的数组,该数组的长度由NT头中的NumberOfSections指出。第一个节头的位置(FOA) = 0 + DOS头的e_lfanew + NT头的PE签名长度 + NT头的文件映像头长度 + NT头的SizeOfOptionalHeader"winnt.h"中给出了IMAGE_SECTION_HEADER的定义: // // Section header format. // #define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //节的名称(8字节) union { DWORD PhysicalAddress; DWORD VirtualSize; //这个值可以是不准确的 } Misc; DWORD VirtualAddress; //节的RVA DWORD SizeOfRawData; //文件对齐后的节大小 DWORD PointerToRawData; //节的FOA DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; //节的属性,见下表。 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; 其中,节的属性Characteristics的16进制各位含义如下: 宏定义十六进制描述#define IMAGE_SCN_CNT_CODE0x00000020Section contains code.此节包含可执行代码。代码段才用“.text”#define IMAGE_SCN_CNT_INITIALIZED_DATA0x00000040Section contains initialized data.此节包含已初始化的数据。“.data”#define IMAGE_SCN_CNT_UNINITIALIZED_DATA0x00000080Section contains uninitialized data.此节包含未初始化的数据。“.bss”#define IMAGE_SCN_GPREL0x00008000Section content can be accessed relative to GP. 此节包含通过全局指针(GP)来引用的数据。#define IMAGE_SCN_LNK_NRELOC_OVFL0x01000000Section contains extended relocations.此节包含扩展的重定位信息。#define IMAGE_SCN_MEM_DISCARDABLE0x02000000Section can be discarded.此节可被丢弃。因为它一旦被载入,进程就不需要它了。常见的可丢弃节是.reloc(重定位块)#define IMAGE_SCN_MEM_NOT_CACHED0x04000000Section is not cachable.此节不能被缓存。#define IMAGE_SCN_MEM_NOT_PAGED0x08000000Section is not pageable.此节不能被交换到页面文件中。#define IMAGE_SCN_MEM_SHARED0x10000000Section is shareable.此节为可共享节,可以在内存中共享。#define IMAGE_SCN_MEM_EXECUTE0x20000000Section is executable.此节可执行#define IMAGE_SCN_MEM_READ0x40000000Section is readable.此节可读。可执行文件中的节总是设置此标志。#define IMAGE_SCN_MEM_WRITE0x80000000Section is writeable.此节可写。如果PE文件中没有设置此标志,则装载程序就会将内存映像页标记为可读或可执行。

三、PE数据表

3.1 导出表

NT头的第0个数据目录(IMAGE_DATA_DIRECTORY)是导出表的数据目录,其中的VirtualAddress域给出了导出表的RVA,将RVA转换为FOA就能在文件系统中找到导出表(Export Table)。

"winnt.h"中给出了导出表的数据结构定义(IMAGE_EXPORT_DIRECTORY)

// // Export Format // //@[comment("MVI_tracked")] typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; //指向ASCII字符串的RVA,字符串是此导出表所属的DLL名称 DWORD Base; //序号的基数。当通过导出序号查询导出函数地址时,用导出序号减去Base就得到函数地址表(Function Address Table)的索引下标。 DWORD NumberOfFunctions; // 函数地址数组的长度 DWORD NumberOfNames; // 函数名称数组的长度 DWORD AddressOfFunctions; // 函数地址数组的RVA DWORD AddressOfNames; // 函数名称数组的RVA DWORD AddressOfNameOrdinals; // 函数名称序号数组的RVA } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

通过导出表数据结构中给出的3个RVA值能得到3个数组,分别是函数名称数组、函数名称数组、函数名称序号数组。

(1)函数地址数组(Function Address Table) 导出表的AddressOfFunctions域指向一个"函数地址数组(Function Address Table)",数组中保存了此DLL的所有被导出函数在此DLL中的地址(RVA),数组长度由AddressOfFunctions指定,每个元素占4字节。 函数地址数组的索引下标加上Base值就得到该函数的“导出序号”。 (2) 函数名称数组(Function Name Table) 如果一个函数的导出方式是“按名称导出”,那么指向其名称字符串的RVA被保存在所属DLL文件的“函数名称数组(Function Name Table)”中,数组长度由AddressOfNames指定,每个元素占4字节。 注意,如果一个函数以”按序号导出“,那么这个函数只有导出地址而没有导出名称,所以”函数名称数组“的长度可能小于”函数地址数组“。(3)函数名称序号数组(Function Ordinal Table) “函数名称序号数组(Function Ordinal Table)”用于联系前2个数组,“名称序号数组”中存放的是“函数地址数组”的索引下标值(每个元素占2字节)。“序号数组”的长度一定与“名称数组”的长度相同,且每个元素都与“名称数组”的每个元素相对应。 例如,“名称序号数组”的第1个元素对应了“名称数组”的第1个函数名,“名称序号数组”第1个元素的值是该函数名在”函数地址数组“中的索引下标。通过这个联系,可以通过导出函数名查找导出函数地址。

导出函数的“导出序号” 如果一个函数按名称导出,则既有序号又有名称;即:即“函数地址数组”和“函数名称数组”都有这个函数。 如果一个按序号导出,那么仅有导出序号;即:即只有“函数地址数组”有这个函数。 总之,每个导出函数都有一个导出序号,且都在“函数地址数组”中有一个地址值。 计算公式:导出序号 = 函数地址数组的索引下标 + 导出表数据结构的Base域

实例分析

已知DLL中的一个导出函数的名称,求此导出函数在DLL中的函数地址。 (1)遍历“函数名称数组”对应的字符串找到函数名称,记录索引=index; (2)找到“函数名称序号数组”索引为index的元素,记录元素值=value; (3)找到“函数地址数组”中索引为value的元素,元素值就是所求的导出函数地址。

//************************************ // Method: Tool_GetFuncRVABy导出函数名称 // Description:根据函数名称查找导出表的函数地址RVA //************************************ DWORD PE::Tool_GetFuncRVABy导出函数名称(char* FuncNameStr) { //1.遍历“函数名称数组”所指向的名称字符串,记录索引=index for (int i = 0; i < this->DataTable.ExportTable.IMAGE_EXPORT_Directory.NumberOfNames; i++) { if (0 == strcmp(FuncNameStr, this->DataTable.ExportTable.函数名称数组[i].FunctionName_Str))//已事先将函数名称RVA转为名称字符串 { //2.找到“函数名称序号数组”索引为index的元素,记录元素值=value; WORD value = this->DataTable.ExportTable.函数名称序号数组[i]; //3.找到“函数地址数组”中索引为value的元素,元素值就是所求的导出函数地址。 return this->DataTable.ExportTable.函数地址数组[value]; } } return 0; }

已知DLL中的一个导出函数的“导出序号”,求此导出函数在DLL中的函数地址。 (1)用导出序号减去Base值,得到所求函数在“函数地址数组”中的索引下标,记为index。 (2)找到“函数地址数组”中索引为index的元素,元素值就是所求的导出函数地址。

//************************************ // Method: Tool_GetFuncRVABy导出序号 // Description:根据导出序号查找导出表的函数地址RVA //************************************ DWORD PE::Tool_GetFuncRVABy导出序号(int 导出序号) { //1.用导出序号减去Base值,得到所求函数在“函数地址数组”中的索引下标,记为index。 int index = 导出序号 - this->DataTable.ExportTable.IMAGE_EXPORT_Directory.Base; //2.找到“函数地址数组”中索引为index的元素,元素值就是所求的导出函数地址。 return this->DataTable.ExportTable.函数地址数组[index]; }

建立【 导出序号->导出函数RVA->导出函数名称】的联合视图 (1)遍历“函数地址数组”的每个元素加上Base值,得到一组导出序号。 (2)遍历“函数地址数组”的每个元素得到一组导出函数RVA。 (3)遍历“函数序号表”的元素值,找到与“函数地址数组”的索引相同的元素,找到其在“函数名称表”中相对应的元素,得到一组函数名称。 示例:联合视图

3.2 导入表

(1) 导入函数的调用机制

导入函数是被程序调用但其执行代码不在调用者EXE模块中而在被导入的DLL模块中的函数。导入函数在调用者EXE模块中只保留相关的函数信息,例如函数名、DLL模块名等。在调用者EXE模块中存在一组被称为IAT表(Import Address Table)的数据结构,一旦DLL模块被加载,IAT表中会包含所要调用的导入函数的地址(相对于DLL基址的RVA)。当Windows加载器载入EXE模块时,会根据其导入表(Import Table)加载其所需的DLL模块,当载入这些DLL模块时又会根据DLL模块的导入表去加载这些DLL模块所需的其他DLL模块。调用导入函数的机制

(2) 导入表的结构(内存加载前)

NT头数据目录的第2个成员指向导入表(Import Table),导入表是一个数组,每个元素都是一个 IMAGE_IMPORT_DESCRIPTOR结构体,PE文件每导入一个DLL模块都会在数组中添加一个元素用于描述这个DLL,所以数组长度就是被导入的DLL个数,但没有哪个字段直接写明该长度,可以根据数组末尾额外有一个值全为0的结构体,据此计算数组长度。IMAGE_IMPORT_DESCRIPTOR结构体定义如下:typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // DWORD OriginalFirstThunk; // 指向导入名称表(INT)的RVA } DUMMYUNIONNAME; DWORD TimeDateStamp; // DWORD ForwarderChain; // DWORD Name; // 指向DLL名称的RVA。DLL名称是一个以00结尾的ASCII字符串。 DWORD FirstThunk; // 指向导入地址表(IAT)的RVA } IMAGE_IMPORT_DESCRIPTOR; 其中OriginalFirstThunk域指向一个的INT数组,FirstThunk域指向一个IAT数组,这是两个结构和长度完全相同的数组。整个数组对应一个被导入的DLL中的所有函数,数组每个元素对应一个具体的“被导入函数”,且每个元素都是一个IMAGE_THUNK_DATA结构体,以全为0的结构体作为末尾。需要知道的是,在PE文件被Windows内存加载前, IAT和INT数组中的内容是完全相同的。IAT或INT数组的每一项在64位PE文件中占8字节,在32位PE文件中占4字节。 (1)当一项的最高位为1时,表示此函数是“由序号导出”,此时低32位(64位PE文件下是低63位)被看成“函数序号”(是导出序号吗??)。 (2)当一项的最高位为0时,表示函数“由名称导出”,此时整个结构体的值表示一个RVA,指向一个仅占1字节的IMAGE_IMPORT_BY_NAME结构体,通过此结构体可以获取"被导入函数"在其DLL中的导出名称字符串。IMAGE_IMPORT_BY_NAME结构体定义如下:typedef struct _IMAGE_IMPORT_BY_NAME { //Hint表示函数在其所驻留的DLL的导出表中的导出序号。该域被PE装载器用来在DLL的输出表里快速查询函数。 //但该值不是必须的,可能被设为0 WORD Hint; //函数名,一个ASCII字符串,大小不确定,以null结尾。 CHAR Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; 被导入的DLL、被导入函数、导入表、INT数组、IAT数组的关系如下:(PE被内存加载前)

(3) 导入表的结构(内存加载后)

PE文件被内存加载后,INT数组不会被改变,而IAT数组会被重写。

IAT数组重写过程如下: (1)Windows加载器先搜索DLL的导入表的OriginalFirstThunk域(如果不为0),由此找到INT数组,迭代搜索数组中的每个元素(即每个IMAGE_THUNK_DATA结构体)。 (2)根据INT每项的值的最高位找到"被导入函数的"在其原始DLL中的导出序号(IMAGE_THUNK_DATA的低位)或“在其原始DLL中的导出函数名”(即IMAGE_THUNK_DATA指向的IMAGE_IMPORT_BY_NAME的Name域) (3)然后根据导出序号或函数名从其原始DLL的导出表中查找函数RVA (4)最后将RVA值加上原始DLL在当前DLL中的载入基址,把结果写入IAT对应项。 (5)至此,IAT表的各元素的值就是导入函数在EXE程序中的真实地址 Virtual Address了。EXE调用导入函数时的指令【 Jmp dword prt [XXXXX] 】中的[XXXXX]所指向的就是IAT表中的某个元素值,即导入函数VA。 (6)另一种情况是,如果OriginalFirstThunk域等于0,则加载器就根据FirstThunk查找IAT数组,然后继续步骤2~4(因为此时IAT内容与INT相同)。

PE内存加载后的导入表如下:

如果直接读取内存加载后的PE文件,那么需要注意:如果OriginalFirstThunk=0,那么无法直接获取INT数组。 在读取“内存加载前的PE”时,我们可以利用“IAT数组的值=INT数组的值”去间接得到INT数组。 但在读取“内存加载后的PE”时,由于IAT数组已经被完全改写成外部模块中的函数RVA值(IAT值必定大于当前模块的ImageSize),所以不能直接也不能间接得到INT数组了。(即无法得到函数名或函数导出序号) 因此:当 OriginalFirstThunk==0 && 获取的INT值大于当前模块的ImageSize 时,不再尝试获取函数名或导出序号 (其实也可以强行获取到INT数组:先得到当前模块的文件路径,然后分析FifleBuffer状态的PE结构就可以间接通过IAT数组得到INT数组)。

(4) 手动模拟修复IAT表

PE加载器重写IAT数组的流程图: 代码//************************************ // Method: RepairIAT_ForImageBuffer // Description:修复IAT表 // Returns: void - //************************************ bool PE::RepairIAT_ForImageBuffer() { Log::INFO(HString::Format("开始修复IAT表...")); //1 循环加载所有的延申的DLL模块(递归,直到一个DLL已被加载或此DLL不再导入任何模块) for (int i = 0; i < this->DataTable.ImportTable.导入表视图.size(); i++) { char* dllName = this->DataTable.ImportTable.导入表视图[i].DLL名称; HMODULE hModule = PE::MyLoadLibraryA(dllName); PE dllPE; dllPE.AnalyzePE_ByImageBufferOrExecutableBuffer((PBYTE)hModule); //分析目标模块的PE结构 //HANDLE funcVA1 = PE::MyGetProcAddressA(hModule, "A_SHAFinal"); //HANDLE funcVA1 = PE::MyGetProcAddressA("ntdll.dll", "A_SHAFinal"); //HANDLE funcVA2 = PE::MyGetProcAddressA(hModule, 9); //2 循环获取导入函数的地址(VA) for (int j = 0; j < this->DataTable.ImportTable.导入表视图[i].INT和IAT的联合视图.size(); j++) { HANDLE pFunc = 0; if (this->DataTable.ImportTable.导入表视图[i].INT和IAT的联合视图[j].导入函数的导出序号 != -1) { pFunc = dllPE.MyGetProcAddressA(hModule, this->DataTable.ImportTable.导入表视图[i].INT和IAT的联合视图[j].导入函数的导出序号); //其实这里通过WIN32 API【GetProcAddress】获取我加载的模块中的函数地址的话,效率更高,也不会影响模块隐藏。 //pFunc = GetProcAddress(hModule, (char*)pe.DataTable.ImportTable.导入表视图[i].INT和IAT的联合视图[j].导入函数的导出序号); } else { pFunc = dllPE.MyGetProcAddressA(hModule, this->DataTable.ImportTable.导入表视图[i].INT和IAT的联合视图[j].导入函数的导出名称); //其实这里通过WIN32 API【GetProcAddress】获取我加载的模块中的函数地址,效率更高,也不会影响模块隐藏。 //pFunc = GetProcAddress(hModule, pe.DataTable.ImportTable.导入表视图[i].INT和IAT的联合视图[j].导入函数的导出名称); } } } Log::INFO(HString::Format("成功修复IAT表!")); return TRUE; }

3.3 基址重定位表

(1) 什么是重定位表?在何处需要使用?

PE文件中哪些数据需要被重定位? 当编译链接一个PE文件时,会假设这个模块被装载至一个默认的地址(PE头的ImageBase处),代码中的一些指针值会被写死。那么如果模块没有按默认基址载入时,这些指针值就需要被“重定位”。博客https://www.cnblogs.com/predator-wang/p/4962775.html中给出了例子。重定位表的作用 重定位表中记录了那些需要被重新改写的数值的“位置”(RVA)。哪些PE文件有重定位表 对于EXE文件来说,因为每个进程都是独立的虚拟内存空间,所以一般EXE文件总是能按照默认基址被载入,所以一般EXE文件不需要重定位表,Vistual Studio的链接器会为所生成的Debug和Release模式的EXE文件省略重定位表。 对于DLL文件来说,必须包含重定位表,因为DLL模块是逐个加载的,就不能保证默认基址不被其他DLL先占用。

(2) 重定位表的设计思路分析

如果重定位表中存放了每个需要重定位的“完整地址”,那么所占空间会很大,例如有n个地址需要重定位,那么重定位目录中的重定位项的总大小是4×n字节。重定位目录为了节省所占空间,设计思路如下: 在比较靠近的需要重定位的地址中(如0x8001、8002、8003),如果把这些相近地址的高位地址统一表示,那么就可以省略一部分的空间。我们知道一个内存页是1000h字节,用来表示这个内存页只需要000h~FFFh个地址,即只需3个十六进制位(12个二进制位)就可以表示一个内存页的所有地址。所以,所有需要重定位的地址就分成一个个内存页大小的区间(即每1000h一个区间),如从0x8000到0x8FFFF的所有需要重定位地址值都放入第一个块,从0x9000到0x9FFF的所有需要重定位的地址值都放入第二个块。用一个4字节存放块的“块基地址virtual address”,用2字节作为“项”,项的低12位存放“块基地址的偏移offset”,两者相加就得到需要重定位的地址值。 例如第一块的块基地址是0x8000,块有3个项,项里面的块基地址的偏移分别为0x1,0x2,0x3,那么相加后得到:在0x8000~0x8FFFF内,需要重定位的地址有0x8001、8002、8003。同时,这样也保证“项”的剩下高4位没有浪费,被用来表示重定位类型,类型"3"表示该位置整个地址都需要被修正。

(3) 重定位表的结构

重定位表的位置 NT头之数据目录中的第5个数据目录中给出了"重定位表"的起始RVA,通过这个RVA可以找到重定位表。

重定位表的结构 “重定位表"由数个连续的IMAGE_BASE_RELOCATION结构组成(以下称"重定位块”),每个"重定位块"又由VirtualAddress、SizeOfBlock、TypeOffset[ ] 三个部分组成。其中TypeOffset是一个数组,数组的每个元素(以下称"重定位项"’)占2字节共16位,高4位表示重定位类型,低12位的值加上VirtualAddress值就等于需要被重定位的数据的RVA。

重定位表中需要手动计算的值 连续的"重定位块"构成了整个"重定位表",并且以一个VirtualAddress字段=0的重定位块结构作为结束。 所以: 【重定位表的长度】 = 每个重定位块的sizeofBlock相加 【重定位表中"重定位块"的个数】 = 每读一个IMAGE_BASE_RELOCATION计数加一,直到读取了VirtualAddress=0的IMAGE_BASE_RELOCATION。 【重定位块中"重定位项"的个数】 = ( 重定位块的SizeOfBlock-8 ) / 2 ,因为每个重定位项占2字节。

typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; //当前"重定位块"中各个"重定位项"的基础RVA,加上重定位项的RVA就得到需要重定位的数据的RVA DWORD SizeOfBlock; //当前"重定位块"的大小 // WORD TypeOffset[1]; //每个"重定位块"中有若干个"重定位项" } IMAGE_BASE_RELOCATION;

重定位类型 常见的重定位类型如下图,对于x86可执行文件来说,所有的需要重定位的数据的重定位类型都是IMAGE_REL_BASED_HIGHLOW。在一组重定位结束的地方会出现一个类型是IMAGE_REL_BASED_ABSOLUTE类型的重定位项,表示仅用于填充不需要重定位,以便下一个重定位块按4字节分界线对齐。对于IA-64可执行文件,重定位类型似乎总是IMAGE_REL_BASED_DIR64。

(4) 更改基址时,根据重定位表修正数据

在PE文件被编译时,有些地方的地址值是写死的,如果模块以默认基址被载入,那么这些地址值没问题,但如果模块被载入的位置与默认基址不同,则需要重新修正这些被写死的地址值。如下图,0x0040100E处代码的作用是将一个指针压入堆栈,指针0x00402000指向一个字符串,在这个例子中,当前模块的基址=0x00400000,所以指针指向的是在当前模块RVA=0x2000处的字符串;如果当前模块被载入的基址变为0x00500000,那么0x0040100E处的代码也要同步被修正,将指针值改为0x00502000。 PE文件中有很多这样的代码需要被修正,需要修正的位置坐标都被保持在重定位表中。 【每个需要修正的位置的RVA = 重定位块的VirtualAddress域 + 重定位块中重定位项的TypeOffset域的低12位】注意,64位PE文件中需要重定位的地方所存储的是一个8字节的地址值,32位PE文件中需要重定位的地方所存储的是一个4字节的地址值。手动修正重定位表的代码(以ImageBuffer为例,省略了需要被修正的位置的RVA的计算)//************************************ // Method: Tool_RepairRelocTable_ForImageBuffer // Description:根据新基址为ImageBase修复重定位表 // Parameter: DWORD newImageBase - 新基址 // Parameter: DWORD oldImageBase - 原基址 // Returns: bool - //************************************ bool PE::Tool_RepairRelocTable_ForImageBuffer(DWORD64 newImageBase, DWORD64 oldImageBase) { Log::INFO(HString::Format("开始修复重定位表,新基址=6llX", newImageBase)); //合法性检查 if (newImageBase == 0x0) { Log::Error("修复重定位表失败,因为新基址=0x0"); return false; } if (this->PE_Header.NT_Header.IMAGE_OPTIONAL_Header.IMAGE_DATA_Directory[5].Size == 0x0) { Log::Error("修复重定位表失败,因为重定位表不存在。(若是exe文件,则需编译为release版本才有重定位表)"); return false; } //1.修改PE头结构、修改ImageBuffer中的ImageBase字段 this->PE_Header.NT_Header.IMAGE_OPTIONAL_Header.ImageBase = newImageBase; if (this->IsPE64 == TRUE) { *(DWORD64*)(this->pImageBuffer + this->PE_Header.DOS_Header.MSDOS_Header.e_lfanew + 0x30) = newImageBase; //64位PE文件中ImageBase字段占8字节 } else { *(DWORD*)(this->pImageBuffer + this->PE_Header.DOS_Header.MSDOS_Header.e_lfanew + 0x34) = (DWORD)newImageBase; } //2.根据重定位表修正ImageBuffer中写死的地址值(指针值) for (int i = 0; i < this->DataTable.RelocationTable.重定位块数组.size(); i++) { for (int j = 0; j < this->DataTable.RelocationTable.重定位块数组[i].重定位项数组.size(); j++) { if (this->DataTable.RelocationTable.重定位块数组[i].重定位项数组[j].计_type == IMAGE_REL_BASED_DIR64 //64位PE文件中会出现 || this->DataTable.RelocationTable.重定位块数组[i].重定位项数组[j].计_type == IMAGE_REL_BASED_HIGHLOW) //32位PE文件中会出现 { if (this->IsPE64) { //64位PE文件需要重定位的地方所存储的是一个8字节的地址值 DWORD64 oldData = *(DWORD64*)(this->pImageBuffer + this->DataTable.RelocationTable.重定位块数组[i].重定位项数组[j].计_destRVA); DWORD64 offset = oldData - oldImageBase; DWORD64 newData = newImageBase + offset; *(DWORD64*)(this->pImageBuffer + this->DataTable.RelocationTable.重定位块数组[i].重定位项数组[j].计_destRVA) = newData;//写入需要被重定位的位置 } else { //32位PE文件需要重定位的地方所存储的是一个4字节的地址值 DWORD oldData = *(DWORD*)(this->pImageBuffer + this->DataTable.RelocationTable.重定位块数组[i].重定位项数组[j].计_destRVA); DWORD offset = oldData - oldImageBase; DWORD newData = newImageBase + offset; *(DWORD*)(this->pImageBuffer + this->DataTable.RelocationTable.重定位块数组[i].重定位项数组[j].计_destRVA) = newData;//写入需要被重定位的位置 } } else if (this->DataTable.RelocationTable.重定位块数组[i].重定位项数组[j].计_type == IMAGE_REL_BASED_ABSOLUTE) { //IMAGE_REL_BASED_ABSOLUTE类型的重定位项仅仅为了凑4字节对齐,没有含义。 } else { MessageBox(0, L"发现重定位项的类型有问题....", 0, 0); return FALSE; } } } // Log::SUCCESS("修复重定位表成功!"); return TRUE; } 验证修复重定位表的函数是否正确:读一个imageBuffer,修正其重定位表,还原至文件,最后验证此文件能否运行。

3.4 绑定导入表

待完成…

参考资料

PE结构详解 (64位和32位的差别) PE文件结构详解(一)基本概念 PE结构之重定位表
最新回复(0)