PE结构详解(一)

本系列主要是参考英文原本ARTeam的PE File Format Tutorial并加以注解,以最简洁的语言来阐述PE格式,帮助大家快速入门。在开始之前,请确定您懂得C语言,至少是基本数据类型、数组和结构体,以及会WinHEX的基本使用方法。任何错误都欢迎指出,感激不尽!
(一)介绍PE 格式

    PE格式(Portable Executable,可移植可执行文件)是原生的Win32文件格式,任何的32位应用程序(Executable)包括.NET、动态链接库(DLL)、控件(OCX)以及控制面板程序(CPL)均为PE格式,其实NT内核ntoskrnl.exe也是PE格式。

    我们研究PE格式的目的有:一是给应用程序添加代码功能,比如注册机代码注入(Keygen Injecton);二是手动给EXE脱壳。

    现在很多程序都加“壳”,一来是减少映像文件尺寸,二来可以保护程序文件不被轻易修改。被加壳的EXE一般它的输入表(import table)与数据区段(.data)都被加密过,并且被插入了一段脱壳的代码,当程序执行时会先执行这段代码给内存中的数据解密,接着会修复输入表和各个区段,然后再跳到原始入口点执行。

    原文中使用的范例程序是delphi写的进制转换程序,本文中我使用的是VC9编写的一个很简单的数组求和程序,在debug模式下编译。这并不影响我们学习PE格式。

    首先两个概念,PE文件被加载器加载到内存后被称为一个模块(module),存储模块首地址的称为模块句柄(handle of module),简写为HMODULE。

    然后我们看一下PE格式的总体结构(这张图MS已经遍布全球了):


                                PICTURE MISSING

    前4块我们后面会细讲,就区段来说(Section),一个PE文件至少要有2个区段,代码区段用来存储程序代码,以及数据区段用来存储各种数据。NT为PE预定义了9个区段:.text,.bss,.rdata,.data,.rsrc,.edata,.idata,.pdata,.debug。一个PE既可以选择其中的几个区段,也可以自己定义额外的区段满足特别需要。

    一般来说,.text是代码区段,.data、.rdata、.bss是数据区段,.rsrc是资源区段,.edata与.idata分别是输出输入表区段,.debug是调试信息区段。这些名字(.text, .data…)实际上是给程序员看着方便的,执行程序时,系统会完全忽略它们。

    最后一点,就是PE文件在硬盘中的结构顺序和当它们被加载到内存中后的顺序并非一样,记录硬盘上排列的是File Alignment域(域是含有特定信息的二进制片段),记录内存中排列的是Section Alignment域,这既是PE加载器调整的结果,也是虚拟内存机制的作用。关于Windows虚拟内存机制,请参考相关资料,或者我的另一篇文章。就是有一点要注意的是,PE文件中的每一个区段总是在一个新的页面中存放的。

(二)DOS MZ header

一个PE程序总是以一个64字节的DOS header结构开头,目的就是为了如果程序在DOS中运行,DOS会识别它为正确的EXE并进而运行DOS stub,它的作用就是输出字符串”This program cannot run in DOS mode.”然后退出。

    下面就是DOS  Header在C语言中的结构表示(来自MinGW的winnt.h)

typedef struct _IMAGE_DOS_HEADER {

    WORD e_magic;               // Magic DOS 签名 MZ (4Dh 5Ah)

    WORD e_cblp;                // 文件最后一页的字节数

    WORD e_cp;              // 文件中的页数

    WORD e_crlc;                // 重定位的个数

    WORD e_cparhdr;             // 段落中头的长度

    WORD e_minalloc;            // 额外段落需要的最小分配值

    WORD e_maxalloc;            // 额外段落需要的最大分配值

    WORD e_ss;              // 初始SS寄存器值

    WORD e_sp;              // 初始SP寄存器值

    WORD e_csum;                // 校验和

    WORD e_ip;              // 初始IP值

    WORD e_cs;              // 初始CS值

    WORD e_lfarlc;          // 重定位表在文件中的地址

    WORD e_ovno;                // 覆盖数量

    WORD e_res[4];          // 保留字

    WORD e_oemid;           // OEM标识符

    WORD e_oeminfo;             // OEM信息

    WORD e_res2[10];            // 保留字

    LONG e_lfanew;          // PE header的起始偏移量

} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;

    首先声明, WORD表示unsigned short int(2bytes), 而LONG和后面会出现的DWORD表示unsigned int(4 bytes),其定义符合C99标准。

    我们只关心其中两个成员,e_magic是Magic Number,它总是等于0x4d5a,也就是MZ这两个字符,这是为了几纪念Mark Zbikowsky,MS-DOS设计者之一。

    e_lfanew指向的是PE header,这才是PE文件真正的开始,Win32EXE加载器会读取其中的地址并找到PE header,DOS stub因此被跳过。我们可以来实践一下下,用WinHEX打开我们的范例程序testPE.exe。


                                    PICTURE MISSING   


    可以看到前两个byte,确实是MZ表示一个合法的EXE文件,图中的前四行(0h~3Fh)共64字节即为IMAGE_DOS_HEADER结构,e_lfanew是最后四个字节,从图中我们可以读出,位于3Ch~3Fh的依次是D8,00,00,00,因为机器中整型存放遵循高位在高地址处的原则,所以实际的值是00,00,00,D8,我们顺势找到D8h,马上就发现了PE两个字,从这里开始便是真正的PE header。那么位于40h到D7h 的东西从PE结构图就可以看出是DOS stub,其实就是一段汇编代码,我们就不管了。

(三)PE header

PE header对应C中的IMAGE_NT_HEADER32结构,如下:

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                // PE Signature PE.. (50h 45h 00h 00h)
    IMAGE_FILE_HEADER FileHeader;               // PE的文件头信息
    IMAGE_OPTIONAL_HEADER OptionalHeader;               // 可选头
} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
再是真实PE文件的截图:
                            PICTURE MISSING
    (一)中我们已经得到PE header的起始地址D8h,直到DBh,共4个字节,就是结构中的Signature成员,数据为50h 45h 00h 00h,对应值为00 00 45 50h(注,以后凡说到值,均指按高位在高地址的原则倒过来后的实际值,不再说明),对应winnt.h中的常数IMAGE_NT_SIGNATURE,如果Signature等于454Ch表示是IMAGE_VXD_SIGNATURE,即Win3.X中的Virtual Device Driver;等于454Eh表示IMAGE_OS2_SIGNATURE,即OS2的程序等等,请自行查阅winnt.h。
IMAGE_NT_HEADERS32结构只有三个成员,比较简单,但是展开后两个成员的结构体IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER,就会让人绝倒了。。。我们首先看一下IMAGE_FILE_HEADER结构:
typedef struct _IMAGE_FILE_HEADER {
    WORD Machine;               // 支持机器的类型
    WORD NumberOfSections;              // 区段的个数
    DWORD TimeDateStamp;            // 连接器创建EXE时的日期时间
    DWORD PointerToSymbolTable; //  旧文件中,COFF符号表地址,没有是0
    DWORD NumberOfSymbols;          // COFF符号表中符号的个数
    WORD SizeOfOptionalHeader; //   Optional header的长度 (32位EXE中是224字节)
    WORD Characteristics;           // 见下面的说明
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
    结合图,分析如下:
    Machine值为014Ch,对应IMAGE_FILE_32BIT_MACHINE常数,即32位机器,其他值请查阅winnt.h。
    接着两个字节表示NumberOfSections区段的个数,值为0007h,也就是我们的testPE.exe有7个区段。
    SizeOfOptionalHeader位于ECh,EDh,值为00E0h,即后面的Optional Header结构总长为224字节。
    再看一个Characteristics,表示这个PE文件的类型,图中位于EEh和EFh,值为0102h,是使用二进制位来标记的,二进制为100000010 = IMAGE_FILE_32BIT_MACHINE | IMAGE_FILE_EXECUTABLE_IMAGE,表示该文件是32位的EXE文件,如果它的值是2XXXh,则表示是一个DLL,具体还请查阅winnt.h。(唉。。。这句话打的好累啊,以后就说自行查阅了)

(四)Optional header

特地把Optional header放一篇文章,是因为它比较复杂与庞大,也因为它比较重要。
         老样子,先是IMAGE_OPTIONAL_HEADER结构:
typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD Magic;                     // 又是Magic
    BYTE MajorLinkerVersion;        // 链接器主要版本
    BYTE MinorLinkerVersion;        // 链接器次要版本
    DWORD SizeOfCode;   // 所有代码区段的长度之和
    DWORD SizeOfInitializedData;        // 同上
    DWORD SizeOfUninitializedData;      // 同上
// 代码起始执行处,对DLL可选,没有是0
    DWORD AddressOfEntryPoint;
    DWORD BaseOfCode;       // 载入内存后代码的起始地址,相对ImageBase而言
    DWORD BaseOfData;       // 载入内存后代码的起始地址,相对ImageBase而言
    DWORD ImageBase;        // 期望的内存中映像载入地址
    DWORD SectionAlignment;         // 内存中区段排列基数
    DWORD FileAlignment;        // 磁盘上区段排列基数
// OS最低主要版本号
    WORD MajorOperatingSystemVersion;
// OS最低次要版本号
WORD MinorOperatingSystemVersion;
    WORD MajorImageVersion;         // 映像文件的主要版本号
    WORD MinorImageVersion;         // 映像文件的次要版本号
    WORD MajorSubsystemVersion;     //  子系统的主要版本号
    WORD MinorSubsystemVersion;     //  子系统的次要版本号
    DWORD Win32VersionValue;        //  由编译器定义,PE规定这里是保留位置
// 加载到内存中的映像文件所占内存的大小,一定是SectionAlignment的整数倍
    DWORD SizeOfImage;
    DWORD SizeOfHeaders;    // 所有头与区段表的长度之和
// 映像文件校验和,仅仅针对内核模式驱动和一些系统DLL
    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;
    为了方便比对,我把前面那张PE截图再贴一下:
                    PICTURE MISSING
    _IMAGE_OPTIONAL_HEADER占用从0F0h~14Fh,也就是图中第三行开始总共六行。
    同样,下面挑选一些有用的结构成员讲一下。
    Magic。位置F0~F1,值为010Bh。Winnt.h 为我们定义了如下三个常数:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
    也就是符合第一个,表示32位程序。最后一个是ROM,指刷在ROM里面的程序,反正我从来没见过。
    MajorLinkerVersion。也就是接下去的一个字节,图中是09h,说明我们的连接器版本是9.0(VC9),MinorLinkerVersion同理。
    SizeOfCode。值为00 00 36 00,即13824字节,指所有代码区段的长度和。
    SizeOfInitializedData。值为00 00 42 00,即16896字节,是所有已初始化数据的长度和。
    AddressOfEntryPoint。截图中的位置,100h~103h,就是右边有个x开始的那里,别找不到咯~值为00 01 10 78,程序将从地址为00011078h处的指令开始执行,也就是俗称的程序入口点。经过加壳的EXE通常入口点处先是一段解密程序,完了才会跳到原始入口点(OEP)真正执行。
    BaseOfCode与BaseOfData分别指代码区段与数据区段的基地址,自己看看是哪里?
    ImageBase。指最希望把PE文件载入到地址空间中的ImageBase地址开始的地方,如果这个地址已经被占用,那只好换地方了,但99%的情况下,ImageBase指向的地址肯定是空闲的。
    SectionAlignment与FileAlignment。位置为110~117,值分别为00 00 10 00和00 00 02 00。指的是任意区段的起始地址必须是SectionAlignment(或FileAlignment)中值的整数倍,不同的是,前者指在内存中,后者指在文件中。在内存中起始地址是1000h,即4096的整数倍,是因为Windows中一个虚拟内存页面的大小就是4096字节,这样就可以保证,一个新的区段一定会被分配到一个新的页面中,而不会出现两个区段在一个页面中的情况。同样,在磁盘上必须是200h,即512的整数倍,是因为磁盘一个扇区的大小就是512字节。
    SizeOfImage。PE文件被载入内存后所占所有内存空间的总和(包括各个区段)。位置在128~12B,值为00 01 B0 00,即110592字节,合108KB。告诉你,这个程序文件只有正好30KB大小,那又怎么会在内存中变大了呢?原因就是上面说的,在内存中区段地址是按4096的整数倍分配的,而在磁盘上是按512的整数倍,这样就必定会造成很多未使用的内存空间浪费,使占用变大。但是,这也是出于性能优化的角度考虑,没什么好说的。
    SizeOfHeaders。所有的头的长度总和,像什么DOS header,Optional header等等,再加上区段表(Section Table,最先的结构图里有)的长度,就是这个值。这里是1024字节,正好1K。注意:这里的Size指文件中(或磁盘中的)Size,而上面一个指内存中的,别混了。
    Subsystem。位置134h~135h,值为00 03,对着下面的常数,发现是Windows控制台程序(Windows CUI)。
#define IMAGE_SUBSYSTEM_UNKNOWN 0
#define IMAGE_SUBSYSTEM_NATIVE  1
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3
#define IMAGE_SUBSYSTEM_OS2_CUI     5  /* Not in PECOFF v8 spec */
#define IMAGE_SUBSYSTEM_POSIX_CUI   7
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS  8  /* Not in PECOFF v8 spec */
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI  9
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER  12
#define IMAGE_SUBSYSTEM_EFI_ROM 13
#define IMAGE_SUBSYSTEM_XBOX    14
    DllCharacteristics。DLL文件特征,只对作为DLL时有效。值为81 40,计算方法仍然是使用二进制位来标记。这里原文中在胡扯,后来查了MSDN,如下
#define IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE      0x0040 // 允许重定位
#define IMAGE_DLL_CHARACTERISTICS_FORCE_INTEGRITY   0x0080 // 强制完整性检查
#define IMAGE_DLL_CHARACTERISTICS_NX_COMPAT     0x0100 // 开启数据执行保护DEP
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION       0x0200 // 不被孤立
#define IMAGE_DLLCHARACTERISTICS_NO_SEH         0x0400 // 关闭结构化异常处理
#define IMAGE_DLLCHARACTERISTICS_NO_BIND        0x0800 // 不被绑定
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER     0x2000 // 这是个WDM Driver
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE  0x8000 //此映像能得知终端服务 
    由此,我们的这个程序是IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE | IMAGE_DLL_CHARACTERISTICS_NX_COMPAT | IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE,虽然这个参数没什么意义。
    DataDirectory。它是一个IMAGE_DATA_DIRECTORY类型的数组,共有IMAGE_NUMBEROF_DIRECTORY_ENTRIES个元素,一般IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16。每一个元素都指向PE文件内的一个很重要的数据结构,具体IMAGE_DATA_DIRECTORY的结构,我们下回讨论。

(五)IMAGE_DATA_DIRECTORY结构

上次文章因为实在太长,所以不得不把这么简单的一点内容单独成文。回顾一下,上次先说到PE header,然后说到其中的成员Optional header,它的最后一个结构成员DataDirectory,是一个含有16个IMAGE_DATA_DIRECTORY类型元素的数组,每一个元素都指向PE文件内的一个很重要的数据结构。
    首先列出IMAGE_DATA_DIRECTORY结构:(简单的结构就不注释了)
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;
    DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
    VirtualAddress。是所指向的数据结构的虚拟地址。
    Size。这个数据结构的大小。
    这一个结构占8个字节,16项一共占128字节。
    另外序号与目录的对应关系在winnt.h中定义如下:
#define IMAGE_DIRECTORY_ENTRY_EXPORT    0
#define IMAGE_DIRECTORY_ENTRY_IMPORT    1
#define IMAGE_DIRECTORY_ENTRY_RESOURCE  2
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
#define IMAGE_DIRECTORY_ENTRY_SECURITY  4
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE  7
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
#define IMAGE_DIRECTORY_ENTRY_TLS   9
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG   10
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT  11
#define IMAGE_DIRECTORY_ENTRY_IAT   12
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT  13
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR    14
    最后15是预留位置。
    下图便是testPE.exe的DataDirectory,我们选其中几项来看看。
                PICTURE MISSING
    图中其实很整齐地帮我们划分了每一个目录表项,每一行有明显分开的两组数据,每一组长即为8字节,也就是一个表项,这样看起来就方便很多。
    第一个表项,全零,由于对应的IMAGE_DIRECTORY_ENTRY_EXPORT,意味着testPE.exe没有DLL输出表(Export Table)。
    第二个是输入表(Import Table),显然不为0(不然还叫Windows程序啊),推断出VirtualAddress = 00 01 80 00,Size = 00 00 00 3C,有空自己挑个EXE去看一下去,以后我们会再遇到的,现在跳过。
    其实每一个表项都有挺实在的意思,看最后一个单词就都明白了。到这,PE header部分就全部结束了。下次讲的是区段表(Section Table)。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值