今天通过《逆向工程核心原理》学习一下PE文件格式,先放一篇别的大佬写的
关于逆向工程核心原理-PE文件 – P.Z’s Blog (ppppz.net)
1.介绍
PE文件时Windows操作系统下使用的可执行文件格式。它的全称是Portable Executable。
Portable的意思是便携的、轻便的,Executable的意识是可执行的,据说由第一个单词是因为一开始设计PE文件是来提高程序在不同程序上的移植性,但最后只在Windows之中使用。
可以看到PE文件不仅仅局限于我们所熟知的exe文件。还有上图中的几种。
2.基本结构
这本书以自带的notepad.exe程序来分析它的PE格式
图片是程序加载到内存中的情形,从DOS头到节区头的部分是PE头部分,其下的节区都叫做PE体,图中的数据在文件中用偏移(offset),在内存中则用虚拟地址来表示(Virtual Address)来表示。文件加载到内存时,如图中所示,节区的大小和位置会发生改变,文件的内容分为代码、数据、资源节。
上面的虚拟地址和节区啥的在IDA中倒是有所体现。
图中的NULL是NULL填充,看下面的描述
这里的思想我记得在学习汇编语言的时候有提过
大概是类似的思想,为了寻址的方便还有提高寻址的效率,我觉得是这样的,如果不是最小单位的倍数,那可能CPU就无法用地址来表示数据的位置了。
3.DOS头
微软早期为了兼容DOS文件,也就是十六位操作系统上的文件,在PE头的最前端增加了一个结构体IMAGE_DOS_HEADER,如下
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; // Checksum
WORD e_ip; // Initial IP value
WORD 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;
这是在16位操作系统之下,所以总共有40个字节,该结构体有两个很重要的成员,就是第一个和最后一个需要重点关注,这两个的直接修改也会导致程序无法运行
WORD e_magic; //DOS签名:4D5A("MZ")
LONG E_lfanew; //指示NT头的偏移
这个”MZ”我们逆向中通常用来判断一个程序是否是Windows可执行程序,关于它的来源
DOS头中除了这两个之外的其他数据修改似乎不会影响程序的打开
4.DOS存根
DOS存根在DOS头下面,大小不固定,即使没有,也可以正常运行,由代码和数据混合而成,下图为即为书中提供程序的DOS存根
文件偏移40到4D区域是汇编指令,只有在DOS环境中运行该指令,才会执行这里的代码,而在Windows中并不会运行该命令,我们可以用DOSBox打开试试,如图
确实输出了存储在DOS存根中的字符串
5.NT头
NT头IMAGE_NT_HEADERS,结构体如下
typedef struct IMAGE_NT_HEADERS
{
DWORD Signature; //PE Signature : 50450000("PE"00)
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
结构体由签名、文件头、可选头组成
该结构体的大小为F8,相当大,签名的内容是固定的,如上图红圈内所示,即”PE”,和”MZ”的标识差不多。
NT头:文件头
文件头是表现文件大致属性的一个结构体
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;//CPU的架构类型
WORD NumberOfSections;//文件的节区数目
DWORD TimeDateStamp;//文件创建的用时间戳标识的日期
DWORD PointerToSymbolTable;//指向符号表(用于调试)
DWORD NumberOfSymbols;//符号表中符号的个数
WORD SizeOfOptionalHeader;//IMAGE_OPTIONAL_HEADER32结构大小
WORD Characteristics;//文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
每个CPU都有唯一的Machine码,它指明了CPU的架构
然后是NumberOfSections,用来指出文件当中存在的节区数量,这个值一定要大于零,并且如果修改它,使它的数量与实际节区不同时,会发生运行错误。如图
NT头的最后一个结构体是IMAGE_OPTIONAL_HEADER32,而文件头的倒数第二个成员SizeOfOptionalHeader用来指明IMAGE_OPTIONAL_HEADER32结构体的大小,看它名字就知道了。
文件头的Characteristics字段用来标识文件的属性,文件是否是可运行的形态,,是否位DLL文件等信息。以bit OR的形式组合起来,就是所有的属性特征值进行OR运算,如图
TimeDateStamp成员的值用来记录文件创建的用时间戳标识的日期,它不影响文件的运行。
NT头:可选头
可选头,即IMAGE_OPTIONAL_HEADER32是PE头结构当中最大的。其结构如下
typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic; //* PE标志字:32位(0x10B),64位(0x20B)
BYTE MajorLinkerVersion; // 主链接器版本号
BYTE MinorLinkerVersion; // 副链接器版本号
DWORD SizeOfCode; // 代码所占空间大小(代码节大小)
DWORD SizeOfInitializedData; // 已初始化数据所占空间大小
DWORD SizeOfUninitializedData; // 未初始化数据所占空间大小
DWORD AddressOfEntryPoint; //* 程序执行入口RVA,(w)(Win)mainCRTStartup:即0D首次断下来的自进程地址
DWORD BaseOfCode; // 代码段基址
DWORD BaseOfData; // 数据段基址
DWORD ImageBase; //* 内存加载基址,exe默认0x400000,dll默认0x10000000
DWORD SectionAlignment; //* 节区数据在内存中的对齐值,一定是4的倍数,一般是0x1000(4096=4K)
DWORD FileAlignment; //* 节区数据在文件中的对齐值,一般是0x200(磁盘扇区大小512)
WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
WORD MajorImageVersion; // 可运行于操作系统的主版本号
WORD MinorImageVersion; // 可运行于操作系统的次版本号
WORD MajorSubsystemVersion; // 主子系统版本号:不可修改
WORD MinorSubsystemVersion; // 副子系统版本号
DWORD Win32VersionValue; // 版本号:不被病毒利用的话一般为0,XP中不可修改
DWORD SizeOfImage; //* PE文件在进程内存中的总大小,与SectionAlignment对齐
DWORD SizeOfHeaders; //* PE文件头部在文件中的按照文件对齐后的总大小(所有头 + 节表)
DWORD CheckSum; // 对文件做校验,判断文件是否被修改:3环无用,MapFileAndCheckSum获取
WORD Subsystem; // 子系统,与连接选项/system相关:1=驱动程序,2=图形界面,3=控制台/Dll
WORD DllCharacteristics; // 文件特性
DWORD SizeOfStackReserve; // 初始化时保留的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
DWORD LoaderFlags; // 已废弃,与调试有关,默认为 0
DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,此字段自Windows NT发布以来,一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];// 数据目录表
} IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;
第一个成员Magic,32位系统为IMAGE_OPTIONAL_HEADER32,此时Magic码为10B,64位系统为IMAGE_OPTIONAL_HEADER64,此时Magic码为20B。
6.节区头
节区头定义了各个节区的属性。
创建多个节区可以保证程序的安全性,可以防止不同节区的数据放在一起,写数据导致其他类别数据被覆盖的问题,每个节区有着不同的访问权限和特性
每一个节区都有它自己的结构体IMAGE_SERCTION_HEADER
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_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;//区块的属性(bit OR)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name字段不像C语言成员一样以NULL结束,没有必须ASCLL码的限制。PE规范没有明确节区的Name,所以可以向其中放入任何值,也可以填充NULL值。这里面的意思我理解为可以任意对节区的名字命名,存储数据的节区我们甚至也可以命名为.code,而且在CTF当中,也见过对节区名的改动。
7.IAT
IAT (Import Address Table,导人地址表),IAT保存的内容与Windows操作系统的核心进程、内存、DLL结构等有关。IAT用来记录程序正在使用哪些库中的哪些函数。
IAT是关于DLL的,DLL是啥在这里就不介绍了,加载DLL的方式实际有两种:一种是“显式链接” (Explicit Linking),程序使用DLL时加载,使用完毕后释放内存;另一种是“隐式链接” (Implicit Linking),程序开始时即一同加载DLL,程序终止时再释放占用的内存。IAT提供的机制即与隐式链接有关。
IMAGE_IMPORT_DESCRIPTOR
IMAGE_IMPORT_DESCRIPTOR结构体当中记录着PE文件要导入哪些库文件,其结构如下
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
union
{
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT(Import Name Table) address 指向IMAGE_IMPORT_BY_NAME的地址(RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // 库名称字符串的地址(RVA)
DWORD FirstThunk; // IAT(Import Address Table) IAT的地址(RVA)
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_IMPORT_BY_NAME
{
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
执行一个普通程序往往需要导入多个库,导入多少库就有多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体会构成一个数组,结构体最后以NULL结尾,如图