逆向工程 October 21, 2019

PE文件

Words count 8.4k Reading time 8 mins. Read count 1000000

PE文件的全称是便携式可执行文件,意为可移植的替换的文件,常见的EXE,DLL,OCX,SYS,COM都是PE文件,PE文件是Microsoft Windows操作系统上的程序文件(可能是间接被执行,如DLL)

基地址定义:当PE文件通过Windows加载器被装入内存后,内存中的版本被称作模块(Module)。映射文件的起始地址被称作模块句柄(hMoudule),可以通过模块句柄访问其他的数据结构。这个初始内存弟子就是基地址。
内存中的模块代表着进程从这个可执行文件中所需要的代码,数据,资源,输入表,输出表以及其他有用的数据结构所使用的内存都放在一个连续的内存块中,编程人员只要知道装载程序文件映像到内存的基地址即可。在32位系统中可以直接调用GetModuleHandle以取得指向DLL的指针,通过指针访问DLL module的内容,例如:
HMODULE GetmoduleHandle(LPCTSRT lpModuleName);
当调用该函数时,传递一个可执行文件或者DLL文件名字字符串。如果系统找到该文件,则返回该可执行文件的或者DLL文件映像加载到的基地址。也可以调用GetModuleHandle,传递NULL参数,则返回调用的可执行文件的基地址。
相对虚拟地址:在可执行文件中,有相当多的地方需要指定内存的地址。例如:引用全局变量时,需要指定它的地址。PE文件尽管有一个首选的载入地址(基地址),但是他们可以载入到进程空间的任意地方,所以不能依赖与PE的载入点。由于这个原因,必须有一个方法来指定一个地址而不是依赖于PE载入点。
为了在PE文件中避免有确定的内存地址,出现了相对虚拟地址(Relative Virtual Addres,简称RVA)的概念。RVA只是内存中的一个简单的相对于PE文件装入地址的偏移地址,它是一个“相对”地址,或者称位“偏移量”地址。例如:假设一个EXE文件从地址40000h处载入,并且它的代码区块开始于4010000h,代码区的RVA将是:
目标地址401000h ——转入地址400000h则RVA=1000h。
将RVA地址转换成真实地址,只需简单的翻转这个过程:将实际装入地址加上RVA即可得到实际的内存地址。顺便一提,在PE用语里,实际的内存地址被称作虚拟地址(Vritual Address,简称VA),另外也可以把虚拟地址想象为加上首选装入地址的RVA。不要忘了前面提到的装入地址等同于模块句柄,它们之间的关系如下:
虚拟地址(VA)=基地址(ImageBase)+相对虚拟地址(RVA)

1)MS-DOS头部

每个PE文件是以一个DOS程序开始的,有了它,一旦程序在DOS下执行,DOS就能辨别出这是个有效的执行体,然后运行紧随MZ header(后面会介绍)之后的DOS stub(DOS块)。DOS stub实际上是一个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This Program cannot be run in MS-DOS”。用户通常对DOS stub 不感兴趣,因为大多数情况下他们由汇编器自动生成。平常把DOS stub和DOS MZ头部合称为DOS文件头。

PE文件的第一个字节起始于一个传统的MS-DOS头部,被称作IMAGE_DOS_HEADER。其IMAGE_DOS_HEADER的结构如下(左边的数字是到文件头的偏移量):

        IMAGE_DOS_HEADER STRUCT 


{

+0h WORD e_magic   // Magic DOS signature MZ(4Dh 5Ah) DOS可执行文件标记 

+2h   WORD  e_cblp  // Bytes on last page of file

+4h WORD  e_cp   // Pages in file 

+6h WORD  e_crlc   // Relocations 

+8h WORD  e_cparhdr   // Size of header in paragraphs 

+0ah WORD  e_minalloc  // Minimun extra paragraphs needs 

+0ch WORD  e_maxalloc  // Maximun extra paragraphs needs 

+0eh WORD  e_ss// intial(relative)SS value  DOS代码的初始化堆栈SS 

+10h WORD  e_sp// intial SP value DOS代码的初始化堆栈指针SP 

+12h WORD  e_csum// Checksum 

+14h WORD  e_ip//intial IP value DOS代码的初始化指令入口[指针IP] 

+16h WORD  e_cs// intial(relative)CS valueDOS代码的初始堆栈入口 

+18h WORD  e_lfarlc// File Address of relocation table 

+1ah WORD  e_ovno//Overlay number 

+1ch WORD  e_res[4]// Reserved words 

+24h WORD  e_oemid//OEM identifier(for e_oeminfo) 

+26h WORD  e_oeminfo   //OEM information;e_oemid specific  

+29h WORD  e_res2[10]   //Reserved words 

+3ch DWORD   e_lfanew // Offset to start of PE header 指向PE文件头 

} IMAGE_DOS_HEADER ENDS

这个结构中有两字段很重要,一个是e_magic,一个是e_lfanew。e_magic(一个字大小)字段需要被设置为5A4Dh这个也是PE程序载入的重要标志,这个值非常有意思,他们对应的字符分别位Z和M,是为了纪念MS-DOS的最初创建者Mark Zbikowski而专门设置的,由于在hex编辑器中显示是由低位到高位故显示为4D5Ah,刚好是创建者的名字缩写。另一个字段是e_lfanew。这个字段表示的是真正的PE文件头部相对偏移地址(RVA),它指出了真正PE头部文件偏移位置。它占用四个字节,位于文件开始偏移的3ch字节中。

2)PE文件头文件

紧跟着DOS头文件下面的就是peheader。PEheader是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。执行体在支持PE文件结构的操作系统执行时,PE装载器将IMAGE_DOS_HEADER结构中的e_fanew字段找到PEheader的起始偏移量,加上基址得到PE文件头的指针:

PNTHeader=IMAGBase+dosHeader->e_lfanewr(其实就是去字段e_lfanew的值)

下面来讨论IMAGE_NT_HEADER的结构,它是由三个字段组成(左边的数字是PE文件头的偏移量):
STRUCT

{ 



+0h Signature DWORD //PE文件标志

+4h FileHeader IMAGE_FILE_HEADER  //文件头初始偏移地址

+18 optionalHeader IMAGE_OPTION_HEADER //另一个重要头部初始偏移地址



} IMAGE_NT_HEADER ENDS

Signature字段
这个字段是PE文件的标志字段,通常设置成00004550h,其ASCII码为PE00,这个字段是PE文件头的开始,前面的DOS_HEADER结构中的字段e_lfanew字段就是指向这里。

IMAGE_FILE_HEADER字段

这个字段也是包含几个字段结构,它包含了PE文件的一些基本信息,最重要的是其中一个域指出了IMAGE_OPTIONAL_HEADER的大小。
typedef struct _IMAGE_FILE_HEADER {

WORD Machine;//运行平台

WORD NumberOfSections;//文件的区块数目

DWORD TimeDateStamp;//文件创建的用时间戳标识的日期

DWORD PointerToSymbolTable;//指向符号表(用于调试)

DWORD NumberOfSymbols;//符号表中符号的个数

WORD SizeOfOptionalHeader;//IMAGE_OPTIONAL_HEADER32结构大小

WORD Characteristics;//文件属性

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

上图标出七个字段的位置及各自的值。

1)Machine字段,表示目标CPU 的类型。

几个常见的及其标识如下:

机器 标识

Intel I386 14ch

MIPS R3000 162h

Alpha AXP 184h

Power PC 1F0h

MIPS R4000 184h

根据以上信息我们知道这个PE文件要运行在Intel I386机器上。

2)NumberOfSection,标识区块的数目,关于区块后面会详细讲。

3)TimeDateStamp

这个字段没啥好说的,指的就是PE文件创建的事件,这个时间是指从1970年1月1日到创建该文件的所有的秒数。

4)PointerToSymbolTable。这个字段用的比较少,略

5)NumberOfSymbol。这个字段也用得很少,略

6)SizeOfOptionalHeader:紧跟着IMAGE_FILE_HEADER后面的数据大小,这也是一个数据结构,它叫做IMAGE_OPTIONAL_HEADER,其大小依赖于是64位还是32位文件。32位文件值通常是00EOh,对于64位值通常为00F0h。

7)Characteristics:文件属性,普通EXE文件这个字段值为010fh,DLL文件这个字段一般是0210h。

3)IMAGE_OPTIONAL_HEADER概述

这个结构是IMAGE_FILE_HEADER结构的补充。这个结构异常复杂,但真正我们用得到的其实不多。看下面的代码:
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+18hWORDMagic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1AhBYTE MajorLinkerVersion; // 链接程序的主版本号
+1BhBYTE MinorLinkerVersion; // 链接程序的次版本号
+1ChDWORD SizeOfCode; // 所有含代码的节的总大小
+20hDWORD SizeOfInitializedData;// 所有含已初始化数据的节的总大小
+24hDWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28hDWORD AddressOfEntryPoint;// 程序执行入口RVA
+2ChDWORD BaseOfCode; // 代码的区块的起始RVA
+30hDWORD BaseOfData; // 数据的区块的起始RVA
//
// NT additional fields.以下是属于NT结构增加的领域。
//
+34hDWORD ImageBase; // 程序的首选装载地址 !
+38hDWORD SectionAlignment; // 内存中的区块的对齐大小 !
+3ChDWORD FileAlignment; // 文件中的区块的对齐大小 !
+40hWORDMajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42hWORDMinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44hWORDMajorImageVersion; // 可运行于操作系统的主版本号
+46hWORDMinorImageVersion; // 可运行于操作系统的次版本号
+48hWORDMajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4AhWORDMinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4ChDWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
+50hDWORD SizeOfImage; // 映像装入内存后的总尺寸
+54hDWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
+58hDWORD CheckSum; // 映像的校检和
+5ChWORDSubsystem; // 可执行文件期望的子系统
+5EhWORDDllCharacteristics; // DllMain()函数何时被调用,默认为 0
+60hDWORD SizeOfStackReserve; // 初始化时的栈大小
+64hDWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68hDWORD SizeOfHeapReserve;// 初始化时保留的堆大小
+6ChDWORD SizeOfHeapCommit;// 初始化时实际提交的堆大小
+70hDWORD LoaderFlags;// 与调试有关,默认为 0
+74hDWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来// 一直是16
+78hDWORD DataDirctory[16]; !
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

最常用的是用后面叹号标注的

前面我们已经知道了PE文件头在40h的位置,则上面的偏移量推断IMAGE_OPTIONAL_HEADER字段的首个字段在40h+18h=58h的地方,我们还是用hexwrokshop打开那个PE文件。Ctrl+G打开转移窗口,输入58则找到了第一个字段位置,如下图:

下面是DataDirctory[16]即数据目录表的各个成员:

这张表的16个成员中第一个成员IMAGE_DIRECTORY_ENTRY_EXPORT(导出表)和第二个成员IMAGE_DIRECTORY_ENTRY_EXPORT(导入表)非常重要。
用Hexwrokshop打开文件,首先找到PE文件头位置,一般都是在载入起始位+3ch处,如下图所示。

找到了PE文件头的位置,接下来我们来找DataDirctory[16]各个成员位置。第一个成员输出表位于PE文件头+78h位置即100h+78h=178h处,如下图:

由于每个结构都占8个字节,所以可以知道输出表的其实位置在4000h处,大小为45h

输入表的位置位100h+80h=180h处,如下图:

由上图可知输入表的起始位置在3000h处,大小为52h。

查找输入表和输出表的方法还可以用peid或是lordpe

0%