文章中的图片均为自己做截屏,转载请注明出处。
接上一篇,上一篇了解了一些常用的结构的基本结构,这一篇从异常结构开始学习。
7.异常
PE文件中的异常目录用于描述异常处理函数、SEH地址等信息,存储于.pdata 段内,数据目录项的第四项指向结构的RVA。异常处理和处理器平台有关,所以书上是用64位处理器的平台作为例子讲述的,先来看异常目录的数据结构:
typedef struct_IMAGE_IA64_RUNTIME_FUNCTION_ENTRY {
DWORD BeginAddress; //与SEH相关代码的起始偏移地址
DWORD EndAddress; //与SEH相关代码的末尾偏移地址
DWORD UnwindInfoAddress;//指向描述上面两个字段之间代码异常信息的UNWIND_INFO
} IMAGE_IA64_RUNTIME_FUNCTION_ENTRY,*PIMAGE_IA64_RUNTIME_FUNCTION_ENTRY;
UNWIND_INFO结构称为展开处理程序(unwindhandlers),此结构用于面熟堆栈堆栈指针的记录属性和寄存器保存的地址属性:
typedef struct _UNWIND_INFO {
UBYTE Version : 3; //版本,一般为0x001
#define UNW_FLAG_NHANDLER 0x0 //表示无异常处理函数
#define UNW_FLAG_EHANDLER 0x1 //此处有异常处理函数
#define UNW_FLAG_UHANDLER 0x2 //此处有系统默认的终止函数
#define UNW_FLAG_CHAININFO 0x4 //FunctionEntry字段指向的是前一个//RUNTIME_FUNCTION的RVA
UBYTE Flags: 5; //标识位,使用上面定义的4个常量中的一个
UBYTE SizeOfProlog; //函数起始部分字节的长度
UBYTE CountOfCodes; //展开代码数组的成员数
UBYTE FrameRegister : 4;//寄存器帧指针,为0则指定函数不使用框架
UBYTE FrameOffset : 4;//若上一字段不为0则由偏移处取出RSP设置FP寄存器
UNWIND_CODE UnwindCode[1];//指定永久寄存器和RSP的数组项数目
union {
// If (Flags & UNW_FLAG_EHANDLER)
OPTIONAL ULONG ExceptionHandler;//异常/终止函数的映像相对地址指针
// Else if (Flags & UNW_FLAG_CHAININFO)
OPTIONAL ULONG FunctionEntry;//展开信息链的映像相对地址指针
};
// If (Flags & UNW_FLAG_EHANDLER)
ULONG ExceptionData[1];//异常处理程序的数据
} UNWIND_INFO, *PUNWIND_INFO;
8.安全
这项所在目录也可以在数据目录项中找到,这个目录一般保存着此映像的数字签名,以证实此映像的可信程度。这个结构保存着PE文件的散列值,一旦PE文件被修改,该结构就会被破坏,所以这个结构用于保存文件的完整性和正确性。下面两张截图便是该结构在文件属性显示的内容:
9.基址重定位
数据目录表中的IMAGE_DIRECTORY_ENTRY_BASERELOC结构指向重定位目录。由于dll文件并不一定可以装载到默认的基址,所以需要进行重定位。例如在dll文件中有一条确定地址的指令,这时就需要用到重定位。这里不再细述,等以后用到再详细来看。
10.调试
此结构称为调试目录,往往保存在.debug 的区段里面,主要负责协调第三方程序调试本程序。下面是这个结构:
typedef struct _IMAGE_DEBUG_DIRECTORY {
DWORD Characteristics; //保留
DWORD TimeDateStamp;//日期时间戳
WORD MajorVersion;
WORD MinorVersion;
DWORD Type; //调试信息格式
DWORD SizeOfData; //除调试目录以外的调试数据大小
DWORD AddressOfRawData;//加载到内存时调试数据的RVA,为0则不偏移
DWORD PointerToRawData;//调试数据的文件偏移
} IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;
上面的Type定义如下:
#define IMAGE_DEBUG_TYPE_UNKNOWN 0 //未定义
#define IMAGE_DEBUG_TYPE_COFF 1 //COFF调试信息
#define IMAGE_DEBUG_TYPE_CODEVIEW 2 //VC++调试信息
#define IMAGE_DEBUG_TYPE_FPO 3 //框架指针忽略信息
#define IMAGE_DEBUG_TYPE_MISC 4 //DBG文件位置
#define IMAGE_DEBUG_TYPE_EXCEPTION 5 //.pdata区段的拷贝
#define IMAGE_DEBUG_TYPE_FIXUP 6 //保留
#define IMAGE_DEBUG_TYPE_OMAP_TO_SRC 7 //将此映像RVA映射到源映像的RVA
#define IMAGE_DEBUG_TYPE_OMAP_FROM_SRC 8 //将源映像RVA映射到此映像的RVA
#define IMAGE_DEBUG_TYPE_BORLAND 9 //保留给Borland公司使用
#define IMAGE_DEBUG_TYPE_RESERVED10 10 //保留
根据调试信息类型的不同,调试信息的结构也都不一样,所以相应的数据结构需要根据具体情况去查找,这里就不写出了,用到的时候再去找。
11.特殊结构数据(版权)
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE指向该结构,微软定义这个结构为0,所以在x86和x64的平台上PE文件都未用到这个结构,一般都是在MIPS、ALPHA等平台上用来保存全局指针寄存器的RVA地址,且多用于参数传递,这里不再多讲。
12.TLS(ThreadLocal Storage)
IMAGE_DIRECTORY_ENTRY_TLS指向该结构。根据字面上的意思,这就是保存线程局部变量的地方,在声明为TLS变量之后,系统保证该线程变量对于每一个线程的唯一性,这是系统调用LdrpAllocateTls函数保证该信号量的互斥实现的。系统在调用主函数之前需要调用用户自己定义的回调函数以完成这个变量的初始化工作,且必须要在OEP之前完成。下面来看具体的结构:
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData;//TLS模板在内存中起始RVA地址,模板是线程建立时被用于初始化TLS的数据,系统为每个线程建立一个副本
DWORD EndAddressOfRawData;// TLS模板在内存中结束RVA地址
PDWORD AddressOfIndex; //存放TLS索引的位置
PIMAGE_TLS_CALLBACK *AddressOfCallBacks;//指向一个以0x00000000结尾的TLS回调数组,为0则无回调数组
DWORD SizeOfZeroFill;//用于指定非零化数据后面空白空间的大小
DWORD Characteristics;//保留
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 *PIMAGE_TLS_DIRECTORY32;
13.装入配置(x86/x64)
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG指向该结构。由于这个结构很少用到,主要就是用来描述一些因为太大或是太负杂而不适合在PE头中描述的结构,现在都用这个结构的一个新的版本。下面列出该结构,至于具体的细节就不去了解了:
typedef struct {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD GlobalFlagsClear;
DWORD GlobalFlagsSet;
DWORD CriticalSectionDefaultTimeout;
DWORD DeCommitFreeBlockThreshold;
DWORD DeCommitTotalFreeThreshold;
DWORD LockPrefixTable; // VA
DWORD MaximumAllocationSize;
DWORD VirtualMemoryThreshold;
DWORD ProcessHeapFlags;
DWORD ProcessAffinityMask;
WORD CSDVersion;
WORD Reserved1;
DWORD EditList; // VA
DWORD Reserved[ 1 ];
} IMAGE_LOAD_CONFIG_DIRECTORY32,*PIMAGE_LOAD_CONFIG_DIRECTORY32;
14.绑定导入表
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT指向该结构。该结构就是用于减少程序加载的时间。在程序加载的过程中,PE加载器会检查输入表并将相应的dll文件映射到自己的进程空间里,然后用真实的函数地址替换IAT中的地址,这一步需要花的时间将会比较长。但是当使用了绑定导入之后,IAT中的地址就不需要每次都用上面说的过程了,只需要将该系统的实际地址直接改写就可以了。但是这也是需要前提的:
(1) 进程初始化之前确保所有的dll文件均被加载到首选的基址中,而没有被重定位。
(2) 程序执行绑定之后,dll导出表中的符号位置不变。
只要上面的前提有一个不成立,加载器便会忽略绑定表。同时编写程序是并不知道程序会在哪一个具体的OS版本中运行,所以绑定操作只能是在程序加载的时候动态执行。
15导入地址表
IMAGE_DIRECTORY_ENTRY_IAT指向该结构。程序加载之后会依据导入表中的导入信息保存其导入函数的真正地址。导入地址表并非不一定存在,一般只在安装配置程序中用到。
16.延迟加载表
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT指向该结构。这也是为了加快程序的加载速度用的,使程序不再为了一些没有必要的dll文件而浪费加载时间。在下面两种情况下使用延迟加载:
1程序运行过程中不一定会调用此dll文件中的函数
2程序运行时并非在启动之初调用此dll文件中的函数
17.COM描述符
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR指向此结构。根据功能不同,COM描述符通常位于一个名为.cormeta(保存CLR的相关信息)或.sxdata(次区段保存异常句柄列表,此列表包含每个句柄的COFF符号索引)区段里。
18.总结
至此,对于PE文件的初步学习就在这两天内完成了,今后在看到相关应用的时候也不会手足无措了。至于具体的使用在后面相信会用到很多。