分析PE文件资源
PE文件,全称Portable Executable文件,是Windows系统可执行文件采用的普遍格式,像我们平时接触的EXE、DLL、OCX,甚至SYS文件都属于PE文件的范畴。为了在可执行文件中方便的引用其它类型资源内容,PE文件有一个独立的资源段,将程序执行所需要的全部资源文件链接到PE文件内部方便使用。今天我们就来研究一下PE文件资源段的内容与格式。
内存映射文件
俗话说,欲善其功,先利其器。为了分析PE文件结构,我们必须将PE文件映射入进程的内存空间,这里隆重推荐Windows 2000及其以后版本Windows支持的新特性,内存映射文件,这种机制可以将一个文件完全或部分映射入当前进程的地址空间,由系统托管全部的磁盘IO操作,自动实现诸如缓冲区管理,读写控制等相关功能,效率极高。
这里提供本人基于该项技术写的一个文件读取类,该类在构造函数中通过引用返回特定文件的内存映射首地址指针和大小,然后程序就可以像使用本地内存一样使用该指针了。类的代码如下。
//声明
class CMapFile
{
public:
CMapFile(LPCTSTR pPath,bool bWrite,PVOID &pMap,DWORD &dwFileSize);
~CMapFile(void);
private :
HANDLE hFileHandle;
HANDLE hFileMapHandle;
PVOID &pImageView;
DWORD &dwSize;
};
//构造函数
CMapFile::CMapFile(LPCTSTR pPath,bool bWrite,PVOID &pMap,DWORD &dwFileSize)
:pImageView(pMap)
,dwSize(dwFileSize)
{
this->hFileHandle=INVALID_HANDLE_VALUE;
this->hFileMapHandle=INVALID_HANDLE_VALUE;
this->pImageView =NULL;
this->dwSize =0;
try
{
if(bWrite)
{
this->hFileHandle = CreateFile(pPath, GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL , NULL);
}
else
{
this->hFileHandle = CreateFile(pPath,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
}
if(this->hFileHandle==INVALID_HANDLE_VALUE)
{
throw (0);
}
this->dwSize=GetFileSize(this->hFileHandle,NULL);
if(this->dwSize==0)
{
throw (1);
}
if(bWrite)
{
this->hFileMapHandle=CreateFileMapping(this->hFileHandle, NULL,PAGE_READWRITE,0,0,NULL);
}
else
{
this->hFileMapHandle=CreateFileMapping(this->hFileHandle,NULL, PAGE_READONLY,0,0,NULL);
}
if(this->hFileMapHandle==INVALID_HANDLE_VALUE)
{
throw (2);
}
CloseHandle(this->hFileHandle);
this->hFileHandle= INVALID_HANDLE_VALUE;
if(bWrite)
{
this->pImageView=MapViewOfFile(this->hFileMapHandle, FILE_MAP_WRITE|FILE_MAP_READ,0,0,0);
}
else
{
this->pImageView=MapViewOfFile(this->hFileMapHandle, FILE_MAP_READ,0,0,0);
}
if(this->pImageView==NULL)
{
throw (4);
}
}
catch(...)
{
this->pImageView=NULL;
}
}
//析构函数
CMapFile::~CMapFile(void)
{
try
{
if(this->pImageView!=NULL)
{
UnmapViewOfFile(this->pImageView);
}
if(this->hFileMapHandle!=INVALID_HANDLE_VALUE)
{
CloseHandle(this->hFileMapHandle);
}
if(this->hFileHandle!=INVALID_HANDLE_VALUE)
{
CloseHandle(this->hFileHandle);
}
}
catch(...)
{
}
}
在研究PE文件资源段之前,我们必须了解PE文件头结构。下面我们来分析PE文件头的相关格式。PE文件头总体结构如图1所示,可见PE文件的格式是相对复杂的。我将在下文依次分析各个部分的含义。
图1 PE文件结构
MS-DOS头部
第一个结构是MS-DOS 头,这个是为了兼容旧的DOS程序而设计的,如果一个Win32程序在DOS模式下运行(所谓的DOS模式是指纯DOS环境,而不是Windows控制台),DOS头部会把执行定位到MS-DOS实模式残余程序,该程序会调用int 21中断输出一个字符串“This program cannot be run in DOS mode”,然后直接退出。MS-DOS头部在“winnt.h”里面有定义。
typedef struct _IMAGE_DOS_HEADER {// DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp;// Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss;// Initial (relative) SS value
WORD e_sp;// Initial SP value
WORD e_csum; // Checksum
WORD e_ip;// Initial IP value
WORD e_cs;// Initial (relative) CS value
WORD e_lfarlc;// File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4];// Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew;// File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
第一成员变量e_magic被称为魔术数字,它被用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。
至于其余的成员变量,基本上都是为了DOS下实模式设计,如今已经没有什么实际作用,除了最后一个成员变量e_lfanew。这个成员变量用来表示PE头部在这个PE文件中的偏移量。通过如下代码可以获得PE头部地址:
BYTE *pFileImage = (BYTE*)pPeImage;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
注意,在计算偏移地址的时候除了偏移量pDosHeader->e_lfanew,还有一个DWORD的偏移,这个DWORD 是存储PE文件标志的,值为0x4550,对应ASCII字符“PE”。
PE头部
下面我们来介绍PE头部的内容。PE头部在“winnt.h”中定义如下。
typedef struct _IMAGE_FILE_HEADER {
WORDMachine;
WORDNumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORDSizeOfOptionalHeader;
WORDCharacteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
这个结构比较简单,Machine表示这个可执行文件被构建的目标机器种类,本程序获得的Machine值是0x14c,代表i386;NumberOfSection表示本PE文件具有多少个有多少个段头部和多少个段实体,每一个段头部和段实体都在文件中连续地排列着,所以要决定段头部和段实体在哪里结束的话,段的数目是必需的;TimeDataStamp是一个时间戳变量;PointerToSymbolTable和NumberOfSymbols确定了符号表的位置和大小。SizeOfOptionalHeader表示选项头部的大小,选项头部就在PE文件头部后面线性排列,这个结构容后介绍,但是大家不要被名称迷惑,选项头部是对PE文件执行至关重要的结构,并非“Optional”。Characteristics表示文件的一些特征,比如对于一个可执行文件而言,分离调试文件是如何操作的。
选项头
选项头在“winnt.h”中的定义如下。
typedef struct _IMAGE_OPTIONAL_HEADER {
// Standard fields.
WORDMagic;
BYTEMajorLinkerVersion;
BYTEMinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
// NT additional fields.
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORDMajorOperatingSystemVersion;
WORDMinorOperatingSystemVersion;
WORDMajorImageVersion;
WORDMinorImageVersion;
WORDMajorSubsystemVersion;
WORDMinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORDSubsystem;
WORDDllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IM