Pe Structure On Disk

1.什么是PE文件

PE(Portable Executable)文件是Windows操作系统中广泛使用的可执行文件格式。
通常所说的PE文件是指32位可执行文件,也称为PE32。
而64位的可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式。

2.物理存储

PE文件由多个不同的区块组成,每个区块具有特定的功能和结构。
以下是PE文件的主要组成部分:

  1. DOS头部(DOS Header):这是一个固定大小的结构,用于兼容早期的MS-DOS系统。
  2. PE头部(PE Header):这是PE文件的主要头部信息,在文件的开头位置。PE头部包含了关于文件结构、区块布局和可执行代码的详细信息。
  3. 节表(Section Table):节表列出了文件中的各个节(Sections),每个节对应一块数据或代码。
  4. 节数据(Section Data):这是 PE文件中实际的数据和代码。每个节的内容可以包括可执行代码、只读数据、初始化数据、未初始化数据等。

注意:以上物理结构在磁盘上并非连续存储,因此必须通过一定的计算才能正确解析PE文件。

2.1 DOS头

_IMAGE_DOS_HEADER 是PE文件格式中的一个结构体,用于兼容早期的MS-DOS系统。它存在于PE文件的开头位置,作为DOS头部(DOS Header)的一部分。

名称 长度(32/64位) 含义
e_magic 2 通常情况下,它的值为"MZ",代表MS-DOS的标记。
e_cblp 2 文件的最后一个扇区的字节数。通常情况下,这个字段的值会是0或者文件尺寸对512取模的余数。
e_cp 2 文件的扇区数。与e_cblp类似,通常情况下,这个字段的值是0或者以512字节为单位的文件大小。
e_crlc 2 重定位项的数量。在旧的MS-DOS可执行文件中,这个字段很少被使用。
e_cparhdr 2 文件头的大小(以段为单位)。对于标准的MS-DOS可执行文件,这个字段的值通常是4。
e_minalloc 2 程序加载时所需的最小附加段的数量。
e_maxalloc 2 程序加载时所需的最大附加段的数量。
e_ss 2 栈段(stack segment)在程序运行时的初始值。
e_sp 2 栈指针(stack pointer)在程序运行时的初始值。
e_csum 2 文件校验和。
e_ip 2 程序运行时的初始 IP(instruction pointer)。
e_cs 2 代码段(code segment)在程序运行时的初始值。
e_lfarlc 2 重定位项的地址。
e_ovno 2 扩展头部(extended header)的偏移量。
e_res 8 预留未使用。
e_oemid 2 制造商 ID。
e_oeminfo 2 制造商信息。
e_res2 10 预留未使用。
e_lfanew 4 新的PE头部(PE Header)的偏移量。
_IMAGE_DOS_HEADER结构体定义了PE文件的DOS头部的格式和数据布局,为了向后兼容早期的MS-DOS系统。在现代的Windows操作系统中,一般并不直接使用DOS头部,而是通过**e_lfanew**字段来跳转到新的PE头部,进而解析和执行PE文件的内容。

2.2 NT头

_IMAGE_NT_HEADERS 是用于描述Windows可执行文件(PE 文件)格式的结构体。
它包含了PE文件的头部信息、可选头部(Optional Header)以及数据目录(Data Directory)的相关信息。

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
名称 长度(32/64位) 含义
Signature 4 PE 文件的标识。通常情况下,它的值为 "PE\0\0"。
Machine 2 目标 CPU 架构的类型
NumberOfSections 2 文件中的节区(section)数量
TimeDateStamp 4 文件的创建时间和日期
PointerToSymbolTable 4 符号表的偏移量
NumberOfSymbols 4 符号表的条目数量
SizeOfOptionalHeader 2 可选头部的大小
Characteristics 2 可执行文件的属性
Magic 2 可选头部的格式。通常情况下,它的值可以用来区分32位/64位PE文件。
MajorLinkerVersion 1 链接器的主要版本号
MinorLinkerVersion 1 链接器的次要版本号
SizeOfCode 4 代码节区的大小
SizeOfInitializedData 4 已初始化的数据节区的大小
SizeOfUninitializedData 4 未初始化的数据节区的大小
AddressOfEntryPoint 4 程序入口点的 RVA(Relative Virtual Address)
SizeOfInitializedData 4 已初始化的数据节区的大小
BaseOfCode 4 代码节区的起始地址
BaseOfData 4/0 数据节区的起始地址
ImageBase 4/8 可执行文件的首选装载基址
SectionAlignment 4 节区对齐的内存页面大小
FileAlignment 4 文件对齐的内存页面大小
MajorOperatingSystemVersion 2 操作系统的主要版本号
MinorOperatingSystemVersion 2 操作系统的次要版本号
MajorImageVersion 2 可执行文件的主要版本号
MinorImageVersion 2 可执行文件的次要版本号
MajorSubsystemVersion 2 子系统的主要版本号
MinorSubsystemVersion 2 子系统的次要版本号
Win32VersionValue 4 预留字段
SizeOfImage 4 可执行文件在内存中的映像大小
SizeOfHeaders 4 可执行文件头部的总大小
CheckSum 4 文件的校验和
Subsystem 2 应用程序所需的子系统类型
DllCharacteristics 2 DLL 文件的特性
SizeOfStackReserve 4/8 堆栈的保留大小
SizeOfStackCommit 4/8 堆栈的提交大小
SizeOfHeapReserve 4/8 堆的保留大小
SizeOfHeapCommit 4/8 堆的提交大小
LoaderFlags 4 加载器的标志
NumberOfRvaAndSizes 4 数据目录的数量
DataDirectory 128 数据目录数组

2.3 数据目录

含义
IMAGE_DIRECTORY_ENTRY_EXPORT 导出表,包含函数和符号的导出信息
IMAGE_DIRECTORY_ENTRY_IMPORT 导入表,包含从其他模块导入的函数和符号
IMAGE_DIRECTORY_ENTRY_RESOURCE 资源表,包含从其他模块导入的函数和符号
IMAGE_DIRECTORY_ENTRY_RESOURCE 异常处理表,包含用于异常处理的函数指针
IMAGE_DIRECTORY_ENTRY_SECURITY 安全表,包含数字签名或其他安全相关信息
IMAGE_DIRECTORY_ENTRY_SECURITY 基址重定位表,包含用于重定位可执行文件的地址信息
IMAGE_DIRECTORY_ENTRY_DEBUG 调试信息表,包含调试器使用的调试信息
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 体系结构相关数据,适用于特定的体系结构
IMAGE_DIRECTORY_ENTRY_GLOBALPTR 全局指针,用于在实模式下访问全局变量
IMAGE_DIRECTORY_ENTRY_TLS TLS(Thread Local Storage) 数据,包含线程本地存储信息
IMAGE_DIRECTORY_ENTRY_TLS 加载配置表,包含可执行文件的加载配置信息
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 绑定导入表,描述绑定到特定模块的延迟加载的函数
IMAGE_DIRECTORY_ENTRY_IAT 导入地址表,包含从其他模块导入的函数和符号的实际地址
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 延迟导入表,包含从其他模块延迟导入的函数和符号
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR Com描述符表,包含与COM组件相关的信息
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;  // 表的相对虚拟地址(**RVA**)
    DWORD   Size;            // 表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

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;

引申:
设VA为PE文件的绝对地址,RVA地址是相对于模块基址的偏移地址,
那么从RVA计算绝对地址FOA的方式,

  1. 遍历节表,定位当前RVA地址所在的节
  2. 计算RVA相对节的偏移量 1 = RVA - 目标节的起始虚拟地址(Virtual Address)
  3. 计算RVA相对文件的偏移量 = 偏移量 1 + 目标节的文件偏移地址(Pointer to Raw Data)

3.导入表解析示例

int RVATOFOA(PIMAGE_FILE_HEADER fileHeader, DWORD rva) {
  PIMAGE_SECTION_HEADER sectionHeader =
      (PIMAGE_SECTION_HEADER)((DWORD_PTR)fileHeader + IMAGE_SIZEOF_FILE_HEADER +
                              fileHeader->SizeOfOptionalHeader);
  if (rva < sectionHeader->VirtualAddress) {
    return rva;
  }

  for (int i = 0; i < fileHeader->NumberOfSections; i++) {
    // 判断是否处在当前节表
    if (rva >= sectionHeader[i].VirtualAddress &&
        rva <=
            sectionHeader[i].VirtualAddress + sectionHeader[i].SizeOfRawData) {
      return sectionHeader[i].PointerToRawData + rva -
             sectionHeader[i].VirtualAddress;
    }
  }

  return rva;
}

int LoadDependdncy(LPCWSTR filepath) {
  HANDLE hFile = CreateFile(filepath, GENERIC_READ, FILE_SHARE_READ, NULL,
                            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    return 1;
  }

  DWORD dwFileSize = GetFileSize(hFile, NULL);
  HANDLE hMapping =
      CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL);
  if (hMapping == INVALID_HANDLE_VALUE) {
    return 1;
  }

  LPVOID lpBaseAddress =
      MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, dwFileSize);
  if (dwFileSize == NULL) {
    return 1;
  }

  PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
  // 检查DOS头
  if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
    CloseHandle(hFile);
    return 1;
  }

  PIMAGE_NT_HEADERS ntHeader =
      (PIMAGE_NT_HEADERS)((DWORD_PTR)lpBaseAddress + dosHeader->e_lfanew);
  // 检查NT头
  if (ntHeader->Signature != IMAGE_NT_SIGNATURE) {
    CloseHandle(hFile);
    return 1;
  }

  DWORD importTableRva = 0;
  // 根据PE类型获取导入表RVA
  if (ntHeader->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
    importTableRva =
        ((PIMAGE_NT_HEADERS32)(ntHeader))
            ->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
            .VirtualAddress;
  } else {
    importTableRva =
        ((PIMAGE_NT_HEADERS64)(ntHeader))
            ->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
            .VirtualAddress;
  }

  // 获取导入表描述符
  PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor =
      (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)lpBaseAddress +
                                 RVATOFOA(&ntHeader->FileHeader,
                                          importTableRva));
  // 遍历导入表
  while(pImportDescriptor->Name) {
    // 获取模块名
    char* moduleName = (char*)lpBaseAddress +
                       RVATOFOA(&ntHeader->FileHeader, pImportDescriptor->Name);
    printf("module name = %s\n", moduleName);

    // 获取导入函数
    IMAGE_THUNK_DATA* pImportThunk =
        (IMAGE_THUNK_DATA*)((char*)lpBaseAddress +
                            RVATOFOA(&ntHeader->FileHeader,
                                     pImportDescriptor->FirstThunk));
    // 遍历导入函数
    while (pImportThunk->u1.AddressOfData != 0) {
      // 判断是否为序号导入
      if (IMAGE_SNAP_BY_ORDINAL(pImportThunk->u1.Ordinal)) {
        // 对于序号导入,低16位存储了序号值
        WORD ordinal = static_cast<WORD>(pImportThunk->u1.Ordinal & 0xFFFF);
        printf("ordinal = %d\n", ordinal);
      } else {
        // 对于名称导入,低31位存储了名称表RVA
        IMAGE_IMPORT_BY_NAME* pImportByName =
            (IMAGE_IMPORT_BY_NAME*)((char*)lpBaseAddress +
                                    RVATOFOA(&ntHeader->FileHeader,
                                             pImportThunk->u1.AddressOfData));
        printf("function %d = %s\n", pImportByName->Hint, pImportByName->Name);
      }

      // 移动到下一个导入函数
      pImportThunk++;
    }

    // 移动到下一个导入描述符
    pImportDescriptor++;
  };

  UnmapViewOfFile(lpBaseAddress);
  CloseHandle(hMapping);
  CloseHandle(hFile);
  return 0;
}

你可能感兴趣的:(杂记,windows)