PE文件(一)PE结构概述

本文详细介绍了Windows操作系统下的PE结构文件,包括.exe文件的内存分配、虚拟内存、PE文件的节和节表、DOS头与NT头的作用,以及编译器对文件对齐的影响。展示了不同操作系统文件结构的区别,以及如何手动解析PE文件的DOS头和NT头。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

PE结构简述

在我们以后学习安全方面的知识中,如HOOK,反调试,壳,病毒,破解等等都需要使用到PE文件结构的知识,因此掌握PE文件结构是很重要的

Windows操作系统是只能运行以内存4D 5A开头,翻译是MZ的可执行文件,该文件也叫做PE结构文件,是以.exe,.sys,.dll等等作为后缀的文件。而不同的操作系统能运行的可执行文件都是各自特有的,比如Linux可运行的可执行文件叫做elf结构文件

当我们在运行exe文件时,其实不只是加载了exe文件,还加载了其他的文件,如dll文件。

在以后pe结构文件的学习中,为方便称呼,我们均以.exe文件/程序代指pe文件进行演示与讲解

32位计算机中任何一个.exe文件在运行时所在的内存叫做虚拟内存,这些文件都有自己独立的4GB的虚拟内存,其中2GB用于应用程序,其余2GB用于操作系统

.exe文件有两种状态:

一种是在硬盘(未运行时)的状态:在硬盘上的.exe文件打开后内存首地址是从0开始的(逻辑地址)

另一种是在内存中(运行时)的状态:正在运行时的.exe文件在内存(即虚拟内存)中的内存首地址是从0x1000000开始的(物理地址),该内存首地址根据不同的文件有不同的地址

我们常见的txt文件是可以打开运行它,但它并不是可执行文件,这是因为该文件实际是在exe程序中(比如word)打开的

PE和壳

首先我们简单了解一下壳和PE文件结构的关系

壳的本质就是在PE文件中进行一个代码段的植入

当我们在计算机中运行一个程序时,其会从起始位置即ImageBase开始执行代码。而壳则是对ImageBase进行一个篡改,使得在刚开始运行时,便将执行代码劫持到自己的代码处进行执行。而我们自己的代码可以对PE文件进行加密或压缩/虚拟化,同样的我们也可以加入反调试/免杀/虚拟机对抗/GUI网络验证等功能。当执行完我们自己的代码以后,便会回到原来的程序,即原始ImageBase处继续运行

在结束PE结构的学习以后,我们要手搓一个加密壳作为结课小项目。

PE结构应用

 接下来我们以如下程序进行演示,达到修改MessageBoxA第二个参数的目的

#include<windows.h>

int main()
{
    MessageBoxA(NULL, "rkvir", "Msg", MB_OK);
    return 0;
}

x32dbg修改函数参数

我们将该程序拖入x32dbg进行观察

我们首先通过右键->搜索->所有用户模块->跨模块调用(程序中所用但不在本程序定义的函数)获取调用MessageBoxA的地址处,进入实际代码区

注意:我们上文的程序使用的MessageBoxA显然不是本程序所定义的,而是其他模块加载进来的,因此使用跨模块调用去获取调用地址

现在我们已经来到MessageboxA的调用处,我们开始修改其参数:通过右键->在内存窗口中转到便可以找到参数所在内存中的数据,如下:

我们可以直接在该内存窗口中进行数据的篡改(注意不要超过原字符大小,以防越界,并留一字节空白):选中要修改的数据->右键->二进制编辑->编辑,如下图所示:

此时我们便完成了对第二个参数的修改了

现在我们运行程序观察:

修改成功

但通过x32dbg修改是存在不足的,它只是在该程序运行后在虚拟内存中修改,而非物理内存。因此当我们再一次运行该程序时,之前的修改就无效了

010Editor修改函数参数

为了使得参数的修改实现永久性,我们需要使用010Editor进行物理内存上的永久修改,即篡改PE文件

我们将该程序拖入010Editor,Ctrl + F进行查找:

此时便转到了目标字符串所在处,我们直接像打拼音一样,直接可以修改它了

这时我们便修改了改程序的物理内存,不论我们运行该程序多少次,参数都是我们修改以后的结果:

而这就是我们学习PE文件结构的一个应用

 PE文件地址

PE文件通常涉及以下几种地址:

1.虚拟内存地址(VA):在x86模式下,每个进程都有自己的4GB内存空间,这个空间就是虚拟内存空间,地址范围为:0x0-0xFFFFFFFF。

我们随机找一个exe文件,使用x32dbg打开,进到内存布局观察

我们可以很清晰的直到:最左边的地址便是虚拟内存地址,主模块即exe对应的地址0x530000便是该exe文件虚拟内存的起始地址

我们接着往下看:

我们发现该程序,不仅有自己的exe文件,还有别的dll文件,而这每一个文件都是一个模块。而这些模块的加载地址 + RVA = VA

2.相对虚拟内存地址(RVA):虚拟内存中,目标内存地址距离模块加载基址的距离

例如:

Module Base = 50000

Memory Addr = 50010(VA)

RVA = 10

3.文件偏移地址(FOA):物理内存中,当前地址距离文件首地址距离, 

4.特殊地址

区段

我们在上文的内存布局中,发现了.text,.rdata等等的东西,这些东西叫做区段,其表示一段内存空间。每个区段中存储的东西,各区段的相关权限都是有所不同的,这是为了保证程序和数据的独立性以及安全性所设置的

PE结构分节

PE结构文件内容是分节的,每一节之间以0作为空白区域,其分节有以下几种原因:

1.节省硬盘空间

在此之前我们应该了解到在早期的编译器编译的文件,其硬盘对齐是200h字节,这是因为早期硬盘很昂贵,为了节省硬盘空间导致的。而内存对齐是1000h字节。但在现代的编译器中编译器编译的文件在硬盘和内存中的对齐都是1000h字节

我们首先观察一下早期编译器生成的.exe文件(如notepad.exe)在硬盘和内存中的存储分布

3031259e11e44d9497d2d07103000105.png

如图可知,一个pe文件在存储中分为了多个段,这些段也叫做节。图中左侧是pe文件在硬盘中的存储分布,分布空间紧密。右侧是pe文件在内存中的存储分布,其分布空间稀疏,这是由于pe文件从硬盘到内存有一个拉伸的过程。由图对比可知在硬盘上存储空间更小。如此看来是符合节省硬盘空间的原因的

现在我们观察一下现代编译器生成的pe文件在硬盘和内存中的存储分布:

c4ec7bb41bd74f7da53a27d5999aaa73.png

同一份文件在内存中和在硬盘中的内容是一样的,但是他们文件内存起始位置是不一样的,它们分节之间的空白区域大小是一样的,这似乎与我们之前所作的文件硬盘与内存分布图有所不同,这是由于对齐的机制。对齐是为了提高读写速度,比如一本书一章的结尾可能会在新的一页留一个‘完’字,这个字会单独占有一页,而不是在下一章的内容一起占据同一页,这样的设置让我们更容易的去查找我们想要的内容

到目前为止看来,pe结构分节可能并不只是因为节省硬盘空间

2.多开,比如我们挂几个qq

假设我们现在有一个.exe文件(比如qq),它的文件结构存在只读数据和可读可写数据部分,如下图表示,其每个部分都占有100兆的内存

ab87cd7b6add45a0a98794bd8bc8f69d.png

此时我们多开一个该文件,内存分布是这样的:系统只会为我们多创建一个可读可写的数据的内存

979c4cacca464ea98296ff225e6d1b60.png

这是我们可以发现,正是因为pe文件结构分节,我们才能占用更少的内存,发挥更大的作用

PE文件信息

如下是早期编译器编译的pe文件在硬盘和内存中的结构图

71a79b39dc9d47dca60d7766b08e0005.png

无论哪个块的内容有多大,它所被分配的大小在硬盘中都只有200h,内存中1000h,而块中的数据不论在硬盘还是内存中都是一样的,只是因为内存1000h对齐的原因,所以需要用0填充空出来的区域

如图可知:文件中我们能看到的数据(即.data,.text,.rdata)都被存储在块中,每个块在硬盘和内存中都被分配了200h和1000h的大小。

每一个节,都有一个对应的节表(图中块表)用于记录节的相关信息,如每一个节的概要性信息。这些节表是挨着存放在一个指定的区域的,所以广义上我们称这片区域为节表。

除此以外,pe文件还有两个结构:PE文件头和DOS头,这两个结构记录了该pe文件的概要性信息和特征:比如在内存中拉伸后占多大空间,或此程序启动后要分多大的堆、堆栈等

手动解析pe文件

该部分内容我们学习如何手动查找DOS头,NT头

DOS头和NT头中指定位置和宽度的数据都规定了不同含义,图中左边一列地址是相对于DOS头或PE文件头起始地址的的地址,如图所示,该图也是完整的pe结构图

6a71954b5d4a47aca6366ecb08d7e50a.jpeg

DOS头

一.DOS头的作用

1.我们解析一个文件时会看最开始的两个字节(e_magic)是不是4D 5A(MZ),用于判断该文件是不是pe文件

2. 找到DOS头的最后4字节数据(e_Ifanew),它指向真正的PE文件开始的地址

3.其他DOS头中的数据可以不用理会,这是因为DOS头最初是给16位操作系统使用的,对于32位系统,DOS的作用就是上述两个

从DOS头结尾到PE签名(即NT头开始)之间,有一些空出来的空间。这个空间用于存放不同的编译器存放不同的数据,对于我们来说其实就是一些垃圾数据,而且程序本身也不会使用到这块空间。所以我们可以在这个空间加入我们自己的数据,并且该数据随着文件一起装入内存中,并分配了内存地址。因此虽然程序本身运行时不会使用这块空间,但我们可以利用一些方法访问该内存,如指针

二.手动解析DOS头

我们将ipmsg.exe程序用winhex打开,根据DOS头的结构来分析数据,找出DOS头对应的字节代码(DOS头大小为64字节,十六进制为0x40)

注意:winhex显示的文件数据是按不同含义的字段宽度顺序存的,并且其数据以小端序排列

bcb6981b94b74962bce53d65d0753cb0.png

如图便是我们打开程序所显示的文件的内存数据,接下来我们将按照DOS的结构,依次查找每个成员所对应的数据代码

struct _IMAGE_DOS_HEADER {

    0x00 WORD e_magic;   *   //0x5A4D    MZ,即表示此文件是可执行文件

    0x02 WORD e_cblp;        //0x0090

    0x04 WORD e_cp;          //0x0003

    0x06 WORD e_crlc;        //0x0000

    0x08 WORD e_cparhdr;     //0x0040

    0x0a WORD e_minalloc;    //0x0000

    0x0c WORD e_maxalloc;    //0xffff

    0x0e WORD e_ss;          //0x0000

    0x10 WORD e_sp;          //0x00B8

    0x12 WORD e_csum;        //0x0000

    0x14 WORD e_ip;          //0x0000

    0x16 WORD e_cs;          //0x0000

    0x18 WORD e_lfarlc;      //0x0040

    0x1a WORD e_ovno;        //0x0000

 0x1c WORD e_res[4];      //0x0000000000000000,此处是4个字节数组

    0x24 WORD e_oemid;       //0x0000

    0x26 WORD e_oeminfo;     //0x0000

    0x28WORDe_res2[10]; //0x0000000000000000000000000000000000000000

    0x3c DWORD e_lfanew;  *  //0x000000e0    表示真正的PE文件开始地址为0xe0,即PE签名所在地址

};

NT头

紧接着DOS头便是NT头,现在我们开始讲解NT头

NT头是由三部分组成:PE签名,标准PE头,可选PE头。在NT头中,首先是PE签名字段然后是标准PE头,最后紧跟着就是可选PE头

现在我们开始寻找NT头:

1.找PE签名

ac68e3911f1d4c1b8fd58e320ba27210.png

我们之前在找DOS头时,DOS头以0x000000e0结尾, 指向了左侧地址e0的地方,从图中可知,e0的地址数据为5045,即最右侧的PE两字,即PE签名。这种现象正好说明了此处是pe文件真正开始的地方,即NT头开始,但这个e0并不是一直固定的。

NT头不直接挨着DOS,而是在DOS头的e_lfanew数据指向e0地址开始的原因是,从DOS头结尾到NT头开始(即PE签名字段)之间,不同的编译器会存放不同的数据,但是对于我们来说就是一些垃圾数据,而且程序本身也不会使用到这块空间。所以我们可以在这个空间加入我们自己的数据,并且该数据随着文件一起装入内存中,并分配了内存地址。因此虽然程序本身运行时不会使用这块空间,但我们可以利用一些方法访问该内存,如指针

如下我们将对应PE签名结构体在上图所对应的内存地址进行展示

struct _IMAGE_NT_HEADERS {

0x00 DWORD Signature;   //0x00004550 即PE的签名占4字节

0x04 _IMAGE_FILE_HEADER FileHeader;   //结构体中存在结构体类型的数据,此处是标准pe头

0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader; //此处可选pe头

};

2)找标准PE头

PE文件头(大小为20字节,0x12)

struct _IMAGE_FILE_HEADER {

    0x00 WORD Machine;  *                //0x014c

    0x02 WORD NumberOfSections;  *       //0x0004

    0x04 DWORD TimeDateStamp;  *         //0x4d74bc7e

    0x08 DWORD PointerToSymbolTable;     //0x00000000

    0x0c DWORD NumberOfSymbols;          //0x00000000

    0x10 WORD SizeOfOptionalHeader;  *   //0x00e0

    0x12 WORD Characteristics;  *        //0x010f

};

3)找可选PE头

PE可选头(大小不确定,在32位平台下为E0,在64位平台下位F0,需要根据标准PE头中的SizeOfOptionalHeader的值来判断),如下便是可选pe头对应上图的地址

struct _IMAGE_OPTIONAL_HEADER {

    0x00 WORD Magic;  *                    //0x010b

    0x02 BYTE MajorLinkerVersion;          //0x06

    0x03 BYTE MinorLinkerVersion;          //0x00

    0x04 DWORD SizeOfCode;  *              //0x00021000

    0x08 DWORD SizeOfInitializedData;  *   //0x0001b000

    0x0c DWORD SizeOfUninitializedData;  * //0x00000000

    0x10 DWORD AddressOfEntryPoint;  *     //0x0001d26f

    0x14 DWORD BaseOfCode;  *              //0x00001000

    0x18 DWORD BaseOfData;  *              //0x00022000

    0x1c DWORD ImageBase;  *               //0x00400000

    0x20 DWORD SectionAlignment;  *        //0x00001000

    0x24 DWORD FileAlignment;  *           //0x00001000

    0x28 WORD MajorOperatingSystemVersion; //0x0004

    0x2a WORD MinorOperatingSystemVersion; //0x0000

    0x2c WORD MajorImageVersion;           //0x0000

    0x2e WORD MinorImageVersion;           //0x0000

    0x30 WORD MajorSubsystemVersion;       //0x0004

    0x32 WORD MinorSubsystemVersion;       //0x0000

    0x34 DWORD Win32VersionValue;          //0x00000000

    0x38 DWORD SizeOfImage;  *             //0x0003d000

    0x3c DWORD SizeOfHeaders;  *           //0x00001000

    0x40 DWORD CheckSum;  *                //0x00000000

    0x44 WORD Subsystem;                   //0x0002

    0x46 WORD DllCharacteristics;          //0x0000

    0x48 DWORD SizeOfStackReserve;  *      //0x00100000

    0x4c DWORD SizeOfStackCommit;  *       //0x00001000

    0x50 DWORD SizeOfHeapReserve;  *       //0x00100000

    0x54 DWORD SizeOfHeapCommit;  *        //0x00001000

    0x58 DWORD LoaderFlags;                //0x00000000

    0x5c DWORD NumberOfRvaAndSizes;        //0x00000010

    0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16]; //这个先不分析

};

作业

本次作业我们以notepad为例,进行讲解

如图便是我们notepad的硬盘内存

1.找出所有DOC头数据,并统计DOC头大小.

struct _IMAGE_DOS_HEADER {

    0x00 WORD e_magic;    0x5A4D

    0x02 WORD e_cblp;      0X0090

    0x04 WORD e_cp;         0X0003

    0x06 WORD e_crlc;     0X0000

    0x08 WORD e_cparhdr;    0X0004

    0x0a WORD e_minalloc;    0X0000

    0x0c WORD e_maxalloc;   0XFFFF

    0x0e WORD e_ss;      0X0000

    0x10 WORD e_sp;   0X00B8

    0x12 WORD e_csum;    0X0000

    0x14 WORD e_ip;          0X0000

    0x16 WORD e_cs;      0X0000

    0x18 WORD e_lfarlc;   0X0040

    0x1a WORD e_ovno;      0X0000

    0x1c WORD e_res[4];    0X0000000000000000

    0x24 WORD e_oemid;       0X0000

    0x26 WORD e_oeminfo;     0X0000

    0x28 WORDe_res2[10];    0X00000000000000000000

    0x3c DWORD e_lfanew;  0X000000F0

};

64个字节

2.找出pe签名

0x00 DWORD Signature;  0X00004550

3.找出所有标准PB头数据,并统计标准PB头大小. 

struct _IMAGE_FILE_HEADER {

    0x00 WORD Machine;  0X8664

    0x02 WORD NumberOfSections; 0X0007

    0x04 DWORD TimeDateStamp; 0X4678EC68

    0x08 DWORD PointerToSymbolTable;  0X00000000

    0x0c DWORD NumberOfSymbols;  0X00000000

    0x10 WORD SizeOfOptionalHeader;  0X00F0

    0x12 WORD Characteristics;  0X0022

};

20字节

4.找出所有可选PB头数据,并统计可选PE头大小. 

struct _IMAGE_OPTIONAL_HEADER {

    0x00 WORD Magic;  0X020B

    0x02 BYTE MajorLinkerVersion;  0X0E

    0x03 BYTE MinorLinkerVersion;   0X1E

    0x04 DWORD SizeOfCode; 0X00028000

    0x08 DWORD SizeOfInitializedData; 0X00031000

    0x0c DWORD SizeOfUninitializedData; 0X00000000

    0x10 DWORD AddressOfEntryPoint; 0X000019A0

    0x14 DWORD BaseOfCode;  0X00001000

    0x18 DWORD BaseOfData; 0X40000000

    0x1c DWORD ImageBase;  0X00000001

    0x20 DWORD SectionAlignment;   0X00001000

    0x24 DWORD FileAlignment;  0X00001000

    0x28 WORD MajorOperatingSystemVersion;  0X000A

    0x2a WORD MinorOperatingSystemVersion; 0X0000

    0x2c WORD MajorImageVersion;       0X000A

    0x2e WORD MinorImageVersion;     0X0000

    0x30 WORD MajorSubsystemVersion; 0X000A

    0x32 WORD MinorSubsystemVersion; 0X0000

    0x34 DWORD Win32VersionValue;    0X00000000

    0x38 DWORD SizeOfImage;      0X0005A000

    0x3c DWORD SizeOfHeaders;  0X00001000

    0x40 DWORD CheckSum;  0X0005C43F

    0x44 WORD Subsystem;        0X0002

    0x46 WORD DllCharacteristics;       0XC160

    0x48 DWORD SizeOfStackReserve;   0X00080000

    0x4c DWORD SizeOfStackCommit; 0X00000000

    0x50 DWORD SizeOfHeapReserve;  0X00011000

    0x54 DWORD SizeOfHeapCommit; 0X00000000

    0x58 DWORD LoaderFlags;      0X00100000

    0x5c DWORD NumberOfRvaAndSizes;    0X00000000

    0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16];  这个先不分析

};

因NumberOfRvaAndSizes值为0,则DataDirectory[16]成员没有结构体,所以该可选pe头96字节。但不同的文件不同分析。

最后我们利用petool进行检查

由于该工具具有一定的bug,所以仅供参考。但由于绝大部分都可以匹配的上,所以可以判断我们手动查找dos头和nt头无误

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值