滴水三期:day27.1-PE结构概况

本文详细介绍了PE(Portable Executable)文件结构,包括硬盘与内存中文件的差异、可执行文件与不可执行文件的区别、分节存储的原理以及内存对齐的影响。重点解析了DOS头和NT头,阐述了它们在文件加载到内存过程中的作用,并分析了内存空间管理和数据传输效率。此外,还讨论了文件信息的记录方式,如节表、PE文件头和DOS头等组件,揭示了PE结构对于程序执行的重要性。
摘要由CSDN通过智能技术生成

一、引入PE结构

1.硬盘上的与加载到内存的文件结构异同

  • 通过昨天的作业,我们可以得出在硬盘上的.exe文件的结构和正在运行加载到内存中的.exe文件结构是稍有区别的:
    1. 在硬盘上的exe打开后首地址是从0开始的(逻辑地址);但是正在运行时在内存中是从0x1000000开始的(物理地址)
    2. 还可以发现最开始的一大段数据都是相同的,直到出现了空白区(一堆00),空白区过后又是一大段数据…
    3. 在硬盘上的文件空白区比运行时在内存中的空白区小,但是两者的结构都满足这种分节的方式(数据–空白区–数据–空白–…)

2.可执行文件与不可执行文件

  • 我们发现像.exe、.sys、.dll这种可执行文件的PE结构开头都是4D 5A;但是对于.jpg、.txt、.doc等文件不是可执行文件,结构开头是不统一的
  • 如何理解不是可执行文件双击后可以打开:
    • 比如我们平时在电脑上打开一个.txt文件,发现双击它后它就打开了,但是这种文件不是可执行文件,是没办法运行的,.txt文件之所以能被打开,是因为notepad.exe这个程序运行后帮我们打开了.txt的文件。就像WPS这种可执行软件,它可以帮我们打开其他的.doc、.ppt等文件
    • 综上,不可执行文件是无法单独运行的,可执行文件才是真正能执行的文件
  • 每一个不同的操作系统,都有自己规定的文件格式,只有满足规定格式的文件,才能在此操作系统上运行(所以在不同的操作系统上存储的文件格式不尽相同)
  • 所以这种可执行文件才有PE结构,对于.txt等文件是没有PE结构的

二、PE结构

1.PE文件的两种状态

  • 一种是在硬盘(外存)中的状态;一种是在内存中(运行时)的状态

2.分节(段)

  • PE文件在硬盘和内存中都是分节存储的。把数据分成一段一段的

    比如我们开始观察到的,一段数据,再是一段空白0,再是一段数据,然后又是一堆0…

  • 为什么需要分节:

    • 节省内存空间:

      • 举例:一个应用程序多开,分节可以按照每一段功能的不同分节,比如这一段数据是可读的,那一段数据是可读可写的。现在运行了一个QQ应用程序登录大号,假设我们把此程序在内存中的数据简化成2段,一段只读,一段可读可写;如果现在又运行了一个QQ程序登录了我的小号。假如没有分段,开启了两个qq.exe程序,就需要占用两个同样大小的内存。如果分节了,由于只读的这一段数据多个程序是可以共用的,因为这两个进程都无法对其内容进行修改,且都是相同的,只需要读取当中的数据,所以共用一个即可;只需要把每个进程可能需要修改的可读可写段再在装入在内存中即可。所以每开一个小号,只需要把可读可写段装入内存即可,可以节省内存很多空间。
    • 节省硬盘空间:文件在内存中段与段之间的空隙很大;而文件在硬盘上段与段之间的空隙比较小,即节省了硬盘空间(这一结论只对以前的编译器成立,即内存对齐与硬盘对齐不相等时成立。详见下面的对齐讲解)

      image-20211220152426484

      但是为什么不节省内存空间呢?明明内存条更贵

      • 我们要知道这里的文件运行时所在内存和我们说的内存条不是一个概念,任何一个exe文件在32位计算机上运行时都有自己独立的4GB(232,即寻址范围最大是4GB)虚拟内存----其中有2GB是供应用程序使用的,另外2GB是操作系统用的。
      • 我们可以想象成凡是运行后的程序虚拟上会有这样的4GB内存结构,但是实际上程序的数据都要经过操作系统帮我们管理按照特定的方式存到真实的内存条中

3.硬盘对齐与内存对齐

1)硬盘对齐内存对齐粒度不相等
  • 我们打开xp上的notepad.exe,因为编译器很老的原因,它选择了节省硬盘放弃查找速度(因为以前硬盘的存储空间很小,如果都按照在内存中这样空隙这么大的原样存储到硬盘上,就会很浪费),所以它规定内存对齐粒度为1000h硬盘对齐粒度为200h,这样就达到了节省硬盘空间的效果(硬盘对齐又称为文件对齐

    比如如果一个数据段在内存中的存储起始地址为0x100000,大小为0x150字节,由于为这一段大小没有1000h字节,那么下一个数据段只能空出来850h大小的内存,从0x101000地址开始存储,后面以此类推

    image-20211220152426484
  • 优点:节省了硬盘空间

  • 缺点:减低了硬盘、内存之间数据相互传输的读写速度,因为内存和硬盘的对齐粒度不同,需要换算,定位查找就需要花费一定的时间

2)硬盘对齐内存对齐粒度相等
  • 现在科技的发展,硬盘的存储空间很大,编译器为了更快的追寻读写速度,舍弃一点硬盘的存储空间,规定内存对齐粒度和硬盘对齐粒度都是1000h。此时文件在内存中和硬盘中的存储状态如下图:

    image-20211220154303212
  • 优点:由于对齐粒度一样了,当把文件从硬盘装入到内存中时可以省去很多运算,只需要确定好首地址

  • 缺点:浪费了一定的硬盘空间

  • 误区:如果一个可执行文件的文件对齐和内存对齐相同的话,不是说不管在硬盘上还是文件中它的每一个节的起始偏移地址都是相同的,即原封不动的从硬盘到内存/从内存到硬盘。比如ipmsg.exe飞鸽程序,它的文件对齐粒度和内存对齐粒度都是0x1000,我们发现此程序有四个节,第一个节在文件还是内存中的偏移起始地址都是0x1000,第二个节在文件还是内存中的偏移地址都是0x22000;第三个节同理;但是第四个节会发现在文件和内存中的偏移起始地址不一样了,原因是第三个节中可能含有未初始化数据,内存中大小(VirtualSize)会大于文件中对齐后的大小(SizeOfRawData),那么内存中第三个节起始偏移地址0x27000 + 内存中大小0x884C = 0x2F84C,由于要满足内存对齐,则第四个节应该从0x30000偏移地址处开始存储;而硬盘上第三个节起始偏移地址0x27000 + 文件对齐后大小0x5000 = 0x2C000,所以第四个节应该从0x2C000偏移地址处开始存储。所以即使一个可执行文件的内存对齐和文件对齐一样,他们也不是原封不动的照搬,千万不能有这个误区,还是要具体分析

    现在看理解不了没事,后面学到day34再转头来看看

    image-20211230165935816

4.记录文件信息的PE结构

  • PE文件中有很多个节,那每个节在文件中从哪里开始?有多大?在内存中从哪里开始,有多大?由谁记录呢?PE已经规定了这些节的相关信息都记录在节表

    每一个节都有一个对应的节表,只是这些节表是挨着存放在一个指定的区域的,所以广义上我们称这片区域为节表

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

  • 所以一个文件的PE结构除了有存储数据的节,还要有存储文件和节的相关信息及特征的结构

    image-20211220161007363

5.PE文件结构组成部分

  • 综上那么一个文件完整的PE结构,如下图:

  • 即一个可执行文件虽然是一堆二进制数,但是这些数应该满足PE结构,即所有的可执行文件都是有DOS头,NT头,节表,.idata节,.text节,.data节等节这几部分构成

  • NT头又是由三部分组成:PE签名,标准PE头(PE文件头),可选PE头

  • 每一个结构都是由一定或者不定的长度数据组成,这些结构中也规定了哪个或几个字节的数据表示的含义是什么

    image-20211230172836719

三、手动解析DOS头和NT头

今天我们先解析DOS头和NT头部分

image-20211220164126442

1.DOS头与NT头整体框架

  • DOS头和NT头中指定位置和宽度的数据都规定了不同含义,图中左边一列地址是相对地址,即这个字段的地址是相对于DOS头或者PE文件头起始地址的地址

    比如文件开始的地址0x0(逻辑地址)往后数2字节(WORD)宽度的数据就表示e_magic;接着从0x02开始往后的两个字节数据表示的信息是e_cblp,以此类推

  • NT头是由三部分组成:PE签名,PE文件头,PE可选头。图中为了更清晰的区分开不同的PE组成部分,就单独把PE文件头和PE可选头拿出来展示,其实是很顺溜的往下排的,即NT头中,PE签名字段完了就是PE文件头,PE文件头完了紧跟着就是PE可选头

    可以理解成NT_HEADERS结构体中的成员有4字节的Signature、FILE_HEADER结构体、OPTIONAL_HEADER结构体。所以NT头中的字段应该为:Signature、Machine、NumberOfSections…Characteristics、Magic…

    8E3ECD53953937E897837F57304985FC

2.找出DOS头数据并计算大小

1)DOS头的作用
  • 我们解析一个exe文件时会看最开始的两个字节(e_magic)是不是4D 5A(MZ)

  • 找到DOS头的最后4字节数据(e_Ifanew),去找真正的PE文件开始的地址

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

  • 从DOS头结尾到PE签名(即NT头开始)之间,为什么留出来一些空间?

    • 不同的编译器会往里塞一些不同的数据,大小和内容都是不同的,取决于编译器,而且程序也不会使用到这块空间。但对于我们来说其实就是一些垃圾数据,想往里放什么就放什么,且大小是不确定的,但是我们也可以在这做手脚
    • 那么既然想放什么放什么,我们其实可以往里赛我们想塞的数据,既然属于PE文件的一部分,那么这段空间肯定也会随着文件一起装入内存中,既然装入内存中了,就有了分配的内存地址,那么就可以想办法让程序去访问这个地址中的数据,所以即使程序自身运行时不会使用这块空间,但是我们可以想办法访问(想想函数指针那里)
2)手动解析DOS头
  • 我们将ipmsg.exe程序用winhex打开,来分析数据,找出DOS头

    注意:winhex显示的文件数据是按不同含义的字段宽度顺序存的,但是每一个含义的字段数据内容是以字节为单位倒着存的。比如DOS头中前两个字节先存e_magic表示的字段,接着再顺序存e_cblp表示的字段。但是每一个含义的字段数据是倒着存的,比如最开始的WORD宽度数据表示e_magic,内存中显式4D 5A,我们应该读作0x5A 4D;再比如我们知道DOS头部的最后四个字节表示e_Ifanew,内存中显式E0 00 00 00,我们应该读作0x00 00 00 E0

    image-20211230173147154
  • DOS头的结构如下(大小为64字节,十六进制为0x40)

    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
        0x24 WORD e_oemid;       //0x0000
        0x26 WORD e_oeminfo;     //0x0000
        0x28 WORD e_res2[10];    //0x0000000000000000000000000000000000000000
        0x3c DWORD e_lfanew;  *  //0x000000e0    表示真正的PE文件开始地址为0xe0,即PE签名所在地址
    };
    

3.找出NT头数据

以notepad.exe文件的PE结构中的数据举例

0)NT头组成
  • NT头由三部分组成:PE签名 + PE文件头 + PE可选头

    struct _IMAGE_NT_HEADERS {
        0x00 DWORD Signature;
        0x04 _IMAGE_FILE_HEADER FileHeader;      //结构体中还可以是结构体类型的数据
        0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
    };
    
1)找PE签名
  • PE签名(4字节

    0x00 DWORD Signature;  //0x00004550 即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可选头(大小不确定,需要根据标准PE头中的SizeOfOptionalHeader的值来判断)

    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]; //这个先不分析
    };
    
第1讲:2015-01-12(进制01) 第2讲:2015-01-13(进制02) 第3讲:2015-01-14(数据宽度-逻辑运算03) 第4讲:2015-01-15(通用寄存器-内存读写04) 第5讲:2015-01-16(内存寻址-堆栈05) 第6讲:2015-01-19(EFLAGS寄存器06) 第7讲:2015-01-20(JCC) 第8讲:2015-01-21(堆栈图) 第8讲:2015-01-21(宝马问题) 第9讲:2015-01-22(堆栈图2) 第10讲:2015-01-23(C语言01_后半段) 第10讲:2015-01-23(C语言完整版) 第11讲:2015-01-26(C语言02_数据类型) 第12讲:2015-01-27(C语言03_数据类型_IF语句) 第13讲:2015-01-28(C语言04_IF语句逆向分析上) 第14讲:2015-01-28(C语言04_IF语句逆向分析下) 第15讲:2015-01-29(C语言04_正向基础) 第16讲:2015-01-30(C语言05_循环语句) 第17讲:2015-02-02(C语言06_参数_返回值_局部变量_数组反汇编) 第18讲:2015-02-02(2015-01-30课后练习) 第19讲:2015-02-03(C语言07_多维数组) 第20讲:2015-02-03(2015-02-02课后练习) 第21讲:2015-02-04(C语言08_结构体) 第22讲:2015-02-05(C语言09_字节对齐_结构体数组) 第23讲:2015-02-06(C语言10_Switch语句反汇编) 第24讲:2015-02-26(C语言11_指针1) 第25讲:2015-02-27(C语言11_指针2) 第26讲:2015-02-28(C语言11_指针3) 第27讲:2015-02-28(C语言11_指针4) 第28讲:2015-03-02(C语言11_指针5) 第29讲:2015-03-03(C语言11_指针6) 第30讲:2015-03-04(C语言11_指针7) 第31讲:2015-03-06(C语言11_指针8) 第32讲:2015-03-09(位运算) 第33讲:2015-03-10(内存分配_文件读写) 第34讲:2015-03-11(PE头解析_手动) 第35讲:2015-03-12(PE头字段说明) 第36讲:2015-03-13(PE节表) 第37讲:2015-03-16(FileBuffer转ImageBuffer) 第38讲:2015-03-17(代码节空白区添加代码) 第39讲:2015-03-18(任意节空白区添加代码) 第40讲:2015-03-19(新增节添加代码) 第41讲:2015-03-20(扩大节-合并节-数据目录) 第42讲:2015-03-23(静态连接库-动态链接库) 第43讲:2015-03-24(导出表) 第44讲:2015-03-25(重定位表) 第45讲:2015-03-26(移动导出表-重定位表) 第46讲:2015-03-27(IAT表) 第47讲:2015-03-27(导入表) 第48讲:2015-03-30(绑定导入表) 第49讲:2015-03-31(导入表注入) 第50讲:2015-04-01(C++ this指针 类 上) 第51讲:2015-04-01(C++ this指针 类 下) 第52讲:2015-04-02(C++ 构造-析构函数 继承) 第53讲:2015-04-03(C++ 权限控制) 第54讲:2015-04-07(C++ 虚函数表) 第55讲:2015-04-08(C++ 动态绑定-多态-上) 第56讲:2015-04-08(C++ 动态绑定-多态-下) 第57讲:2015-04-09(C++ 模版) 第58讲:2015-04-10(C++ 引用-友元-运算符重载) 第59讲:2015-04-13(C++ new-delete-Vector) 第60讲:2015-04-14(C++Vector实现) 第61讲:2015-04-15(C++链表) 第62讲:2015-04-16(C++链表实现) 第63讲:2015-04-16(C++二叉树) 第64讲:2015-04-17(C++二叉树实现) 第65讲:2015-04-20(Win32 宽字符) 第66讲:2015-04-21(Win32 事件-消息-消息处理函数) 第67讲:2015-04-22(Win32 ESP寻址-定位回调函数-条件断点) 第68讲:2015-04-23(Win32 子窗口-消息处理函数定位) 第69讲:2015-04-24(Win32 资源文件-消息断点) 第70讲:2015-04-27(Win32 提取图标-修改标题) 第71讲:2015-04-28(Win32 通用控件-VM_NOTIFY) 第72讲:2015-04-29(Win32 PE查看器-项目要求) 项目一:PE查看器 开发周(5天) 需求文档 第73讲:2015-05-07(Win32 创建线程) 第74讲:2015-05-08(Win32 线程控制_CONTEXT) 第75讲:2015-05-11(Win32 临界区) 第76讲:2015-05-12(Win32 互斥体) 第77讲:2015-05-13(Win32 事件) 第78讲:2015-05-14(Win32 信号量) 第79讲:2015-05-15(Win32 线程同步与线程互斥) 第80讲:2015-05-18(Win32 进程创建_句柄表) 第81讲:2015-05-20(Win32 以挂起形式创建进程) 第82讲:2015-05-21(Win32 加密壳_项目说明) 项目二:加密壳 开发周(5天) 需求文档 第83讲:2015-05-28(Win32 枚举窗口_鼠标键盘事件) 第84讲:2015-05-29(Win32 CE练习) 第85讲:2015-06-01(Win32 OD练习) 第86讲:2015-06-03(Win32 ShellCode_远程线程注入) 第87讲:2015-06-04(Win32 加载EXE_模块隐藏) 第88讲:2015-06-09(Win32 IAT_HOOK) 第89讲:2015-06-10(Win32 InlineHook) 第90讲:2015-06-11(Win32 进程通信) 第91讲:2015-06-11(Win32 进程监控_项目说明) 项目三:进程监控 开发周(5天) 需求文档 第92讲:2015-06-15(硬编码_01) 第93讲:2015-06-16(硬编码_02) 第94讲:2015-06-17(硬编码_03) 第95讲:2015-06-18(硬编码_04) 第96讲:2015-06-19(硬编码_05)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值