文章目录
一、名词解析二、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
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"中给出了结构体的定义:
typedef struct _IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress
;
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的定义:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name
[IMAGE_SIZEOF_SHORT_NAME
];
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
, *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)
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
;
通过导出表数据结构中给出的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的元素,元素值就是所求的导出函数地址。
DWORD PE
::Tool_GetFuncRVABy导出函数名称
(char* FuncNameStr
)
{
for (int i
= 0; i
< this->DataTable
.ExportTable
.IMAGE_EXPORT_Directory
.NumberOfNames
; i
++)
{
if (0 == strcmp(FuncNameStr
, this->DataTable
.ExportTable
.函数名称数组
[i
].FunctionName_Str
))
{
WORD value
= this->DataTable
.ExportTable
.函数名称序号数组
[i
];
return this->DataTable
.ExportTable
.函数地址数组
[value
];
}
}
return 0;
}
已知DLL中的一个导出函数的“导出序号”,求此导出函数在DLL中的函数地址。 (1)用导出序号减去Base值,得到所求函数在“函数地址数组”中的索引下标,记为index。 (2)找到“函数地址数组”中索引为index的元素,元素值就是所求的导出函数地址。
DWORD PE
::Tool_GetFuncRVABy导出序号
(int 导出序号
)
{
int index
= 导出序号
- this->DataTable
.ExportTable
.IMAGE_EXPORT_Directory
.Base
;
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
;
} DUMMYUNIONNAME
;
DWORD TimeDateStamp
;
DWORD ForwarderChain
;
DWORD Name
;
DWORD FirstThunk
;
} 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
{
WORD Hint
;
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数组的流程图: 代码
bool PE
::RepairIAT_ForImageBuffer() {
Log
::INFO(HString
::Format("开始修复IAT表..."));
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
);
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
].导入函数的导出序号
);
}
else {
pFunc
= dllPE
.MyGetProcAddressA(hModule
, this->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
;
DWORD SizeOfBlock
;
} 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的计算)
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; }
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
;
}
else {
*(DWORD
*)(this->pImageBuffer
+ this->PE_Header
.DOS_Header
.MSDOS_Header
.e_lfanew
+ 0x34) = (DWORD
)newImageBase
;
}
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
|| this->DataTable
.RelocationTable
.重定位块数组
[i
].重定位项数组
[j
].计_type
== IMAGE_REL_BASED_HIGHLOW
)
{
if (this->IsPE64
) {
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 {
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
) {
}
else {
MessageBox(0, L
"发现重定位项的类型有问题....", 0, 0); return FALSE
;
}
}
}
Log
::SUCCESS("修复重定位表成功!");
return TRUE
;
}
验证修复重定位表的函数是否正确:读一个imageBuffer,修正其重定位表,还原至文件,最后验证此文件能否运行。
3.4 绑定导入表
待完成…
参考资料
PE结构详解 (64位和32位的差别) PE文件结构详解(一)基本概念 PE结构之重定位表