PE结构
https://blog.csdn.net/qq_39654127/article/details/97905901
1.可执行文件(executable file)指的是可以有操作系统进行加载执行的文件。
Windows平台:PE(Portable Executable)文件结构;
Linux平台:ELF(Executable and Linking Format)文件结构;
比如Windows上的exe文件直接双击就能执行,但是txt文件则需要使用文本编辑软件才可以打开执行。
2.PE文件格式的应用
病毒与反病毒;
外挂与反外挂;
加壳与脱壳(保护与破解);
无源码修改功能、软件汉化等等;
3.识别PE文件
(1)PE文件的特征(PE指纹)
分别打开.exe, .dll, .sys等文件,观察特征前2个字节。
用二进制工具如UltraEdit打开该类文件后发现前两个字节对应的字符是MZ,查找3C位置的值x并将其作为地址查找x位置的值,看到对应位置的字符为PE。
(2)不要仅仅通过文件的后缀名来认定PE文件。
4.PE文件的整体结构
PE所有的结构都在winnt.h这个头文件里
一个PE文件在硬盘上存储和在内存中运行时的结构是不一样的。
- DOS部分(是历史遗留的,给DOS系统用的,但是PE结构仍然保留了它)
(1)DOS MZ文件头
也就是IMAGE_DOS_HEADER,有64个字节
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew;
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
DOS MZ最后一个成员是LONG e_lfanew;并且其宽度是4个字节,这4个字节指向了PE头从哪里开始。(也就是上图中的第二部分“PE文件头”)
这也就是在找PE指纹的时候,要在3C位置找到PE头开始的位置。3C-3F这4个字节正好就是DOS MZ的最后4个字节。
而DOS MZ(前64个字节)和PE文件头(由PE指纹找到)之间的部分就是DOS块。
(2)DOS块
也就是DOS Stub,它的大小是不确定的,因为这块数据是给链接器用的, 链接器会往里面插入自己的一些数据。(自己也可以修改,而不会影响当前程序的运行)
病毒程序也可以往这里加入一些危险的代码。
- PE文件头
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;//PE标识,4个字节
IMAGE_FILE_HEADER FileHeader;//子结构体(标准PE头)
IMAGE_OPTIONAL_HEADER OptionalHeader;//子结构体(扩展PE头)
} IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS;
(1)PE文件头标志
占4个字节,“PE”,0,0
(2)标准PE头
也就是IMAGE_FILE_HEADER结构,占20个字节。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;//这个成员用来标识扩展PE头的大小,在这里改变扩展PE头的大小。
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
WORD表示占2个字节。BYTE表示占1个字节。DWORD表示占4个字节。
正常情况下,在我们不改变SizeOfOptionalHeader的值的时候,32位的PE文件中SizeOfOptionalHeader的值是16进制的E0,也就是十进制的224。
如果是64位程序,这个SizeOfOptionalHeader值默认是十六进制的F0 。
(3)扩展PE头
这个结构体特别大(因为用数组带了16个子结构体[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]),而且32位的和64位的PE文件这个结构体的大小是不一样的。
如果是32位的PE文件,那么这个结构体的大小为224字节。
IMAGE_OPTIONAL_HEADER32)(224)
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;//上面的成员共占36个字节,这个成员从第37个字节开始
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Reserved1;
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];//这是一个数组,数组的每个成员又是一个结构体,而且数组的长度是16
}
扩展PE头是可以修改的,也就是可以“加料”的。
- 节表
节表的大小,每一个结构体的成员是40个字节
IMAGE_SECTION_HEADER)(40)
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;
比如第一个40个字节是.text节,第二个40个字节是.data节,第三个40个字节是.rsrc节…这些节成员的后面可能还会跟着一些编译器插入的一些其他数据比如说常量字符串。
扩展PE头里有一个成员DWORD SizeOfHeaders;–这个头的大小表示的是(DOS头+PE文件头+节表)按文件对齐以后的大小。(扩展PE头第61、62、63、64个字节的值,也就是FileAlignment后面再过20个字节后的值【此时看到的是00 04 00 00,实际上就是00 00 04 00】)
扩展PE头里还有一个成员DWORD FileAlignment;叫文件对齐,通常情况下这个值是十六进制的200也可能是1000 。(扩展PE头第37、38、39、40个字节的值【假如UE中看到是00 02 00 00,倒着看实际上就是00 00 02 00】)
SizeOfHeaders中存储的值一定是FileAlignment值的整数倍
比如DOS头+PE文件头+节表加完以后是302个字节,此时因为FileAlignment的值是200,所以SizeOfHeaders中的值应该为400 。
使用文件对齐的方式是为了提高执行的效率,这是一种牺牲空间换取时间的方法。
为了使SizeOfHeaders的值是FileAlignment的值的整数倍,节表后面节数据前面会填充零/编译器插入一些常量字符串/自己“加料”。
节数据有几部分取决于节表有几个成员(也就是几个40个字节)
- 节数据(节表有几个成员PE文件就会有几个节)
而每个节的大小也一定是文件对齐的,200/1000的整数倍。这就导致两个节之间很有可能是有零填充的。
5.执行后的PE文件
WinHex工具,运行notepad.exe后,Tools-Open RAM-选择notepad,点击notepad.exe后点击确定。
这时新出现的这个窗口中就是PE文件在内存中执行以后的状态,而之前的那个就是PE文件在硬盘中的状态。
可以看到,在内存中执行以后的PE文件起始位置不是从0开始的,这里是01000000 。
硬盘中的PE文件的节数据是从400H开始的,但是内存中的PE文件却不是,而是从1000H开始的。
扩展PE头中FileAlignment成员的前一个成员DWORD SectionAlignment是内存对齐成员。
内存对齐和文件对齐的值可能一样也可能不一样。 本例中我们找到文件对齐是200H字节,而内存对齐是1000H字节。
一个PE文件在内存中和在文件中都有一个对齐参数。
这就是为什么会有PE文件的两种状态(如上图)。
头和节与节和节的空白区大小在硬盘和内存中是不一样的。
----》硬盘中的状态和内存中拉伸后的状态
6.DOS头属性说明
DOS头中真正有用的其实只有e_magic和e_lfanew成员。
(1)DOS块(DOS Stub)不是结构体,而是一堆由单个字节组成的数据。
这里的内容可以任意修改。比如将其全改为零后另存为notepad.exe仍然能够正常运行。
(2)DOS MZ头结构体的内容不用记,因为这本来是给16位程序准备的,现在的32位和64位程序用不上。但是有两个结构体中的成员例外。
typedef struct _IMAGE_DOS_HEADER{
WORD e_magic;//Magic number(占2个字节)
.........
LONG e_lfanew;//File address of new exe header(占4个字节)
}IMAGE_DOS_HEADER,* PIMAGE_DOS_HEADER;
将DOS MZ64个字节中除了前2个字节和后4个字节之外的中间的所有数据都改为零,并另存为,这个新生成的notepad.exe文件照样能运行而且没有什么影响。
前2个字节4d 5a对应的字符就是MZ,这是PE文件的标识之一,而后4个字节则是我们找到PE头开始位置的根据。
7.PE头
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;//PE标识,4个字节
IMAGE_FILE_HEADER FileHeader;//子结构体(标准PE头)(20)
IMAGE_OPTIONAL_HEADER OptionalHeader;//子结构体(扩展PE头)(224)
} IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS;
(1)PE标识
PE标识不能破坏,操作系统在启动一个程序的时候会检测这个标识(以及MZ标识)
未完待续~~