[原创]64位CreateProcess逆向:(三)PE格式的解析与效验
MiVerifyImageHeader是一个较小的函数。不过,恰恰由于它的较简单,是一个很好的示范,教会我们怎么从逆向的角度获取新知:大部人学习PE文件格式,是根据公开的文档以及头文件winnt.h中所提供的相关结构体。然而,PE中的有些字段,可能只是微软预留的位置,在当下操作系统中并没有实际作用。而随着微软操作系统的更新,有些字段是否检验,在不同版本(如WIN XP与WIN 7)策略是不同的。文档不可能完全跟得上操作系统的变更,winnt.h中的结构体也没法告诉我们哪些字段有意义哪些是预留占位的。
而逆向相关代码,可以得到关于PE校验的最可靠答案。
如前所述,MiVerifyImageHeader用于完成PE文件头的解析及校验工作,MiVerifyImageHeader的函数原型如下:
NTSTATUS MiVerifyImageHeader( OUT IMAGE_INFO* pOutInfo, IN PIMAGE_NT_HEADERS pNtHeader, IN PIMAGE_DOS_HEADER pDosHeader, IN ULONG ntHeaderSize )
第一个传出参数,指向一个IMAGE_INFO结构体,它在MiVerifyImageHeader校验完成后,把后续还会使用的PE头字段,保存起来并传出。它的格式在微软提供的头文件中是找不到的,不过借助于IDA及分析相关代码,我们可以还原得到它的结构:
各字段的意义从字段名上很容易猜到,就不再多说明。
MiVerifyImageHeader的三个传入参数也很容易理解:指向IMAGE_NT_HEADER的指针、指向IMAGE_DOS_HEADER的指针以及NT头的大小。
其中pDosHeader与ntHeaderSize是对于NE格式(Win3.x 所引入的16位可执行文件格式,我们将不做讨论。
整个MiVerifyImageHeader的流程如下:
在函数的开始,会对第二个参数(pNtHeader)进行检验,若该参数所指向地址不是以4字节对齐的话,则直接返回错误码、结束流程:
接着,会检查NT头在Signature字段(以上代码中的cmp ‘EP’),以检测是否是PE文件,如上所述,NE文件及非PE文件不在我们讨论范围。我们直接看流程为’PE’后所做的事情。
首先,是对NtHeader中的FileHeader相关字段进行校验:
不过,对FileHeader的校验,并不全在在MiVerifyImageHeader中完成,结合静态分析及文件修改、试错。我们得出了FileHeader的必检字段,大家可在附件的头文件中
接着,会校验OptionalHeader中的Magic字段,它决定着之后对IMAGE_INFO的填充方式,字段值为0x20B时,对应着64位的格式,这里我们只讨论64位的格式的填充。
对于IMAGE_INFO中非数据目录相关的字段,MiVerifyImageHeader简单地将NT头中对应的字段填充上去:
而对于数据目录字段,则是受NumberOfRvaAndSizes决定的:
因为我们知道,在Winnt.h的IMAGE_OPTIONAL_HEADER64结构体中,有一个数组成员,称为数据目录(DataDirectory)。
IMAGE_NUMBEROF_DIRECTORY_ENTIRES的值为16:
这个数据目录就是我们学习PE结构体据悉的各种“表”,如导入表、导出表等等。
各个表所对应的序号在winnt.h头中也查得到:
但是,我们也知道,一个PE文件,并不一定要有这所有的表项才能正常运行。那哪些表项是必须的,哪些是可选的呢,以及操作系统怎么去判断一个PE文件里有几个表呢?
事实上,从代码中可以看出,MiVerifyImageHeader通过判断NumberofRvaAndSizes这个字段,来判断某些可选项数据表项是否存在。比如,若NumberofRvaAndSizes的值大于6,则说明Debug Directory一定存在(它对应的宏IMAGE_DIRECTORY_ENTRY_DEBUG的值为6),因此,会置IMAGE_INFO的相关字段(isHaveEntryDebug)为true。
IMAGE_INFO中的其它与数据目录有关的字段也是同样的道理,大家看附件中的IDA文件即可知,在此不再赘述。
这些在IMAGE_INFO中的数据,在MiVerifyImageHeader调用返回后还会被系统用到。
在填充完IMAGE_INFO结构体之后,函数返回前,MiVerifyImageHeader还会检查文件对齐值、内存对齐值、镜像大小等进行相应的检测:
文件对齐值、内存对齐值相关检测:
检测文件对齐值是否0x200的倍数,不是则检测是否和内存对齐值相等,不是0x200的倍数且和内存对齐值不相等则出错返回。
检查内存对齐值和文件对齐值是否2的倍数,不是则出错返回。
检查内存对齐值是否小于文件对齐值,小于则出错返回。
文件映像大小检测:
检查映像装入内存后的总大小,大于77000000h则出错返回,也就是说,一个PE文件的最大限制是1904MB,这与StackOverflow上一实际测试的哥们的结果是一致的:
相关标志的检测:
检查OptionalHeader.Magic和FileHeader.Machine值是否合法,这分为64位系统与32位系统两种情况。
当64为系统时,要求OptionalHeader.Magic为020Bh,且FileHeader.Machine为0200h或者8664h。
当32为系统时,OptionalHeader.Magic为010Bh,且FileHeader.Machine为014Ch。
以上检测若有不通过的项,均返回STATUS_INVALID_IMAGE_FORMAT。
PE结构体中需要校验字段
要注意,PE格式的检查,并不全部在MiVerifyImageHeader函数中完成。我们通过分析系统代码及反复试错,确认了64位PE头结构中必需检验的字段。在此分享给大家,代码如下,大家也可在附件的头文件中查看。

一并附上32位的相关结构体,大家在学习PE时,可以对照:


应用--畸形PE
研究PE文件的校验函数,得出其校验流程,是很有实践意义的。其最直接的例子,就是可以通过写出畸形PE来躲过反病毒分析系统的检测:只要分析系统的校验逻辑与Windows有一个地方不致,就可能被利用并躲过。
附件中是我们所写的一个畸形PE,它只有0x118个字节,在Windows 7 系统上可正常运行,弹出一个对话框:
[ATTACH][/ATTACH]
而我们将它上传到几个知名的文件行为分析系统进行测试:
腾讯的哈勃系统,未能检测出程序行为:
Anubis系统认定该文件不能正常执行
金山的火眼系统认定不是一个有效的PE文件:
这说明,我们所写的畸形PE,在物理机上能正常执行的前提下,成功躲避了以上各个自动检测系统。
IDB文件:
ntoskrnl.7z.001.rar
ntoskrnl.7z.002.rar
ntoskrnl.7z.003.rar
请将IDB文件的后缀名“.rar”删除后用7z解压
