PE文件解析

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_magic2通常情况下,它的值为"MZ",代表MS-DOS的标记。
e_cblp2文件的最后一个扇区的字节数。通常情况下,这个字段的值会是0或者文件尺寸对512取模的余数。
e_cp2文件的扇区数。与e_cblp类似,通常情况下,这个字段的值是0或者以512字节为单位的文件大小。
e_crlc2重定位项的数量。在旧的MS-DOS可执行文件中,这个字段很少被使用。
e_cparhdr2文件头的大小(以段为单位)。对于标准的MS-DOS可执行文件,这个字段的值通常是4。
e_minalloc2程序加载时所需的最小附加段的数量。
e_maxalloc2程序加载时所需的最大附加段的数量。
e_ss2栈段(stack segment)在程序运行时的初始值。
e_sp2栈指针(stack pointer)在程序运行时的初始值。
e_csum2文件校验和。
e_ip2程序运行时的初始 IP(instruction pointer)。
e_cs2代码段(code segment)在程序运行时的初始值。
e_lfarlc2重定位项的地址。
e_ovno2扩展头部(extended header)的偏移量。
e_res8预留未使用。
e_oemid2制造商 ID。
e_oeminfo2制造商信息。
e_res210预留未使用。
e_lfanew4新的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位)含义
Signature4PE 文件的标识。通常情况下,它的值为 "PE\0\0"。
Machine2目标 CPU 架构的类型
NumberOfSections2文件中的节区(section)数量
TimeDateStamp4文件的创建时间和日期
PointerToSymbolTable4符号表的偏移量
NumberOfSymbols4符号表的条目数量
SizeOfOptionalHeader2可选头部的大小
Characteristics2可执行文件的属性
Magic2可选头部的格式。通常情况下,它的值可以用来区分32位/64位PE文件。
MajorLinkerVersion1链接器的主要版本号
MinorLinkerVersion1链接器的次要版本号
SizeOfCode4代码节区的大小
SizeOfInitializedData4已初始化的数据节区的大小
SizeOfUninitializedData4未初始化的数据节区的大小
AddressOfEntryPoint4程序入口点的 RVA(Relative Virtual Address)
SizeOfInitializedData4已初始化的数据节区的大小
BaseOfCode4代码节区的起始地址
BaseOfData4/0数据节区的起始地址
ImageBase4/8可执行文件的首选装载基址
SectionAlignment4节区对齐的内存页面大小
FileAlignment4文件对齐的内存页面大小
MajorOperatingSystemVersion2操作系统的主要版本号
MinorOperatingSystemVersion2操作系统的次要版本号
MajorImageVersion2可执行文件的主要版本号
MinorImageVersion2可执行文件的次要版本号
MajorSubsystemVersion2子系统的主要版本号
MinorSubsystemVersion2子系统的次要版本号
Win32VersionValue4预留字段
SizeOfImage4可执行文件在内存中的映像大小
SizeOfHeaders4可执行文件头部的总大小
CheckSum4文件的校验和
Subsystem2应用程序所需的子系统类型
DllCharacteristics2DLL 文件的特性
SizeOfStackReserve4/8堆栈的保留大小
SizeOfStackCommit4/8堆栈的提交大小
SizeOfHeapReserve4/8堆的保留大小
SizeOfHeapCommit4/8堆的提交大小
LoaderFlags4加载器的标志
NumberOfRvaAndSizes4数据目录的数量
DataDirectory128数据目录数组

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_TLSTLS(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_DESCRIPTORCom描述符表,包含与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;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值