正如我们在初识PE文件一节中看到的,PE文件头中包含几个重要的结构,DOS头、DOS块(DOS Stub)和NT头。NT头就是PE特征码+文件头(COFF 文件标头)+扩展头(可选标头),合称为NT头。这一节我们将详细讲解这几个重要的结构。我们将DOS头和DOS 块合称为MS-DOS 存根。COFF 对象文件(obj)标头由 COFF 文件标头和可选标头组成。
本节必须掌握的知识点:
DOS头
DOS块
NT头
3.2.1 DOS头
DOS头(DOS Header)是可执行文件中的一个数据结构,它是用于支持早期的DOS操作系统的标准格式。DOS头位于可执行文件的开头,包含了一些关于文件的基本信息和可执行程序的入口点。
MS-DOS 存根是在 MS-DOS 下运行的有效应用程序。 它放置在 EXE 映像的前面。 链接器在此处放置默认存根,当映像在 MS-DOS 中运行时,此存根会输出消息“此程序不能在 DOS 模式下运行”。 用户可以使用 /STUB 链接器选项指定不同的存根。
在位置 0x3c,存根具有 PE 签名(PE特征码“PE\0\0”)文件偏移量。 此信息使 Windows 能够正确执行映像文件,即使此文件具有 MS-DOS 存根也不例外。 链接期间,此文件偏移量放在位置 0x3c。
实验九:在winnt.h头文件中查看DOS头、文件头和扩展头的结构定义
在VS中输入#include "winnt.h" ,点击右键,打开文档。然后搜索 IMAGE_DOS_HEADER 或者在程序里面输入IMAGE_DOS_HEADER 按F12转到定义。
■IMAGE_DOS_HEADER结构
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // DOS 魔数
WORD e_cblp; // 文件的最后一页的字节数
WORD e_cp; // 文件中的页数
WORD e_crlc; // 重定位项的数量
WORD e_cparhdr; // 标头的段数
WORD e_minalloc; // 程序所需的最小附加段数
WORD e_maxalloc; // 程序所需的最大附加段数
WORD e_ss; // 初始堆栈段的相对偏移量
WORD e_sp; // 初始堆栈指针
WORD e_csum; // 文件校验和
WORD e_ip; // 初始指令指针
WORD e_cs; // 初始代码段的相对偏移量
WORD e_lfarlc; // 重定位表的文件偏移量
WORD e_ovno; // 覆盖号
WORD e_res[4]; // 保留字段
WORD e_oemid; // OEM 标识符(用于 e_oeminfo)
WORD e_oeminfo; // OEM 信息;由 e_oemid 指定
WORD e_res2[10]; // 保留字段
LONG e_lfanew; // 新的 PE 头的文件偏移量
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
■下面是DOS头中一些重要字段的说明:
●e_magic:这是一个16位的字段,用于表示可执行文件的标识符。对于标准的可执行文件,该字段应为5A4DH,即"MZ"的ASCII码,取自微软开发人员名字。
●e_lfanew:这是一个32位的字段,它表示PE头的偏移量。PE头(Portable Executable Header)是在DOS头之后的一个数据结构,包含了更详细的可执行文件信息。
●e_cblp和e_cp:这两个字段分别表示文件的最后一个页(512字节)的字节数和文件中的页数。
●e_crlc和e_cparhdr:这两个字段分别表示重定位表项数量和标准头的字节数。
●e_minalloc和e_maxalloc:这两个字段分别表示程序所需的最小和最大内存量。
●e_ss和e_sp:这两个字段分别表示程序的初始堆栈段和堆栈指针。
●e_csum:这是一个16位的字段,用于存储文件的校验和。在早期的DOS操作系统中,可以使用该字段进行简单的文件完整性校验。现在已经没什么用了,可以随便改。
●e_ip和e_cs:这两个字段分别表示初始指令指针和代码段。
●e_lfarlc:这是一个16位的字段,表示重定位表的偏移量。
●e_ovno:这是一个16位的字段,用于存储一些附加信息。
为了方便读者阅读,我们将winnt.h头文件中DOS头结构定义的英文注释改为中文注释。
DOS头是可执行文件格式中的一个重要组成部分,它允许DOS操作系统识别和加载可执行文件。然而,现代的Windows操作系统已经不再依赖DOS头来执行可执行文件,而是使用PE头和其他相关结构来解析和加载可执行文件。虽然如此,Windows PE文件中还是保留了DOS头结构。
●DOS头结构IMAGE_DOS_HEADER中最重要的成员有两个:
1.第一个是e_magic当我们判断一个文件是否为PE文件时,我们需要判断DOS头结构的第一个字段e_magic 为5A4DH (“MZ”),同时NT头的第一个字段Signature为0x00004550(“PE/0/0”)。
2.第二个字段是e_lfanew,这个字段是一个文件内的偏移地址,指向PE头。我们可以根据DOS头中的这个字段查找PE头。
我们以notepad32.exe为例,使用WinHex打开notepad32.exe,如下所示:
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ............
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ?......@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 E0 00 00 00 ............?..
00000040 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 ..?.???L?Th
00000050 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F is program canno
00000060 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 t be run in DOS
00000070 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 mode....$.......
00000080 EC 85 5B A1 A8 E4 35 F2 A8 E4 35 F2 A8 E4 35 F2 靺[〃?颞?颞??
00000090 6B EB 3A F2 A9 E4 35 F2 6B EB 55 F2 A9 E4 35 F2 k?颟?騥險颟??
000000A0 6B EB 68 F2 BB E4 35 F2 A8 E4 34 F2 63 E4 35 F2 k雋蚧?颞?騝??
000000B0 6B EB 6B F2 A9 E4 35 F2 6B EB 6A F2 BF E4 35 F2 k雓颟?騥雑蚩??
000000C0 6B EB 6F F2 A9 E4 35 F2 52 69 63 68 A8 E4 35 F2 k雘颟?騌ichㄤ5?
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000E0 50 45 00 00 4C 01 03 00 87 52 02 48 00 00 00 00 PE..L...嘡.H....
000000F0 00 00 00 00 E0 00 0F 01 0B 01 07 0A 00 78 00 00 ....?.......x..
DOS头结构IMAGE_DOS_HEADER最后一个字段e_lfanew的值为0xE0,再看一下文件偏移地址000000E0处的值刚好是PE特征码0x00004550(“PE/0/0”)。
●下面是给出一段判断是否为PE文件的汇编代码
;检测PE文件是否有效
mov esi,@lpMemory
assume esi:ptr IMAGE_DOS_HEADER ;esi指向DOS头
;判断是否有MZ字样
.if [esi].e_magic!=IMAGE_DOS_SIGNATURE ;判断DOS头特征
jmp _ErrFormat
.endif
;调整ESI指针指向PE文件头
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
;判断是否有PE字样
.if [esi].Signature!=IMAGE_NT_SIGNATURE
jmp _ErrFormat
.endif
练习
请读者使用WinHex打开任意一个PE文件,然后对照IMAGE_DOS_HEADER结构,写出每个结构成员的数值。
3.2.2 DOS Stub
我们将PE头和DOS头之间的部分称为DOS块(DOS Stub)。DOS Stub(DOS占位程序)是可执行文件中的一段代码,位于PE文件的DOS头和PE头之间。它是为了保持对早期DOS操作系统的兼容性而存在的。
DOS Stub是一个小型的程序或一段指令集,通常是用汇编语言编写的。它的主要作用是在运行可执行文件时,如果操作系统无法识别PE文件格式,或者在非Windows环境下执行,会将执行权转移到DOS Stub上,以提供一些友好的提示信息或执行相关的操作。
DOS Stub通常包含一些文本、图形或其他形式的信息,例如欢迎信息、版本号、作者信息,甚至可以是一段小的动画效果。它的大小通常是固定的,为64字节(16位DOS时代)或128字节(32位DOS时代)。而事实是DOS块的大小并不是固定的,我们可以修改DOS头最后一个字段e_lfanew的值,扩展DOS块。DOS块本身及扩展部分的空间是可以被我们用来存储其他信息的(虽然我们很少这样使用)。
当在DOS环境下执行可执行文件时,DOS操作系统会首先加载DOS Stub并执行它。如果可执行文件是一个有效的PE文件,DOS Stub会在执行完自己的任务后,通过跳转或调用指令将控制权转移到PE头,进而由操作系统继续解析和执行PE文件。
在现代Windows操作系统中,DOS Stub的作用相对较小,因为操作系统已经能够正确识别和解析PE文件格式。然而,为了保持对早期DOS应用程序的兼容性,PE文件仍然保留了DOS Stub。
注意
1.DOS Stub并非所有的PE文件都必须包含。在一些特殊的情况下,开发人员可以选择不包含DOS Stub,从而使得PE文件更加紧凑。
2.当PE加载器加载PE文件后,PE文件头部所在的页面被设置为只读属性。因此扩展部分只能存放只读数据。通常我们建议DOS块扩展后的大小不能使整个PE文件头部的大小超过400H,否则需要修改下面各个节区内的所有文件偏移地址,造成不必要的麻烦。
实验十:在DOS系统中运行32位PE文件
●第一步:将第一章编写的HelloWorld.exe 32位PE文件拖入WinHex中,观察DOS块的数据如下所示:
00000040 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 ..?.???L?Th
00000050 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F is program canno
00000060 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 t be run in DOS
00000070 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 mode....$.......
00000080 5D 5C 6D C1 19 3D 03 92 19 3D 03 92 19 3D 03 92 ]\m?=.?=.?=.?
00000090 97 22 10 92 1E 3D 03 92 E5 1D 11 92 18 3D 03 92 ?.?=.掑..?=.?
000000A0 52 69 63 68 19 3D 03 92 00 00 00 00 00 00 00 00 Rich.=.?.......
●第二步:修改HelloWorld.exe程序名为1234.exe。
32位PE文件需要修改文件名才能在DOSBox虚拟机上运行的原因是因为DOSBox是一个模拟DOS环境的虚拟机,它主要用于运行早期的DOS应用程序和游戏。DOSBox模拟了DOS操作系统的行为和环境,但它实际上是在现代操作系统上运行的。
DOSBox是为了兼容旧的DOS应用程序而设计的,它对32位PE文件的支持相对有限。32位PE文件通常是为Windows操作系统设计的,并使用了Windows特定的API和功能。而DOSBox主要模拟的是早期的DOS环境,因此无法直接运行32位的Windows PE文件。
为了在DOSBox中运行32位PE文件,可以尝试将文件名修改为具有DOS兼容性的8.3格式(最多8个字符的文件名和3个字符的扩展名)。
DOSBox在加载可执行文件时,会根据文件名的扩展名来判断文件类型,并使用相应的处理方式。通过将32位PE文件的文件名修改为DOS兼容的格式,DOSBox会将其识别为DOS应用程序,尽管它实际上是一个32位的Windows PE文件。
注意
即使将文件名修改为DOS兼容格式,仍然无法保证所有的32位PE文件都能在DOSBox中正常运行。这是因为DOSBox并不是为运行32位Windows应用程序而设计的,它的功能和兼容性有限。在某些情况下,可能需要使用其他工具或虚拟机来运行32位PE文件。
●第三步:验证DOS块内的数据是什么?
安装DOSBox虚拟机,打开虚拟机后,输入命令:
mount c d:\code\winpe\ch03
c:
将虚拟机C盘根目录对应真实机HelloWorld.exe程序所在的目录。
第四步:命令行输入1234.exe后,回车运行。如图3-3所示,窗口显示一行提示信息:“This program cannot be run in DOS mode.”。这正是WinHex看到的字符串。
第五步:使用debug.exe调试器加载1234.exe,然后输入U命名,查看反汇编代码,如图3-4所示。学习过 16位汇编的读者一定非常熟悉。这是一段汇编代码中看到的内容正是我们在WinHex中看到的DOS块中的内容。前面14个字节为一段汇编代码,调用int 21h的9号功能(DX为入口参数,AH为功能号),输出偏移地址0EH处的’$’结尾的字符串,正是我们运行1234.exe程序在窗口显示的提示信息。最后调用int 21h的4CH号功能,入口参数为0,结束程序。
图3-3 在DOS系统上运行32位PE文件
图3-4 使用debug调试器查看反汇编代码
实验十一:使用IDA分析32位PE文件的DOS块
我们还可以使用IDA分析HelloWorld.exe程序的DOS块。
第一步:将HelloWorld.exe拖入WinHex,将文件偏移地址0x3C处(DOS头指向PE头的e_lfanew字段修改为0),如下所示:
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ............
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ?......@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
第二步:将修改后的HelloWorld.exe程序拖入IDA,主窗口显示的反汇编代码如下所示:
seg000:0000 ; File Name : D:\code\winpe\HelloWorld.exe
seg000:0000 ; Format : MS-DOS executable (EXE)
seg000:0000 ; Base Address: 1000h Range: 10000h-10450h Loaded length: 450h
seg000:0000 ; Entry Point : 1000:0
seg000:0000
seg000:0000 .686p
seg000:0000 .mmx
seg000:0000 .model large
seg000:0000
seg000:0000 ; ===========================================================
seg000:0000
seg000:0000 ; Segment type: Pure code
seg000:0000 seg000 segment byte public 'CODE' use16
seg000:0000 assume cs:seg000
seg000:0000 assume es:nothing, ss:seg000, ds:nothing, fs:nothing, gs:nothing
seg000:0000
seg000:0000 ; =============== S U B R O U T I N E =======================================
seg000:0000
seg000:0000 ; Attributes: noreturn
seg000:0000
seg000:0000 public start
seg000:0000 start proc near
seg000:0000 push cs
seg000:0001 pop ds
seg000:0002 assume ds:seg000
seg000:0002 mov dx, 0Eh
seg000:0005 mov ah, 9
seg000:0007 int 21h ; DOS - PRINT STRING
seg000:0007 ; DS:DX -> string terminated by "$"
seg000:0009 mov ax, 4C01h
seg000:000C int 21h ; DOS - 2+ - QUIT WITH EXIT CODE (EXIT)
seg000:000C start endp ; AL = exit code
seg000:000C
seg000:000C ; -------------------------------------------------------------------
seg000:000E aThisProgramCan db 'This program cannot be run in DOS mode.',0Dh,0Dh,0Ah
seg000:000E db '$',0
seg000:003A align 8
IDA看到的反汇编代码与我们在debug调试器中看到的反汇编代码几乎一致。
实验十二:扩展32位PE文件的DOS块
接下来我们做一个扩展DOS块的实验。仍然以HelloWorld.exe为例,将其重命名为HelloWorld2.exe,拖入WinHex内, 将DOS头的最后一个字段e_lfanew(指向PE头的文件偏移地址)0x00000C8修改为0x00000268,如下所示。
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ............
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ?......@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 68 02 00 00 ............h...
读者可能会有疑问,为何要改为0x00000268大小呢?在前文中有提到,我们可以扩展DOS块的大小,但是扩展后PE文件头部最好不要超过1024个字节,否则我们将改变PE文件头部后面的几个节区在PE文件内的偏移,如此一来就需要修改所有与此相关的偏移,带来很多麻烦。
注意观察,节表和第一个.text节区之间的空白区域全部为0。这部分空间在文件偏移00000260H~00000400H之间,.text节区的起始地址为00000400H。我们只要保证.text节区的起始地址不变,因此所有节区的地址都不会发生变化。那么DOS块可以扩展的字节数就是400H-260H=1A0H个字节(十进制数416个字节)。
可以扩展的最大字节数计算出来之后,下一步就是在DOS块内粘贴零字节了。为了部破坏原DOS块的内容,我们选择在PE头特征码的前一个字节的地址处粘贴416个零字节(因为DOS块没有什么用途,我们也可以选在DOS块的任意位置)。
具体操作方法:
鼠标选中0xC7地址处,点击鼠标右键,点击“编辑”>“粘贴零字节”,弹出一个对话框窗口,如图3-5所示。填写字节数416,点击“OK”按钮。
图3-5 粘贴零字节
此时,我们观察一下PE头位于文件0x268偏移地址处。我们将DOS头的最后一个字段e_lfanew(指向PE头的文件偏移地址)0x00000C8修改为0x00000268,删除节表与.text节区间多余的零字节,保持.text节区地址不变。然后点击WinHex
工具栏“保存”按钮。
最后测试一下HelloWorld.exe是否可以正常运行。
练习
请读者按照上述实验十、十一、十二的方法分别测试notepad32.exe和notepad64.exe或者其他任意PE文件。
结论
1.不论32位还是64位PE文件都可以扩展DOS块的大小,方法是修改DOS头结构的最后一个字段e_lfanew的值,指向一个新的PE头的文件偏移地址。
2.为了不改变PE文件头部后面节区的文件偏移地址,扩展后的PE文件头部的大小不超过400H(1024)个字节。