本文将讨论PE文件中非常重要的一部分信息。(转载请指明来源于breakSoftware的CSDN博客)
首先说一下VC中对应的数据结构。“签名、COFF文件头和可选文件头”这三部分信息组合在一起是一个叫IMAGE_NT_HEADERS的结构体。
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
其中Signature对应于“签名”,FileHeader对应于“COFF文件头”,OptionalHeader对应于“可选文件头”。
对于PE镜像文件,Signature对应的数据是0x00004550(‘PE\0\0’)。对于如何找到这个位置,在前一篇文章中已经有了解说:从文件头偏移0x3C读取一个DWORD大小的数据,从文件头偏移该数据长度,就到了Signature的起始位置。 看一下COFF文件头结构
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
以notepad为例
Machine字段为0x014C,其对应的信息是“Intel 386或其后续处理器及兼容处理器”。
NumberOfSections是0x0003,它是个非常重要的字段,表示节的数目。PE文件是由一系列“节”构成的,比较常见的是.text和.data等节,这样的独立的区块是用来存储“代码”、“数据”和“资源”等信息的。如xp上notepad,从数据中我们可以看到它有3个节,我们用其他工具分析得到它确实存在如下3个节。
TimeDateStamp是0x41107CC3,该字段记录的是文件创建时间离1970年1月1日00:00的秒数。
PointerToSymbolTable是0x00000000,该字段记录了该PE文件中调试信息符号表。由于符号表信息是在程序运行时不需要加载进入内存的,所以这个偏移使用的是相对文件头偏移RA。目前微软推荐是:将映像文件调试符号表信息独立的放在PDB文件中,所以不会在PE文件中再保存调试符号表信息,于是这个字段应该为0。当然这并不是硬性要求,我发现我电脑上就存在很多该字段不为0的文件。刚开始时我也不是很明白它们为什么要使用这个字段,特别是其指向的字符表个数(NumberOfSymbols)为0!!你说既然大小为0,那你指向有什么意思呢?其实这种设计是非常有深意的,我会在之后的章节中介绍这种深意。
NumberOfSymbols是0x00000000,该字段记录了该PE文件中调试信息符号表元素个数。对于映像文件,该字段为0(非硬性要求),,理由在PointerToSymbolTable中已经说明。通过NumberOfSymbols和PointerToSymbolTable,我们可以找到字符串表起始位置,因为字符串表紧跟在符号表之后。
SizeOfOptionalHeader是0x00E0,该字段用于描述“可选文件头”的大小。之后会看到“可选文件头”的中有个具有16个元素是数组,该数组保存了一系列“块信息”,但是并不是所有文件都有全部的“块信息”,于是链接器在链接生成PE文件时,也是根据实际存在的“块信息”位置(以后会说明为什么是位置而不是数量)去填充这个数组的。也就是说我们可能只是填充了1个元素,而剩下的15个元素直接被砍掉,而不是在内存中使用0来填充。
这儿就引入一个问题,就是我们不能从“签名”位置开始,就直接memcpy一段IMAGE_NT_HEADERS大小的空间到一个IMAGE_NT_HEADERS对象中。因为“可选文件头”还要看“COFF文件头”中的SizeOfOptionalHeader数据。
Characteristics字段用于标记该文件属性,notepad.exe该字段值为0x010F。下面我们来解释下该组合属性
标志 | 值 | 说明 |
---|---|---|
IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 仅适用于映像文件。它表明此文件不包含机制重定位信息,于是它只能被加载到其首选基地址。如果首选基地址不可用,则加载器会报错。链接器默认会移除可执行文件中的重定位信息。一般情况下,Exe文件会设置该值(如notepad.exe,但ntoskrnl.exe就没设置),而因为DLL文件为了其良好的兼容性是不会去设置这个值的(如Kernel32.dll、User32.dll等)。 |
IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 仅适用于映像文件。它用于表明该文件是合法的,可以被运行。如果没有设置,则代表链接出现问题。这个一般都会设置。 |
IMAGE_FILE_LINE_NUMS_STRIPPED | 0x0004 | COFF行号信息已经被移除。不赞成使用该标志。但是我发现notepad.exe、Kernel32.dll、User32.dll等都设置了该标志。而一般我们编译的PE文件是不设置该项的。 |
IMAGE_FILE_LOCAL_SYMS_STRIPPED | 0x0008 | COFF符号表中有关局部符号的项已经被移除。不赞成使用该标志。但是我发现notepad.exe、Kernel32.dll、User32.dll等都设置了该标志。而一般我们编译的PE文件是不设置该项的。 |
IMAGE_FILE_AGGRESSIVE_WS_TRIM | 0x0010 | 该标志已经被废弃。 |
IMAGE_FILE_LARGE_ADDRESS_ AWARE | 0x0020 | 应用程序可以处理大于2GB的地址。 |
0x0040 | 为未来保留的字段。 | |
IMAGE_FILE_BYTES_REVERSED_LO | 0x0080 | 小尾,LSB在MSB前面。不赞成使用该标志。windows xp就是小尾。 |
IMAGE_FILE_32BIT_MACHINE | 0x0100 | 适用于32位系统。我的xp系统上DLL和Exe文件基本都设置了该标志。 |
IMAGE_FILE_DEBUG_STRIPPED | 0x0200 | 调试信息已经从该映像文件中移除。 |
IMAGE_FILE_REMOVABLE_RUN_ FROM_SWAP | 0x0400 | 如果该文件是在移动介质上,需要将其完全加载到交换文件中。 |
IMAGE_FILE_NET_RUN_FROM_SWAP | 0x0800 | 如果该文件是在网络介质上,需要将其完全加载到交换文件中。 |
IMAGE_FILE_SYSTEM | 0x1000 | 该映像文件是一个系统文件,不是一个用户文件。 |
IMAGE_FILE_DLL | 0x2000 | 此文件是DLL文件。 |
IMAGE_FILE_UP_SYSTEM_ONLY | 0x4000 | 该文件仅能运行于单处理机器上。 |
IMAGE_FILE_BYTES_REVERSED_HI | 0x8000 | 大尾,LSB在MSB后面。 |
我观察了我系统上几个文件,发现以下规律:
1 Sys和Exe的该属性为0x010E或者0x010F。
2 DLL文件该属性一般为0x210E。DLL文件一般不会设IMAGE_FILE_RELOCS_STRIPPED(0x0001),因为它为了良好的兼容性,不能设置它必须要被加载的地址。一个Exe可能会加载多个DLL,如果系统“不小心”把某个DLL加载到0x70000000,那么如果有某个DLL设置了IMAGE_FILE_RELOCS_STRIPPED并将其首选加载地址正好也设置为0x70000000,那么系统为该Exe加载这个DLL将会失败。但是的确存在这样的文件,比如我电脑上ResourceCache.dll。DLL文件肯定要设置IMAGE_FILE_DLL。所以即使某个DLL文件的后缀名改了,你可以结合这个“特征码”来还原其真面目。
这儿我还要说一个认知的误区。 IMAGE_FILE_32BIT_MACHINE标志可以用于标志这个文件是适用于32位系统,但是如果仅仅通过该标志就去鉴别这个文件是32位文件还是64位文件是不正确的。我也不知道微软为什么设计了该标志而没有严格限制这个标志。我通过扫描我电脑里所有文件,发现了一个可能具有指导性的鉴别策略:
1 如果没有设置 IMAGE_FILE_32BIT_MACHINE但是设置了IMAGE_FILE_LARGE_ADDRESS_ AWARE的文件是64位文件。没有设置IMAGE_FILE_32BIT_MACHINE意味着该文件可能是64位程序,而设置了IMAGE_FILE_LARGE_ADDRESS_ AWARE,则说明该文件可以处理大于2G的空间的内存,则该文件是64位文件。如我本机上wwst64.exe。
2 除了以上判断之外的其他可能标志该文件是32位文件。
比如设置了IMAGE_FILE_32BIT_MACHINE而没有设置IMAGE_FILE_LARGE_ADDRESS_ AWARE,则说明这个文件可以处理2G以内内存空间,是32位文件;
比如没有设置IMAGE_FILE_32BIT_MACHINE和IMAGE_FILE_LARGE_ADDRESS_ AWARE,怎么解释呢?反正它不是64位文件,因为不能处理大于2G内存空间,那它只能是32位文件了。如我本机上文件sqlite3.dll。
比如设置了IMAGE_FILE_32BIT_MACHINE和IMAGE_FILE_LARGE_ADDRESS_ AWARE,那说明这是个可以处理大于2G内存空间的32位文件。如我本机上AcroBroker.exe。
BOOL CGetPEInfo::GetFileType() {
CHECKFILETYPE();
GETFILEHEADER();
m_eFileType = E32Bit;
m_bIsDllFile = ( m_FileHeader.Characteristics & IMAGE_FILE_DLL ) ? TRUE : FALSE;
if ( !( IMAGE_FILE_32BIT_MACHINE & m_FileHeader.Characteristics )
&& !( IMAGE_FILE_LARGE_ADDRESS_AWARE & m_FileHeader.Characteristics ) ) {
//_ASSERT(FALSE);
}
else if ( !( IMAGE_FILE_32BIT_MACHINE & m_FileHeader.Characteristics )
&& ( IMAGE_FILE_LARGE_ADDRESS_AWARE & m_FileHeader.Characteristics ) )
{
m_eFileType = E64Bit;
}
else if ( ( IMAGE_FILE_32BIT_MACHINE & m_FileHeader.Characteristics )
&& ( IMAGE_FILE_LARGE_ADDRESS_AWARE & m_FileHeader.Characteristics ) ) {
}
return TRUE;
}