一、PE文件基础介绍
pe文件是windows操作系统下的可执行文件的一种称呼,常见的有exe文件和dll文件等,32位是PE32,64位的是PE+或者PE32+。
二、PE文件格式
1、PE文件种类如下表所示
种类 | 主扩展名 |
---|---|
可执行系列 | EXE, SCR |
库系列 | DLL, OCX, CPL, DRV |
驱动程序系列 | SYS, VXD |
对象文件系列 | OBJ |
2、我们直接用010editer看下test.exe文件的情况
往后还有3个节区,分别是.text、.rdata、.data这三个,其中dos头到节区头是我们熟知的pe文件头,后面的节区统称为pe文件体。
节区之间一般有null填充,文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数上,在计算地址时有一条黄金公式:
虚拟地址=基地址+相对虚拟地址
按照自己的理解就是:真实地址=基地址+偏移地址
三、PE头
Dos头
考虑的是pe文件对于早期的dos文件进行兼容而存在的,其结构体为IMAGE_DOS_HEADER,大小是64字节(0x40),其中有2个重要的成员:
- e_magic: Dos签名(4D 5A, MZ)
- e_ifanew: 指示NT头的偏移(文件不同,值不同),表示从哪里开始
Dos存根
stub,位于Dos头下方,可选,大小不固定,由代码和数据混合组成,非必要,可删除,或者用来合并。
NT头
结构体为IMAGE_NT_HEADERS,大小为F8,由3个成员组成:
签名结构体,值为50450000h(“PE”00)
1、文件头,表现文件大致属性,结构体为IMAGE_FILE_HEADER,重要成员有4个:
Machine:每个cpu都拥有的唯一的Machine码,兼容32位Intel x86芯片的Machine码为14C;
NumberOfSections:指出文件中存在的节区数量;
SizeOfOptionalHeader:指出结构体IMAGE_OPTIONAL_HEADER32(32位系统)的长度
Characteristics:标识文件属性,文件是否是可运行形态、是否为DLL等,以bit OR形式进行组合
2、可选头,结构体为IMAGE_OPTIONAL_HAEDER32,重要成员有9个:
Magic: IMAGE_OPTIONAL_HAEDER32为10B,IMAGE_OPTIONAL_HAEDER64为20B。
AddressOfEntryPoint: 持有EP的RVA,指出程序最先执行的代码起始地址的偏移地址
ImageBase:指出文件的优先装入地址(32位进程虚拟内存范围为:0~7FFFFFFF)(建议)
SectionAlignment,FileAlignment:前者制定了节区在内存中的最小单位,后者制定了节区在磁盘文件中的最小单位,简单地理解就是内存对齐和文件对齐的大小
主操作系统和子系统版本号:
SizeOfImage:指定了PE Image在虚拟内存中所占空间的大小
SizeOfHeaders:指出整个PE头的大小
Subsystem:区分系统驱动文件和普通可执行文件
堆栈预备申请大小:
NumberOfRvaAndSize:指定DataDirectory数组的个数,每8字节为一个数组单元
DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成的数组
sizeofcode这里是可以自己定义size大小,造成反调试,MajorSubsystemVersion不可改,前后的可以改,造成反调试
看看这个:导入表在内存中的相对虚拟地址0x2010,然后导入表的大小0x3c
再看看这个,导入地址表在内存中的相当虚拟地址以及它的大小:
3、节区头中定义了各节区的属性,包括不同的特性、访问权限等,结构体为IMAGE_SECTION_HEADER,重要成员有4个:
- VirtualSize:内存中节区所占大小
- VirtualAddress:内存中节区起始地址(RVA)
- SizeOfRawData:磁盘文件中节区所占大小
- Charateristics:节区属性(bit OR)
节名称其实是可以改变的,简称可利用
每2行半为一个节单位元,其中选中的是text段,解析为:从文件偏移为0x400的位置读取0x200的字节,映射到内存偏移为0x1000的位置,有效大小为0x40,剩下的3个以此类推。结尾那个值,只有低位有效,所以中间都是可改值。
下面看下导入的表和函数调用在内存中的情况:
每1.4行就是一个数据单元,例如从0x402010到0x402020,表示从相对虚拟地址为0x206a的位置(隔开一个word),得到name=kernel32.dll(库名),然后0x204c那里获取到0x205c,确认ExitProcess函数名,写到0x2000的偏移处,那么0x756258f0就是我们的ExitProcess的真实地址,我们检验下:
没问题,再来一个,0x402024到0x402034,就是第二个数据单元,从0x2086处获取到user32.dll库名,然后从0x2054处获取到0x2078开始的MessageBoxA函数名,从而在0x2008处写入MessageBoxA真实地址,检验下:
至此函数导入表和真实地址绑定都搞定了。
四、PE文件从磁盘到内存的映射:
查找RVA所在节区
使用简单的公式计算文件偏移:
RAW - PointerToRawData = RVA - ImageBase
RAW = RVA - ImageBase + PointerToRawData
example:ImageBase为0x10000000,节区为.text,文件中起始地址为0x00000400,内存中的起始地址为0x01001000,RVA = 5000,RAW = 5000 - 1000 + 400 = 4400。
起始本质就是偏移地址不变进行一个转换的运算工作