本篇笔记以我个人能够理解为基础,尽可能将其写成其他人也能明白的笔记。如果发现其中存在错误,请务必指正。

范本与工具:010Editor & Notepad.exe & kernel32.dll

PE文件种类:

种类主拓展名
可执行EXE / SCR
驱动程序SYS / VXD
DLL / OCX / CPL / DRV
对象文件OBJ (但这并不是可执行的,在逆向分析中不怎么需要关心)

正经的PE结构头包括:

    DOS头(DOS header) & DOS存根(Dos Stub) & 节区头(Section header) & NT头(NT header)

    其中,NT头包括了 文件头 与 可选头 。而节区头包括了 .text / .bss / .rdata / .data / .rsrc / .edata / .idata / .pdata / .debug 这九个预定义段,其分别规定了不同区块的访问权限、特性等内容。但并不是说每个应用程序都一定要规规矩矩的保留这些义段,对于那些用不到的区段是在程序中没有的,这一点可以自行打开程序确认。

(比如:Notepad.exe只有 .text / .data / .rsrc 这三个义段和节区)

(节区头的作用:PE文件包含多个节区,其包括了 Code节区 / Data节区 / Resource节区 等诸多节区,正因为节区之间相互区分,所以需要规定好程序可以对 一个节区做些什么 ,因此需要在节区头中去规定。所以这些义段和节区是一一对应的关系。)

    PE头的详细内容将在下面写出,但在此之前,我觉得有必要先介绍一下VA,RVA等内容。以下也是些一概而论的东西,细节都将在之后解释。

    VA(Virtual Address):虚拟地址。

    RVA(Relative Virtual Address):相对虚拟地址

    FOA(File Offset Address):文件偏移地址。但是在很多地方并不这么称呼,他们会用FA,RAW来称呼FOA,实际上是一个东西。

    Image Base:模块地址。指可执行文件加载到内存的时候所在的位置。

    虚拟地址间的关系:

RVA+Image Base = VA

    在很多时候,将一个程序加载到内存的时候,他的实际物理地址是不确定的。但文件总不会自己去寻址,必须要有人事先告诉他将要调用的函数在什么地方,如果用实际地址去描述的话,将会变得十分困难。为解决这个问题,人们构造出了“虚拟地址”的概念。将一个文件载入内存的时候,不管他被载入到了什么地方,都将其头地址映射到一块规定大小的虚拟地址空间(虚拟内存空间的大小可能比实际加载进内存所用的大小还大),之后在调用任何一个函数的时候,都只需要访问虚拟地址即可。

    但实际在访问的时候,也不是直接访问虚拟地址(特别是对DLL等动态链接库),而是利用RVA来访问。比方说初始位置在0x1000,而某个函数在0x1400,则在访问该函数的时候通过0x1000+0x400来访问(RVA即是指0x400)。之所以这样,还是因为PE文件加载进内存的时候,也可能发生“当前位置已经被占用”的问题,但加载必然是按顺序进行的,所以相对位置不会发生变化。

    (注:我觉得这样解释还是有些晦涩,所以再换了一种说法————将一个文件加载进内存,但现在我们无法知道其实际地址被放到了哪里。但我们一定清楚,我们想要调用的函数在文件开头往下找0x400的地方,那么程序在访问的时候将虚拟地址基址加上这个RVA就能找到实际的虚拟地址,然后再映射回去就能到达实际的物理地址。)

    接下来将详细对PE头的内容进行介绍,这里用Notepad.exe来示范。将其用010Editor打开(用Hex Editor也行,但010的自动识别功能会在这里提供很大的方便,对我来说减少了很多不必要的烦恼......)

    如图,010会将上述的PE结构头全都识别出来,并标好位置等。这将为接下来的介绍减少很多不必要的检索操作。

DOS头

    对应IMAGE_DOS_HEADER。在Microsoft Platform SSDK-winnt.h中可以找到他的成员,实际上就是一个C语言中的结构体。(通常是64字节的大小,但一些可以为缩减而设计的PE文件惊人的小,整个PE文件都只有97字节。但那都是特例,在学习过程中,我们可以权且将PE头每个部分都当作固定长度的结构体理解,不需要在意那些特例)

(注:结构体代码放在结尾,其成员在下图可见)

    

    MZSignature:DOS签名(4D5A经过ASCII值转换会为“MZ”,但图中写的是5A4D,这与Intel系列的CPU储存方式有关,该方法被称为“小端序标识法”,具体内容可自行搜索了解,在汇编的学习过程中,教科书上通常也会有介绍)。在一些书中,作者将把这一栏称之为e_magic(原因出自于结构体定义的时候写下的名称,但几经迭代后可能就变得不一样了)。另外,MZ取自DOS可执行文件设计者的名字首字母

    AddressOfNewExeHeader:指示NT头的偏移(不同文件可能有不同的值,也被称之为e_lfanew),但注意,其数值应为000000E0(小端序)。

DOS存根:

    比较特殊的一项,即便没有这个结构体,程序也能在Windows下运行。但在DOS环境下,将会执行DOS存根中保留的代码。在本例中,将其在DOS环境下将会输出“This program cannot  be run in DOS mode”后退出(具体的执行方式可以查看其汇编代码)。(所用用这个特性也能做很多乱七八糟的事情,比如在EXE文件中创建另一个文件,然后支持DOS和Windows两个环境等)

NT头:(大小为F8)

    Signature:签名。(同DOS签名相似,其数值经ASCII转换后为"PE")

    IMAGE_FILE_HEADER文件头:(FileHeader)

    Machine:每个CPU都有唯一的Machine码,算是一种规定。

#define IMAGE_FILE_MACHINE_I386 0x14c // Intel 386.

    诸如这样的定义,其表示兼容32位的Intel x86芯片。Notepad中的Machine码即位14C。类似的定义还有很多很多,细节可自查。

    NumberOfSections:用于指出文件中存在的节区数量。(如果实际的节区数和这里记录的不一样,运行的时候会出错)

    SizeOfOptionalHeader:用于指出IMAGE_OPTIONAL_HEADER32结构体的长度。(其实这一项是给PE装载器看的,结构体的长度都是固定好了的,不会因为这一项数值改变而改变)

    Characteristics:用于标识文件的属性。这一栏的属性比较不好逐个说明,详细的内容放在最后的附录里面,可自行对照每一栏的用处。

    TimeDataStamp:标识文件被编译器创建的时间。(应该是没太大用处的一项)

    IMAGE_OPTIONAL_HEADER32可选头:(OptionalHeader)

    

    这一栏太大了,以至于我没办法一张屏幕把全部都包括进图里......

    Magic:标识32位与64位的标记(10B——32位,20B——64位)。

    AddressOfEntryPoint:EP(EntryPoint)的RVA值。指出最先执行的代码的位置。

    ImageBase:指出文件的优先装入地址(32位的虚拟内存的范围在0~FFFFFFFF,不同类型的文件回被写入不同的值。在执行的时候,PE装载器创建进程后,将会把EIP寄存器的值设定为ImageBase+AddressOfEntryPoint)

    SectionAlignment / FileAliganment:前者指定了节区在内存中的最小单位,后者指定了节区在磁盘中的最小单位。(磁盘文件或内存的节区大小一定和这二者成整数倍)

    SizeOfImage:指定PE Image在虚拟内存中所占的空间的大小。

    SizeOfHeaders:用于指出整个PE头的大小。

    Subsystem:标识文件的类型。

含义备注
1Driver系统驱动(如:ntfs.sys)
2GUI窗口应用程序(如:notepad.exe)
3GUI控制台应用程序(如:cmd.exe)

    NumberOfRvaAndSize:指定DataDirectory数组(本例中也叫DataDirArray)的个数。

    DataDirectory(DataDirArray):这些数组里只有两个元素,VirtualAddress和Size。这些内容能够用于计算RAW的实际地址。

IMAGE_SECTION_HEADER节区头:

    能够规定不同节区的特性、访问权限等内容。同样按照数组的方式排列。一个单元对应一个节区。

    VirtualAddress:内存中节区的起始地址

    VirtualSize:内存中节区的大小

    SizeOfRawData:磁盘文件中节区所占的大小

    PointerToRawData:磁盘文件中节区的起始位置

    Characteristics:节区属性

    其中,VA和PTRD(都是简写)不带任何值,由SectionAlignment和FileAlignment决定。

RVA to RAW:

RAW - PointerToRawData = RVA - VirtualAddress

    公式如上。在了解了以上信息后,即可通过该公式计算出RAW的值了。

    范例:以Notepad.exe为例。在节区头的第一个单元中可找到VA=1000h,以及PointerToRawData=400h。

    而RVA在DataDirArray中IMAGE_DATA_DIRECTORY Import中可见。其值为7604h。最后得出RAW=6A04h

    (可能会有人和我一样开始疑惑为什么RVA是这个值。事实上这个值是随意规定的,这个公式的目的是“我知道RVA,现在想计算RAW”,所以其实可以随意设定RVA值。但有必要说明的是,不同的RVA值会处在不同的节区中,例如RVA=5000就在.text节区中,所以才到节区头中的第一个单元找VirtualAddress和PointerToRawData)

    (如果你直接在010中转到6A04这个位置,你会发现它确实对应了了comdlg32.dll的数据块起始位置)

动态链接库DLL

    加载DLL的方式主要有两种——“显示链接”(用到时加载,用完就释放)和“隐式链接”(程序开始时加载,程序结束时释放)。而IAT提供的机制与隐式链接有关。如果使用OD或者x64dbg等反汇编软件打开范例,将在其调用函数的时候发现其写法套用了两层(call 1001104,而1001104处的值为7C8107F0,然后才是7C8107F0地址处存放的函数)。其中,1001104是一个固定的值,但7C8107F0则根据操作系统的不同而出现差异,于是在加载程序的时候,PE装载器会将正确的地址装入1001104处,以保证程序在各种环境下都能够正常使用(这样做的理由很多,除了让其能在多平台兼容外,也有因为实际地址可能出现不同的原因存在)。

    

    以该链接库为例。

    库名称Name:在注释里就有标出。通过7990算处RAW后直接查找过去,也能找到comdlg32.dll的字符串。

    OriginalFirstThunk(INT):包含函数导入信息的结构体指针。通过相同的方法到达6D90可见多个指针。(这实际上是一个数组,以NULL结尾,所以到00000000的时候就算结束了)

    自7A7A开始,每4个字节代表了一个指针。如果跟入7A7A(算出的RAW为6E7A),就能找到函数的名称。(名称也是数组,同样用\0结尾。而000F为库内的函数的编号)

    导入地址表FirstThunk(IAT——Import Address Table):将12C4换为RAW=6C4,跟入。

    标蓝的区段即为IAT数组区域,对应了comdlg32.dll库。与INT类似,也用NULL结尾,以结构体指针为成员。

    但76344906这个指针没有实际意义,当程序加载的内存的时候,准确的地址值会取代这个数值(这其中大概是PE装载器做了很多,但我不太了解这个东西)。

EAT:

    IMAGE_EXPORT_DIRECTORY:

    NumberOfFunction:实际Export函数的个数

    NumberOfNames:Export函数中有名字的函数个数、

    AddressOfFunctions:Export函数地址数组

    AddressOfNames:函数名称地址数组

    AddressOfNameOrdinals:Ordinal地址数组

    实际上,从库中获取函数需要调用GetProcAddress()函数。以下为该过程的流程。

    首先,利用AddressOfNames成员转到函数名称位置。通过比较字符串的方法,查找到我们所想要的函数名称(这时候该数组的索引是name_index)。(可以假设我们在AddressOfNames[2]的位置找到了目标的名称,那么index=2)

    再利用AddressOfNameOrdinals数组找到对应的Ordinal值。(上一步找到了Index=2,AddressOfNameOrdinals[Index]=Ordinal,所以Ordinal=2)

    通过AddressOfFunctions和刚才获得的Ordinal值即可在AddressOfFunctions数组中获取目标函数的地址。(AddressOfFunctions[Ordinal]=目标函数的RVA)

最后是一些定义:

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;

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;

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;

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   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;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

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

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_TARGET_HOST       0x0001  // Useful for indicating we want to interact with the host and not a WoW guest.
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-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  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#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_ARM64             0xAA64  // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE