PE 文件格式详解(下)
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:16004488
微信公众号:计算机与网络安全
ID:Computer-network
接上篇《PE 文件格式详解(中)》
十、调试
数据目录的IMAGE_DIRECTORY_ENTRY_DEBUG项指向此结构,此结构也叫做调试目录,它往往保存在一个名为.debug的区段里,主要负责协助第三方程序调试本程序,并为其提供调试数据块的位置与大小。
调试目录使用一个名为IMAGE_DEBUG_DIRECTORY的结构作为索引,用数据目录表中的Size字段与IMAGE_DEBUG_DIRECTORY的结构的大小可以计算出调试目录中的元素个数。相关代码如下:
上面的结构说明如下。
1)Characteristics:保留字段,必须为0。
2)TimeDateStamp:调试数据建立的时间与日期。
3)MajorVersion:调试数据的主版本号。
4)MinorVersion:调试数据的子版本号。
5)Type:调试数据的格式,这个字段中指明调试数据的类型,用以支持多种调试器,Windows SDK对其做的定义如表17所示。
6)SizeOfData:除调试目录(IMAGE_DEBUG_DIRECTORY)本身以外的调试数据大小。
7)AddressOfRawData:加载到内存时的调试数据RAV,此字段为0则不映射。
8)PointerToRawData:调试数据的文件偏移。
十一、全局指针
数据目录的IMAGE_DIRECTORY_ENTRY_GLOBALPTR项指向此结构,目录项中的VirtualAddress字段保存着全局指针寄存器的RAV地址,字段Size为0x00000000。
在x86与x64系列平台上的PE文件中并未应用全局指针这个概念。就我的调查来看,目前全局指针只应用在MIPS、ALPHA与NiosII平台上,且多用于参数传递。以下这段代码会在NiosII平台中生成使用全局指针的代码。
以上代码编译后会生成一个名为.rwdata的区段,以下是从.rwdata区段Dump出来的部分数据。
由上面Dump的数据可以得出表18所示的内容。
由表18可以得出gp-&a=0x80d948-0x805948=0x8000,而0x8000的十进制是-32768。这时我们再看看以上代码的部分反汇编指令,即可明白全局指针是怎样工作的了。如下所示:
十二、TLS
数据目录的IMAGE_DIRECTORY_ENTRY_TLS项指向此结构,TLS变量往往保存在一个名为.tls的区段里。TLS是静态线程局部存储器提供给PE/COFF的一种比较特殊的存储类型,其本质上仍属于一个局部变量,单独存在于每个线程中。
TLS分为变量与回调函数两部分,其中变量又分为静态模式与动态模式,动态模式是指用TlsAlloc、TlsFree、TlsSetValue与TlsGetValue这几个系统提供的API并使用TLS变量的方式,而静态模式是指使用Visual C++中提供的__declspec(thread)关键字来声明的TLS变量。
当我们使用如下代码声明一个TLS变量时,系统就会自动生成.tls节。
需要注意的是,这种静态声明的变量不能使用在动态链接库(DLL)里,微软并不能保证静态TLS变量能在动态库中正确工作(API函数LoadLibrary可能会拒绝加载此动态库),除非你十分了解这个动态库。
当我们声明了以上TLS变量后,如果不同线程访问并修改这个tlsNum,系统会负责保证这个变量相对于每个线程的唯一性(即非共用的,像局部变量一样不受影响)。这个特性是程序启动时,由在LdrpInitializeThread之后的LdrpAllocateTls函数实现的。
1、TLS的回调函数
由于TLS回调函数会优先于程序运行,因此会给调试过程带来很多的不便,如果采用默认加载方式,TLS回调函数会在调试器中断在OEP(程序入口点)之前执行完毕,因此这是一种在病毒木马与反调试领域里应用得比较广泛的技术。
在Windows系统中TLS回调函数使用以下类似于DLL入口的声明格式:
其中,参数Reason用以接收导致此回调函数执行的事件类型,我们可以通过判断这个参数来对特定的事件做一定的处理。能触发TLS回调函数的事件类型共有几种,如表19所示。
以下是一个TLS回调函数的使用示例:
通过以上示例可以看出,创建一个TLS的回调函数还是比较容易的,但是仅仅存在这样一个函数的话并不足以使其运行,我们还需要做一些其他工作。代码清单4就是一个使用TLS的例子,这个例子可以很全面地反映出TLS的诸多特性。
代码清单4 一个TLS回调函数的应用示例
以上代码的执行结果如下:
由以上代码及执行结果可以发现以下几个特点:
线程1在启动时已经将全局的TLS变量g_nNum修改为0x2222222,且直到线程1退出后线程2才会启动,但是这一操作并没有影响线程2正常使用。
由TLS回调函数打印信息的顺序可知,TLS回调函数的执行是有序的,这取决于你注册TLS回调函数时所用的顺序。
由以上内容可知,TLS这个x86所支持的特性是一个颇为精妙的设计,它可以帮助程序设计人员完成很多有意思的功能。但是在使用TLS回调函数时也要注意,例如MFC框架或Socket等需要在运行时动态加载链接库的程序会具有特殊性,如果你想在TLS使用这些功能,那么你就必须自己完成必要的加载与环境的初始化工作。
2、TLS的结构(x86/x64)
TLS结构是由IMAGE_TLS_DIRECTORY32结构组成的,通常这个结构位于.data区段中。以下是IMAGE_TLS_DIRECTORY32结构的声明:
上述结构说明如下。
1)StartAddressOfRawData:TLS模板在内存中的起始虚拟地址(VA),模板是线程建立时被用于初始化TLS的数据,系统会为其复制的所有数据建立一个本线程私有的副本。
2)EndAddressOfRawData:TLS模板在内存中的结束虚拟地址(VA)。
3)AddressOfIndex:存放TLS索引的位置。
4)AddressOfCallBacks:指向一个以0x00000000结尾的TLS回调函数数组,如果为0则证明未使用TLS回调函数。
5)SizeOfZeroFill:用于指定非零初始化数据后面的空白空间的大小。
6)Characteristics:保留。
3、识别TLS
TlsDemo.exe是以前面的示例代码编译出来的程序,根据其数据目录表的IMAGE_DIRECTORY_ENTRY_TLS项可知,TLS目录IMAGE_TLS_DIRECTORY32结构的RAV为0x00002200,大小为0x00000018,由此得到以下十六进制信息:
图15是对以上数据的解读。
图15 TlsDemo.exe中TLS目录的信息
由于IMAGE_TLS_DIRECTORY32结构的成员里保存的都是VA地址信息,因此我们要将其转换为文件偏移才能继续。可根据ImageBase与各个区段的信息计算出偏移,如表20所示。
通过表格中的StartAddressOfRawData字段与EndAddressOfRawData字段的文件偏移地址,可找到以下十六进制信息:
这是我们在前面的示例代码中用__declspec(thread)关键字所声明的两个TLS静态变量。
而通过表格中AddressOfCallBacks字段的文件偏移地址又可找到如下信息:
由以上十六进制信息可知这是一个有两个成员的函数指针数组,其地址分别为0x00401000与0x00401020。
由于AddressOfIndex所指向的地址并没有任何数据,所以到此为止我们已经找齐了与TLS相关的所有关键数据。当我们熟悉了TLS在PE文件中存在的结构之后,便可以很容易地手工为某个应用程序添加TLS机制了。
十三、载入配置(x86/x64)
数据目录的IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG项指向此结构,加载配置结构最早是被应用在Windows NT操作系统中的,但是一般情况下极少用到。它通常用来描述一些因为太大或太复杂而不适合在PE头或选项头中描述的特征。当前版本的Microsoft linker和Windows 后续产品用的是该结构的一个新版本,这个新版本是专门为32位的x86系统设计的,其中包括了SEH技术。这里包括一个安全结构的异常处理程序清单,当异常发生时其可供系统调配,如果处理程序的地址在映像的VA范围内,而且标记为SEH敏感的话(即IMAGE_DLLCHARACTERISTICS_NO_SEH在可选头文件的DllCharacteristics字段中明确说明了),那么在这个映像的已知安全结构处理程序清单中就必须包含此处理程序。否则系统就会终止该程序,这有助于防止黑客利用“x86异常句柄劫持”攻击并控制操作系统。
Microsoft连接器自动提供了一个默认的加载配置结构来存放SEH数据。如果用户代码已经提供了一个加载配置结构,那么它必须包括一个新的预制SEH字段,否则连接器不能包含SEH数据,以至于映像也不会显式包含SEH。
在Windows的32位平台上,载入配置目录的大小恒为64个字节。其具体结构如下:
上述结构说明如下。
1)Characteristics:指示文件属性的标记,目前没有用到。
2)TimeDateStamp:日期和时间戳的值。该值是按照系统时间以秒表示的,具体数值为通用协调时间下,从1970年1月1日的00:00:00开始到当前时间。可以通过调用C runtime(CRT)time函数来打印时间戳。
3)MajorVersion:主版本号。
4)MinorVersion:子版本号。
5)GlobalFlagsClear:当加载程序启动该进程时,用来清除加载标记。
6)GlobalFlagsSet:当加载程序启动该进程时,用来设置加载标记。
7)CriticalSectionDefaultTimeout:临界区默认超时值,已经废弃,不再使用。
8)DeCommitFreeBlockThreshold:在返回系统前内存必须被释放(单位Byte)。
9)DeCommitTotalFreeThreshold:释放内存的总数(单位Byte)。
10)LockPrefixTable:[x86专用]预加锁虚拟地址表,在单处理器系统中可以代替NOP。
11)MaximumAllocationSize:最大配置尺寸(单位为Byte)。
12)VirtualMemoryThreshold:最大虚拟内存尺寸(单位为Byte)。
13)ProcessHeapFlags:进程堆标记与HeapCreate函数的第一个参数相对应,这些标记用于在程序启动过程中为其创建堆。
14)ProcessAffinityMask:如果此字段的值不为0,则在进程启动时用这个值调用SetProcessAffinityMask函数(仅对EXE文件有效)。
15)CSDVersion:服务包版本指示器。
16)Reserved1:保留。
17)EditList:保留,供系统记录编辑表VA使用。
18)Reserved:保留。
十四、绑定导入表
数据目录的IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT项指向此结构,绑定导入的目的在于减少程序的加载时间。当我们试图执行一个PE文件时,PE加载器会检查输入表并将相关的DLL映射到此进程的系统空间中,然后遍历IAT里的IMAGE_THUNK_DATA数组并用导入函数的真实地址替换它,这一步显然是需要花很多时间的。
但是如果我们事先可以知道导入函数的地址,那么PE加载器就不用每次都为这个程序修正IMAGE_THUNK_DATA数组中的值了,绑定输入就是让这成为可能的好办法。
如果我们对一个应用程序做了绑定导入的操作,那么IAT中的IMAGE_THUNK_DATA数组就会被此系统中导入函数的实际地址改写,这样很显然会大幅提升应用程序的启动速度。但是这些操作都是基于以下两个假设的:
当进程初始化时,此程序所需要的所有DLL都加载到了它们的首选加载基址中。
此程序执行绑定导入操作以后,DLL导出表中引用的符号位置一直不会改变。
如果上面两个假设条件有一个不成立,那么IAT中的所有地址就都是无效的,加载器会加以判断并作出相应的反应,忽略绑定导入的存在。
由于用户在编写程序时无法预先知道用户的操作系统类型及版本,因此只有在程序被安装时才能执行绑定操作。Windows安装器的BindImage会负责完成这些工作,imagehlp.dll提供的BingImageEx函数用于完成这个操作。
1、绑定导入表结构
在系统加载一个绑定输入的可执行文件时,会验证可执行文件中保存的相关系统的DLL信息,以此来检查绑定是否仍然有效,Windows使用IMAGE_BOUND_IMPORT_DESCRIPTOR结构来描述绑定导入。
上述结构说明如下。
1)TimeDateStamp:被导入DLL的时间戳,用于辅助加载器判断此绑定信息是否仍然可用。
2)OffsetModuleName:被导入DLL名称的偏移。
3)NumberOfModuleForwarderRefs:此结构后面的IMAGE_BOUND_FORWARDER_REF结构的数量,这个结构的定义如下所示。
IMAGE_BOUND_FORWARDER_REF结构除了最后一个保留字段外,剩下的与绑定导入结构中的成员作用都是一样的。
绝大多数情况下绑定导入的NumberOfModuleForwarderRefs字段都是0,除非这个函数采用了转发链机制,例如KERNEL32.dll里的HeapAlloc函数就会被转向NTDLL.dll里的RtlAllocateHeap函数中,这样的话其NumberOfModuleForwarderRefs字段就会为1,后面的IMAGE_BOUND_FORWARDER_REF结构则保存NTDLL.dll的相关信息。
2、识别绑定导入表
以下是从Windows 7的画图程序mspaint.exe中Dump出的一段绑定导入表的十六进制信息。
可按照绑定导入表结构将0x00000320之前的信息总结为图16。
图16 识别绑定导入表
由图16可知,我们的例子中一共包含两个IMAGE_BOUND_IMPORT_DESCRIPTOR结构与一个IMAGE_BOUND_FORWARDER_REF结构,第一个结构的NumberOfModule-ForwarderRefs字段为1,这表明其后面跟着一个IMAGE_BOUND_FORWARDER_REF结构,它告诉我们第一个结构中的某个导出函数需要转发一次。
而第二个结构的NumberOfModuleForwarderRefs字段为0,这代表它是一个很普通的绑定导入表。
接下来我们就可以根据图16的"Offset……"(OffsetModuleName)字段并与上面的十六进制相结合找到DLL名称了。这里OffsetModuleName字段里的内容指的是从绑定导入目录起始位置(也就是0x00000280)开始的偏移。整理后的信息如下:
十五、导入地址表
数据目录的IMAGE_DIRECTORY_ENTRY_IAT项指向此结构,导入地址表是数据目录表中名称为IMAGE_DIRECTORY_ENTRY_IAT的结构,其本身就是一个IMAGE_THUNK_DATA结构体数组。在程序加载后,这个结构体数组会依据导入表中的导入项信息保存其导入函数的真正地址。
在现有的Windows平台下,导入地址表并非是一个必须存在的结构,一般情况下它只在一些安装配置程序中有些许用途,例如在程序安装之初,安装程序可以根据现有的系统环境重写导入地址表中的结构,以求被安装程序在以后能有良好的加载速度。
在反汇编代码中,我们往往会看到类似这样的代码call dword ptr[xxxxxxxx],这里的xxxxxxxx就是导入至地址表中某一项的VA,这条语句的意思就是在xxxxxxxx处取出值,并将其当成一个地址跳转过去。
十六、延迟加载表
数据目录的IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT项指向此结构,延迟加载的作用与绑定导入是大致一样的,本质上都是为了加快PE文件的加载速度而设定的,只不过使用的方法不一样而已。
延迟加载是指在调用某一DLL时再将其加载的一种行为,其目的是为了避免程序在启动之初因加载了不必要的DLL而浪费了时间。微软的工程师建议在以下两种情况下使用延迟加载:
程序的运行过程中并非一定会调用此动态链接库中的函数。
程序在运行时并非在启动之初就会调用此动态链接库中的函数。
就本质来讲,延迟加载并非是操作系统支持的一个特性,事实上它完全是由编译器控制的,实现延迟加载是通过连接器在程序中置入相关代码来完成的,此代码在DelayHlp.cpp中可以找到(由此可以推断,如果我们愿意,也可以让其植入一些其他的代码)。
1、延迟加载表结构
在DelayImp.h中的ImgDelayDescr结构体描述了延迟加载结构。
上述结构说明如下。
1)grAttrs:延迟加载结构的属性,0为采用老版本,1为采用新版本。
2)rvaDLLName:指向一个被导入DLL名称的RAV,此字符串将被传送给LoadLibrary函数使用。
3)rvaHmod:指向延迟加载模块的句柄,提供给延迟加载辅助函数使用。
4)rvaIAT:指向导入地址表(IAT)的RAV。
5)rvaINT:指向导入名称表(INT)的RAV。
6)rvaBoundIAT:指向绑定输入表的RAV。
7)rvaUnloadIAT:指向原始IAT可选拷贝的RVA,总被设为0。
8)dwTimeStamp:延迟载入DLL的时间戳,通常为0。
在PE文件结构中,对于输入表的描述共有以下几个部分:
数据目录表第2项的导入表。
数据目录表第12项的绑定导入表。
数据目录表第13项的导入地址表。
数据目录表第14项的延迟加载表。
以上4个结构共同构成了整个PE文件结构的函数导入体系,其中3项都是出于效率的考虑。而ImgDelayDescr结构本身就包含了以上所有的信息,但是我们一定要知道,这些信息并非是给操作系统使用的,而是由程序中的相关运行库代码解析的,这一点很重要。
2、识别延迟加载表
以下是从Windows 7的画图程序mspaint.exe中Dump出的一段延迟加载表的十六进制信息:
图17是对以上十六进制数据的解读。
图17 延迟加载表
根据区段.text的起始RAV与起始文件偏移,可以计算出字段rvaDLLName所指的文件偏移为0x00085140。而我们通过以上十六进制信息可知0x00085140处有个一个字符串gdiplus.dll,至此我们已经知道了这个延迟加载表的所有信息。
十七、COM描述符
数据目录的IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR项指向此结构,根据其功能的不同,COM描述符通常位于一个名为.cormeta或.sxdata的区段里。
如果区段名为.cormeta,则代表此处保存的信息是与公共语言运行时环境(CLR)相关的信息。
如果区段名为.sxdata,则代表此区段内保存有异常句柄列表,此列表内包含每个句柄的COFF符号索引,每个索引使用4字节(区段属性必须为IMAGE_SCN_LNK_INFO,有关于此属性的信息请参考表5)。
PE文件格式是一个异常庞大且较为复杂的结构,也是Windows下研究网络安全所必须掌握的知识。从MS-DOS头部开始,逐步为大家讲解了PE文件格式中的每一个结构,是迄今为止对PE文件格式介绍得最为全面的资料。
微信公众号:计算机与网络安全
ID:Computer-network