本博客系统讲解了PE文件结构。
PE文件结构其实不复杂,但内容较多,希望朋友们能“沉心静气做学问”。
可移植的可执行文件(PE,Portable Executable File),Win32平台可执行文件使用的一种格式
。
其他EXE文件格式:
DOS
:MZ格式Windows 3.0/3.1
:
可执行程序的不同形态(以QQ为例)
何为文件感染?[或控制权获取]
具备[或启动]病毒功能[或目标程序]
不破坏
目标PE文件原有
功能和外在形态(如图标)等病毒代码如何与目标PE文件融为一体?
PE文件至少包含两个段,即数据段和代码段
。Windows NT 的应用程序有9个预定义的段,分别为 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的
,当然,也可以根据需要定义更多的段(比如一些加壳程序)
。
在应用程序中最常出现的段有以下6种:
中文名 | 英文名 |
---|---|
.执行代码段 | 通常 .text (Microsoft)或 CODE(Borland)命名; |
.数据段 | 通常以 .data 、.rdata 或 .bss(Microsoft) |
.资源段 | 通常以 .rsrc命名 |
.导出表 | 通常以 .edata命名 |
.导入表 | 通常以 .idata命名 |
.调试信息段 | 通常以 .debug命名 |
使用UltraEdit打开之后看到的是PE文件的十六进制代码。
以text.exe为例:
看雪学院论坛可以下载。
PEView
可按照PE文件格式对目标文件的各字段
进行详细解析。
Stud_PE
可按照PE文件格式对目标文件的各字段
进行详细解析。与 PEView
功能类似。
Ollydbg
可跟踪
目标程序的执行过程,属于用户态调试工具。
无法调试内核程序。
设置之后可以使用右键用Ollydbg打开PE文件。
可对目标文件进行16进制查看和修改
。
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; // ChecksumWORD e_ip; // Initial IP valueWORD 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;
所有的PE文件都是以一个64字节的DOS头(MZ文件头)开始。这个DOS头只是为了兼容早期的DOS操作系统。
e_magic
和最后一个字段 e_lfanew
字段
e_magic
字段:#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
e_lfanew
字段:定位PE文件头开始位置
,也可用于PE文件合法性检测
更正:上图中的“PE文件”表述应为“PE文件头"。特此说明。”
IMAGE_NT_HEADERS是一个宏,其定义如下:
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader) IMAGE_FIRST_SECTION64(ntheader)
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader) IMAGE_FIRST_SECTION32(ntheader)
#endif
该头分为32位和64位两个版本,其定义依赖于是否定义了_WIN64。这里只讨论32位的PE文件格式,来看一下IMAGE_NT_HEADERS32的定义,如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //该结构体中的Signature就是PE标识符,标识该文件是否是PE文件。该部分占4字节,即“50 45 0000”。
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
如何简单判断一个文件是否为PE文件?
首先要判断DOS头部的开始字节是否是“MZ”。如果是“MZ”头部,则通过DOS头部找到PE头部,接着判断PE头部的前四个字节是否为“PE\0\0”。如果是的话,则说明该文件是一个有效的PE文件。
可以看到,开始位置正是 00B0h 。
文件头结构体IMAGE_FILE_HEADER
是IMAGE_NT_HEADERS
结构体中的一个结构体,紧接在PE标识符的后面。IMAGE_FILE_HEADER结构体的大小为20字节,起始位置为0x000000CC,结束位置在0x000000DF。结构域包含了关于PE文件物理分布的信息。
节数目
、后续可选文件头大小
、机器类型
等。//
//File header format.
//
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;
#define IMAGE_SIZEOF_FILE_HEADER 20
- Machine: 该字段是WORD类型,占用2字节。该字段表示可执行文件的目标CPU类型。
- NumberOfSections:该字段是WORD类型,占用两个字节。该字段表示PE文件的节区的个数。
- TimeDataStamp:该字段表明文件是何时被创建的,这个值是自1970年1月1日以来用格林威治时间计算的秒数。
- PointerToSymbolTable:该字段很少被使用,这里不做介绍。
- NumberOfSymbols:该字段很少被使用,这里不做介绍。
- SizeOfOptionalHeader:该字段为WORD类型,占用两个字节。该字段指定IMAGE_OPTION AL_HEADER结构的大小。注意,在计算IMAGE_OPTIONAL_HEADER的大小时,应该从IMAGE_FILE_HEADER结构中的SizeOfOptionalHeader字段指定的值来获取,而不应该直接使用 sizeof ( IMAGE_OPTIONAL_HEADER )来计算。
- Characteristics:该字段为WORD类型,占用2字节。该字段指定文件的类型。
IMAGE_OPTINAL_HEADER在几乎所有的参考书中都被称作“可选头”。虽然被称作可选头,但是该头部不是一个可选的,而是一个必须存在的头,不可以没有。该头被称作“可选头”的原因是在该头的数据目录数组中,有的数据目录项是可有可无的,数据目录项部分是可选的,因此称为“可选头”。它定义了PE文件的很多关键信息。大小可从 文件头-IMAGE_FILE_HEADER 中得知。可选头紧挨着文件头,文件头的结束位置在 0x000000DF,那么可选头的起始位置为0x000000E0。
IMAGE_OPTIONAL_HEADER是一个宏,其定义如下:
#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL64_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL32_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif
32位版本和64位版本的选择是根据是否定义了_WIN64而决定的,这里只讨论其32位的版本。IMAGE_OPTIONAL_HEADER32的定义如下:
//
//Optional header format.
//
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;
可选头的定位:
定义了PE文件的很多关键信息
可选头是对文件头的一个补充。文件头主要描述文件的相关信息,而可选头主要用来管理 PE 文件被操作系统装载时所需要的信息。该头同样有 32 位版本与 64 位版本之分。
ImageBase+RVA
。
Relative Virtual Address
(RVA
),相对虚拟地址,它是相对内存中 ImageBase的偏移位置
,可由此得知PE文件真正的装载地址。一般为
ImageBase+ RVA
,即00400000h+RVA
。
PE文件在内存中的优先装载地址
。对于大多数程序都是 00400000h和77F40000h。正因为
对齐粒度
的存在,PE文件中才有有很多“00”
字节(或是“CC”
字节)。
内存中的镜像大小
、文件头大小
本小节为实际操作,讲述如何修改程序的入口地址
。
在D8H处的4个字节处修改1000H为1016H:
更正:上图文字应为“将地址D8h处的 00 10修改成16 10之后...”。
DataDirectory是可选映像头的最后128个字节(16项 * 8 bytes)
,也是IMAGE_NT_HEADERS(PE文件头)的最后一部分数据。
它由16个IMAGE_DATA_DIRECTORY结构组成的数组构成,指向输出表、输入表、资源块、重定位
等数据目录项的RVA(相对虚拟地址)和大小。
IMAGE_DATA_DIRECTORY的结构如下:
//
//Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据块的起始RVA
DWORD Size; //数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
读图可知Import table(输入表)
的地址为 00402014h ,大小为 003Ch 。
在数据目录中,并不是所有的目录项都会有值,很多目录项的值都为 0。因为很多目录项的值为0,所以说数据目录项是可选的。
可选头的结构体介绍完了,各位可以按照该结构体中各成员变量的含义自行学习可选头中的十六进制值的含义。只有参考结构体的说明去对照分析PE文件格式中的十六进制值,才能更好、更快地掌握PE结构。
区块表包含每个块在映像中的信息
(如位置、长度、属性),分别指向不同的区块实体。最后以一个空的IMAGE_SECTION_HEADER结构作为结束
,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一
。IMAGE_NT_HEADERS->FileHeader.NumberOfSections
字段来指定的。(因为节表的个数是节的个数+1)typedef struct _IMAGE_SECTION_HEADER {
Name //8个字节的块名
union
{
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //区块尺寸
DWORD VirtualAddress; //区块的RVA地址
DWORD SizeOfRawData; //在文件中对齐后的尺寸
DWORD PointerToRawData; //在文件中偏移
DWORD PointerToRelocations; //在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; //行号表的偏移(供调试使用地)
WORD NumberOfRelocations; //在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; //行号表中行号的数目
DWORD Characteristics; //区块属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
IMAGE_SECTION_HEADER 的成员名称 | IMAGE_SECTION_HEADER 的成员意义 |
---|---|
Name | 这是一个8位的ASCII(不是Unicode内码),用来定义块名,多数块名以,开始(如.Text),这个实际上不是必需的,注意如果块名超过了8个字节,则没有最后面的终止标志NULL字节,带有 的 区 块 的 名 字 会 从 编 译 器 里 将 带 有 的区块的名字会从编译器里将带有 的区块的名字会从编译器里将带有的相同名字的区块被按字母顺序合并。 |
VirtualSize | 指出实际的,被使用的区块大小,是区块在没有对齐处理前的实际大小.如果VirtualSize < SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。 |
VirtualAddress | 该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。 |
SizeofRawData | 该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。 |
PointerToRawData | 该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。 |
PointerToRelocations | 在PE中无意义 |
PointerToLinenumbers | 行号表在文件中的偏移值,文件调试的信息 |
NumberOfRelocations | 在PE中无意义 |
NumberOfLinenumbers | 该块在行号表中的行号数目 |
Characteristics | 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志: |
每个区块的名称都是唯一的,不能有同名的两个区块
。
但事实上节的名称不表示任何含义
,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” (一般为.text)或者说将包含数据的区块命名为“.Code”(一般为.rdata等) 都是合法的。
当我们要从PE 文件中读取需要的区块的时候,不能以区块的名称作为定位的标准和依据
,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位
。
疑问: 可是 IMAGE_OPTIONAL_HEADER32 中并没有各区块的地址啊,咋定位?
由于一些PE文件为减少体积,磁盘对齐值不是一个内存页 1000h,而是 200h,当这类文件被映射到内存后,同一数据相对于文件头的偏移量在内存中和磁盘文件中是不同的,这样就存在着文件偏移地址与虚拟地址的转换
问题。
从这张图可以看到:
.data
或DATA.bbs
函数引入机制。
引入目录表由一系列的 IMAGE_IMPORT_DESCRIPTOR结构
组成
引入目录表的组成部分-IMAGE_IMPORT_DESCRIPTOR结构
是咋样的?
其中 OriginalFirstThunk 和 FirstThunk 得特别说明一下:
FirstThunk
指向引入函数节(.data)中的引入名字表
(Import Name Table,与引入目录表平级)OriginalFirstThunk
指向引入函数节(.data)中的引入地址表
(Import Address Table,与引入目录表平级)IMAGE_THUNK_DATA
结构的数组引入名字表
和引入地址表
。
IMAGE_THUNK_DATA
结构定义了一个
导入函数(注意不是一个.dll)的信息, 实际为一个双字
,不同时刻可能代表不同含义。0
的 IMAGE_THUNK_DATA
结构作为结束。OriginalFirstThunk和FirstThunk :
在文件中:它们所指向IMAGE_THUNK_DATA结构的数组所有值都是相同
的;
在内存中:FirstThunk
所指向IMAGE_THUNK_DATA结构的数组的值会改变
。
(下图是PE文件中的数据,不是内存中的,看得到 OriginalFirstThunk 和 FirstThunk 指向的数据都是相同的。)
注意:
一个OriginalFirstThunk或FirstThunk只指向一个.dll。上图每一个红框中都含有两个.dll,所以分别由两个OriginalFirstThunk或FirstThunk指向其对应红框。
- 一个IMAGE_THUNK_DATA结构数组(上图一个红框中有两个数组)为一个.dll中的所有导入函数数据:
如:Data 0002064 + Data 00000000(Value 0080 ExitProcess + Kenel32.dll)- 一个IMAGE_THUNK_DATA结构是一个导入函数数据(双字):
如:Data 00002064 (Value 0080 ExitProcess)
补充:IMAGE_THUNK_DATA结构
一个IMAGE_THUNK_DATA结构实际上就是一个双字,它在不同时刻有不同的含义。
.rdata
↑
IMPORT Directory Table
↑
IMAGE_IMPORT_DESCRIPTOR
↑
OriginalFirstThunk和FirstThunk
(O…和 F…均指向一个包含一系列 IMAGE_THUNK_DATA
结构的数组(引入名字表和引入地址表))
是上一节中的IMAGE_THUNK_DATA结构数组
。
函数名
引入函数改成通过序号
引入函数如图,将其修改为80002064
。(00002064 -> 80002064)
2、
通过序号引入函数
时无法在kenel32.dll中找到序号为8292(2064h)
的函数,故程序无法执行。
3、
经过查找,函数ExitProcess序号为183(B7h)
。
现在程序正常运行!
当IMAGE_THUNK_DATA结构(双字)的最高位为0
时,表示函数以字符串类型的函数名
方式输入,这时双字的值是一个RVA
,指向一个(IAMGE_IMPORT_BY_NAME)结构,该结构定义如下:
STRUCT IAMGE_IMPORT_BY_NAME
{
DWORD Hint; //本函数在其所驻留DLL的输出表中的序号
BYTE name //输入函数的函数名,函数名是一个ASCII码字符串,以NULL结尾
};
可通过可选文件头中的DataDirectory的第13项定位
]
引出函数节一般名为.edata
,这是本文件向其他程序提供调用函数的列表,函数所在的地址及具体代码实现的区块。有时合并入.text节
(如下图)。
dwExportRVA
dwForwarderRVA
该表保存的是各导出函数的函数地址在导出地址表的序号
,但序号并不是data中的值,而是待寻找函数在导出序号表排第几个,例如AddAtomA()的排第二
个所以序号是2
。
为何需要导出序号表?
导出函数名字和导出地址表中的地址不是一一对应关系
。
先通过AddressOfNames
查到到函数名,然后通过AddressOfNameOrdinals
查找到函数序号,再通过AddressOfFunctions
找到函数的RVA。
任务:查找HashData函数。
操作:按照下图的顺序操作,一共分为4
步
资源节一般名为.rsrc
这个节放有如图标、对话框等程序要用到的资源
资源节是树形结构的,它有一个主目录,主目录下又有子目录,子目录下可以是子目录或数据。
通常有3层目录(资源类型、资源标识符、资源语言ID),第4层是具体的资源
3个重要结构
在exe文件中一般没有,但在dll文件中基本都会有。
从上图我们能可以看到,定位项的数量是
不定
的。
VirtualAddress
:是一个4KB(一页)的边界。该值加上后面的TypeOffset数组的成员便得到了需要重定位数据的地址。SizeBlock
:为这一结构块的大小。该大小减去前两项(VirtualAddress和SizeBlock本身)的字节数8便得到了第3项(下一项,也就是重定位项数组TypeOffset[]
)的大小,再除以2(因为重定位项
大小为2个字节)即得到了重定位项
的个数。重定位项
:每项都是16位的,其中的最高4
位代表了所需要的重定位类型
,剩下的12
位代表了页面中重定位地址的偏移量(delta)
。
重定位的类型
MAGE_REL_BASED_HIGHLOW(3)
偏移量(delta)
添加到原来的偏移位置(RVA)
的32位字段上,它是32位地址重定位的首选
类型。更正:上图中的“定位项”表述应为“重定位项数组”。
观察上图,可以发现重定位项数组
中最高4
位的值是3
(代表MAGE_REL_BASED_HIGHLOW(3)
),剩下的12
位就是偏移量(delta)
。
无法加载到预期ImageBase
时,这些地址就需要修正。参考资料:
云课堂武大慕课
PE文件格式分析 https://blog.csdn.net/shitdbg/article/details/49734495
用于验收PE文件结构学习情况。
如何判断目标程序是否为合法PE文件?
使用PEView找到PE文件头(IMAGE_NT_HEADERS)的字串(Signature)
若其开头双字内容为 “50 45 00 00
”,则说明给定文件是有效PE文件。
如果不使用引入函数节,如何使用外部DLL中的API函数?
暂时还不会。下次一定补上。
Kenel32.dll提供了GetProcAddress函数,用于获取指定函数的地址。该函数的具体是怎样实现的?
暂时还不会。下次一定补上。
熊猫烧香病毒感染其他PE文件后,目标文件图标会变成容易被用户差距的熊猫图案,为什么?如何解决问题?