PE文件格式复习

前言

前两天遇到了和PE文件格式有些关系的问题,有点儿蒙,发现以前看的有点些忘了,于是就拿出书又看了一遍,简单的写了点笔记。

PE文件简述

PE文件是指Windows系统下32位的可执行文件,又叫PE32。Windows系统下64位的可执行文件称为PE+或PE32+。

PE文件是由UNIX平台的COFF为基础制作的。

PE文件的种类(后缀)有:

  • 可执行文件

    • .exe
    • .scr
  • 驱动文件

    • .sys
    • .vxd
  • 库文件

    • .dll
    • .ocx
    • .cpl
    • .drv
  • 对象文件

    • .obj(PE文件中只有它文件本身不能以任何形式执行)

PE文件的基本结构

DOS头
DOS存根
NT头
节区头
NULL
节区
NULL
节区
NULL
……(一直是 节区、NULL ……)

NULL为NULL填充。

PE头

从DOS头到节区头的合称。

PE头由许多结构体组成,储存了可执行文件运行所需要的所有信息。

以下各结构体中只列出要讲解的成员。

DOS头

用于对DOS文件保持兼容性。

typedef struct _IMAGE_DOS_HEADER {
    WOED e_magic; //DOS签名
    ……
    LONG e_lfanew;//NT文件头的偏移
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

IMAGE_DOS_HEADER

此结构体的大小为64字节。

e_magic

所有PE文件在开始部分都有DOS签名,且DOS签名固定为 4D5A (即ASCII值 MZ ),取自微软设计了DOS可执行文件的开发人员名字的首字母(有兴趣的可以看看这个故事)。

e_lfanew

此成员的值指向NT头所在位置,NT头的名称为 IMAGE_NT_HEADERS 。

DOS存根

为可选项,且大小不固定。由代码和数据混合而成,用于在其他环境(非32位环境)下输出字符串提示或执行其他操作。

NT头
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;	//签名
    IMAGE_FILE_HEADER FileHeader;	//文件头
    IMAGE_0PTIONAL_HEADER32 OptionalHeader;	//可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

_IMAGE_NT_HEADERS

大小为 F8 。

Signature

值为 50450000h (即ASCII值 PE 00)。

FileHeader

文件头,结构体,用于表现文件大致属性。

typedef struct _IMAGE_FILE_HEADER {
    WORD Machine;	//CPU的Machine码
    WORD NumberOfSections;	//文件中存在的节区数量
    DWORD TimeDateStamp;	//记录编译器创建此文件的时间
    ……
    WORD SizeOfOptionalHeader;	//IMAGE_0PTIONAL_HEADER32结构体的大小 
    WORD Characteristics;	//用于表示文件的属性,文件是否可运行,是否为DLL文件等信息 
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine

每个CPU都有唯一的Machine码,例:IA64的Machine码为 0x0200 。

NumberOfSections

用来指出文件中存在的节区数量,该值一定要大于零,且当定义的节区数量与实际节区不同时,将发生运行错误。

TimeDateStamp

用于记录编译器创建此文件的时间。

SizeOfOptionalHeader

用于指出IMAGE_0PTIONAL_HEADER32结构体的大小。

另外,PE+格式的文件中使用的是IMAGE_0PTIONAL_HEADER64结构体。两个结构体尺寸不同,需要在 SizeOfOptionalHeader 成员中明确指出结构体的大小。

Characteristics

用于表示文件的属性,文件是否可运行,是否为DLL文件等信息,以bin OR形式组合。

OptionalHeader

可选头,由结构体、预定义等组成,其中IMAGE_OPTIONAL_HEADER32是PE头结构体中最大的。

……
typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD Magic;
    ……
    DWORD AddressOfEntryPoint;	//EP的RVA值
    ……
    DWORD ImageBace;	//基准地址
    DWORD SectionAlignment;	//指定节区在内存中的最小单位
    DWORD FileAlignment;	//指定节区在磁盘文件中的最小单位
    ……
    DWORD SizeOfImage;	//PE Image在虚拟内存中所占空间的大小
    DWORD SizeOfHeaders;//PE头的大小
    ……
    WORD Subsystem;	//用来区分系统驱动文件与普通的可执行文件
    ……
    DWORD NumberOfRvaAndSizes;	//用来指定DataDirectory数组的个数
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];	
}IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

Magic

IMAGE_OPTIONAL_HEADER32结构体的Magic码为10B;

IMAGE_OPTIONAL_HEADER64结构体的Magic码为20B。

AddressOfEntryPoint

EP的RVA值,该值指出程序最先执行的代码起始地址(入口点)。

ImageBace

PE文件被加载到内存时,指出文件的优先装入地址。

SectionAlignment

指定了节区在内存中的最小单位。

FileAlignment

指定了节区在磁盘文件中的最小单位。一个文件的SectionAlignment和FileAlignment值可能相同,也可能不同。磁盘文件或内存的节区大小必定为值得整数倍。

SizeOfImage

加载PE文件到内存时,指定了PE Image在虚拟内存中所占空间的大小。

SizeOfHeaders

用来指出整个PE头的大小,该值必须是 FileAlignment 的整数倍。

Subsystem

用来区分系统驱动文件(.sys)与普通的可执行文件(.exe、.dll)。

含义备注
1Driver系统驱动(.sys)
2GUI文件窗口应用程序(.exe)
3CUI文件控制台应用程序(.exe)

NumberOfRvaAndSizes

用来指定 DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] 数组的个数。

DataDirectory

由IMAGE_DATA_DIRECTORY结构体组成的数组。

DataDirectory[0] = EXPORT Directory	//EAT结构体数组的起始地址(RVA值)
DataDirectory[1] = IMPORT Directory	//IAT结构体数组的起始地址(RVA值)
DataDirectory[2] = RESOURCE Directory
……
DataDirectory[9] = TLS Directory
……
节区头

各节区头定义了各节区在文件或内存中的大小、位置、属性等。将PE文件创建成多个节区结构的好处是,可以保证程序的安全性(比如缓冲区溢出攻击)。

节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。

……
typedef struct _IMAGE_SECTION_HEADER {	
    BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节区的名字
    union {
        ……
        DWORD VirtualSize;	//内存中节区所占大小 
    } Misc;
    DWORD VirtualAddress;	//内存中节区起始地址
    DWORD SizeOfRawData;	//磁盘文件中节区所占大小
	DWORD PointerToRawData;	//磁盘文件中节区起始位置 
    ……
    DWORD Characteristics;	//节区属性(bin OR)
}IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name

PE未明确规定节区的名字,所以可以向其中放入任何值,甚至可以填充空值,所以节区的名字(例:.text)仅供参考,不能百分之百确定被用作某种信息。

VirtualSize

内存中节区所占大小。

SizeOfRawData

磁盘文件中节区所占大小,与 VirtualSize 一般具有不同的值,即磁盘文件中节区的大小与加载到内存中的节区大小是不同的。

VirtualAddress

内存中节区起始地址,即 RVA。不带有任何值,由 IMAGE_OPTIONAL_HEADER32 中的 SectionAlignment 确定。

PointerToRawData

磁盘文件中节区起始位置。不带有任何值,由 IMAGE_OPTIONAL_HEADER32 中的 FileAlignment 确定。

PE体

各节区的合称。

PE文件格式把可执行文件分成若干个节区,不同的资源(例:字符串、菜单、图标、字体、位图……)被存放在不同的节中, 标准PE文件通常包含的节名及含义如下:

  • .text(代码)

    由编译器产生,存放着二进制的机器代码,也是反汇编和调试的对象。

    访问权限:执行、读取。

  • .data(数据)

    初始化的数据块,如宏定义、全局变量、静态变量等。

    访问权限:非执行、读写。

  • .idata

    可执行文件所使用的动态链接库等外来函数与文件的信息。

  • .rsrc(资源)

    存放程序的资源,如图标、菜单等。

    访问权限:非执行、读取。

  • ……

节区的名字只是为了方便人的记忆与使用,节名可以自己定义,另外,如果可执行文件经过加壳处理,节信息就会变得非常奇怪。

IAT

一种表格(其实是长整型数组),用来记录程序正在使用哪些库中的哪些函数,未规定大小。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        ……
        DWORD OriginalFirstThunk;	//INT的地址(RVA值)
    };
    ……
    DWORD Name;	//库名称字符串的地址(RVA值)
    DWORD FirstThunk;	//IAT的地址(RVA值)
} IMAGE_IMPORT_DESCRIPTOR;
……

Name

字符串指针,指向导入函数所属的库文件名称。

IMAGE_IMPORT_DESCRIPTOR 结构体记录PE文件要导入哪些库文件。

导入多少库就存在多少个 IMAGE_IMPORT_DESCRIPTOR 结构体,这些结构体形成数组,且结构体数组最后以NULL结构体结束。

IAT在PE体中,但查找其位置的信息在PE头中。

INT

记录导入函数信息的长整型结构体数组,元素为IMAGE_IMPORT_BY_NAME结构体,以NULL结构体结束,大小与IAT相同。

标准的PE文件中,INT与IAT的各元素同时指向相同地址。

EAT

不同的应用程序通过EAT来得到从相应库中导出函数的起始地址。 PE文件内用结构体来保存导出信息。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    ……
    DWORD NumberOfFunctions;	//实际Export函数的个数
    DWORD NumberOfNames;		//Export函数中具名的函数个数
    DWORD AddressOfFunctions;	//Export函数地址数组(数组元素个数 = NumberOfFunctions)
    DWORD AddressOfNames;		//函数名称地址数组(数组元素个数 = NumberOfNames)
    DWORD AddressOfNameOrdinals;//Ordinal地址数组(数组元素个数 = NumberOfNames)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

文件到内存的映射

文件中使用偏移,内存中使用VA来分别表示位置,文件加载到内存时,节区的大小,位置等会发生变化。

VA与RVA的换算

RVA + Image Base = VA

相对虚拟内存地址 + 基准地址 = 虚拟内存地址

PE头内部信息大多以RVA形式存在,PE文件加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件,因此必须通过重定位加载到其他空白位置,使用RVA来定位信息,即使发生了重定位,只要相对于基准地址的相对地址没有发生变化,就能正常访问到指定信息。

32位的Windows OS中,各进程分配有4GB的虚拟内存,进程中VA的值范围是00000000~FFFFFFFF(关于Windows OS中的内存管理和资源分配机制,可以通过学习操作系统的相关知识,比如银行家算法来深入了解) 。EXE、DLL文件被装载到用户内存的 0~7FFFFFFF 中,SYS文件被载入到内核内存的 80000000~FFFFFFFF 中。

基准地址可以通过修改编译选项更改,在默认情况下,EXE文件在内存中的基准地址是0x00400000,DLL文件在内存中的基准地址是0x10000000。

PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBace + AddressOfEntryPoint。

一般而言,文件的大小与加载到内存中的大小是不同的。其原因是文件数据存放的基本单位和内存数据存放的基本单位不同,按照磁盘或内存数据标准存放时,不足一个基本单位的数据会被0x00填充,超出的将分配给下一个,这种由存储单位差异引起的节基址差叫做节偏移。

RVA与RAW的换算

《逆向工程核心原理》的13.4节 RVA & RAW 中的公式:

RAW = RVA - VirtualAddress + PointerToRawData

把公式变形得

RAW = (VA - Image Base) - VirtualAddress + PointerToRawData

???似乎有哪里不对??? : — )

这里的 VirtualAddress 为 RVA所在节区的 VA - Image Base 得到的相对虚拟偏移量RVA,PointerToRawData为文件偏移量,(- VirtualAddress + PointerToRawData)即为节偏移。

文件偏移地址 = 相对虚拟地址 - 内存中节区的起始地址 + 磁盘文件中节区的起始位置

= RVA - 节偏移

涉及到的术语解释

PE

Portable Executable,Windows系统下使用的可执行文件格式。

COFF

Common Object File Format,通用对象文件格式,UNIX平台的文件格式。

PE header

PE文件的头部分。

Section header

节区头。

DOS header

DOS头。

VA

Virtual Address,虚拟地址,进程虚拟内存的绝对地址,也是PE文件中的指令被装入内存后的位置 。

RVA

Relative Virtual Address,相对虚拟地址,从某个基准位置开始的相对地址,内存地址相对于映射基址的偏移量 。

Image Base

基准地址,也叫装载地址,PE装入内存时的基地址 。

NULL padding

NULL填充。

Relocation

重定位。

RAW

File offset,文件偏移地址,文件在磁盘上存放时相对于文件开头的偏移 。

Image

映像,PE文件装入到内存中的形态。

IAT

Import Address Table,导入地址表。

EAT

Export Address Table,导出地址表。

DLL

Dynamic Link Library,动态链接库。

Import

导入,向库提供服务(函数)。

Export

导出,从库向其他PE文件提供服务(函数)。

Table

PE头中提到的Table指数组。

INT

Import Name Table,导入名称表。

IMPORT Directory Table

IMAGE_IMPORT_DESCRIPTOR 结构体数组的别称。

学习与参考

《逆向工程核心原理》

《0day安全:软件漏洞分析技术(第2版)》

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值