0x08 PE文件
0. 基本概念
- PE文件
- 是一种文件格式,目前所学的 exe,dll,sys等都是PE文件
- 通过检查DOS头的“MZ”(0x5A4D)和NT头的“PE”(0x00004550)来判断一个文件是不是有效的PE文件
- 术语解释
- RVA: 数据在虚拟内存中的偏移地址
- VA: 数据在虚拟内存中的绝对地址
- VA =实际加载基址+ RVA
- FOA: 数据在文件中的偏移地址
- FOA =需要转换的RVA -所在的区段RVA +所在区段的FOA
1. PE 头信息
- IMAGE_DOS_HEADER
- e_magic: “MZ” - 0x5A4D
- e_lfanew:指向NT头的偏移
- IMAGE_NT_HEADERS
- signature: “PE” - 0x00004550
- IMAGE_FILE_HEADER
- NumberOfSections:区段的数量
- SizeOfOptionalHeader: 扩展头的大小,会被用于查找区段表
- Characteristics:可以判断当前是不是DLL,是不是32位,重定位已分离(表示不需要重定位)
- IMAGE_OPTIONAL_HEADER32
- Magic:CPU架构:x32(0x10B)x64(0x20B)
- AddressOfEntryPoint:程序的入口点(OEP)RVA
- ImageBase:镜像的默认加载基址
- SectionAlignment :内存对齐粒度,通常是0x1000,要求大于文件粒度
- FileAlignment:文件对齐粒度,通常是0x200
- SizeOfImage:镜像大小,即内存中的大小,最后一个区段的RVA +最后一个区段的大小
- SizeOfHeaders:整个PE头部大小,通常是 0x400
- DllCharacteristics :特征标识0x40 有随机基址
- NumberOfRvaAndSizes:数据目录表个数 0x10
- DataDirectory:数据目录表
- IMAGE_SECTION_HEADER(重要)
- Name[8]:区段名
- virtualSize:内存中大小
- VirtualAddress:内存位置RVA
- SizeOfRawData:文件中大小
- PointerToRawData:文件位置FOA
- Characteristics:区段属性(可读可写可执行),是否有代码
2. 导入表
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // 模块名称
DWORD FirstThunk; // IAT
} IMAGE_IMPORT_DESCRIPTOR;
- 导入表中保存的是一组 以全0为结尾 的 IMAGE_IMPORT_DESCRIPTOR 结构。
- INT:导入名称表,指向了一组以0结尾的 IMAGE_THUNK_DATA 结构,可以找到函数的ID或名称,但是有些时候其中会被填充为0,在文件和内存中它们保存的值是相同的。
- 当 IMAGE_THUNK_DATA 字段的最高位为1,表示这是一个序号导入的函数
- 序号的值为 0x0000???? & 0x0000FFFF = 0x0000????
- 最高位为0,表示这是一个名称导入的函数,
- 名称和序号保存在 0x???????? 的位置 IMAGE_IMPORT_BY_NAME
- 当 IMAGE_THUNK_DATA 字段的最高位为1,表示这是一个序号导入的函数
- INT:导入名称表,指向了一组以0结尾的 IMAGE_THUNK_DATA 结构,可以找到函数的ID或名称,但是有些时候其中会被填充为0,在文件和内存中它们保存的值是相同的。
- IAT:导入地址表,在文件中存放的是INT的数据,当程序被加载到内存时,会被填充为函数的真实地址。
3. 导出表
typedef struct _IMAGE_EXPORT_DIRECTORY
{
// 省略了无意义的字段
DWORD Name; // 模块名RVA
DWORD Base; // 序号的起始数值
DWORD NumberOfFunctions; // 函数的数量
DWORD NumberOfNames; // 有名字的函数的数量
DWORD AddressOfFunctions; // 导出地址表的的RVA 【DWORD】
DWORD AddressOfNames; // 导出名称表的RVA【DWORD-RVA】
DWORD AddressOfNameOrdinals; // 导出序号表的RVA【WORD】
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
- 导出表只能有一个
- 名称表的数量和序号表的数量相同
- 导出函数的序号 = 基址(Base) + 索引(函数地址表下标)
4. 重定位表
typedef struct _IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
- 重定位表由多组重定位块组成,每个重定位块保存的是一个分页(RVA)中需要重定位的所有数据。
- 重定位项的个数 = (SizeOfBlock– sizeof(IMAGE_BASE_RELOCATION)) / 2
- 每一个重定位项都是一个WORD类型的值,标识了当前数据的类型以及偏移(相对VirtualAddress)
- 需要重定位的数据所在的位置 = 实际加载基址 + VirtualAddress + 重定位数据的偏移,其中保存的是一个偏移
- 重定位公式:需要重定位的地址 - 默认加载基址 + 实际加载基址
5. 资源表
- 三层结构
- 一层:资源种类;
- 二层:此种资源的个数,名称;
- 三层:资源数据
- 每一层都是 IMAGE_RESOURCE_DIRECTORY 开始,后面跟着一组IMAGE_RESOURCE_DIRECTORY_ENTRY结构体,保存的是资源的类型和名称
- 结构体个数 = NumberOfNamedEntries + NumberOfIdentries
- 第一层&第二层:
- 资源的类型名称,当 NameIsString 为1时,标识这是一个自定义的名称,使用 NameOffset
- 字段可以找到 IMAGE_RESOURCE_DIR_STRING_U 字段,其中保存了字符串名称。
- 第三层:
- 指向 IMAGE_RESOURCE_DATA_ENTRY,保存了数据的偏移和大小
6.TLS表
TLS表IMAGE_TLS_DIRECTORY
- 使用 __declspec(thread) int nNum = 99; 定义TLS变量
TLS 函数的原型如下,和DllMain相比,它们只有返回值类型不同
Void NTAPI t_TlsCallBackA(PVOID DllHandle,DWORD Reason,PVOID Red);
TLS 函数的调用顺序位于main函数前,通常用于进行反调试