最近在参考OpenShell为任务栏设置图片背景时,发现里面使用了IAT Hook,这一块没有接触过,去查资料的时候发现IAT Hook需要对PE文件结构有一定的了解,索性将PE文件结构的资料找出来,系统学习一下。
PE文件结构
Portable Executable (PE),可移植的可执行文件。在Windows平台下,所有的可执行文件(包括.exe, .dll, .sys, .ocx, .com等)均使用PE文件结构。这些使用了PE文件结构的可执行文件也称为PE文件。
PE结构包含的结构体有DOS头,PE标识 、文件头、可选头、目录头、目录结构、节表等。
整体结构如下
从上图可以看出PE结构分为4大部分,其中每个部分又进行了细分。
从数据管理的角度来看,可以把PE文件大致分为两部分,
1、DOS头、PE头和节表属于PE文件的数据管理结构或数据组织结构部分,
2、节表数据才是PE文件真正的数据部分,其中包含着代码、数据、资源等内容。
DOS头
DOS头分为“MZ头部”和"DOS存根“。
”MZ头部“是真正的DOS头部,由于其开始处的两个字节为"MZ",因此DOS头也可以叫作MZ头部。
这个我们用十六进制编辑器随便打开一个exe就可以看到
该部分用于程序在DOS系统下加载,它的结构被定义为IMAGE_DOS_HEADER
IMAGE_DOS_HEADER定义如下所示:
1 //大小为: 0x40(64)字节 2 #define IMAGE_DOS_SIGNATURE 0x5A4D // MZ 3 4 typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header 5 WORD e_magic; // MZ标记 0x5a4d 6 WORD e_cblp; // 最后(部分)页中的字节数 7 WORD e_cp; // 文件中的全部和部分页数 8 WORD e_crlc; // 重定位表中的指针数 9 WORD e_cparhdr; // 头部尺寸以段落为单位 10 WORD e_minalloc; // 所需的最小附加段 11 WORD e_maxalloc; // 所需的最大附加段 12 WORD e_ss; // 初始的SS值(相对偏移量) 13 WORD e_sp; // 初始的SP值 14 WORD e_csum; // 补码校验值 15 WORD e_ip; // 初始的IP值 16 WORD e_cs; // 初始的SS值 17 WORD e_lfarlc; // 重定位表的字节偏移量 18 WORD e_ovno; // 覆盖号 19 WORD e_res[4]; // 保留字 20 WORD e_oemid; // OEM标识符(相对m_oeminfo) 21 WORD e_oeminfo; // OEM信息 22 WORD e_res2[10]; // 保留字 23 LONG e_lfanew; // NT头(PE标记)相对于文件的偏移地址 24 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
DOS存根是一段简单的程序,主要用于输出“This program cannot be run in DOS mode.”类似的提示字符串。
为什么PE结构的最开始位置有这样一段DOS头部呢?
为了该可执行程序可以兼容DOS系统。通常情况下,Win32下的PE程序不能在DOS下运行,因此保留了这样一个简单的DOS程序用于提示“不能运行于DOS模式下”。
DOS头部IMAGE_DOS_HEADER详解
IMAGE_DOS_HEADER的定义在前面我们列出来了,该结构体中需要掌握的字段 只有两个分别是第一个字段 e_magic和最后一个字段e_lfanew。
e_magic:DOS可执行文件的标识,占用2字节,该位置保存着字符是“MZ",该标识符在Winnt.h头文件中有一个宏定义,如下所示:
1 #define IMAGE_DOS_SIGNATURE 0x5A4D
我们创建一个简单的控制台程序
1 #include <iostream> 2 3 int main() 4 { 5 std::cout << "Hello World!\n"; 6 }
使用16进制编辑器(我这里用的是ImHex,使用个人习惯的软件即可)打开编译出来的二进制文件(.exe)。
可以看到在0x00000000的位置保存着2字节的内容0x5A4D(ASCII的MZ)这里使用的是小尾(小端)方式存储,即高位保存高字节,低位保存低字节,所以上图中写的是4D 5A,这也是适合阅读顺序。
说明:
-
大端模式(Big-Endian)。在内存中,多字节数据类型的高位字节存储在低地址处,而低位字节存储在高地址处。这种模式与我们阅读数字的方式相似,即先读高位,后读低位。
-
小端模式(Little-Endian)。在内存中,多字节数据的低位字节存储在低地址处,而高位字节存储在高地址处。这种模式与我们阅读数字的方式相反,即先读低位,后读高位。
如0x0102这样一个数据,
使用大端方式存储,存储方式为:01 02
使用小端方式存储,存储方式为:02 01
我们可以看到下面这样一段程序
1 #include <iostream> 2 #include<Windows.h> 3 #include<winnt.h> 4 5 int main() 6 { 7 8 HANDLE hFile = CreateFile(L"a.bin", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); 9 10 BYTE buffer[4] = { 1,2,3,4 }; //写入 1 2 3 4 4个字节 11 12 DWORD dwHexValue = 0x5D4A; //写入0x5D4A(实际存储是4A 5D) 13 DWORD dwDecValue = 1234; //写入1234 (0x04D2 实际存储D2 04) 14 15 LPOVERLAPPED lv{}; 16 if (hFile) 17 { 18 WriteFile(hFile, buffer, 4, NULL, NULL); 19 WriteFile(hFile, (PVOID)&dwHexValue, 4, NULL, NULL); 20 WriteFile(hFile, (PVOID)&dwDecValue, 4, NULL, NULL); 21 CloseHandle(hFile); 22 } 23 }
用十六进制编辑器打开可以看到
为什么是这种情况,因为对于 Microsoft Visual C++ 的目标平台(x86、x64、ARM、ARM64),所有本机标量类型都是小字节序。
-------------------------
好的,让我们继续回归主题,在0x0000003C位置处,也就是IMAGE_DOS_HEADER的e_lfanew字段,该字段保存着PE头部的起始位置。
e_lfanew字段是LONG类型,所以这里是4个字节,F8 00 00 00,因为是使用的小端字节序,所以我们可以在0x000000F8位置,看到50 45 00 00,与之对应的ASCII字符为”PE\0\0“,这里就是PE头部开始的位置。
IMAGE_DOS_HEADER(e_lfanew字段之后)到"PE\0\0"之间的内容就是DOS存档,可以将该部分删除,然后将PE头部整体向前移动,也可以将一些配置数据保存在此处等。
我们将这里全部填充为0,程序也是可以正常执行的。
我写了下面这样一段测试程序:
1 #include <iostream> 2 #include<Windows.h> 3 #include<winnt.h> 4 5 int main() 6 { 7 8 HANDLE hFile = CreateFile(L"exepath", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 9 10 if (hFile) 11 { 12 SetFilePointer(hFile, 0x00000040, NULL, FILE_BEGIN); 13 14 BYTE buffer[8] = { 1,2,3,4,5,6,7,8 }; 15 WriteFile(hFile, buffer, 8, NULL, NULL); 16 17 CloseHandle(hFile); 18 } 19 }
在e_lfanew字段之后写入了8个字节的数据,程序也是可以照常执行的。
参考资料:
pe format
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
endian
https://learn.microsoft.com/en-us/cpp/standard-library/bit-enum?view=msvc-170
标签:WORD,字节,Windows,编程,PE,DOS,NULL,hFile From: https://www.cnblogs.com/zhaotianff/p/18186676