PE文件是以64字节的DOS文件头开始的(IMAGE_DOS_HEADER),接着是一段小DOS程序,然后是248字节的
NT文件头(IMAGE_NT_HEADERS),NT的文件头位置由IMAGE_DOS_HEADER的e_lfanew给出!
NT文件头的前4个字节是文件签名(“PE00"字符串),紧接着是20字节的IMAGE_FILE_HEADER结构,它的
后面是224字节的IMAGE_OPTIONAL_HEADER结构,而就在这个结构里,里面有模块基地址,代码和数据大
小和基地址、线程堆栈和进程堆的配置,程序入口点的地址,还有数据目录表指针,PE文件还保留着16
个数据目录,常见的有导入表,导出表,资源和重定位表,而我们这里就是用的到了导入表
IMAGE_IMPORT_DESCRIPTOR,代码如下
[cpp] view plaincopy
- int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
- {
- int nRetCode = 0;
- // initialize MFC and print and error on failure
- if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
- {
- // TODO: change error code to suit your needs
- cerr << _T("Fatal Error: MFC initialization failed") << endl;
- nRetCode = 1;
- }
- else
- {
- // TODO: code your application‘s behavior here.
- CString strHello;
- strHello.LoadString(IDS_HELLO);
- cout << (LPCTSTR)strHello << endl;
- }
- //这里开始
- HMODULE hMod = ::GetModuleHandle(NULL);
- IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER *)hMod;
- IMAGE_OPTIONAL_HEADER *pOptHeader = (IMAGE_OPTIONAL_HEADER *)((BYTE *)hMod +
- pDosHeader->e_lfanew + 24);
- IMAGE_IMPORT_DESCRIPTOR *pImportDesc = (IMAGE_IMPORT_DESCRIPTOR *) ((BYTE *)hMod +
- pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
- while(pImportDesc->FirstThunk)
- {
- char *pszDllName = (char *)((BYTE *)hMod + pImportDesc->Name);
- printf("/n模块名称:%s/n", pszDllName);
- IMAGE_THUNK_DATA *pThunk = (IMAGE_THUNK_DATA *)((BYTE *)hMod + pImportDesc
- ->OriginalFirstThunk);
- int n = 0;
- //MessageBox(NULL, "Test", "MESS", MB_OK);
- char *pszFunName = NULL;
- while(pThunk->u1.Function)
- {
- pszFunName = (char *)((BYTE *)hMod + (DWORD)pThunk-
- >u1.AddressOfData + 2);
- PDWORD lpAddr = (DWORD *)((BYTE *)hMod + pImportDesc->FirstThunk) +
- n;
- try
- {
- printf("function name : %-25s", (char *)pszFunName);
- }
- catch(...)
- {
- printf("function name :unknown!");
- }
- printf("addr :%0X/n", lpAddr);
- n++;
- pThunk++;
- }
- pImportDesc++;
- }
- return nRetCode;
- }
顺便带上一些检查是否为PE的代码
[cpp] view plaincopy
- CFileDialog dlg(true);
- if(dlg.DoModal() != IDOK)
- {
- return ;
- }
- HANDLE hFile = ::CreateFile(dlg.GetFileName(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
- if(hFile == INVALID_HANDLE_VALUE)
- {
- ::MessageBox(NULL, "INVALID FILE", "VALID PE", MB_OK);
- }
- IMAGE_DOS_HEADER dosHeader;
- IMAGE_NT_HEADERS ntHeader;
- bool bValid = false;
- DWORD dwRead;
- ::ReadFile(hFile, &dosHeader, sizeof(dosHeader), &dwRead, NULL);
- if(dwRead == sizeof(dosHeader))
- {
- if(dosHeader.e_magic == IMAGE_DOS_SIGNATURE)
- {
- if(::SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN) != -1)
- {
- ::ReadFile(hFile, &ntHeader, sizeof(ntHeader), &dwRead, NULL);
- if( dwRead == sizeof(ntHeader) )
- {
- if( ntHeader.Signature == IMAGE_NT_SIGNATURE )
- {
- bValid = true;
- }
- }
- }
- }
- }
- if( bValid)
- ::MessageBox(NULL, "It‘s a PE", "PE FILE", MB_OK);
- else
- ::MessageBox(NULL, "It‘s not a PE", "PE FILE", MB_OK);
1、PE 件格式的背景和由来:
在开始介绍PE 结构之前,有必要向读者提一提常用的PE件结构分析工具:Win32 SDK
提供的 DUMPBIN 可以转储PE文件和COFF OBJ/LIB 件;Borland 的使用者可用TDUMP 观
察PE 文件,但TDUMP 不支持COFF OBJ。
2、PE 件的顺序结构:
我们可以把PE 的内存映象结构用下面的图示简要的表示出来:
DOS MZ HEADER
DOS STUB
PE
Header Signature( “PE/0/0”)
FileHeader
OptionalHeader
Section Table(array of IMAGE_SECTION_HEADER)
.text
.data
.edata
.idata
.reloc
…
COFF Line Number
COFF Symbols
Code View Debug nformation
2.1、DOS header 和 DOS Stub:
所有的 PE 文件 (或32位的DLLS)都必须以一个简单的DOS MZ header 为起始
(IMAGE_DOS_HEADER 结构体)。在实际中,除了e_lfanew (PE header 的 件偏移量)我
们可以不必太关心其余成 数据。DOS Stub只是提供了PE 文件在DOS 下执行时 ,DOS
会把它当作有效的执行文件而顺利执行。通常会在屏幕上输出 " This program cannot
run in DOS mode " 之类的提示语。程序员也可以改变DOS Stub,根据自己的意图实现
完整的 DOS 代码。
2.2、PE Header:
PE 表头内含程序代码和各种资料的大小位置、适用的操作系统、堆栈(stack)最初大小
等等重要信息。犹如执行 件的纲目。整个PEHeader是一个IMAGE_NT_HEADERS 结构体,
在Win32 SDK 中定义如下:
[cpp] view plaincopy
- typedef struct _IMAGE_NT_HEADERS
- {
- DWORD Signature;//PE 标记,值为50h,45h,00h,00h (ASCII:”PE/0/0”)
- IMAGE_FILE_HEADER FileHeader;
- IMAGE_OPTIONAL_HEADER32 OptionalHeader;
- }IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
PE ignature 为校验标记,由连接器产生。通常装载器通过指向此位的指针e_lfanew (DOS
header 中)来检验此文件是否为PE 格式。FileHeader域包含了关于PE 文件物理分布的
一般信息, opionalHeader域包含了关于PE 文件逻辑分布的信息。显然可以pNTHeader=
dosHeader + dosHeader->e_lfanew 获得pe 头的地址。
2.2.1、File Header 结构域:
在Win32 SDK 中File Header 定义如下:
[cpp] view plaincopy
- 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;
各个成 的说明列表如下:
成员说
Machine 该文件运行所要求的CPU。对于Intel平台,该值是
IMAGE_FILE_MACHINE_I386(14Ch)。
NumberOfSections 文件的节数目。如果我们要在文件中增加或删除一个节,就需要修改
这个值。
TimeDateStamp 连接器创建文件时刻。从1969/12/31 4:00 P.M. 之后的总秒数。
PointerToSymbolTable COFF 符号表格的偏移位置,用于调试。
NumberOfSymbols COFF 符号表格中符号的个数,用于调试。
SizeOfOptionalHeader指示紧随本结构之后的OptionalHeader结构大小,必须为有效值。
Characteristics 关于文件信息的标记,比如文件是exe(0x0002) 还是dll(0x2000)在
实际应用中,有三个域对我们有用:NumberOfSections,SizeOfOptionalHeader和
Characteristics。我们通常不会改变SizeOfOptionalHeader和Characteristics的值,
如果要遍历节表就得使用 NumberOfSections。
2. 2。2、Option Header 结构域:
这是PE 表头的第三个成分。对于PE 文件而言,这一部分其实并不是可有可无。因为COFF
格式允许不同的设计者在标准的File header之后定义一个结构。Optional header 正是
PE 设计者认为在基本的File header 信息之外还需要的一些重要的信息。完整的结构定
义可以参考Win32 SDK 中的WINNT.H 头文件,这里列举了其中的重要成 :
成员说
AddressOfEntryPoint PE 装载器准备运行的PE 件的第一个指令的RVA。若要改变整个
执行的流程,可以将该值指定到新的RVA,这样新RVA 处的指令首先被执行。
ImageBase PE 件的优先装载地址。如果该值是400000h,PE装载器将尝试把文件装到
虚拟地址空间的400000h 处。若该地址区域已被其他模块占用,那PE 装载器会选用其他
空闲地址,这个过程我们把它叫做重定位。
SectionAlignment 内存中节对齐的粒度(granularity)。一旦装载到内存中,每个节(s
ection)保证从一个此值的倍数的虚拟地址开始。如果该值是4096 (1000h),那么每节
的起始地址必须是4096 的倍数。
FileAlignment 件中节对齐的粒度。文件中 成每个section 的原始数据(raw data)
保证是从一个此值的倍数的虚拟地址开始。如果该值是(200h),,那么每节的起始地址必
须是512 的倍数。
MajorSubsystemVersion
MinorSubsystemVersion win32子系统版本。若PE 文件是专门为Win32 设计的,该子系
统版本必定是4.0 否则不会有3维立体感对话框等。
SizeOfImage 内存中整个PE 映像体(map)的尺寸。它是所有头和节经过节对齐处理后的
大小。
SizeOfHeaders 所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以
以此值作为PE 文件第一节的文件偏移量。
Subsystem NT 用来识别PE 文件属于哪个子系统。对于大多数Win32程序,只有两类值:
Windows GUI 和 Windows CUI (控制台)。
DataDirectory IMAGE_DATA_DIRECTORY 结构数 。每个结构给出一个重要数据结构的
RVA,比如引入地址表等。在PE header 中,有很多地址指针是用RVA 来表示的。 RVA 代
表相对虚拟地址 (Relative Virtual Address)。简言之,RVA 是虚拟空间中到参考点的
一段距离,类似文件偏移量。当然它是相对虚拟空间里的一个地址,而不是文件头部。举
例来说,如果PE 文件装入虚拟地址(VA)空间的400000h 处,且进程从虚址401000h 开始
执行,我们可以说进程执行起始地址在RVA 1000h。每个RVA 都是相对于模块的起始VA
(Virtual Address)而言的。
2.2.3、Option Header 的中的重要数据成 Data Directory:
Data Directory 定义为IMAGE_DATA_DIRECTORY 结构体数 ,每个数 元素给出一个重
要数据结构的RVA (Relative Visual Address相对虚地址),比如引入表地址、重定位
表地址。通常共16个成 。(详细的宏定义请参阅WIN32 SDK 的WINNT.H 头 件)
这个数 起到让装载器迅速的在内存中找到特定的节(section)的作用,减去了遍历节表
的麻烦。
2. 3、Section Header:
紧接在PE header 的是section table (IMAGE_SECTION_HEADER)。每个表项包含有该节
的属性、偏移量等。如果PE 文件里有3节,那么此数据结构就有3个元素。为了更好的
理解PE header和Section header在PE 文件中的组织关系,我们可以把PE 文件看作一
逻辑磁盘,PE header 是boot 扇区而sections是各种文件,节表视为逻辑磁盘中的根目
录。节表定义如下:
[cpp] view plaincopy
- typedef truct _IMAGE_SECTION_HEADER
- {
- BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
- union
- {
- DWORD PhysicalAddress;
- DWORD VirtualSize;
- }Misc;
- DWORD VirtualAddress;
- DWORD SizeOfRawData;
- DWORD PointerToRawData;
- DWORD PointerToRelocations;
- DWORD PointerToLinenumbers;
- WORD NumberOfRelocations;
- WORD NumberOfLinenumbers;
- DWORD Characteristics;
- }IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
重要成 的说明列表如下:
成员说
Name 节名。长度不超过8字节。仅仅是个标记而已,注意这里不用null结束。
VirtualAddres 本节的RVA (相对虚拟地址)。PE装载器将节映射至内存时会读取本值。
如果域值是1000h,而PE 文件装在地址400000h 处,那么本节就被载到401000h。
SizeOfRawData 经过文件对齐处理后节尺寸,PE 装载器提取本域值了解需映射入内存的
节字节数。
PointerToRawData 这是节基于文件的偏移量,PE装载器通过本域值找到节数据在 件中
的位置。
----------------------- Page 4-----------------------
Characteristics 包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未
初始数据,是否可写、可读等。
2.4、Section域
节(section)是PE 文件真正内容的划分。每一节是是拥有共同属性的数据的集合。每节
都有相应的命名,不过这个命名不是太重要,只要是相同属性的内容都可以放进一节,命
名只是便于识别。下面重点介绍一下一些重要的段。
2.4.1、text section
此节一般包含有连接器连接的所有obj 目标 件的执行代码。这个执行代码块是一个大
的.text。不同于在DOS 下面的执行 件可以分成几部分。如果是使用的Borland C++,
其 编译器将产生的代码存于名为CODE 的区域,连接器连接到名为CODE 而不是.text 的
节中。
2.4.2、data section
.data 是初始化的数据块。这些数据块包括编译时被初始化的字符串常量、全局(globle)
和静 (static)变量。
2.4.3、bss section
任何没有初始化的全局和局部变量都会存放到.bss节中。这个节并不占用文件的储藏空
间,所以 RawDataOffset 总是为0。
2.4.4、rsrc section
该节包含模块的全部资源。如图标、菜单、位图等等。
2.4.5、idata section
.idata 包含其他外来的如DLL 中的函数及数据信息。PE 文件的每一个输入函数都 确的
列于该节中。
2.4.6、edata section
与.idata对应,.edata 是该PE 文件输出函数和数据的列表,以供其他模块引用。有的
PE 文件没有引出函数或数据,也就没有该节。
3、其余部分:
在节 (Sections)的后面是COFF 符号表格、COFF调试信息、COFF 行号信息。这些域对
我们的作用不大,有兴趣的读者可参阅