[系统安全5] PE格式讲解
0x01 MS-DOS头
MS-DOS头部的字段重点关注e_magic与最后一个e_lfanew是需要关注的。
第一个e_magic字段的值为4D5A,作用是可以作为判断这个文件是否是PE文件。
最后一个e_lfanew字段可以引导我们找到新的EXE文件头,从而进一步判断这个可执行文件是否为PE文件。
如果从这个文件开头用PIMAGE_DOS_HEADER解析,e_magic成员不是IMAGE_DOS_SIGNATURE,那么就不是一个PE文件。
MS-DOS头结构:
typedef struct _IMAGE_DOS_HEADER {// DOS .EXE header
WORD e_magic; //[1] Magic number
WORD e_cblp; //[2] Bytes on last page of file
WORD e_cp; //[3] Pages in file
WORD e_crlc; //[4] Relocations
WORD e_cparhdr; //[5] Size of header in paragraphs
WORD e_minalloc; //[6] Minimum extra paragraphs needed
WORD e_maxalloc; //[7] Maximum extra paragraphs needed
WORD e_ss; //[8] Initial (relative) SS value
WORD e_sp; //[9] Initial SP value
WORD e_csum; //[10] Checksum
WORD e_ip; //[11] Initial IP value
WORD e_cs; //[12] Initial (relative) CS value
WORD e_lfarlc; //[13] File address of relocation table
WORD e_ovno; //[14] Overlay number
WORD e_res[4]; //[15] Reserved words
WORD e_oemid; //[16] OEM identifier (for e_oeminfo)
WORD e_oeminfo; //[17] OEM information; e_oemid specific
WORD e_res2[10]; //[18] Reserved words
LONG e_lfanew; //[19] File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
1-19的含义如下:
1)【重要】MZ标志位:MS-DOS可执行文件的标识位(也叫做魔数),表示这是一个MS-DOS下的可执行文件。
2)文件末页字节数:在可执行页(页大小为512B)最后一页的字节数,如果其值为0,则表示最后一页被全部占用(即有效值为0x200=512)。
3)文件页数:可执行文件中的页数(包括最后一页)。
4)重定位:DOS stub 中重定位项的数目,可能为0。
5)区段中头部大小:程序的数据起始于头部之后,此字段可以用来计算出相应的的文件偏移(包括重定位项)。如果头部大小不是512B的倍数,一些操作系统或程序可能会执行失败。
6)最小附加内存段需求:除了代码大小外最少需分配的内存段大小,如果无法分配相应内存,则程序不能启动。
7)最大附加内存段需求:除了代码大小外最多分配的内存段大小。通常操作系统为程序保留所有剩余的常规内存,但是此字段会对此做出限制。
8)初始SS值:用来初始化SS(堆栈段)以启动可执行文件。
9)初始SP值:用来初始化SP(堆栈指针寄存器)的值。
10)校验和:可执行文件的校验和(或直接置为0)
11)初始IP值:初始化与启动可执行文件相关的IP寄存器值(程序入口点)
12)初始化CS值:初始化与启动可执行文件相关的CS段(与上一个字段组成CS:IP)。
13)重定位表偏移:重定位表的偏移。
14)附加数:代码付加数(0x0则代表仅有主程序)。
15)保留:保留4个WORD大小的空间留作他用。
16)OEM标识:OEM厂商的ID。
17)OEM信息:OEM厂商的信息。
18)保留:保留10个WORD大小的空间留作他用
19)【重要】新文件头地址:新的EXE文件偏移地址
0x02 PE文件头:
PE头位置DOS Stub后面,由MS-DOS头中的e_lfanew字段指向的结构。
PE文件头是Windows NT内核下判断可执行文件的唯一有效结构,它由3个字段组成,结构如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE标识
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
其中3个字段构成了PE文件的基本头部结构。
2.1 Signature字段
Signature字段是PE文件头的表示,其值始终为0x50450000。
表示字符“PE\0\0”,十六进制为00004550h。
Win32SDK中宏定义的值:
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
2.2 IMAGE_FILE_HEADER结构
IMAGE_FILE_HEADER结构中包含了PE的概览信息。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 1)运行平台
WORD NumberOfSections; //【重要】2)区段(section)数目
DWORD TimeDateStamp; //3)时间日期标记
DWORD PointerToSymbolTable; //4)COFF符号指针,这是程序调试信息
DWORD NumberOfSymbols; //5)符号数
WORD SizeOfOptionalHeader; //【重要】6)扩展头大小,IMAGE_OPTIONAL_HEADER的长度
WORD Characteristics; //7)文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
上述结构说明:
1)Machine:用于说明此文件可以运行于哪种CPU上,在不同的平台上PE文件的Machine中的值是不一样的。
2)NumberOfSections:区段的数量
3)TimeDateStamp:表示时间
4)PointerToSymbolTable:指向COFF符号表偏移的指针,由于其已经被DEBUG替代,因此COFF符号表在现今的PE文件中已较少应用(如果符号表不存在,则字段值为0)
5)NumberOfSymbols:符号表中符号的数量,由于COFF符号表是一个大小固定的结构,因此只有通过这个字段才能计算出COFF符号表结构的结尾。
6)SizeOfOptionalHeader:在IMAGE_FILE_HEADER结构后面的扩展头大小。一般情况下,这个结构的大小在32位的系统中是0x00E0,而在64位系统下则为0x00F0,但是这两个值为常用的最小值,不排除在人为修改后或系统升级后此值有变动的可能。
7)Characteristics:此字段用以表示PE文件的属性,可通过几个值的运算得到。普通EXE文件的值为0x010F,DLL文件的值为0x0210。
2.3 IMAGE_OPTIONAL_HEADER结构
IMAGE_OPTIONAL_HEADER这个结构与IMAGE_FILE_HEADER结构并起来统称为“PE文件头结构“。
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; //1)标志位
BYTE MajorLinkerVersion; //2)链接器主版本号
BYTE MinorLinkerVersion; //3)链接器子版本号
DWORD SizeOfCode; //4)所有代码段的总大小
DWORD SizeOfInitializedData; //5)所有已初始化数据块大小
DWORD SizeOfUninitializedData; //6)未初始化数据块大小
DWORD AddressOfEntryPoint; //7)代码入口点地址
DWORD BaseOfCode; //8)代码段的起始RVA
DWORD BaseOfData; //9)数据段的起始RVA
//
// NT additional fields.
//
DWORD ImageBase; //10)程序默认的载入基址
DWORD SectionAlignment; //11)映像文件在内存中的区段对齐值
DWORD FileAlignment; //12)映像文件在磁盘上的区段对齐大小
WORD MajorOperatingSystemVersion;//13)要求的OS最低主版本号
WORD MinorOperatingSystemVersion;//14)要求的OS最低子版本号
WORD MajorImageVersion; //15)由程序作者指定的该程序主版本号
WORD MinorImageVersion; //16)由程序作者指定的该程序子版本号
WORD MajorSubsystemVersion; //17)所需子系统主版本号
WORD MinorSubsystemVersion; //18)所需子系统子版本号
DWORD Win32VersionValue; //19)保留,通常为00
DWORD SizeOfImage; //20)映射文件装入内存后的总大小(从Image Base到最后一个区段的总大小)。
DWORD SizeOfHeaders; //21)DOS头、PE头和区块表的尺寸之和
DWORD CheckSum; //22)映像文件的校验和
WORD Subsystem; //23)PE文件所期望的子系统值,可以用来鉴别程序是命令行操作程序还是图形交互程序
WORD DllCharacteristics; //24)DLL标志位
DWORD SizeOfStackReserve; //25)初始化栈大小
DWORD SizeOfStackCommit; //26)初始化实际提交栈大小,在EXE中栈初始内存大小(默认4KB)
DWORD SizeOfHeapReserve; //27)初始化堆大小,在EXE中进程堆保留的内存(默认1MB)
DWORD SizeOfHeapCommit; //28)初始化实际提交栈大小,在EXE中指派的堆内存大小(默认4KN)
DWORD LoaderFlags; //29)调试相关,默认0x00
DWORD NumberOfRvaAndSizes; //30)数据目录表数量,一般为0x00000010(16个)
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
2.4 数据目录表
数据目录是PE文件中各种数据结构的索引目录,由数个相同的IMAGE_DATA_DIRECTORY结构体组成,其中包含了很多PE文件正常加载执行不可缺少的数据结构。
定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据块的起始RVA地址
DWORD Size; //数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
0x3 区段表
PE文件头的数据目录表后是区段表,区段表用来描述位于其后各个区段的各种属性。PE文件最少要有一个区段才能被加载运行。
区段表是由数个首尾相连的IMAGE_SECTION_HEADER结构体数组构成,可以使用IMAGE_FIRST_SECTION32(NtHeader)这个宏找到第一个区段表所在的位置。
3.1 IMAGE_SECTION_HEADER结构
IMAGE_SECTION_HEADER结构里包含了可以详细描述该区段属性的字段信息,例如区段名、长度、属性等内容,如下所示:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 1)区段名
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; // 2)区段大小
DWORD VirtualAddress; // 3)区段的RVA地址
DWORD SizeOfRawData; // 4)文件中的区段对齐大小
DWORD PointerToRawData; // 5)区段在文件中的偏移
DWORD PointerToRelocations; // 6)重定位的偏移(用于OBJ文件)
DWORD PointerToLinenumbers; // 7)行号表的偏移(用于调试)
WORD NumberOfRelocations; // 8)重定位表项数量(用于OBJ文件)
WORD NumberOfLinenumbers; // 9)行号表项数量
DWORD Characteristics; // 10)区段的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
上面的结构说明如下:
1)Name:区段名,是一个长度为8字节的ASCII字符串数组,一般情况下区段名都以“.”开始
2)VirtualSize:实际被使用的区段大小(即区段在未做对齐处理前的大小),此字段在OBJ文件中为0x00000000。
3)VirtualAddress:此区段载入内存后的RAV,这个地址是按照内存页对齐的(通常为PE头结构中SectionAlignment字段的整数倍)
4)SizeOfRawData:此区段在磁盘中的体积,这个地址是按照文件页对齐的(通常为PE头结构中FileAlignment字段的整数倍)
5)PointerToRawData:此区段在文件中的偏移。
6)PointerToRelocaltions:此区段重定位表的偏移地址,它指向IMAGE_RELOCATION结构数组。
7)PointerToLinenumbers:行号表在文件中的偏移
8)NumberOfRelocations:此区段重定位表项的数量
9)NumberOfLinenumbers:行号表项的数量
10)Characteristics:区段属性,用以描述此区段的读写情况、状态等属性。
3.2 区段名功能约定
虽然区段名是可以自定义,但是微软对实现各种功能区段的名词还是有一个约定俗称的命名标准。
.text:代码段,里面的数据全都是代码
.data:可读写的数据段,存放全局变量或静态变量
.rdata:只读数据区
.idata:导入数据区,存放导入表信息
.edata:导出数据区,导出表信息
.rsrc:资源区段,存放程序用到的所有资源,如图表,菜单等
.bss:未初始化数据区
.crt:用于支持C++运行时库所添加的数据
.tls:存储线程局部变量
.reloc:包含重定位信息
.sdata:包含相对于可被全局指针定位的可读写数据
.srdata:包含相对于可被全局指针定位的只读数据
.pdata:包含异常表
.debug$S:包含OBJ文件中的Codeview格式符号
.debug$T:包含OBJ文件中的Codeview格式类型的符号
.debug$P:包含使用预编译头时的一些信息
.drectve:包含编译时的一些链接命令
.didat:包含延迟装入的数据
这些区段名一般都是由编译器在自动编译连接时生成的,但是我们是可以自己定义区段名的。
在VS中,可以用以下代码控制编译器数据区段的名称:
#pragma data_seg("YourName");
3.3 地址转换
地址转换工具:FFI、Stud PE与Load PE等
0x4 导出表
导出表是PE文件为其他应用程序提供API的一种函数示例导出方式。Windows下存在导出表的可执行文件可以指定自身的一些变量、函数以及类,并将其导出,以便提供给其他第三方程序使用。
4.1 IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 1) 保留,恒为0x00000000
DWORD TimeDateStamp; // 2) 时间戳,导出表创建的时间(GMT时间)
WORD MajorVersion; // 3) 主版本号:导出表的主版本号
WORD MinorVersion; // 4) 子版本号:导出表的子版本号
DWORD Name; // 5) 指向模块名称的RVA,指向模块名(导出表所在模块的名称)的ASCII字符的RVA
DWORD Base; // 6) 导出表用于输出API函数索引值的基数(函数索引值=导出函数索引值-基数)
DWORD NumberOfFunctions; // 7) EAT 导出地址表中的成员个数
DWORD NumberOfNames; // 8) ENT 导出名称表中的成员个数
DWORD AddressOfFunctions; // 9) EAT 函数地址表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数名称或序号
DWORD AddressOfNames; // 10) ENT 函数名称表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数地址或序号
DWORD AddressOfNameOrdinals; // 11) 指向导出序列号的数组,导出序号表的相对虚拟地址(RVA)
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
4.2 识别导出表
导出表由3部分构成,分别是名称表、函数表或序号表,其中函数表与序号表是必须要有的,而名称表则是可选的。
总结公式:
导出表Offset = 导出表RVA - 有导出表的区段RVA + 有导出表的区段Offset
知道导出表的Offset与RAV后,我们就可以计算出一个参照值,以方便后续的其他地址运算。参照值的计算公式如下:
导出表地址计算参照值(差值) = 导出表RVA - 导出表Offset
0x5 导入表
导入表机制是PE文件从其他第三方程序导入API,以供本程序调用的机制。
5.1 IMAGE_IMPORT_DESCRIPTOR结构
IMAGE_IMPORT_DESCRIPTOR(导入表)负责引导系统找到真正保存有导入信息的其他两个结构,这两个结构分别为IMAGE_THUNK_DATA与IMAGE_IMPORT_BY_NAME。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 1) 指向输入名称表(INT)的RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 2)时间标识
DWORD ForwarderChain; // 3) 转发链,如果不转发则此值为0
DWORD Name; // 4)指向导入映射文件的名字
DWORD FirstThunk; // 5)指向输入地址表(IAT)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
1)OriginalFirstThunk:包含指向INT的RVA,INT是一个IMAGE_THUNK_DATA结构的数组
2)TimeDataStamp:一个32位的时间标识
3)ForwarderChain:控制导入表转发器Forwarders的索引值
4)Name:指向导入映像文件的名字
5)FirstThunk:指向导入地址表(IAT)的RVA
在一般情况下,对于导入表我们只需要关注它的两个字段,分别是OriginalFirstThunk和FirstThunk,这两个字段分别指向了保存导出名称与导出地址的IMAGE_THUNK_DATA结构数组。
5.2 识别导入表
这一章没看明白,不做笔记
0x6 资源
windows下程序中的各种界面(数据)组成的部分称为资源,比如菜单、图标、快捷键、版本信息以及其他格式化的二进制资源。
数据目录中IMAGE_DIRECTORY_ENTRY_RESOURCE项指向此结构。
0x7 异常
PE文件中的异常目录用于描述异常处理相关的异常处理函数、SHE相关的地址等信息,这些信息通常位于名为.pdata的区段中,数据目录中IMAGE_DIRECTORY_ENTRY_EXCEPTION指向此结构。
0x8 安全
数据目录里的IMAGE_DIRECTORY_ENTRY_SECURITY项指向此结构,此结构被称为安全目录或属性证书目录,这个目录里一般保存着此映像文件的数字签名,以证实此映像文件的可信程度。
0x9 基址重定位
数据目录的IMAGE_DIRECTORY_ENTRY_BASERELOC项指向此结构,由于在Windows系统中DLL(动态链接库)文件并不是每次都能加载到预设的基址(ImageBase)上,因此基址重定位主要应用于DLL文件中。
9.1 基址重定位表结构
一般情况下重定位位于一个名为.reloc的区块内,重定位结构是由多个IMAGE_DIRECTORY_ENTRY_BASERELOC子结构组成的。
9.2 识别基址重定位表
重定位项数目 = (SizeOfBlock - IMAGE_DIRECTORY_ENTRY_BASERELOC)/2
0x10 调试
数据目录的IMAGE_DIRECTORY_ENTRY_DEBUG项指向此结构,此结构也叫做调试目录,它往往保存在一个名为.debug的区段里,主要负责协助第三方程序调试本程序,并为其提供调试数据块的位置与大小。
0x11 特殊结构数据(版权)
数据目录的IMAGE_DIRECTORY_ENTRY_ARCHITECTURE指向此结构,在微软的官方描述中,此结构为保留字段,必须为0。
0x12 全局指针
数据目录的IMAGE_DIRECTORY_ENTRY_GLOBALPTR项指向此结构
0x13 TLS
数据目录的IMAGE_DIRECTORY_ENTRY_TLS项指向此结构,TLS变量往往保存在一个名为.tls的区段里。
TLS分为变量与回调函数两部分,其中变量又分为静态模式与动态模式,动态模式是指用TlsAlloc、TlsFree、TlsSetValue与TlsGetValue这几个系统提供的API并使用TLS变量的方式。静态模式是指用Visual C++中提供的__declspec(thread)关键字来声明的TLS变量。
13.1 TLS的回调函数
由于TLS回调函数会优先于程序运行,因此会给调试程序过程带来很多的不变,如果采用默认加载方式,TLS回调函数会在调试器中断在OEP(程序入口点)之前执行完毕。
TLS的结构(x86/x64)
TLS结构是由IMAGE_TLS_DIRECTORY32结构组成的,通常这个结构于.data区段中,以下是IMAGE_TLS_DIRECTORY32结构的声明:
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; //1)TLS模板在内存中起始RVA地址,模板是线程建立时被用于初始化TLS的数据,系统为每个线程建立一个副本
DWORD EndAddressOfRawData; //2)TLS模板在内存中结束RVA地址
PDWORD AddressOfIndex; //3)存放TLS索引的位置
PIMAGE_TLS_CALLBACK *AddressOfCallBacks;//4)指向一个以0x00000000结尾的TLS回调数组,为0则无回调数组
DWORD SizeOfZeroFill; //5)用于指定非零化数据后面空白空间的大小
DWORD Characteristics; //6)保留
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 *PIMAGE_TLS_DIRECTORY32;
0x14 载入配置(x86/x64)
数据目录的IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG项指向此结构,加载配置结构最早是被应用在Windows NT操作系统中的。通常用来描述一些因为太大或太复杂而不适合在PE头或选项头中描述的特征。
0x15 绑定导入表
数据目录的IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT项指向此结构,绑定导入的目的在于减少的加载时间。
0x16 导入地址表
数据目录的IMAGE_DIRECTORY_ENTRY_IAT项指向此结构,其本身就是一个IMAGE_THUNK_DATA结构体数组。在程序加载后,这个结构体数组会依据导入项信息保存其导入函数的真正地址。
0x17 延迟加载表
数据目录的IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT项指向此结构,延迟加载的作用与绑定导入是大致一样的,本质上都是为了加快PE文件的加载速度而设定的。
0x18 COM描述符
数据目录的IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR项指向此结构,根据其功能的不用,COM描述符通常位于一个名为.cormeta或.sxdata的区段里。
如果区段名为.cormeta,则代表此处保存的信息是与公共语言运行时环境(CLR)相关的信息。
如果区段名为.sxdata,则代表此区段内保存有异常句柄列表,此列表内包含每个句柄的COFF符号索引。
参考文章
《黑客免杀攻防学习笔记》——PE文件结构1
http://blog.csdn.net/wuyangbotianshi/article/details/17379895
《黑客免杀攻防学习笔记》——PE文件结构2
http://blog.csdn.net/wuyangbotianshi/article/details/17380835
黑客免杀攻防学习笔记》——PE文件结构3
http://blog.csdn.net/wuyangbotianshi/article/details/17381113
《黑客免杀攻防》 第七章 PE文件格式详解 阅读笔记
http://blog.csdn.net/dalerkd/article/details/40931221
PE文件结构及其加载机制
http://www.cnblogs.com/bokernb/articles/6116512.html
深入剖析PE文件
http://lwglucky.blog.51cto.com/1228348/283812/
注: RAV 代表相对虚拟地址。RVA是虚拟空间中到参考点的一段距离
黑客免杀攻防勘误
http://a1pass.lofter.com/post/b21d8_4a5b475?act=qbbloglofter_20150506_01