前面两帖子介绍了PE文件的大概组成。由DOS_MZ头,DOS_STUB,PE标识,标准PE头,可选PE头,节表,PE节区组成。
这些组成以字节为单位,紧密有序相连,能够很方便的读取修改。
DOS_MZ头,DOS_STUB,PE标识,标准PE头的读取已在前面帖子实现了读取与修改,并提供源代码下载。
这个帖子来实现可选PE头的读取与修改,同样在尾部提供源代码下载。
源代码在前面两帖子基础上新增可选PE头读取功能,界面如下:
PE文件学习最好的方法就是自己动手编写代码实现读取修改,
所以可以自己尝试编写代码学习PE文件结构。
点击界面的小按钮,还可以对可选PE头成员相应成员详细修改。
可选PE头对应结构体为IMAGE_OPTIONAL_HEADER,
在编程软如例程使用的VS2010中,选中可选PE头结构体类型,按F12,可定位到其具体定义。
如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
// Standard fields.
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
// NT additional fields.
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
每个成员变量的含义放大后面介绍。
这里选来看下如何通过代码读取PE文件的可选PE头。
由前面介绍的PE文件的组成方式,可以很方便定义获取可选PE头地址,如例程代码:
- void COptionalHeadDlg::UpdateData(PIMAGE_DOS_HEADER pDosHeader,bool bSetGet)
- {
- if(pDosHeader == NULL)
- return;
- PIMAGE_NT_HEADERS pNtHeader=(PIMAGE_NT_HEADERS )((char *)pDosHeader+pDosHeader->e_lfanew);
- PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeader->OptionalHeader;
- CString sText;
- if(bSetGet)
- {
- sText.Format("%04X",pOptionalHeader->Magic);
- SetDlgItemText(IDC_EDIT1,sText);
- sText.Format("%02X",pOptionalHeader->MajorLinkerVersion);
- SetDlgItemText(IDC_EDIT2,sText);
- sText.Format("%02X",pOptionalHeader->MinorLinkerVersion);
- SetDlgItemText(IDC_EDIT3,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfCode);
- SetDlgItemText(IDC_EDIT4,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfInitializedData);
- SetDlgItemText(IDC_EDIT5,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfUninitializedData);
- SetDlgItemText(IDC_EDIT6,sText);
- sText.Format("%08X",pOptionalHeader->AddressOfEntryPoint);
- SetDlgItemText(IDC_EDIT7,sText);
- sText.Format("%08X",pOptionalHeader->BaseOfCode);
- SetDlgItemText(IDC_EDIT8,sText);
- sText.Format("%08X",pOptionalHeader->BaseOfData);
- SetDlgItemText(IDC_EDIT9,sText);
- sText.Format("%08X",pOptionalHeader->ImageBase);
- SetDlgItemText(IDC_EDIT10,sText);
- sText.Format("%08X",pOptionalHeader->SectionAlignment);
- SetDlgItemText(IDC_EDIT11,sText);
- sText.Format("%08X",pOptionalHeader->FileAlignment);
- SetDlgItemText(IDC_EDIT12,sText);
- sText.Format("%04X",pOptionalHeader->MajorOperatingSystemVersion);
- SetDlgItemText(IDC_EDIT13,sText);
- sText.Format("%04X",pOptionalHeader->MinorOperatingSystemVersion);
- SetDlgItemText(IDC_EDIT14,sText);
- sText.Format("%04X",pOptionalHeader->MajorSubsystemVersion);
- SetDlgItemText(IDC_EDIT15,sText);
- sText.Format("%04X",pOptionalHeader->MinorSubsystemVersion);
- SetDlgItemText(IDC_EDIT16,sText);
- sText.Format("%04X",pOptionalHeader->MajorImageVersion);
- SetDlgItemText(IDC_EDIT17,sText);
- sText.Format("%04X",pOptionalHeader->MinorImageVersion);
- SetDlgItemText(IDC_EDIT18,sText);
- sText.Format("%08X",pOptionalHeader->Win32VersionValue);
- SetDlgItemText(IDC_EDIT19,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfImage);
- SetDlgItemText(IDC_EDIT20,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfHeaders);
- SetDlgItemText(IDC_EDIT21,sText);
- sText.Format("%08X",pOptionalHeader->CheckSum);
- SetDlgItemText(IDC_EDIT22,sText);
- sText.Format("%04X",pOptionalHeader->Subsystem);
- SetDlgItemText(IDC_EDIT23,sText);
- sText.Format("%08X",pOptionalHeader->DllCharacteristics);
- SetDlgItemText(IDC_EDIT24,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfStackReserve);
- SetDlgItemText(IDC_EDIT25,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfStackCommit);
- SetDlgItemText(IDC_EDIT26,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfHeapReserve);
- SetDlgItemText(IDC_EDIT27,sText);
- sText.Format("%08X",pOptionalHeader->SizeOfHeapCommit);
- SetDlgItemText(IDC_EDIT28,sText);
- sText.Format("%08X",pOptionalHeader->LoaderFlags);
- SetDlgItemText(IDC_EDIT29,sText);
- sText.Format("%08X",pOptionalHeader->NumberOfRvaAndSizes);
- SetDlgItemText(IDC_EDIT30,sText);
- }
- else
- {
- }
- }
复制代码 读取PE文件后,可以得到DOS_MZ头,通过DOS_MZ头成员e_lfanew获取PE头IMAGE_NT_HEADERS 。
我们知道PE头包含PE标识,标准PE头,可选PE头,所以通过PE头结构体就可方便访问可选PE头。
如上面例程代码,读取可选PE头成员变量,显示在界面上。
至于保存修改后的数据,在后续的最后例程实现,这里都先空着。
可选PE头数据已实现读取与显示,那么它的每个成员含义是什么呢?
下面详细介绍:
Magic:
单字长度。
魔术字。
用于说明文件的类型。
0x10B表示32位PE文件。
0x20B表示64位PE文件。
0x107表示ROM映像文件。
MajorLinkerVersion,MinorLinkerVersion:
两个字段都为单字节长度。
表示链接器主次版本号,对程序执行无用。
SizeOfCode:
双字长度。
以字节为单位,基于文件对齐后,所有代码节的总大小。
SizeOfInitializedData:
双字长度。
包含已初始化数据的全部节总和大小。
SizeOfUninitializedData:
双字长度。
包含未初始化数据的全部节总和大小。
这些被定义为未初始化的数据,在文件中不占空间。
但加载到内存后,PE加载器会为这些数据分配适当的虚拟地址空间。
AddressOfEntryPoint:
双字长度。
程序入口RVA,记录了启动代码距离PE文件加载后起始地址的字节数。
可以简化理解如下,
对于exe这个地址可以理解为WinMain的RVA。
对于DLL,这个地址可以理解为DllMain的RVA。
如果是驱动程序,可以理解为DriverEntry的RVA。
但实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成。
BaseOfCode:
双字长度。
代码节的起始RVA,表示PE文件被加载到内存后,代码节的开头相对映像基地的相对地址。
一般情况代码节是紧跟在PE头后面,节的名称默认为“.text”。
BaseOfData:
双字长度。
数据节的起始RVA,表示PE文件被加载到内存后,数据节的开头相对于映像基地址的相对地址。
一般情况数据节位于文件尾部,节的名称默认为“.data”。
ImageBase:
双字长度。
指定PE文件优先加载到内存的地址。
AddressOfEntryPoint的值为相对此值的偏移量。
这个地址是建议的,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。
exe文件默认值是0x400000,dll文件默认值是0x10000000。
PE加载器负责文件中用到多个dll文件的正确加载。
修改时,此值范围必须在进程地址空间内,还必须是64KB整数倍。
SectionAlignment:
双字长度。
加载到内存中节的对齐粒度。也就是加载到内存后,节的大小必须是此值的整数倍。
内存对齐可以提高内存数据调度效率。
内存中数据存取以页为单位,WIN32页大小为4KB,节对齐粒度也为4KB,十六进制表示为0x1000.
必须大于等于另一成员FileAlignment。
SectionAlignment小于WIN32系统页大小时,SectionAlignment必须等于FileAlignment。
FileAlignment:
双字长度。
PE文件保存在磁盘中节的对齐粒度。
文件对齐粒度可以提高磁盘数据加载效率。
磁盘以簇为单位管理文件,默认一个簇大小为512字节,最大4KB。
文件对齐粒度默认为512字节,十六进制表示为0x200。
MajorOperatingSystemVersion,MinorOperatingSystemVersion:
每字段都为单字长度。
表示操作系统的主,次版本号。
MajorSubsystemVersion,MinorSubsystemVersion:
每字段都为单字长度。
表示运行所需子系统主,次版本号。
MajorImageVersion,MinorImageVersion:
每个字段都不单字长度。
表示PE文件的主,次版本号。
Win32VersionValue:
双字长度。
子系统版本的值,系统保存,必须为0.
SizeOfImage:
双字长度。
加载到虚拟内存中,整个PE文件的映射尺寸。
必须大于或等于实际值,必须为内存对齐粒度SectionAlignment整数。
SizeOfHeaders:
双字长度。
加载到虚拟内存中,整个PE文件头的映射尺寸(DOS头+PE头+节表)。
必须按FileAlignment对齐。
CheckSum:
双字长度。
PE文件校验和,大多数PE文件中,此值为0.
在一些内核模式驱动程序,系统DLL文件中,该值必须存在且正确。
校验和的具体计算方法与代码,会在后续帖子中实现。
Subsystem:
单字长度。
指定使用界面的子系统,确定了系统如何为程序建立初始界面。
有如下取值,只能为任一的数值,具体设置可以参考例程源代码。
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // 未知.
#define IMAGE_SUBSYSTEM_NATIVE 1 // 无子系统(一般为驱动)
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Windows图形界面
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 //Windows 控制台
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // OS/2控制台
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // Posix 控制台
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // Windows9x 驱动
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Windows CE
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10 //EFI 应用程序
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11 //EFI 引导服务设备
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12 //EFI 运行时间驱动
#define IMAGE_SUBSYSTEM_EFI_ROM 13 //EFI 只读储存器
#define IMAGE_SUBSYSTEM_XBOX 14 //X-BOX
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16//窗口启动应用
DllCharacteristics:
单字长度。
DLL文件属性,为一个标志集,并不针对DLL文件而是针对全部PE文件。
字段定义了PE文件加载时的一些特征。
可以为以下多个数值相或,具体如何读取与设置,可以下载参考例程。
#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040 // DLL可以在加载时被重定位
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 0x0080 // 强制代码施行完整性验证
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 //此映像兼容DEP
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200 // 可以隔离,但并不隔离此映像
#define IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 // 映像不使用SEH
#define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800 // 不绑定映像
// 0x1000 //保留
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000 // 此映像为WDM驱动
// 0x4000 // 保留.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000//可用于终端服务器
SizeOfStackReserve:
双字长度。
表示初始化时保留栈的大小,真正栈大小由下一成员SizeOfStackCommit决定。
默认0x100000,对应1MB。
在我们编程调用函数CreateThread,传递参数NULL时,创建出的栈大小也为1MB.
SizeOfStackCommit:
双字长度。
表示初始化时实际提交的栈大小。
对于微软链接器默认值为0x1000,对于TLINK32默认值为0x2000。
SizeOfHeapReserve:
双字长度。
初始化时保留的椎大小。
每个进程默认都会有一个默认的进程堆,在进程启动时创建进程退出前不可被删除。
默认大小为1MB,编程时可由GetProcessHeap函数获取堆句柄。
SizeOfHeapCommit:
双字长度。
初始化时实际提交的堆大小,默认为0x1000.
LoaderFlags:
双字长度。
表示加载表示,系统保存,默认为0.
NumberOfRvaAndSizes:
双字长度。
定义数据目录结构的数量,默认为0x10,即16个。
此值由标准PE头成员SizeOfOptionalHeaders决定,NumberOfRvaAndSizes取值2-16.
DataDirectory:
为一个结构体数组IMAGE_DATA_DIRECTORY DataDirectory[16];
数组每个成员类型为IMAGE_DATA_DIRECTORY, 定义如下,
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;//不同数据类型的RVA或FOA,
DWORD Size;//数据类型数据块的长度。
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
计算得DataDirectory共占32个双字长度。
此字段记录一PE文件中不同类型数据的目录信息。
_IMAGE_DATA_DIRECTORY 记录每一个类型数据的起始RVA和数据块的长度。
这些不同数据按先后顺序存储,依次为:
16种类型数据依次为:导出表,
导入表,
资源表,
异常表,
属性证书表,
基地址重定位表,
调试信息表,
预留不用,
全局指针寄存器,
线程局部存储,
加载配置表,
绑定导入表,
导入函数地址表,
延迟导入表,
CLR运行时头数据,
预留不用。
由于在编程时,代码编写不方便,所以将数据中16个元素展开来使用,如上图。
例程中对应定义了自己的结构体类型。
typedef struct _DATA_DIRECTORY
{
DWORD Export_VirtualAddress;
DWORD Export_isize;
DWORD Import_VirtualAddress;
DWORD Import_isize;
DWORD Resource_VirtualAddress;
DWORD Resource_isize;
DWORD Exception_VirtualAddress;
DWORD Exception_isize;
DWORD Certificate_VirtualAddress;
DWORD Certificate_isize;
DWORD Relocation_VirtualAddress;
DWORD Relocation_isize;
DWORD Debug_VirtualAddress;
DWORD Debug_isize;
DWORD Architecture_VirtualAddress;
DWORD Architecture_isize;
DWORD Global_Ptr_VirtualAddress;
DWORD Global_Ptr_isize;
DWORD TLS_VirtualAddress;
DWORD TLS_isize;
DWORD Load_Config_VirtualAddress;
DWORD Load_Config_isize;
DWORD Bound_Import_VirtualAddress;
DWORD Bound_Import_isize;
DWORD IAT_VirtualAddress;
DWORD IAT_isize;
DWORD Delay_Import_VirtualAddress;
DWORD Delay_Import_isize;
DWORD CLR_VirtualAddress;
DWORD CLR_isize;
DWORD Reserved_VirtualAddress;
DWORD Reserved_isize;
}DATA_DIRECTORY,*PDATA_DIRECTORY;
到此可选PE头的介绍就结束了。
相关成员的含义可能不是很好理解,可以先自己结合例程源代码编写程序熟悉。
在后续的帖子中多编程实践。
例程下载地址:
上面介绍的16种数据更多功能也可以先作一个粗略了解,
比如导入导出表的作用以及一些常见属性是什么。后面会通过编程,详细介绍与使用。
0.导出表:导出数据所在的节通常被命名为.edata,包含可以被其他程序访问的有关符号的相关信息。
例如导出函数与资源。
这些符号通常在DLL中,DLL中也可以包含导入符号。
某些EXE文件中也可以有导出符号。
1.导入表:
导入数据所在的节通常被命名为.idata,包含pe映像中所有导入的符号。
导入信息几乎在DLL,exe中都存在。
2.异常表:
异常表数据所在的节通常被命名为.pdata,由用于处理异常函数表项组成数组。
在存入最终映像文件前,这些表项按函数地址进行进行排序,表项描述符合特定平台。
异常表主要用于基于表的异常处理,适用X86外所有类型平台。
3.资源表:
资源数据所在的节通常命名为.rsrc,该节为多层二叉排序树,树的节点指向各种类型的资源。
如图标,对话框,菜单资源等。
树深度可达2的31次方。
但PE中经常使用的只有3层:类型层,名称层,语言代码层。
4.属性证书表:
属性证书数据的作用类似于PE文件的校验和或MD5码,通过这种方式可验证PE文件是否被非法修改过。
为PE文件添加属性证书表可以使此PE文件与属性证书关联。
属性证书表是由一组连续的按八进制边界对齐的属性证书表项组成,每个证书表项为一个结构体WIN_CERTIFICATE.
typedef struct _WIN_CERTIFICATE {
DWORD dwLength;
WORD wRevision;
WORD wCertificateType;
BYTE bCertificate[ANYSIZE_ARRAY];
} WIN_CERTIFICATE, *LPWIN_CERTIFICATE;
要注意的是此部分不作为映像映射到内存,所在在可选PE头的_DATA_DIRECTORY.Certificate_VirtualAddress为文件偏移。
结合_WIN_CERTIFICATE.dwLength,_DATA_DIRECTORY.Certificate_isize,可以读取全部属性证书表项。
5.基地址重定位表:
基地重定位信息所在节通常命名为.reloc,包含映像中全部需要重定位的内容。
分为许多块,每块表示4KB页面范围内基址重定位信息。
6.调试信息表:
调试数据所在的节命名通常为.debug,为连续多个IMAGE_DEBUG_DIRECTORY数组。
数组每个元素描述调试的一些信息。
7.预留不用:
这是一个系统保留的节,必须为0.
8.全局指针寄存器:
表示被储存在全局指针寄存器中的一个值。
9.线程局部存储:
线程本地储存数据所在的节,通常命名为.tls。
线程本地存储是Windows支持的一种特殊存储类别。
10.加载配置表:
加载配置信息用于包含保留的SEH技术,提供了一个安全的结构化异常处理程序列表,由操作系统异常处理时调用 。
11.绑定导入表:
优化导入信息,提高PE加载效率。
PE加载内存时,加载器会先检查导入表,然后加载需要的DLL到地址空间。
加载器还会根据导入信息的描述使用动态链接库输入函数的实际地址去替换IAT表内容。
但可以事先实现绑定,保存于表,提高加载效率。
12.导入函数地址表:
英文名为IAT,准确说它是导入表一部分,此双字数组内定义了导入函数的VA,程序可以直接跳转到此VA处执行。
13.延迟导入表:
延时导入数据也与DLL调用有关,用于在程序首次调用DLL中某个函数或数据时才加载这个DLL。
14.CLR运行时头数据:
所处节通常命名为.cormeta,是.net框架重要组成部分,所有基于.net程序都通过此部分初始化。
PE加载时会通过此结构加载代码托管机制要用的所有动态库文件,并完成CLR相关的其他操作。
15.预留不用。
|