PE文件格式分析及修改

来源:互联网 发布:优化网络连接 编辑:程序博客网 时间:2024/05/15 08:44


PE 的意思是 Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行文件格式。它的一些特性继承自Unix的Coff(common object file format)文件格式。“Portable Executable”(可移植的执行体)意味着此文件格式是跨Win32平台的;即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。

  PE文件在文件系统中,与存贮在磁盘上的其它文件一样,都是二进制数据,对于操作系统来讲,可以认为是特定信息的一个载体,如果要让计算机系统执行某程序,则程序文件的载体必须符合某种特定的格式。要分析特定信息载体的格式,要求分析人员有数据分析、编码分析的能力。在Win32系统中,PE文件可以认为.exe、.dll、.sys 、.scr类型的文件,这些文件在磁盘上存贮的格式都是有一定规律的。

  一、PE格式基础

  下表列出了PE的总体结构

DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2

Section n

  一个完整的PE文件,前五项是必定要有的,如果缺少或者数据出错,系统会拒绝执行该文件如下图

                              

                                                                       图1 文件头格式错误

               

                                                                            图2 格式数据错误

                                

                                                                                              图3 代码错误

  DOS MZ header部分是DOS时代遗留的产物,是PE文件的一个遗传基因,一个Win32程序如果在DOS下也是可以执行,只是提示:“This program cannot be run in DOS mode.”然后就结束执行,提示执行者,这个程序要在Win32系统下执行。

    DOS stub 部分是DOS插桩代码,是DOS下的16位程序代码,只是为了显示上面的提示数据。这段代码是编译器在程序编译过程中自动添加的。

  PE header 是真正的Win32程序的格式头部,其中包括了PE格式的各种信息,指导系统如何装载和执行此程序代码。

  Section table部分是PE代码和数据的结构数据,指示装载系统代码段在哪里,数据段在哪里等。对于不同的PE文件,设计者可能要求该文件包括不同的数据的Section。所以有一个Section Table 作为索引。Section多少可以根据实际情况而不同。但至少要有一个Section。如果一个程序连代码都没有,那么他也不能称为可执行代码。在Section Table后,Section数目的多少是不定的。

  二、程序的装入

  当我们在explorer.exe(资源管理器)中双击某文件,执行一个可执行程序,系统会根据文件扩展名启动一个程序装载器,称之为Loader。Loader会首先检查DOS MZ Header,如果存在,就继续寻找PE header,如果这两项都不存在,就认为是DOS 16位代码,如果只存在DOS MZ Header,而其中又指示了而其中又指示了PE Header 的位置,那么Loader 就判定此文件不一个有效的PE文件,拒绝执行。

  如果DOS Header 和PE Header都正常有效,那么Loader就会根据PE Header 及Section Table的指示,将相应的代码和数据映射到内存中,然后根据不同的Section进行数据的初始化,最后开始执行程序段代码。

        三、PE格式高级分析

  下面我们以一个真实的程序为例详细分析PE格式,分析PE格式最好有PE分析器,常用的软件是Lord PE,也有其它的分析工具和软件如PE Editor 、Stud PE等。

  先分析一下磁盘文件的内容,这里我们使用UltraEdit32(UE)工具,这是一个实用的文件编辑器,可以编辑文本和二进制文件。

                   

                                                                         图4 PE文件开始的磁盘数据

  在文件的一开始有两位16进制数据4D 5A,其对应的ASCII字符是MZ,这个标志就是DOS MZ Header 的标志。下面是通过Load PE列出的DOS MZ Header

  1. DOS Header

数据结构名称

e_magic:

0x5A4D->‘MZ’

e_cblp:

0x0090

e_cp:

0x0003

e_crlc:

0x0000

e_cparhdr:

0x0004

e_minalloc:

0x0000

e_maxalloc:

0xFFFF

e_ss:

0x0000

e_sp:

0x00B8

e_csum:

0x0000

e_ip:

0x0000

e_cs:

0x0000

e_lfarlc:

0x0040

e_ovno:

0x0000

e_res:

0x0000000000000000

e_oemid:

0x0000

e_oeminfo:

0x0000

e_res2:

0x0000000000000000000000000000000000000000

e_lfanew:

0x000000F8

  这是一个PE文件的DOS Header,其中我们最关心的就是e_lfanew这个字段的值,它指向了PE Header 在磁盘文件中相对于文件开始的偏移地址,这里是F8。在本文件00F8h处果然找到了“PE”两个字符,那么在00F8h处就是PE Header 的有效头载荷。

  2. PE Header

  我们可以在winnt.h这个文件中找到关于PE文件头的定义:

typedef struct _IMAGE_NT_HEADERS
{
DWORD Signature;//PE文件头标志:PE/0/0。在开始DOS header的偏移3CH(e_lfanew)处所指向的地址开始
IMAGE_FILE_HEADER FileHeader;//PE文件物理分布的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//PE文件逻辑分布的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

  2.1 IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER

typedef struct _IMAGE_FILE_HEADER
{
WORD Machine;//该文件运行所需要的CPU,对于Intel平台是14Ch
WORD NumberOfSections;//文件的节数目
DWORD TimeDateStamp;//文件创建日期和时间
DWORD PointerToSymbolTable;//用于调试
DWORD NumberOfSymbols;//符号表中符号个数
WORD SizeOfOptionalHeader;//OptionalHeader 结构大小
WORD Characteristics;//文件信息标记,区分文件是exe还是dll
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
重要的 Characteristics值
#define IMAGE_FILE_RELOCS_STRIPPED 0001h // 文件中是否存在重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE 0002h // 文件是可执行的
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0020h // 程序可以触及大于2G的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0080h // 保留的机器类型低位
#define IMAGE_FILE_32BIT_MACHINE 0100h // 32位机器
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0400h // 不可在可移动介质上运行
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0800h // 不可在网络上运行
#define IMAGE_FILE_SYSTEM 1000h // 系统文件
#define IMAGE_FILE_DLL 2000h // 文件是一个DLL
#define IMAGE_FILE_UP_SYSTEM_ONLY 4000h // 只能在单处理器计算机上运行
#define IMAGE_FILE_BYTES_REVERSED_HI 8000h //保留的机器类型高位


typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic;//标志字(总是010bh)
BYTE MajorLinkerVersion;//连接器高版本号
BYTE MinorLinkerVersion;//连接器低版本号
DWORD SizeOfCode;//代码段大小
DWORD SizeOfInitializedData;//已初始化数据块大小
DWORD SizeOfUninitializedData;//未初始化数据块大小
DWORD AddressOfEntryPoint;//PE装载器准备运行的PE文件的第一个指令的RVA,若要改变整个执行的流程,

可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。DWORD BaseOfCode;//代码段起始RVA
DWORD BaseOfData;//数据段起始RVA
DWORD ImageBase;//PE文件的装载地址
DWORD SectionAlignment;//块对齐因子
DWORD FileAlignment;//文件块对齐因子
WORD MajorOperatingSystemVersion;//所需操作系统高位版本号
WORD MinorOperatingSystemVersion;// 所需操作系统低位版本号
WORD MajorImageVersion;//用户自定义高位版本号
WORD MinorImageVersion;//用户自定义低位版本号
WORD MajorSubsystemVersion;//win32子系统版本。若PE文件是专门为Win32设计的
WORD MinorSubsystemVersion;//该子系统版本必定是4.0否则对话框不会有3维立体感
DWORD Win32VersionValue;//保留值,系统没用到的,一般被作为是否感染的标志
DWORD SizeOfImage;//内存中整个PE映像体的尺寸
DWORD SizeOfHeaders;//所有头+节表的大小
DWORD CheckSum;//校验和
WORD Subsystem;//NT用来识别PE文件属于哪个子系统
WORD DllCharacteristics;// 用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记
DWORD SizeOfStackReserve;//
DWORD SizeOfStackCommit;//
DWORD SizeOfHeapReserve;//
DWORD SizeOfHeapCommit;//
//堆栈大小 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请。在默认情况下,

栈和堆都拥有1个页面的申请值以及16个页面的保留值
DWORD LoaderFlags;// 告知装载器是否在装载时中止和调试,或者默认地正常运行
DWORD NumberOfRvaAndSizes;// 该字段标识了接下来的DataDirectory数组个数。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等
}IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

2.2 IMAGE_DATA_DIRECTORY

typedef struct _IMAGE_DATA_DIRECTORY{DWORD VirtualAddress;//表的RVA地址DWORD Size;//大小} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

                              

                                                   图5 Load PE 读取的PE Header 的重要部分数据

                                    

                                                              图6 Subsystem 类型

                             

                                图7 Load Pe 读取的IMAGE_DATA_DIRECTORY 信息

  在IMAGE_OPTIONAL_HEADER32后部一般是16项IMAGE_DATA_DIRECTORY数据,其中最后一项是保留数据。每一项数据都有其固定的含义,并且位置不可改变。

Export Table 导出函数表,主要用于DLL中的导出函数Import Table 导入函数表,使用外部函数的数据表Resource  资源数据表Exception  异常处理表Security  安全处理数据表Relocation  重定位信息表,一般和DLL相关Debug   调试信息表Copyright  版权信息表Globalptr  机器值(MIPS GP)Tls Table  线程信息表LoadConfig  装配信息表BoundImport  输入函数绑定信息表IAT    也ImportTable对应,由Loader填写的输入函数地址DelayImport  延迟装入的函数信息COM    公用组件信息表Reserved  保留信息,系统没有使用,为以后扩展使用这16项数据,其所在位置由RVA指定,大小由Size指定。对于一般的一个可执行程序(.exe),

最重要的是导入表(Import Table和IAT)、资源数据表(Resoruce)。

  2.3 IMAGE_SECTION_HEADER

PE文件头后是节表,在winnt.h下如下定义
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text” 
//IMAGE_SIZEOF_SHORT_NAME=8
union {
DWORD PhysicalAddress;//物理地址
DWORD VirtualSize;//真实长度,这两个值是一个联合结构,可以使用其中的任何一个,
//一般是节的数据大小
} Misc;
DWORD VirtualAddress;//RVA
DWORD SizeOfRawData;//物理长度
DWORD PointerToRawData;//节基于文件的偏移量
DWORD PointerToRelocations;//重定位的偏移
DWORD PointerToLinenumbers;//行号表的偏移
WORD NumberOfRelocations;//重定位项数目
WORD NumberOfLinenumbers;//行号表的数目
DWORD Characteristics;//节属性 如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name:节名称 
VOffset:相对于ImageBase的虚拟偏移
VSize:实际大小
ROffset:相对于文件起始处的偏移
RSize所占的文件空间大小
Flags:节属性

                                      

重要的节属性定义:
#define IMAGE_SCN_CNT_CODE 00000020h // 节中包含代码
#define IMAGE_SCN_CNT_INITIALIZED_DATA 00000040h // 节中包含已初始化数据
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 00000080h // 节中包含未初始化数据
#define IMAGE_SCN_MEM_DISCARDABLE 02000000h // 是一个可丢弃的节,即节中的数据在进程开始后将被丢弃
#define IMAGE_SCN_MEM_NOT_CACHED 04000000h // 节中数据不经过缓存
#define IMAGE_SCN_MEM_NOT_PAGED 08000000h // 节中数据不被交换出内存
#define IMAGE_SCN_MEM_SHARED 10000000h // 节中数据可共享
#define IMAGE_SCN_MEM_EXECUTE 20000000h // 可执行节
#define IMAGE_SCN_MEM_READ 40000000h // 可读节
#define IMAGE_SCN_MEM_WRITE 80000000h // 可写节

  [注]

  RVA:虚拟偏移地址。RAV是指的某一处由Loader装入内存后,这一处应该在虚拟内存的什么地方,RAV也称为虚拟偏移地址。

  Alignment:对齐因子。与对齐因子相关的值有2个地方,一处是文件对齐因子,另一处是内存对齐因子。对齐因子指示出某一类型的对齐方式,以文件对齐为例,如果Alignment 为200h,说明文件中的内容是以200h为单位的,如果数据大小正好是200h的整数倍,则不存在对齐问题,如果数据大小是非200h的整数倍,则要使用Alignment 对数据所占的空间进行修正,取其上限数值(如310h->400h),使其所占的空间是200h的整数倍。

  2.3 Improt Table 和IAT

  IMAGE_DATA_DIRECTORY的第2项和第13项,指示导入表和导入函数地址表的位置。这部分对于一个PE文件相当重要,很多系统函数都是由此导入。

  Import Table 的VirtualAddress指向了一个RVA,他是一个导入表结构数组,数组以全0作为结束标记,该结构定义如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;// 指向一个 IMAGE_THUNK_DATA 结构数组的RVA
}
DWORD TimeDateStamp;// 文件生成的时间
DWORD ForwarderChain;// 这个数据一般为0,可以不关心
DWORD Name1;  // RVA,指向DLL名字的指针,ASCII字符串
DWORD FirstThunk; //指向一个 IMAGE_THUNK_DATA 结构数组的RVA,这个数据与IAT所指向的地址一致
}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR
IMAGE_THUNK_DATA 这是一个DWORD类型的集合。通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针,其定义如下:
IMAGE_THUNK_DATA{
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;//判定当前结构数据是不是以序号为输出的,如果是的话该值为0x800000000,此时PIMAGE_IMPORT_BY_NAME不可做为名称使用
PIMAGE_IMPORT_BY_NAME AddressOfData;
}u1;
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;


typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;// 函数输出序号
BYTE Name1[1];//输出函数名称
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME

  3.IMP和IAT的关系

  静态分析时,OriginalFirstThunk与FirstThunk指向的数据是同一组IMAGE_IMPROT_BY_NAME。

OriginalFirstThunk

 

IMAGE_IMPORT_BY_NAME

 

FirstThunk

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

...

IMAGE_THUNK_DATA

--->

--->

--->

--->

--->

--->

Function 1

Function 2

Function 3

Function 4

...

Function n

<---

<---

<---

<---

<---

<---

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

...

IMAGE_THUNK_DATA

  Loader 在装入一个可执行的代码时,会分析该文件的导入表(IMP),然后通过导入表的指引,修改IAT指向的数据,这们在装载完成后,数据会变成如下形式

OriginalFirstThunk

 

IMAGE_IMPORT_BY_NAME

 

FirstThunk

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

...

IMAGE_THUNK_DATA

--->

--->

--->

--->

--->

--->

Function 1

Function 2

Function 3

Function 4

...

Function n

 

Address of Function 1

Address of Function 2

Address of Function 3

Address of Function 4

...

Address of Function n

  可以看出,OriginalFirstThunk与FirstThunk所指向的数据分离。FirstThunk指向的不再是IMAGE_IMPORT_BY_NAME,而是指向了函数的真实地址。代码段对于一个外部函数的引用始终使用的是FirstThunk处的RVA。当程序真正执行时,就可以跳到真正的函数入口了。

四、修改PE文件

  PE文件是由源代码经由编译器编译、链接后形成的可执行文件,由系统加载执行。通过对PE文件的分析,给理论上修改PE文件提供了可能。下面我们分几个步骤修改PE文件。这些步骤不是修改PE的必须步骤,但从中我们可以讨论如何修改PE文件。

  1.给PE文件增加一个新节

  PE文件节的信息保存在文件头的最后部分,如果预留磁盘空间足够大(大于或等于了个节的结构数据大小),我们就可以为其增加一个新节。

  由IMAGE_SECTION_HEADER的结构定义可知,IMAGE_SECTION_HEADER的大小为10个DWORD类型数据的大小,也就是40(28h)个字节,我们观察要修改的目标

                             

                                                           图10 PE文件的节部分数据

  再由图8的数据我们得知,该PE文件的第一个节的数据的文件偏移地址为400h,而最后一节.rsrc,的末尾偏移是中28fh,400h-28fh=171h>28h,可以增加新的节标志。增加新的节标志数据,首先要修改IMAGE_FILE_HEADER结构的NumberOfSections数值(4->5),然后在290h处开始按IMAGE_SECTION_HEADER结构填入相应的数值。这里我们使用Load PE添加新的数据

                                      

                                                             图11 添加了新节的信息

  由于新的节没有实际数据,所以其VSize和RSize大小为0,新节的属性与.text代码段相同,添加新节后,这里只是添加了节信息,还要补充节数据,补充数据我们使用UE进行复制粘贴就可,这一步涉及了两处Alignment,要注意使用,我们先补充100h字节,但由于文件对齐因子,所在至少在文件末尾处添加200h个空白数据,最后修改节的信息和IMAGE_OPTIONAL_HEADER的SizeOfImage(003f000h ->0040000h),使得我们添加的数据也可以由Loader加载到内存中。添加数据完成后,先执行程序,确定PE头信息的正确。

  图12修正后的文件头部信息

  图13 修改后的程序正常执行的界面

  2. 在新节中添加代码

  在新节中添加代码是本文修改PE文件的关键,我们的目的不仅仅是添加数据,而是添加可执行的代码,通过添加代码研究PE文件的可感染性。由于高级编译器都会将数据段和代码段分开来编译,所以我们添加的代码将会因为找不到数据而使程序崩溃,因此我们要将我们需要的数据和代码放在同一个Section 内,方便编程。

  示例汇编代码

pushad     ;以下两行为保存当前程序上下文
pushfd
mov esi, ImageBase+SectionRVA+messbody   ;提示信息体
mov edi, ImageBase+ SectionRVA +messtitle  ;提示信息标题
push 0     ;MB_OK的类型的消息框
push esi    ;参数入栈
push edi    ;参数入栈
push 0     ;hWND
call MessageBoxA  
popfd     ;恢复程序上下文
popad
mov eax,ImageBase+oldoep;原始的入口地址送到eax
jmp eax     ;跳到原始入口处
messbody db    ‘New Section Add This File’,0
messtitle db    ‘I Sucess’,0

  在这一段代码中我们的是程序首先执行植入代码,完成特定功能,在执行完毕后,跳到原代码入口处继续执行(要注意保存初始环境),此段代码的主要目的就是给用户一个提示,表示我们成功的感染了这个程序。这段代码中涉及的数据有消息框的标题和内容,我们都要在此段中进行定义。我们可以通过NASM编译这段代码为纯二进制代码。(关于NASM编译器,可以通过网络查找其编译程序和文档)。

  为使编译通过,我们首先确定MessageBoxA的地址和oldoep的地址

                          

  图14 Load PE 文件的人Import Table

  通过查阅MSDN,我们知道MessageBoxA的函数由USER32.dll导出,而应用程序使用的这个信息就在导入表中。通过Load PE 查看文件的Import Table,我们找到USER32.dll 和MessageBox的地址在0002743Ch,要执行0002743Ch处的索引函数,就在在其前面加上ImageBase的值。也就在此代码在0042743Ch处,实际调用参考为 Call dword [0042743ch]。Oldoep由IMAGE_OPTIONAL_HEADER的AddressOfEntryPoint得到

  (0000D1B5h+00400000h=0040d1b5h)  

  图15 通过NASM编译后的代码

  将这段代码复制到修改后的.exe 的38c00h处,然后保存。如下图: 

  图16 修改后的新节的数据

  运行程序:

                                                              

  图17 节的程序可以照常执行

  程序首先弹出对话框,单击确定后看到了程序的初始界面

  3. 其它植入方法

  启动OllDbg(Ring3)调试器,调试上面刚处理过的程序,发现调试器会给出一个警告,如下图

                        

  图18 OllDbg的警告信息

  通过实验得出,之所以OllDbg 会发出如此的警告,是因为该文件的PE信息中AddressOfEntryPoint超出了Code Section(.text)段所记录的地址(0000000h~00270000h)。将.text判定为代码段,由PE头的BaseOfCode得出。如果将此段代码植入.text 段,那么将不会出现此提示。

  PE文件能否正常加载执行,与磁盘文件结构密切相关,但一旦将磁盘文件映射为内存镜像后,就与磁盘文件脱离了关系。所以磁盘仅仅是一个规范的数据结构。

  通过实验,我们可以得出这样的结论:对于一个小的代码段(其二进制代码长度小于代码段下一节的RVA-(BaseOfCode+代码段的VirtualSize)),植入是成功的。

  那么我们通过分析磁盘文件和内存映射的关系,就可以修改代码段,将代码植入到代码段。在植入代码段后,要对PE文件的磁盘数据和进行一次定位修复,就可以完成代码的整体植入。

  代码段一般是PE文件的第一个Section,如果此段变长,就要将其后续段的RVA和磁盘偏移地址都要进行修正。修正完成后,仅仅是保证了PE文件的磁盘格式正确,接下来主要就是修改导入表数据,资源表数据。最后要参照原PE文件将新PE文件对数据段的数据的引用进行修正。

  至此,我们完成了一次对PE文件新代码的引入问题的研究。但对于一个复杂的PE文件,修改还远不如此。还要处理输出表、TLS表及其它数据的表的内容。

0 0