接下来的内容是对IMAGE_OPTIONAL_HEADER32中的最后一个成员DataDirectory,虽然他只是一个结构体数组,每个结构体的大小也不过是个字节,但是它却是PE文件中最重要的成员。PE装载器通过查看它才能准确的找到某个函数或某个资源。
一:IMAGE_DATA_DIRECTORY------数据目录结构
cs
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; /**指向某个数据的相对虚拟地址 RAV 偏移0x00**/
DWORD Size; /**某个数据块的大小 偏移0x04**/
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
此数据目录表结构中有俩个成员VirtualAddress和Size,这俩成员的含义比较简单,前者指定了数据块的相对虚拟地址(RVA)Size则指定了该数据块的大小,有时并不是该类型数据的总大小,可能只是该数据类型一个数据项的大小。这俩成员成为定位各种表的关键,所以一定要了解知道每个数组元素所指向的数据类型,请看下表:
cs
//定位目录项的方法(以导出表为例): 所有操作都在FileBuffer状态下完成
//1、指向相关内容
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);
PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));
//2、获取导出表的地址(目录项的第0个成员)
DWORD ExportDirectory_RAVAdd = pOptionalHeader->DataDirectory[0].VirtualAddress;
DWORD ExportDirectory_FOAAdd = 0;
// (1)、判断导出表是否存在
if (ExportDirectory_RAVAdd == 0)
{
printf("ExportDirectory 不存在!\n");
return ret;
}
// (2)、获取导出表的FOA地址 转换函数看上一章作业提示
ret = RVA_TO_FOA(FileAddress, ExportDirectory_RAVAdd, &ExportDirectory_FOAAdd);
if (ret != 0)
{
printf("func RVA_TO_FOA() Error!\n");
return ret;
}
//3、指向导出表
PIMAGE_EXPORT_DIRECTORY ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)FileAddress + ExportDirectory_FOAAdd);
二:IMAGE_EXPORT_DIRECTORY------导出表
cs
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 未使用,总为0
DWORD TimeDateStamp; // 文件创建时间戳
WORD MajorVersion; // 未使用,总为0
WORD MinorVersion; // 未使用,总为0
DWORD Name; // **重要 指向一个代表此 DLL名字的 ASCII字符串的 RVA
DWORD Base; // **重要 函数的起始序号
DWORD NumberOfFunctions; // **重要 导出函数地址表的个数
DWORD NumberOfNames; // **重要 以函数名字导出的函数个数
DWORD AddressOfFunctions; // **重要 导出函数地址表RVA
DWORD AddressOfNames; // **重要 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // **重要 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
导出表由前面的目录表找到相对应的位置,其中导出表是什么?
导出表包含以下主要信息:
-
**导出函数的名称和地址: 列出了该可执行文件中导出的函数或符号的名称和相应的内存地址。
-
**导出函数的序号:每个导出函数分配的唯一序号,方便通过序号进行引用。
-
**导出函数的起始地址:指定导出函数的实际执行代码在内存中的起始地址。
-
**导出函数的外部名称:如果函数具有外部别名,该别名也会在导出表中记录。
导出表在动态链接库(DLL)中尤为重要,因为其他程序或模块可以通过导出表来动态链接到DLL中的函数,实现模块间的交互和共享代码。对于PE文件的分析和调试,导出表是一个关键的信息来源,可用于理解文件的功能和与其他模块的交互关系。
导出表我们需要注意标注的地方
AddressOfFunctions
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的地址表,这个地址表可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfFunctions进行限定,地址表中的成员也是一个RVA地址,在内存中加上ImageBase后才是函数真正的地址。
AddressOfNames
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的名称表,这个名称表也可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员也是一个RVA地址,在FIleBuffer状态下需要进行RVA到FOA的转换才能真正找到函数名称。
AddressOfNameOrdinals
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的序号表,这个序号表可以当作一个成员宽度为2的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员是一个函数序号,该序号用于通过名称获取函数地址。
NumberOfFunctions
注意,这个值并不是真的函数数量,他是通过函数序号表中最大的序号减去最小的序号再加上一得到的,例如:一共导出了3个函数,序号分别是:0、2、4,NumberOfFunctions = 4 - 0 + 1 = 5个。
导出表结构图:
通过导出表查找函数地址的两种方法:
1、通过函数名查找函数地址:
(1)、首先定位函数名表,然后通过函数名表中的RVA地址定位函数名,通过比对函数名获取目标函数名的在函数名表中的索引。
(2)、通过获取函数名表的索引获取函数序号表中对应索引中的函数序号。
(3)、通过把该序号当作函数地址表的下标,就可以得到该下标中的函数地址。
2、通过函数序号查找函数地址:
(1)首先计算出函数地址表的索引:index = 目标函数的函数序号 - 导出表的基地址(Base).
(2) 通过计算出的索引就可以在函数地址表中获取到目标序号的函数地址
注意:相比于通过函数名字,通过序号获取函数地址不需要使用函数名称表和函数序号表就可以直接获取函数地址,实现上相对来说比较方便。
三:IMAGE_BASE_RELOCATION------重定位表
cs
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; 重定位数据所在页的RVA
DWORD SizeOfBlock; 当前页中重定位数据块的大小
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
重定位表简介:正如我们知道的,在程序运行时系统首先会给程序分配一个4gb的虚拟内存空间,低2g空间用于存放EXE文件和DLL文件,高2g空间则是用于取得程序使用。系统随后就会将EXE文件第一个贴入低2g空间占据文件指定的imageBase,所以EXE文件有时会木有重定位表。贴完EXE文件后接下来就会将大量程序使用的DLL文件贴入虚拟空间,然后这些dll文件和imagebase可能发生冲突,所以有些dll文件不能贴入指定的地址,但是为了让程序正常运行,需要重新给它分配一个地址,由此产生重定位表。
重定位表就是记录这些需要修正的地址,在imagebase发生改变时就会就行修正重定位表
修正方法:
需要重定位的地址 - 以前的基址 + 当前的基址
VirtualAddress
这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址。
SizeOfBlock
它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
重定位项
重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节,每一个重定位项分为两个部分:高4位和低12位。高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址),低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。
注:如果修改了EXE文件的ImageBase,就要手动修复它的重定位表,因为系统会判断程序载入地址和ImageBase是否一致,如果一致就不会自动修复重定位表,双击运行时就会报错。
重定位表结构:
通过重定位表找到需要修正的数据:
四:IMAGE_IMPORT_DESCRIPTOR------导入表
cs
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //导入名称表(INT)的RVA地址
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间戳多数情况可忽略 如果是0xFFFFFFFF表示IAT表被绑定为函数地址
DWORD ForwarderChain;
DWORD Name; //导入DLL文件名的RVA地址
DWORD FirstThunk; //导入地址表(IAT)的RVA地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
导入表简介:PE文件使用来自于其他DLL的代码或数据是,称作导入(或者输入)。当PE文件装入时,Windows装载器的工作之一就是定位所有被输入的函数和数据,并且让正在被装入的问渐渐可以使用这些地址。这个过程就是通过PE文件的导入表来完成的,导入表中保存的是函数名和其驻留的DLL名等动态链接所需的信息。
OriginalFirstThunk
这个值是一个4字节的RVA地址,这个地址指向了导入名称表(INT),INT是一个IMAGE_THUNK_DATA结构体数组,这个结构体的最后一个成员内容为0时数组结束。这个数组的每一个成员又指向了一个IMAGE_IMPORT_BY_NAME结构体,这个结构体包含了两个成员函数序号和函数名,不过这个序号一般没什么用,所以有的编译器会把函数序号置0。函数名可以当作一个以0结尾的字符串。(注:这个表不在目录项中。)
Name
DLL名字的指针,是一个RVA地址,指向了一个以0结尾的ASCII字符串。
FirstThunk
这个值是一个4字节的RVA地址,这个地址指向了导入地址表(IAT),这个IAT和INT一样,也是一个IMAGE_THUNK_DATA结构体数组,不过它在程序载入前和载入后由两种状态,在程序载入前它的结构和内容和INT表完全一样,但却是两个不同的表,指向了IMAGE_IMPORT_BY_NAME结构体。在程序载入后,他的结构和INT表一样,但内容就不一样了,里面存放的都是导入函数的地址。(注:这个表在目录项中,需要注意。)
EXE文件载入后对应的导入表结构图:
cs
IMAGE_THUNK_DATA------INT、IAT的结构体定义如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
//注:这个结构体是联合类型的,每一个成员都是4字节,所以为了编程方便,完全可以用一个4字节的数组取代它。
IMAGE_IMPORT_BY_NAME 结构体定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
//注:这个结构体由两个成员组成,大致一看它的大小是3个字节,其实它的大小是不固定的,
// 因为无法判断函数名的长度,所以最后一个成员是一个以0结尾的字符串。
EXE文件载入前对应的导入表结构图:
五:IMAGE_BOUND_IMPORT_DESCRIPTOR------绑定导入表
cs
IMAGE_BOUND_IMPORT_DESCRIPTOR的结构体定义如下:
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址偏移
WORD NumberOfModuleForwarderRefs; //该结构后IMAGE_BOUND_FORWARDER_REF数组的数量
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
绑定导入表简介:绑定导入是一个文件快速启动的技术,但是只能起到辅助的效果,它的存在只会影响到PE文件的加载过程,并不会影响PE文件的运行结果,这也就是说把绑定导入的信息从PE文件中清除后对这个PE文件的运行结果没有任何影响。从导入表部分我们可以知道,FirstThunk这个成员指向了IAT表,在程序加载时加载器会通过INT表来修复IAT表,使里面存放上对应函数的地址信息,但是如果导入的函数太多在加载过程中就会使程序启动变慢,绑定导入就是为了减少IAT表的修复时间。它会在程序加载前修复IAT表,然后在PE文件中声明绑定导入的数据信息,让操作系统知道这些事情已经提前完成。这就是绑定导入表的作用。
TimeDateStamp
这个时间戳相对来说还是比较重要的,因为这个值只有和导入DLL的IMAGE_FILE_HEADER中的TimeDateStamp值相同才能起到绑定导入的效果,如果不一致加载器就会重新计算IAT表中的函数地址。(由于DLL文件的版本不同或者DLL文件的ImageBase被重定位时,IAT绑定的函数的地址就会发生变化)
OffsetModuleName
这个偏移不是RVA页不是FOA,所以模块名的定位与之前的方法不同,它的定位方式是以第一个IMAGE_BOUND_IMPORT_DESCRIPTOR的地址为基址,加上OffsetModuleName的值就是模块名所在的地址了,这个模块名是以0结尾的ASCII字符串。
NumberOfModuleForwarderRefs
这个值是在IMAGE_BOUND_IMPORT_DESCRIPTOR结构后跟随的IMAGE_BOUND_FORWARDER_REF结构的数量。在每一个IMAGE_BOUND_IMPORT_DESCRIPTOR结构后都会跟随着大于等于0个IMAGE_BOUND_FORWARDER_REF结构,然后在其后面又会跟上绑定表结构体,直至全部用0填充的绑定表结构。
cs
IMAGE_BOUND_IMPORT_DESCRIPTOR的结构体定义如下:
typedef struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址偏移
WORD Reserved; //保留
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
//注:
// 该结构中的成员和绑定导入表的成员含含义一致,所以不再过多叙述。
// 由于IMAGE_BOUND_IMPORT_DESCRIPTOR和IMAGE_BOUND_FORWARDER_REF的大小结构相同,所以可以相互转型,方便编程。