Windows操作系统的PE文件(Portable Executable)和Linux操作系统下的ELF文件都是可执行文件的一种,均以UNIX平台的COFF(Common Object File Format,通用对象文件格式)制作而来.
PE文件是32位的可执行文件,也称为PE32,后来的64位可执行文件称为PE+或PE32+,是PE的扩展.
PE文件有如下几种格式:
严格来说,除了OBJ文件,其他的格式都是可执行的(部分可以以调试,服务等特殊方式运行)
我们首先使用010 Editor进行分析,分析样例使用Windows XP SP3操作系统下的notepad记事本程序(32位).
需要明确的一点是,PE文件是可执行文件,这意味着其需要载入到内存中,PE文件在磁盘和内存中的结构是不同的.
下图展示了二者的差异:
从DOS头到节区头是PE头部分,下面的合称为PE体.
为了定位PE中的数据,在文件中使用偏移(offset),在内存中使用VA(虚拟地址)进行定位.从图中可以看出,PE加载到内存中后,节区的大小和位置会发生变化,而不是原封不动地载入内存.
计算机中,为了提高处理文件、内存,网络包的效率,使用"最小基本单位"这一概念,PE文件中也类似.各节区的起始位置都在文件/内存最小单位的倍数位置处,空白的部分使用NULL进行填充.
VA指的是进程虚拟内存的绝对地址,RVA(Relative Virtual Address)则为相对地址,即相对基址的偏移.
PE头内部的信息大多为RVA,由于在载入内存时,该处可能已经加载有其他数据,所以需要重定向到其他位置,因此,只要能够保证RVA不变,根据不同的VA基地址,都可以正确找到各个数据.
换算公式如下:
RVA+ImageBase=VA
32位操作系统中,各进程有4GB的虚拟内存,所以VA范围从00000000~FFFFFFFF.
PE头包含各种结构体,保存着PE文件的各种信息.
PE头创建之时,DOS文件正在被广泛使用,理所当然地PE文件对DOS文件进行了兼容,方法就是在PE头最前面加上一个DOS头.
该结构体占用40字节,捡关键的说:
e_magic成员:DOS签名(4D5A即为ASCII"MZ")
e_lfanew成员:该成员的值指向后续的NT头所在位置
(注意小端序存储)
该部分可选,有无均可,不影响程序运行,大小不固定.
其中40到4D区域为16位的汇编指令,这里用于输出一个错误提示,即告诉用户该程序不能够在DOS模式下运行.
合理运用DOS存根可以产生一个既可以在DOS下运行,还可以在win32下运行的程序.
该结构体大小为F8,非常大.
Signature:值为50450000的ASCII"PE"00的签名
FileHeader:文件头
OptionalHeader:可选头
该结构体存储了文件的大致属性.其中的几个十分重要,错误设置将导致文件无法正常运行.
Machine:CPU的Machine码,兼容Intel x86芯片的Machine码为14C,其余还有:
NumberOfSections:指出节区数,如果与实际不符,则会运行错误.
SizeOfOptionalHeader:尽管NT头的最后一个成员(IMAGE_OPTIONAL_HEADER32结构体)的大小已经定义,但是windows的PE装载器需要查看SizeOfOptionalHeader的值,以确定IMAGE_OPTIONAL_HEADER32结构体的大小.
Characteristics:根据该成员来识别文件的属性-是否可运行,是否为DLL文件等.使用bit OR形式组合起来.
值如下:(记住0002h和2000h这两个值)
该头是PE头中最大的一个.
重要的成员:
Magic:为IMAGE_OPTIONAL_HEADER32时,值为10B;为IMAGE_OPTIONAL_HEADER64时,值为20B.
AddressOfEntryPoint:存有EP(代码入口点)的RVA值.十分重要.
ImageBase:指出最先被装载的地址.
EXE,DLL文件被装载到07FFFFFFF;SYS文件被装载到80000000FFFFFFFF.
SectionAlignment,FileAlignment:指定最小单位.
SizeOfImage:指出PE Image在虚拟内存中所占空间的大小.一般和文件中不同.
SizeOfHeader:指出整个PE头的大小.
Subsystem:用来区分系统驱动文件和普通的可执行文件.
NumberOfRvaAndSizes:用来指定DataDirection数组的个数(?).
DataDirectory:各种表项,例如导入表和导出表等.每个元素都对应一种表项.
节区头定义了各个节区的属性.PE文件将各种数据存储在不同的节区.而且不同的节区会有不同的权限:
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区:
结构体如下:
重要的属性有如下几个:
这些属性定位了后续PE体中对应的一个节区,例如.text节区.
这是一个最基本的转换,由于PE磁盘文件与其载入到内存的镜像文件并不完全一致,所以需要将进行RAW与RVA之间的转换.
首先查找RVA所在的节区,然后找到该节区的起始地址Virtual Address,注意这里的Virtual Address仍然是RVA(?).
然后找到PointerToRawData,就可以做差了.公式如下:
含义即为:文件中该位置(RAW)到节区起点的偏移(两者之差) 与 内存映像中位置(RVA)到本节区起点的偏移(两者之差) 是相等的.
Windows过去并没有DLL,只有库(Library)这一概念,这导致每一个程序要调用某一个库代码,都要进行包含,这就导致大量的空间浪费(每个使用该库的程序都有其一本副本).
现在,引入了DLL这一概念,可执行文件直接加载该DLL即可.在内存中只有一个DLL的代码.
加载DLL的方式有两种,一种是"显式链接",即使用时进行加载,使用后释放内存;另一种是"隐式链接",程序开始即加载,运行结束后释放,这种方式就与IAT有关.
PE文件提供了IAT内存区域,这里是编译器指定的一些内存,文件执行时,PE装载器将DLL中某些函数的实际地址(运行时确认)写入到这个位置,在程序代码中,访问一个库函数并不会将其硬编码到代码中,而是以IAT内存区域中某个内存中存储的地址值去进行call,这个地址值即为PE装载器在启动程序时确认的地址.这样实现由2个原因:
之所以这样间接调用,是因为由于操作系统版本的不同,软件版本的不同,各个DLL中函数的实际地址并不相同,为了保证准确,将获取库函数实际地址的任务交给了PE装载器,在运行时确认当前DLL中库函数的地址.
这就是所谓的DLL重定向,我们无法在编写应用程序的时候就绝对确定一个DLL在内存中的位置.
因此,实际上编译器需要指定程序中的一个内存空间,供PE装载器在执行时将正确的地址写入这个内存空间,在需要调用某个函数时,就从这个内存空间中读取载入的地址,然后进行call调用.
该结构体中记录着PE文件要导入哪些库文件.
每一个导入的库都会对应这样的一个结构体,组成结构体数组,最终以一个NULL填充的结构体结束:
有时候,INT数组与IAT数组指向同一个位置(如下图),但是很多情况下并不是这样的.
PE装载器将导入函数写入IAT的顺序如下:
以notepad为例.
该数组并不在PE头中,而是在PE体中,不过,查找其位置的信息存储在PE头中.
在PE头的IMAGE_OPTIONAL_HEADER32中,DataDirectory[1].VirtualAddress的值即为IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址.
另外,IMAGE_IMPORT_DESCRIPTOR结构体数组也称为IMPORT Directory Table.
查看notepad的DataDirectory数组如下:
找到对应的RVA(7604),根据公式转换为RAW(6A04),从文件中找到该位置:
上图就是找到的数组,当前定位到其第一个元素,也就是第一个导入的dll库.
OriginalFirstThunk - INT:INT为一个包含导入函数信息的结构体指针数组.根据这个数组的信息才能够找到对应函数的地址(参考后面EAT的内容).
跟踪该地址(同样计算RAW),得到:
这里的每一个地址都指向一个IMAGE_IMPORT_BY_NAME结构体(如下所示).
根据每一个元素,例如第一个0x00007A7A(注意小端序),转为RAW为6E7A,继续跟踪可找到第一个函数名:
这里的前2个字节(000F)为Ordinary,为库中函数的固有编号.
FirstThunk - IAT:IAT即为Import Address Table
IAT的RVA:12C4-->RAW:6C4,跟踪过去得:
这里的第一个元素被硬编码为76344906,但无实际意义,运行时会被准确的地址值覆盖.
为了获取库文件中的函数信息,库必须使用EAT机制,用来求得库中各函数的地址.PE文件中,仅有一个IMAGE_EXPORT_DIRECTORY结构体来说明库EAT,而不是向IAT那样的数组,因为IAT可以导入多个库,而一个库只能导出自己.
重要成员如下:
总结来说就是,从函数名称数组中找到该函数名称的字符串,根据该字符串的下标name_index去查找ordinal数组,对应位置的元素值为ordinal,最后,在函数地址数组中以ordinal为下标找到函数的起始地址.
和IAT同样,在NT头的DataDirectory数组中找到EAT的位置:
跳转到0x1A2C即可找到EAT:
接下来就可以根据前面说的步骤去查找某个具体的函数了.
PE文件是Windows系统的可执行文件格式,了解PE文件结构才能够进一步学习更深的逆向技术.
后续还会看到各种PE文件的变体,例如被特殊的压缩程序进行压缩,用于非正常行为的程序(例如病毒等).