《逆核》01-PE文件结构
PE文件格式
PE文件格式是Windows操作系统中的核心内容,学习PE文件格式可以理解可执行文件如何储存,如何进入内存被执行。
PE文件定义
PE文件是windows操作系统下的可执行文件
其中32位的可执行文件称为PE或PE32,64位的被称为PE32+或PE+。
PE文件分为4种类型:
可执行系列:.EXE .SCR
库系列:.DLL .OCX .CPL .DRV
驱动程序系列:.SYS .VXD
对象文件系列:.OBJ(除了该项外,上面三种都可执行,
PE正式规范中OBJ文件也是PE文件)
PE文件结构
PE文件分为PE头和PE体,又可细分为DOS头,DOS存根,NT头,节区头(分别为.text,.data和.rsrc)然后是节区(分别为.text,.data和.rsrc节区)。text为代码区,data为数据区存放数据结构,rsrc为资源区,windows特有,存放应用程序资源。
每个节区前后存在NULL填充,即用00填充在节区头与节区,节区与节区之间,令文件或内存中节区的起始位置在各文件/内存最小单位的倍数上(这个值与NT头中的成员有关)。
PE头:PE头的构成是许多结构体(DOS头,DOS存根,NT头,节区头)。以下介绍结构体中的部分重要成员
1)DOS头
- (IMAGE_DOS_HEADER)(19个成员):
- e_magic:dos签名:4D5A->MZ
- e_lfanew(long):指向NT头的偏移(注意,这是long值,但是数据表明了从文件开头到NT头的偏移值,可以被用作指针.同时该值也是小端序储存)
2)DOS存根:
dos存根不是结构体,是存在于DOS头下方的一段命令与数据的混合,如果在dos环境下运行程序,会执行这里的指令。
3)NT头
(IMAGE_NT_HEADERS)(三个成员):
Signature(dword):签名:50450000h->”PE”00
FileHeader
(IMAGE_FILE_HEADER):文件头,是一个结构体Machine:每种类型的值表明能在哪种cpu上编译运行:14ch=x86,8664h=x64等等,但是并不是说32位的只能在32位系统上运行,现在有很多方法可以兼容运行。
NumberOfSelection:节区数量,一定大于0
SizeOfOpionalHeader:指出NT头中最后一个成员 (IMAGE_OPTIONAL_HEADER32或64)的大小供系统查看
Characteristics(word):标识文件属性,每一位都代表一个属性,如0x0002表示这是可执行文件,0x1000表示是系统文件,0x2000表示这是dll文件
TimeDateStamp:标识编译时间
OptionalHeader
(IMAGE_OPTIONAL_HEADER32OR64):可选头:Magic:可选头为32位时为10B,64位时为20B
AddressOfEnterPoint:EP的rva值,指出起始代码地址
ImageBase:指出优先装入地址0~FFFFFFF,由于原位置可能已经加载了其它PE文件(主要是dll)
SectionAlignment,FileAlignment:指定节区在磁盘位置和内存储存的最小单位
SizeOfImage:指定image在虚拟内存中占的大小
SizeOfHeader:指出PE头的大小,应是SectionAlignment的整数倍
Subsystem:该值表示文件类型:1:驱动文件,2:窗口应用程序,3:控制台应用程序
NumberOfRvaAndSizes:该值指定下一个元素(DataDirectory)的元素个数,标准是16个(10h)
DataDirectory(结构体数组):重点是0和1和9,该项存了许多表
4)节区头
(节区头是由结构体组成的数组,每个结构体对应了一个节区):
IMAGE_SECTION_HEADER是结构体的类型,其中有部分重要成员:
VirtualAdress(无值,已被可选头内容定义)
VirtulSize:内存中节区大小(无值,已被可选头内容定义)VirtulSize:表明内存中节区的大小
SizeOfRawData:磁盘文件中节区大小
- VirtualSize与SizeOfRawData一般不同,说明磁盘文件中节区大小与加载到内存后的节区大小不同,若VirtualSize大于SizeOfRawData,节区剩余部分会被填充为0
PointerToRawData:磁盘文件中节区起始位置(必须为FileAlignment的整倍数)
Characteristics:属性,由位或而来
Name:可以填入任何值,所以仅供参考不一定叫.code的区是.code区
PE文件的映射
RVA(相对虚拟地址) to RAW(文件偏移)
RAW=RVA-VA+PointerToRawData
IAT和EAT
IAT:
导入地址表
IMAGE_IMPORT_DESCRIPTOR(又叫IMPORT Directory Table):
一个库就对应了一个IMAGE_IMPORT_DESCRIPTOR结构体,所有需要导入的库的所有结构体形成了数组,最后以NULL结构体结束。
其中的重要成员是:
1 | OriginalFirstThunk:导入名称表(该表是指向函数名的指针数组)INT 地址rva(注意该值和characteristics是一个联合) |
作用方式:
编译时,INT被创建,每个库都把指向不同函数名的指针(指向字符串)写入INT表和IAT表,运行时,程序先查找绑定输入表,然后根据信息在INT地址找到函数名,把函数地址写入IAT,供后续使用。
此结构体数组被储存在PE体中,但PE头中IMAGE_OPTIONAL_HEADER32OR64.DataDirectory[1].VirtualAddress是该结构体数组的起始位置(RVA)(前4字节),后4字节是大小。
EAT:
导出地址表
IMAGE_EXPORT_DIRECTORY:
同上,每个模块(dll)都有一个结构体,一起构成数组,重要成员有:
1 | NumberOfFuctions:实际导出函数个数 |
作用方式:
1)通过名称:当程序发现一个函数调用不在内部时,会查找dll是否包含这个函数,如果包含,就会用名称先从AddressOfNames中比较寻找所需函数地址,用得到的索引再在AddressOfNameOrdinals中得到序号,用序号在AddressOfFunctions找到对应的地址进入。然后就会连接地址与这个函数。之所以这么麻烦是因为一个函数可能被多个程序使用,一个程序也可能使用多个动态链接库。查找名称也可以保证即使有新版本,旧编译的程序也可以调用这个函数。
该结构体数组存在在PE体中,RVA存在于DataDirectory[1].VirtualAddress(前4字节),后4字节是大小。
关于修改PE文件
添加节区:
添加节区首先要加节区头,使文件能识别到这个节区,由于一个节区头大小为40字节(
节表起始位置 = DOS头的e_lfanew字段值 + sizeof(IMAGE_NT_HEADERS)
节表大小 = 文件头的NumberOfSections字段值 * sizeof(IMAGE_SECTION_HEADER)
用这个公式计算节表后是否大于40字节,防止截断,如果可以放入一个节区头就放入,如果不行,一般有两种方法:一个是删掉dos存根,把后面的pe头部分前移,在后面的位置加节区头,这时要注意IMAGE_DOS_HEADER 的e_flanew要修改为NT头的偏移,因为NT头移动过,不过不用管对齐,因为并没有更改文件大小和节区位置,只要注意后面加的节区就可以了。第二个是在节表后面加数据,让它变长,这就要在SizeOfHeader里面增加字节,在多出来的地方写节区头,但是这样要把每个节区头里的偏移调到对应的偏移,因为节区头数据变多之后,节区的地址相应增加了那么多。但要注意增加的字节必须要对齐为最小单位的倍数。
还要把NumberOfSections+1,可选头中的sizeOfImage加对应的值
删除节区:
直接0填充对应节区头,就识别不到节区啦。然后要把NumberOfSections-1.
倒是可以这样,但是真的要删除节区的话,参考《逆核》17.2.2删除.reloc节区(位于整个文件的最后):
注:这里使用的是exe文件,因为一般exe文件没用重定位节也可以正常运行- 从节区头找到reloc节区起始偏移,跳转后删除(物理)
- 文件头中有Number of Section填入减1的值(删了几个就减几)
- 可选头中有整个记录映像大小的Size of Image,在节区头中找到.reloc节区的大小RawDataSize让映像大小的值减去这个值