下面先看两个PE文件相关的概念:
1.相对虚拟地址(Relative Virtual Adderss, RVA)
当可执行模块加载到内存中时,会有一个0~4G的虚拟地址空间,在PE文件中,代码和数据也是以这种地址方式组织的,只不过 用的不是最后加载到内存中的那个虚拟地址。因为模块还没有加载,所以模块在内存中的虚拟地址是无法确定的。不过,代码和数据相对模块加载基址的偏移量是可 以确定的,到加载的时候,只要用模块基址加上偏移量,就可以在内存中访问相应的代码和数据了。这个偏移量就是相对虚拟地址(RVA)。
2.对齐(Alignment)
“对齐”这个词,以前在写程序时就遇到过,那是在内存中的对齐,这里的“对齐”和以前的那个对齐一样,只不过这是在文件里的对齐。比如文件是以 0x1000字节对齐的,当第一个块的大小为0x1200字节时,那么下一块的起始地址为0x2000,中间的内容补0。
下面是PE文件在大致结构:
+-----------------------------------+
| DOS-Stub |
+-----------------------------------+
| FileHeader |
+-----------------------------------+
| OptionalHeader |
+-----------------------------------+
| DataDirectories |
+-----------------------------------+
| SectionHeaders |
+-----------------------------------+
| Sections |
+-----------------------------------+
1.DOS-Stub and Signature
我们可以通过位于文件最开始处的两个字节来确定当前文件是否有一个DOS头,它的前两个字节必须为“MZ”(0x5A4D)。然后通过查找是否有PE文件 签名来确定是否为PE文件,我们可以先在距文件头偏移0x3C处读入变量e_lfanew(32bits)的值,再通过变量e_lfanew所指示的偏移 量读取一个DWORD,如果这个DWORD是0x00004550(PE/0/0)的话,此文件就是PE文件。
2.文件头(File Header)
文件头紧跟在签名“PE/0/0”后面,也就是从文件开始处偏移e_lfanew + 4的地方,文件头的结构如下:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //用来表示什么样的系统,我们常见的是0x14C: Intel 80386处理器或更高
WORD NumberOfSections; //节的个数,后面读节的时候会用到
DWORD TimeDateStamp; //在绑定输入目录时有用
DWORD PointerToSymbolTable; //符号表指针,好像总是0
DWORD NumberOfSymbols; //符号数,好总是0
WORD SizeOfOptionalHeader; //可选头的大小,能用来验证PE文件结构的正确性
WORD Characteristics; //一些标志位,这里我们先不关心
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; 3.可选头(Optional Header)
可选头就紧跟在文件头后面,结构如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; //叫“魔数”,好像值总是0x010B
BYTE MajorLinkerVersion; //链接器主版本号,用处不大
BYTE MinorLinkerVersion; //链接器次版本号,用处不大
DWORD SizeOfCode; //可执行代码的大小
DWORD SizeOfInitializedData; //已初始化的数据大小
DWORD SizeOfUninitializedData; //未初始化的数据大小,传说中的.bss段?
DWORD AddressOfEntryPoint; //代码入口点,main/Winmain/DriverEntry/Dllmain
DWORD BaseOfCode; //代码的基址,是个RVA
DWORD BaseOfData; //数据的基址,是个RVA //
// NT additional fields.
//
DWORD ImageBase; //映象文件载入地址,EXE一般是0x400000,DLL一般是0x10000000
DWORD SectionAlignment; //节对齐大小,就是在内存中的对齐大小
DWORD FileAlignment; //文件对齐大小,就是在文件中的对齐大小
WORD MajorOperatingSystemVersion; //操作系统主版本号
WORD MinorOperatingSystemVersion; //操作系统次版本号
WORD MajorImageVersion; //映象主版本号
WORD MinorImageVersion; //映象次版本号
WORD MajorSubsystemVersion; //子系统主版本号
WORD MinorSubsystemVersion; //子系统次版本号
DWORD Win32VersionValue; //好像值都是0
DWORD SizeOfImage; //映象文件大小,提示加载器得用多少个页
DWORD SizeOfHeaders; //头的总大小,也是从文件开始到第一节原始数据的偏移量
DWORD CheckSum; //在NT的版本中,对驱动校验,不是驱动不用
WORD Subsystem; //说明文件运行于什么样的系统,后面会看到CS的这个字段为0x02(Win32二进制图象文件)
WORD DllCharacteristics; //当文件是DLL时,表示何时调用DLL入口,不过好像没用
DWORD SizeOfStackReserve; //保留栈的大小
DWORD SizeOfStackCommit; //初始时指定栈的大小
DWORD SizeOfHeapReserve; //保留堆的大小
DWORD SizeOfHeapCommit; //初始时指定堆的大小
DWORD LoaderFlags; //不知道是做什么用的
DWORD NumberOfRvaAndSizes; //是DataDirectory数组有效的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; 4.数据目录(Data Directory)
是一个IMAGE_DATA_DIRECTORY型的数组,长度是IMAGE_NUMBEROF_DIRECTORY_ENTRIES=16。这些目录中每个目录都描述了一个特定的、位于目录项后面的某一节的信息,包括节的位置和节的大小。
IMAGE_DATA_DIRECTORY结构如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //目录的RVA
DWORD Size; //目录大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; 这16个目录的意义和索引如下:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
输出符号目录,较多用于DLL文件的导出函数,后面会说
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 输入符号目录,你的可执行文件用到了哪些外部库,以及外部库的哪些函数,还有一些其它有用信息
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 资源目录,这个就是一些资源
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 异常目录,不知道怎么用
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 安全目录,不知道怎么用
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 基址重定位表,后面会说
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 调试目录,一些调试代码
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 版权目录,一些关于版权的字符串
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 不知道怎么用
#define IMAGE_DIRECTORY_ENTRY_TLS 9 线程局部存储区目录,具体可以看《Windows核心编程》第21章 线程局部存储区
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 载入配置目录,不知道怎么用
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 绑定输入目录,后面会说
#define IMAGE_DIRECTORY_ENTRY_IAT 12 输入地址表,后面会说
#define IMAGE_DIRECTORY_UNKNOWN1 13
#define IMAGE_DIRECTORY_UNKNOWN2 14
#define IMAGE_DIRECTORY_UNKNOWN3 15 这些目录有些是很常用的,比如EXE来说输入目录就很常见,对于DLL来说输出目录就很常见,而像绑定输入目录,基址重定位表,这一类目录只有在考虑到程 序运行效率,并对程序做相应设置时,才会有用。至于如何以及为什么绑定输入和基址重定位,可以看《Windows核心编程》第20章 DLL高级技术。数据目录的后面是节头数组。
5.节目录(Section Directory)
节由节头(IMAGE_SECTION_HEADER)和节的原始数据组成,节的个数被文件头的成员NumberOfSections确定。我们可以把节头全部读进来,再判断每个节具体有什么信息,IMAGE_SECTION_HEADER的结构如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //这是个长为8的数组,就是节的名字
union {
DWORD PhysicalAddress; //目标文件中,表示重定位到的位置
DWORD VirtualSize; //可执行文件中,表示内容的大小
} Misc;
DWORD VirtualAddress; //节中数据的RVA
DWORD SizeOfRawData; //原始数据大小
DWORD PointerToRawData; //指向原始数据的指针
DWORD PointerToRelocations; //重定位指针
DWORD PointerToLinenumbers; //行数指针
WORD NumberOfRelocations; //重定位数
WORD NumberOfLinenumbers; //行数数
DWORD Characteristics; //描述节的内存该被如何处理
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; 通过节的名字,我们可以大致看到这个节是做什么用的,比如代码节的名字通常都是.text,.code,AUTO之类,数据节通常都是.data,.idata,DATA之类,BSS节通常是.bss,BSS。
还有就是节在PE文件里存储时,需要按FileAlignment对齐存储,多出来的地方用0补齐,在内存中存储时,需要按SectionAlignment对齐存储。
6.输出符号(Exported Symbols)
这个概念在DLL中很常用,DLL的导出函数和导出变量就在这个目录中,在VC提供的小工具Dependency.exe看到一些相关的输出。输出表由 IMAGE_DIRECTORY_ENTRY_EXPORT的数据目录指向,是一个IMAGE_EXPORT_DIRECTORY的结构体:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //好像没什么用
DWORD TimeDateStamp; //时间戳
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //指向导出文件名的RVA
DWORD Base; //基址,一般都是1
DWORD NumberOfFunctions; //导出函数个数
DWORD NumberOfNames; //导出名字个数
DWORD AddressOfFunctions; // RVA from base of image,函数入口地址
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
成员NumberOfFunctions和成员NumberOfNames可能不相等,因为一个函数可以有多个名字导出,成员 AddressOfFunctions就是导出函数们的入口址,而AddressOfNames是名字们的地 址,AddressOfNameOrdinals是序号们的地址,其中AddressOfFunctions有NumberOfFunctions个元 素,AddressOfNames和AddressOfNameOrdinals有NumberOfNames个元素。AddressOfNames的值 可能为0,此时,导出函数以序号定位。我们知道,在写一个DLL时,可以在DEF文件中规定每个导出函数的序号。
当我们要通过一个函数名来查找函数入口时,我们可以先在AddressOfNames数组指向的字符串中查找名字,找到以后,以相同的下标在 AddressOfNameOrdinals中查找序号,得到序号后,就以这个序号为下标,在AddressOfFunctions中找到函数入口地址, 或者是一个中转字符串(详细可以看《Windows核心编程》20.4 函数转发器)。如果要直接按序号找,则直接用序号在AddressOfFunctions里查就行了。
7.输入符号(Imported Symbols)
一个模块一般情况下,是都有输入符号的,因为要运行,至少得用到很多操作系统提供的API,尤其是EXE模块。编译器在产生可执行文件时,会发现本文件会 调用外模块的符号,此时编译器就把需要导入的符号,以及这些符号在哪些库中,都写进PE文件里。编译器会给每个符号产生一个存根,这个存根就是一个跳转指 令,然后跳往一个输入表中。
输入符号是由一个IMAGE_IMPORT_DESCRIPTOR结构的数组描述的,以一个全0的IMAGE_IMPORT_DESCRIPTOR结构终止。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date/time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; //导入文件的名字的RVA
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR; 成员OriginalFirstThunk,指向一个以0结尾的,由IMAGE_THUNK_DATA结构的RVA构成的数组,也就是说 OriginalFirstThunk是一个指针数组,只不过里面存的都是RVA而已。其中每一个IMAGE_THUNK_DATA元素都描述一个函数, 而已此数组不会改变。IMAGE_THUNK_DATA结构如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
由此可以看出,结构IMAGE_THUNK_DATA的大小是4个字节,可以用一个DWORD来接收。至于这些成员们都是做什么用的,我们不是很清楚,只知道,成员Function是导入函数的入口(在程序运行时)。不过这四个字节在程序没有运行时,是这样用的:
这个ThunkData的最高位(MSB)为0,则表示这个函数有一个名字,名字就在ThunkData指向的RVA处,并以0结尾;而如果ThunkData的MSB为1,则表示这个函数没有名字,只有序号,序号由ThunkData的低两位字节表示。
IMAGE_IMPORT_DESCRIPTOR的成员FirstThunk在开始时也指向一个IMAGE_THUNK_DATA的RVA数组,并且内容 与OriginalFirstThunk相同。也许你会问,为什么要搞两个一样的数组呢,其实,FirstThunk还有别的用处。
当我们要绑定输入时(过程可以看《Windows核心编程》20.8 模块的绑定),绑定器会把绑定好的RVA填进FirstThunk指向的数组,这样,当一个程序在加载到内存之前,就知道了某个模块中函数的地址,就可以 在加载时省出一些时间,绑定是Microsoft推荐的一种做法,这样可以让程序速度更快。
到此,我们已经看到了PE文件中大部分的结构,和PE文件的大体布局,还有一些结构我没有说,是因为这些知识平时不是很常用,而且我还需要再看,才能完全 搞清楚。不过,有了上面的知识,一般的PE文件我们就都可以看懂了,要想静态破解一个简单的文件也成为了可能。
class CPEAnalyser
{
public:
CPEAnalyser(char* pPEFilePath);
~CPEAnalyser(); bool ReadSuccessfully();
void PrintInfo();
void PrintImageFileHeader();
void PrintImageOptionalHeader();
void PrintImageSectionHeader();
void PrintImageExportDirectory();
void PrintImageImportDescriptor(); private:
bool IsPEFile();
bool GetImageFileHeader();
bool GetImageOptionalHeader();
bool GetImageSectionHeader();
bool GetImageExportDirectory();
bool GetImageImportDescriptor(); private:
char* pPEFilePath;
bool bIsPEFile;
FILE *fpPEFile;
FILE *fpOutput; DWORD ErrorCode;
int offset;
int nImageImportDescriptor; DWORD e_lfanew;
IMAGE_FILE_HEADER ImageFileHeader;
IMAGE_OPTIONAL_HEADER ImageOptionalHeader;
IMAGE_SECTION_HEADER *pImageSectionHeader;
IMAGE_EXPORT_DIRECTORY ImageExportDirectory;
IMAGE_IMPORT_DESCRIPTOR ImageImportDescriptor[1024];
IMAGE_BOUND_IMPORT_DESCRIPTOR ImageBoundImportDescriptor;
IMAGE_BOUND_FORWARDER_REF ImageBoundForwardRef;
IMAGE_BASE_RELOCATION ImageBaseRelocation;
};为了好玩儿,我把CS1.5的可执行文件cstrike.exe做为输入,试了一下,下面是部分输出:
IMAGE_FILE_HEADER:
------Machine: 0x14C
------NumberOfSections: 0x4
------TimeDateStamp: 0x3CFEB280
------PointerToSymbolTable: 0x0
------NumberOfSymbols: 0x0
------SizeOfOptionalHeader: 0xE0
------Characteristics: 0x10F
IMAGE_OPTIONAL_HEADER:
------Magic: 0x10B
------MajorLinkerVersion: 0x6 (难道CS是用VC6.0做的?)
------MinorLinkerVersion: 0x0 ..........................
------IMAGE_SECTION_HEADER:
------SECTION_HEADER[0]
------------Name: .text
------------Misc: 0xAAA5E
------------VirtualAddress: 0x1000
------------SizeOfRawData: 0xAB000
------------PointerToRawData: 0x1000 ..........................
------IMAGE_EXPORT_DIRECTORY: (不知道为什么CS的EXE还有导出函数?晕)
------------Characteristics: 0x0
------------TimeDateStamp: 0x3CFEB280
------------MajorVersion: 0x0
------------MinorVersion: 0x0
------------Name: 0xC7BC0(cstrike.exe)
------------Base: 0x1 ..........................
------IMAGE_IMPORT_DESCRIPTOR(s)(Total=16):
------IMAGE_IMPORT_DESCRIPTOR[0]:
------------OrignalFirstThunk: 0xC3C80
------------[0] 0x1D: ?GetCommunityId@WON_AuthCertificate1@@QBEKXZ
------------[1] 0x25: ?GetNextKey@WON_AuthPublicKeyBlock1@@QBE?AUPubKeyReturn@1@XZ
------------[2] 0x3F: ?Verify@WON_AuthFamilyBuffer@@QBEHPBEG@Z ..........................