menu Alkaid #二进制初学者 / 网络安全 / 大龄CTF退役选手
重温PE文件格式
43 浏览 | 2020-10-16 | 分类:心路历程,Reverse,C语言 | 标签:

PE文件格式

最近研究写壳,复习下PE文件结构,忘得差不多了(到处收集师傅们的资料)。

0x1 PE文件简介

PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。

认识PE文件不是作为单一内存映射文件被装入内存是很重要的。Windows加载器(又称PE加载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。PE文件的结构在磁盘和内存中是基本一样的,但在装入内存中时又不是完全复制。Windows加载器会决定加载哪些部分,哪些部分不需要加载。而且由于磁盘对齐与内存对齐的不一致,加载到内存的PE文件与磁盘上的PE文件各个部分的分布都会有差异。

PE文件至少包含两个段,即数据段和代码段。Windows NT 的应用程序有9个预定义的段,分别为 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的,当然,也可以根据需要定义更多的段(比如一些加壳程序)。

在应用程序中最常出现的段有以下6种:

名称命名
代码段.text
数据段.data 、.rdata 或 .bss
导出表.edata
导入表.idata
调试信息段.debug
资源段.rsrc

0x2 PE文件中的重要概念

如上图所示,当一个PE文件被加载到内存中以后,我们称之为“映象”(image),一般来说,PE文件在硬盘上和在内存里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”。下面要介绍几个重要概念,分别是基地址(ImageBase),虚拟内存地址(Virtual Address),相对虚拟地址(Relative Virtual Address),文件偏移地址(File Offset)。

基地址 ImageBase

定义:当PE文件通过Windows加载器被装入内存后,内存中的版本被称作模块(Module)。映射文件的起始地址被称作模块句柄(hMoudule),可以通过模块句柄访问其他的数据结构。这个初始内存弟子就是基地址。

内存中的模块代表着进程从这个可执行文件中所需要的代码,数据,资源,输入表,输出表以及其他有用的数据结构所使用的内存都放在一个连续的内存块中,编程人员只要知道装载程序文件映像到内存的基地址即可。在32位系统中可以直接调用GetModuleHandle以取得指向DLL的指针,通过指针访问DLL module的内容,例如:

HMODULE GetmoduleHandle(LPCTSRT lpModuleName);

当调用该函数时,传递一个可执行文件或者DLL文件名字字符串。如果系统找到该文件,则返回该可执行文件的或者DLL文件映像加载到的基地址。也可以调用GetModuleHandle,传递NULL参数,则返回调用的可执行文件的基地址。

虚拟内存地址 VA

虚拟内存地址(Virtual Address)PE 文件被操作系统加载进内存后的地址。也许有人要问,既然有VA这么简单的表示方式为什么还要有RVA呢?因为虽然PE文件为自己指定加载的基地址,但是windows有茫茫多的DLL,而且每个软件也有自己的DLL,如果指定的地址已经被别的DLL占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在PE文件头中大部分是使用RVA来表示地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后VA都失效了,那存在于代码中的那些VA怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定位表的文章中会详细描述。既然有重定位,为什么NT头不能依靠重定位采用VA表示地址呢(十万个为什么)?因为不是所有的PE都有重定位,早期的EXE就是没有重定位的。

相对虚拟地址RVA

在可执行文件中,有相当多的地方需要指定内存的地址。例如:引用全局变量时,需要指定它的地址。PE文件尽管有一个首选的载入地址(基地址),但是他们可以载入到进程空间的任意地方,所以不能依赖与PE的载入点。由于这个原因,必须有一个方法来指定一个地址而不是依赖于PE载入点。

为了在PE文件中避免有确定的内存地址,出现了相对虚拟地址(Relative Virtual Addres,简称RVA)的概念。RVA只是内存中的一个简单的相对于PE文件装入地址的偏移地址,它是一个“相对”地址,或者称位“偏移量”地址。例如:假设一个EXE文件从地址40000h处载入,并且它的代码区块开始于4010000h,代码区的RVA将是:

目标地址401000h ——转入地址400000h则RVA=1000h。

将RVA地址转换成真实地址,只需简单的翻转这个过程:将实际装入地址加上RVA即可得到实际的内存地址。顺便一提,在PE用语里,实际的内存地址被称作虚拟地址(Vritual Address,简称VA),另外也可以把虚拟地址想象为加上首选装入地址的RVA。不要忘了前面提到的装入地址等同于模块句柄,它们之间的关系如下:

虚拟地址(VA)=基地址(ImageBase)+相对虚拟地址(RVA)

文件偏移地址FOA

当PE文件存储在磁盘上时,某个数据的位置相对于文件头的偏移量也称文件偏移地址(FileOffset)或者物理地址(RAW Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始为零。用十六进制工具比如:winhex,hexworkshop都可以查看。注意这个物理地址和虚拟地址的区别,物理地址是文件在磁盘上相对于文件头的地址,而虚拟地址是PE可执行程序加载在内存中的地址。

入口点OEP

首先明确一个概念就是OEP是一个RVA,然后使用OEP +Imagebase ==入口点的VA,通常情况下,OEP指向的不是main函数。

VA和RVA的转换

VA=ImageBase+RVA

RVA和FOA的转换

PE文件中的节等模块加载到内存时,节的数据布局和文件中的内存布局基本保持不变。所以可以根据这个数据位置相对不变的特点来由RVA正确换算出到数据相对文件的偏移。即,每个节(section)中的数据的起始位置相对节的起始位置是不变的,不管节是在文件中还是被加载到内存中。

1.判断指定的RVA在那个节中
2.求得该节的起始地址RVA
3.求出偏移量Offset=RVA-节起始RVA
4.FOA = Offset+该节在磁盘中的起始地址

数据的文件偏移=(数据RVA - 节RVA) + 节的文件偏移

区块的对齐值

之前我们简单了解过区块是要对齐的,无论是在内存中存放还是在磁盘中存放,但他们一般的对齐值是不同的。

PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。

例如,在PE文件中,一个典型的对齐值是200h,这样,每个区块都将从200h 的倍数的文件偏移位置开始,假设第一个区块在400h 处,长度为90h,那么从文件400h 到490h 为这一区块的内容,而由于文件的对齐值是200h,所以为了使这一区块的长度为FileAlignment 的整数倍,490h 到 600h 这一个区间都会被00h 填充,这段空间称为区块间隙,下一个区块的开始地址为600h 。

PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始。

一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的;在IA-64 上,是按8KB(2000h)来排列的。所以在X86 系统中,PE文件区块的内存对齐值一般等于 1000h,每个区块按1000h 的倍数在内存中存放。

Windows 装载器在装载DOS部分,PE文件头部分和节表(节表也称为区块表,块表)部分是不进行任何特殊处理的,而在装载节(节也称为区块)的时候则会自动按节(区块)的属性的不同做不同的处理。

0x3 PE文件各部分详解

MS-DOS header

每个PE文件是以一个DOS程序开始的,有了它,一旦程序在DOS下执行,DOS就能辨别出这是个有效的执行体,然后运行紧随MZ header(后面会介绍)之后的DOS stub(DOS块)。DOS stub实际上是一个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This Program cannot be run in MS-DOS”。用户通常对DOS stub 不感兴趣,因为大多数情况下他们由汇编器自动生成。平常把DOS stub和DOS MZ头部合称为DOS文件头。

PE文件的第一个字节起始于一个传统的MS-DOS头部,被称作IMAGE_DOS_HEADER。其IMAGE_DOS_HEADER的结构如下(左边的数字是到文件头的偏移量):

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

这个结构中有两字段很重要,一个是e_magic,一个是e_lfanew。

e_magic(一个字大小)字段需要被设置为0x5A4D这个也是PE程序载入的重要标志,这个值非常有意思,他们对应的字符分别位Z和M,是为了纪念MS-DOS的最初创建者Mark Zbikowski而专门设置的,由于在hex编辑器中显示是由低位到高位故显示为4D5Ah,刚好是创建者的名字缩写。

另一个字段是e_lfanew。这个字段表示的是真正的PE文件头部相对偏移地址(RVA),它指出了真正PE头部文件偏移位置。它占用四个字节,位于文件开始偏移的0x3C字节中。

NT header

NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头(IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32)。

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

0x01 Signature

类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是PE。

0x02 IMAGE_FILE_HEADER

这个字段也是包含几个字段结构,它包含了PE文件的一些基本信息,最重要的是其中一个域指出了IMAGE_OPTIONAL_HEADER的大小。

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine
该文件的运行平台,是x86、x64还是I64等等,可以是下面值里的某一个。

#该文件的运行平台,是x86、x64还是I64等等,可以是下面值里的某一个。
#define IMAGE_FILE_MACHINE_UNKNOWN      0
#define IMAGE_FILE_MACHINE_I386         0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000        0x0162  // MIPS little-endian, 0x160big-endian
#define IMAGE_FILE_MACHINE_R4000        0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000       0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2   0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA         0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3          0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP       0x01a3
#define IMAGE_FILE_MACHINE_SH3E        0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4         0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5         0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM         0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB        0x01c2
#define IMAGE_FILE_MACHINE_AM33         0x01d3
#define IMAGE_FILE_MACHINE_POWERPC    0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP   0x01f1
#define IMAGE_FILE_MACHINE_IA64        0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16      0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64     0x0284 //ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU     0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16  0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64       IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE    0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF          0x0CEF
#define IMAGE_FILE_MACHINE_EBC          0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64       0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R        0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE          0xC0EE

NumberOfSections
该PE文件中有多少个节,也就是节表中的项数。

TimeDateStamp
PE文件的创建时间,一般有连接器填写。表明文件是何时被创建的。这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,这个值是比文件系统(FILESYSTEM)的日期时间更加精确的指示器。

PointerToSymbolTable
COFF文件符号表在文件中的偏移,主要指向调式信息

NumberOfSymbols
符号表的数量。

SizeOfOptionalHeader
紧随其后的可选头的大小,对于32位系统,通常为0X00E0H,64位系统为0X00F0H。

Characteristics
可执行文件的属性,可以是下面这些值按位相或,定义在winnt.h头文件中。

IMAGE_FILE_HEADER = e_lfanew + 0x4

0x03 IMAGE_OPTIONAL_HEADER

其实这个结构是IMAGE_FILE_HEADER结构的补充。这两个结构合起来才能对整个PE文件头进行描述。这个结构异常复杂,但真正我们用得到的其实不多,下面来看看它的各个字段情况:

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    
    //
    // NT additional fields.
    //
    
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

Magic
表示可选头的类型。

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC  0x10b // 32位PE可选头
#define  IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC   0x107 

MajorLinkerVersion
链接器的版本号

MinorLinkerVersion
链接器的版本号

SizeOfCode
代码段的长度,如果有多个代码段,则是代码段长度的总和。

SizeOfInitializedData
初始化的数据长度。

SizeOfUninitializedData
未初始化的数据长度。

AddressOfEntryPoint
程序入口的RVA,对于exe可以理解为WinMain的RVA。对于DLL可以理解为DllMain的RVA,对于驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成。

BaseOfCode
代码段起始地址的RVA。

BaseOfData
数据段起始地址的RVA。

可选字段部分ImageBase
映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快。当文件被装载到其他地址时,进行重定位操作,会慢一点。

对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入。

这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。

因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1,即DLL中不删除重定位信息,EXE文件中删除重定位信息。

批:#defineIMAGE_FILE_RELOCS_STRIPPED 0x0001
//Relocation info stripped from file.(从文件中删除重定位信息。)

在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为0x0400000h,而DLL文件的默认优先装入地址被定为10000000h。

SectionAlignment
节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。

FileAlignment
节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。

MajorOperatingSystemVersion
所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。

MinorOperatingSystemVersion
所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。

MajorImageVersion
映象的版本号,这个是开发者自己指定的,由连接器填写。

MinorImageVersion
映象的版本号,这个是开发者自己指定的,由连接器填写。

MajorSubsystemVersion
所需子系统版本号。

MinorSubsystemVersion
所需子系统版本号。

Win32VersionValue
保留,必须为0。

SizeOfImage
映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。

SizeOfHeaders
所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。

CheckSum
映象文件的校验和。

Subsystem
运行该PE文件所需的子系统,可以是下面定义中的某一个:

SizeOfStackReserve

运行时为每个线程栈保留内存的大小。

SizeOfStackCommit

运行时每个线程栈初始占用内存大小。

SizeOfHeapReserve

运行时为进程堆保留内存大小。

SizeOfHeapCommit

运行时进程堆初始占用内存大小。

LoaderFlags

保留,必须为0。

NumberOfRvaAndSizes

数据目录的项数,即下面这个数组的项数。

DataDirectory

数据目录,这是一个数组,数组的项定义如下:

typedef struct _IMAGE_DATA_DIRECTORY {

    DWORD  VirtualAddress;  //数据块的起始RVA

    DWORD  Size;            //数据块的长度

} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;**VirtualAddress

是一个RVA,Size:是一个大小。这两个数有什么用呢?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。那他定义的是什么东西的区域呢?前面说了,DataDirectory是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:

索 引索引值在Windows.inc中的预定义值对应的数据块偏移量
0IMAGE_DIRECTORY_ENTRY_EXPORT导出表78h
1IMAGE_DIRECTORY_ENTRY_IMPORT导入表80h
2IMAGE_DIRECTORY_ENTRY_RESOURCE资源88h
3IMAGE_DIRECTORY_ENTRY_EXCEPTION异常(具体资料不详)90h
4IMAGE_DIRECTORY_ENTRY_SECURITY安全(具体资料不详)98h
5IMAGE_DIRECTORY_ENTRY_BASERELOC重定位表A0h
6IMAGE_DIRECTORY_ENTRY_DEBUG调试信息A8h
7IMAGE_DIRECTORY_ENTRY_ARCHITECTURE版权信息B0h
8IMAGE_DIRECTORY_ENTRY_GLOBALPTR具体资料不详B8h
9IMAGE_DIRECTORY_ENTRY_TLSThread Local StorageC0h
10IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG具体资料不详C8h
11IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT具体资料不详D0h
12IMAGE_DIRECTORY_ENTRY_IAT导入函数地址表D8h
13IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT具体资料不详E0h
14IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR具体资料不详E8h
15未使用 保留

IMAGE_OPTIONAL_HEADER = e_lfanew + 0x4 + 0x14

Section Table

节表是PE文件后续节的描述,Windows根据节表的描述加载每个节。PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中IMAGE_SECTION_HEADER结构数量等于节的数量加一。

节表总是被存放在紧接在PE文件头的地方。节表中 IMAGE_SECTION_HEADER结构的总数总是由PE文件头IMAGE_NT_HEADERS(注:即本资料中的NT头) 结构中的FileHeader.NumberOfSections 字段来指定的。

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;

Name
区块名。这是一个由8个ASCII码组成,用来定义区块的名称的数组。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.”实际上是不是必须的。值得我们注意的是,如果区块名达到8 个字节,后面就没有0字符了。前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$”的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”后边的字符的字母顺序进行合并的。每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data”或者说将包含数据的区块命名为“.Code”都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。

VirtualSize
对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。

VirtualAddress
该区块装载到内存中的RVA地址。这个地址是按照内存页来对齐的,因此它的数值总是SectionAlignment的值的整数倍。

PointerToRawData
指出节在磁盘文件中所处的位置。这个数值是从文件头开始算起的偏移量。

SizeOfRawData
该区块在磁盘中所占的大小,这个数值等于VirtualSize字段的值按照FileAlignment的值对齐以后的大小。

Characteristics
该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。

Section Table = e_lfanew + 0x4 + 0x14 + SizeOfOptionalHeader

Import table

输入表是PE(Portable Executable File Format)文件结构中不可或缺的部分,输入表也被称之为“导入表”。可执行文件使用来自于其他动态链接库(DLL)的代码或数据时,称为输入。
输入表就相当于 EXE文件与 DLL文件沟通的钥匙,形象的可以比喻成两个城市之间交流的高速公路,所有的导入函数信息都会写入输入表中,在PE 文件映射到内存后,Windows 将相应的 DLL文件装入,EXE 文件通过“输入表”找到相应的 DLL 中的导入函数,从而完成程序的正常运行,这一动态连接的过程都是由“输入表”参与的。

输入函数,表示被程序调用但是它的代码不在程序代码中的,而在DLL中的函数。对于这些函数,磁盘上的可执行文件只是保留相关的函数信息,如函数名,DLL文件名等。在程序运行前,程序是没有保存这些函数在内存中的地址。当程序运行起来时,windows加载器会把相关的DLL装入内存,并且将输入函数的指令与函数真在内存中正的地址联系起来。输入表(导入表)就是用来保存这些函数的信息的。

在IMAGE_OPTIONAL_HEADER 中的 DataDirectory数组保存了输入表的RVA跟大小。通过RVA可以在OD中加载程序通过ImageBase+RVA 找到输入表,或者通过RVA计算出文件偏移地址,查看磁盘中的可执行文件,通过文件偏移地址找到输入表。
在 PE文件头的 IMAGE_OPTIONAL_HEADER 结构中的DataDirectory(数据目录表) 的第二个成员就是指向输入表的。而输入表是以一个IMAGE_IMPORT_DESCRIPTOR(简称IID) 的数组开始。每个被 PE文件链接进来的 DLL文件都分别对应一个 IID数组结构。在这个 IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0) 的 IID 作为结束的标志。

3 IMAGE_IMPORT_DESCRIPTOR

输入表是以一个IMAGE_IMPORT_DESCRIPTOR(IID)数组 开始的,每一个被PE文件隐式的链接进来的DLL都有一个IID,IID数组的最后一个单元用NULL表示。

typedef struct_IMAGE_IMPORT_DESCRIPTOR {     
_ANONYMOUS_UNION union{              //00h      
  DWORD Characteristics;         
  DWORD OriginalFirstThunk;      
} DUMMYUNIONNAME;   
   DWORD TimeDateStamp;                  //04h  
   DWORDForwarderChain;                 //08h   
   DWORD Name;                             //0Ch  
    DWORD FirstThunk;                      //10h
}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;

OriginalFirstThunk
指向一个IMAGE_THUNK_DATA数组叫做输入名称表Import Name Table(INT),用来保存函数。

TimeDateStamp
该字段可以忽略。如果那里有绑定的话它包含时间/数据戳(time/data stamp)。如果它是0,就没有绑定在被导入的DLL中发生。在最近,它被设置为0xFFFFFFFF以表示绑定发生。

ForwarderChain
一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。它可被设置为0xFFFFFFFF以代表没有forwarder。

Name
DLL名字的指针, 指向一个用NULL作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL

FirstThunk
它也指向IMAGE_THUNK_DATA数组叫做输入地址表Import Address Table(IAT)。
IMAGE_THUNK_DATA结构

typedef struct_IMAGE_THUNK_DATA32 {     
union {         
  DWORDForwarderString;        
  DWORDFunction;        
  DWORDOrdinal;         
  DWORDAddressOfData;   
} u1;
}IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;

当IMAGE_THUNK_DATA 的值最高位为1时,表示函数是以序号方式输入,这时低31为被当作函数序号。当最高位是0时,表示函数是以字符串类型的函数名方式输入的,这时,IMAGE_THUNK_DATA 的值为指向IMAGE_IMPORT_BY_NAME 的结构的RVA。

typedef struct_IMAGE_IMPORT_BY_NAME {     
WORD Hint;   
BYTE Name[1];  
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

Hint
表示这个函数在其所驻留DLL的输出表的序号,不是必须的。

Name
表示 函数名,是一个ASCII字符串以0结尾,大小不固定。

需要两个 IMAGE_THUNK_DATA 数组的原因
当程序加载时,IAT 会被PE加载器重写,PE加载器先搜索INT,PE加载器迭代搜索INT数组中的每个指针,找出 INT所指向的IMAGE_IMPORT_BY_NAME结构中的函数在内存中的真正的地址,并把它替代原来IAT中的值。当完成后,INT就没有用了,程序只需要IAT就可以正常运行了。
可执行程序在磁盘中的时:

可执行程序加载后:

先找到DataDirectory的第二项,然后计算出数据的文件偏移=(数据RVA - 节RVA) + 节的文件偏移。

找到导入表的文件偏移,根据OriginalFirstThunk找INT表位置,还是数据的文件偏移=(数据RVA - 节RVA) + 节的文件偏移。

然后根据INT找函数id和函数名,还是数据的文件偏移=(数据RVA - 节RVA) + 节的文件偏移。

Reloc table

struct_IMAGE_BASE_RELOCATION
{
    DWORD  VirtualAddress;  //重定位数据开始的RVA地址
    DWORD  SizeOfBlock;     //重定位块的长度
    WORD    TypeOffset;     //重定位项位数组
}IMAGE_BASE_RELOCATION;

VirtualAddress
是这一组重定位数据的开始RVA地址.各重定位项的地址加上这个值才是该重定位项完整的RVA地址。

SizeOfBlock
是重定位结构的大小。

TypeOffset
是一个数组.数组每项大小为两个字节,共16位.它又分为高4位和低12位,高4位代表重定位类型;低12位是重定位地址,它与VirtualAddress相加即是指向PE映像中需要修改的地址数据的指针。

0x4 C语言解析PE文件

#include <iostream>
#include <Windows.h>
IMAGE_DOS_HEADER myDosHeader;
IMAGE_NT_HEADERS myNTHeader;
IMAGE_FILE_HEADER myFileHeader;
IMAGE_OPTIONAL_HEADER myOptionHeader;
IMAGE_SECTION_HEADER* pmySectionHeader;
LONG e_lfanew;

int main(int argc, char* argv[])
{
    FILE* pfile;
    errno_t err;
    DWORD fileSize = 0;

    if ((err = fopen_s(&pfile, "1.exe", "r")) != 0)
    {
        printf("打开文件错误!");
        getchar();
    }
    
    //DOS头部分
    printf("================IMAGE_DOS_HEADER================\n");
    fread(&myDosHeader, 1, sizeof(IMAGE_DOS_HEADER), pfile);
    if (myDosHeader.e_magic != 0x5A4D)
    {
        printf("不是MZ开头的文件!");
        fclose(pfile);
        exit(0);
    }
    printf("WORD e_magic:                %04X\n", myDosHeader.e_magic);
    printf("DOWRD e_lfaner:                %08X\n\n", myDosHeader.e_lfanew);
    e_lfanew = myDosHeader.e_lfanew;
    
    //NT头部分
    printf("================IMAGE_NT_HEADER================\n");
    fseek(pfile, e_lfanew, SEEK_SET);
    fread(&myNTHeader, 1, sizeof(IMAGE_NT_HEADERS), pfile);
    if (myNTHeader.Signature != 0x4550)
    {
        printf("文件有问题!");
        fclose(pfile);
        exit(0);
    }
    printf("DWORD Signature:            %08X\n\n", myNTHeader.Signature);
    
    //FILE头部分
    printf("================IMAGE_FILE_HEADER================\n");
    printf("WORD Machine:                %04X\n", myNTHeader.FileHeader.Machine);
    printf("WORD NumberOfSection:            %04X\n", myNTHeader.FileHeader.NumberOfSections);
    printf("DWORD TimeDateStamp:            %08X\n", myNTHeader.FileHeader.TimeDateStamp);
    printf("DWORD pointerToSymbolTable        %08X\n", myNTHeader.FileHeader.PointerToSymbolTable);
    printf("DWORD NumberOfSymbols:            %08X\n", myNTHeader.FileHeader.NumberOfSymbols);
    printf("WORD SizeOfOptionHeader:        %04X\n", myNTHeader.FileHeader.SizeOfOptionalHeader);
    printf("WORD Characteristics:            %04X\n\n", myNTHeader.FileHeader.Characteristics);
    
    //OPTIONAL头部分
        printf("================IMAGE_OPTION_HEADER================\n");
    printf("WORD Magic;                    %04X\n", myNTHeader.OptionalHeader.Magic);
    printf("BYTE MajorLinkerVersion:            %02X\n", myNTHeader.OptionalHeader.MajorLinkerVersion);
    printf("BYTE MinorLinkerVersion:            %02X\n", myNTHeader.OptionalHeader.MinorLinkerVersion);
    printf("DWORD SizeOfCode;                %08X\n", myNTHeader.OptionalHeader.SizeOfCode);
    printf("DWORD SizeOfInitializedData:            %08X\n", myNTHeader.OptionalHeader.SizeOfInitializedData);
    printf(" DWORD SizeOfUninitializedData            %08X\n", myNTHeader.OptionalHeader.SizeOfUninitializedData);
    printf("DWORD AddressOfEntryPoint:            %08X\n", myNTHeader.OptionalHeader.AddressOfEntryPoint);
    printf("DWORD BaseOfCode:                %08X\n", myNTHeader.OptionalHeader.BaseOfCode);
    printf("DWORD BaseOfData:                %08X\n", myNTHeader.OptionalHeader.BaseOfData);
    printf("DWORD ImageBase:                %08X\n", myNTHeader.OptionalHeader.ImageBase);
    printf("DWORD SectionAlignmen:                %08X\n", myNTHeader.OptionalHeader.SectionAlignment);
    printf("DWORD FileAlignment:                %08X\n", myNTHeader.OptionalHeader.FileAlignment);
    printf("WORD MajorOperatingSystemVersion:        %04X\n", myNTHeader.OptionalHeader.MajorOperatingSystemVersion);
    printf("WORD MinorOperatingSystemVersion:        %04X\n", myNTHeader.OptionalHeader.MinorOperatingSystemVersion);
    printf("WORD MajorImageVersion:                %04X\n", myNTHeader.OptionalHeader.MajorImageVersion);
    printf("WORD MinorImageVersion:                %04X\n", myNTHeader.OptionalHeader.MinorImageVersion);
    printf("WORD MajorSubsystemVersion:            %04X\n", myNTHeader.OptionalHeader.MajorSubsystemVersion);
    printf("WORD MinorSubsystemVersion:            %04X\n", myNTHeader.OptionalHeader.MinorSubsystemVersion);
    printf("DWORD Win32VersionValue:            %08X\n", myNTHeader.OptionalHeader.Win32VersionValue);
    printf("DWORD SizeOfImage:                %08X\n", myNTHeader.OptionalHeader.SizeOfImage);
    printf("DWORD SizeOfHeaders:                %08X\n", myNTHeader.OptionalHeader.SizeOfHeaders);
    printf("DWORD CheckSum:                    %08X\n", myNTHeader.OptionalHeader.CheckSum);
    printf("WORD Subsystem:                    %04X\n", myNTHeader.OptionalHeader.Subsystem);
    printf("WORD DllCharacteristics:            %04X\n", myNTHeader.OptionalHeader.DllCharacteristics);
    printf("DWORD SizeOfStackReserve:            %08X\n", myNTHeader.OptionalHeader.SizeOfStackReserve);
    printf("DWORD SizeOfStackCommit:            %08X\n", myNTHeader.OptionalHeader.SizeOfStackCommit);
    printf("DWORD SizeOfHeapReserve:            %08X\n", myNTHeader.OptionalHeader.SizeOfHeapReserve);
    printf("DWORD SizeOfHeapCommit:                %08X\n", myNTHeader.OptionalHeader.SizeOfHeapCommit);
    printf("DWORD LoaderFlags:                %08X\n", myNTHeader.OptionalHeader.LoaderFlags);
    printf("DWORD NumberOfRvaAndSizes :            %08X\n\n", myNTHeader.OptionalHeader.NumberOfRvaAndSizes);
    
    //节表目录
    printf("================IMAGE_OPTIONAL_HEADER================\n");
    pmySectionHeader = (IMAGE_SECTION_HEADER*)calloc(myNTHeader.FileHeader.NumberOfSections, sizeof(IMAGE_SECTION_HEADER));
    fseek(pfile, (e_lfanew + sizeof(IMAGE_NT_HEADERS)), SEEK_SET);
    fread(pmySectionHeader, sizeof(IMAGE_SECTION_HEADER), myNTHeader.FileHeader.NumberOfSections, pfile);
    for (int i = 0; i < myNTHeader.FileHeader.NumberOfSections; i++, pmySectionHeader++)
    {
        printf("BYTE Name:                %s\n", pmySectionHeader->Name);
        printf(":DWORD PhysicalAddress            %08X\n", pmySectionHeader->Misc.PhysicalAddress);
        printf(":DWORD VirtualSize            %08X\n", pmySectionHeader->Misc.VirtualSize);
        printf(":DWORD VirtualAddress            %08X\n", pmySectionHeader->VirtualAddress);
        printf(":DWORD SizeOfRawData            %08X\n", pmySectionHeader->SizeOfRawData);
        printf(":DWORD PointerToRawData            %08X\n", pmySectionHeader->PointerToRawData);
        printf(":DWORD PointerToRelocations        %08X\n", pmySectionHeader->PointerToRelocations);
        printf(":DWORD PointerToLinenumbers        %08X\n", pmySectionHeader->PointerToLinenumbers);
        printf(":WORD NumberOfRelocations        %04X\n", pmySectionHeader->NumberOfRelocations);
        printf(":WORD NumberOfLinenumbers        %04X\n", pmySectionHeader->NumberOfLinenumbers);
        printf(":DWORD Characteristics            %08X\n\n", pmySectionHeader->Characteristics);
    
    }
    pmySectionHeader = NULL;
    free(pmySectionHeader);
    fclose(pfile);
    getchar();
    return 0;
}
温柔正确的人总是难以生存,因为这世界既不温柔,也不正确

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!