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. 导入表


  1. typedef struct _IMAGE_IMPORT_DESCRIPTOR {
  2. union {
  3. DWORD Characteristics;
  4. DWORD OriginalFirstThunk; // INT
  5. } DUMMYUNIONNAME;
  6. DWORD TimeDateStamp;
  7. DWORD ForwarderChain;
  8. DWORD Name; // 模块名称
  9. DWORD FirstThunk; // IAT
  10. } 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
  • IAT:导入地址表,在文件中存放的是INT的数据,当程序被加载到内存时,会被填充为函数的真实地址。image.png

3. 导出表


  1. typedef struct _IMAGE_EXPORT_DIRECTORY
  2. {
  3. // 省略了无意义的字段
  4. DWORD Name; // 模块名RVA
  5. DWORD Base; // 序号的起始数值
  6. DWORD NumberOfFunctions; // 函数的数量
  7. DWORD NumberOfNames; // 有名字的函数的数量
  8. DWORD AddressOfFunctions; // 导出地址表的的RVA 【DWORD】
  9. DWORD AddressOfNames; // 导出名称表的RVA【DWORD-RVA】
  10. DWORD AddressOfNameOrdinals; // 导出序号表的RVA【WORD】
  11. } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • 导出表只能有一个
  • 名称表的数量和序号表的数量相同
  • 导出函数的序号 = 基址(Base) + 索引(函数地址表下标)image.png

4. 重定位表


  1. typedef struct _IMAGE_BASE_RELOCATION
  2. {
  3. DWORD VirtualAddress;
  4. DWORD SizeOfBlock;
  5. // WORD TypeOffset[1];
  6. } 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相比,它们只有返回值类型不同

      1. Void NTAPI t_TlsCallBackA(PVOID DllHandle,DWORD Reason,PVOID Red);
    • TLS 函数的调用顺序位于main函数前,通常用于进行反调试