MS 2.0节是PE文件格式中第一个“节”。其大致结构如下:(转载请指明来源于breaksoftware的csdn博客)
在VCPlatformSDKIncludeWinNT.h文件中有对MS-DOS 2.0兼容EXE文件头的完整定义
代码语言:javascript复制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;
这个结构占用0x40个字节,其中我们将主要关注两个成员变量:e_magic和e_lfanew。
以我xp电脑上notepad为例,我们使用UE打开C:windowsnotepad.exe
可以发现IMAGE_DOS_HEADER结构中e_magic对应的数据位0x5A4D(MZ),e_lfanew对应的是0x000000E0。这个两个数据是这个结构体中最需要关心的两个成员变量。幻数(Magic Num)这个概念是用于区分一个格式文件的类型,就像一个人的姓,知道你姓啥之后,就可以明确你是不是我们族人。同样,解析这些文件的程序也会去尝试读取这样的幻数,以确认这个文件符合它要求的。在我所知道的一些格式中,他们的幻数往往是这个格式发明者的名称缩写(或者是格式后缀)。我们这个MS-Dos 2.0兼容EXE文件头中的幻数MZ也是纪念他的发明者,可以想到,这个名字应该不是盖茨,因为MZ和Bill Gates(BG)一点也没关系,也不是Paul Allen(PA),更不可能是销售出生的Steve Ballmer。它是Mark Zbikowski,中文翻译是马克·茨柏克沃斯基。
那么为什么PE格式文件会有个Dos文件头呢?Dos系统时代,有两种(我所知道的,我压根没经历过那个年代)可执行文件格式,一种是.exe为后缀的文件,其结构是MZ格式。另一种是以.com为后缀的文件,其结构是COM格式。从Wiki上对MZ格式的介绍可以看出来,MZ格式要比COM格式要新,MZ格式头中包含了重定向信息(本文第一个图中),且其支持可执行体大于64KiB。如今我们电脑上PE可执行文件的后缀也是.exe,为了让该后缀程序在Dos和Nt间有个过渡,我们需要让Dos系统能知道它不能“正确”执行该Exe文件。于是我们PE可执行文件一开始处便插入了一个MS-Dos 2.0兼容Exe文件头,Dos系统加载我们PE文件时,从一开始读取我们文件,发现是“DOS下可执行程序”,于是成功且顺利的执行我们的程序中DOS系统可执行部分,这部分DOS程序输出“该程序不能在DOS上”执行的提示。
现在我们来看下MS-2.0节结构图和我们结构体的对应关系:
MS-Dos 2.0兼容Exe文件头 对应于IMAGE_DOS_HEADER中e_magic到e_ovno
未使用 对应于 e_res[4],虽说这段没使用,但是我还是觉得这段很有意思的。我在做注册表沙箱时,研究了下某公司的沙箱,可是它的沙箱不让regedit.exe进入沙箱运行,于是我就改了e_res[4]这段数据中部分,从而让修改后的regedit.exe在它的沙箱中运行。为什么呢?很容易想象,“MD5 签名”是安全公司一大“安全准绳”。我改了这个没啥用的数据段,不会影响程序运行,但是会使MD5不同,且签名被破坏。这段地址是(文件起始偏移0x1C)
OEM标志 对应于 e_oemid
OEM信息 对应于 e_oeminfo
OEM信息和PE文件头偏移 之间存在一段空白,这段空白对应于 e_res2[10],这段数据和之前e_res[4]一样,改改也无妨。这段地址是(偏移0x28)
PE文件头偏移 对应于 e_lfanew,其位于0x3C偏移处。
MS-Dos 2.0占位程序和重定向表和未使用数据段如下图,因为我也没仔细研究过这个结构,所以也不能准确区分出哪块是占位程序,哪块是重定向表,哪块是未使用段。
从上面的数据我们可以看到,如果我们程序运行在Dos下,会输出“This program connot be run in Dos mode"。
那么NT系统加载我们的PE可执行程序呢?它不会去执行DOS占位程序,而会跳到PE头位置继续读取和执行。PE头位置就是e_lfanew字段的值,该值是PE头和文件头的之间的偏移量。如本例中就是0x000000E0。我们去该偏移去查看数据
看到PE了么?这个PE是PE头的Magic Num。我会在之后介绍PE文件头及其相关知识。
以上是非常常见的MS-DOS 2.0兼容Exe文件段,似乎有点枯燥。那我们现在思考一个问题,应该很有意思的。MS-DOS 2.0兼容Exe文件段是为了程序在DOS环境下运行时提示“不兼容”。但是目前DOS环境真的很少了,似乎我们真的没必要去纠结于我们的程序是否会在DOS下提示“不兼容”,即使在DOS不能运行,也没什么大不了的——反正功能也用不了。那么这么一大块空间,我们是不是可以放点别的?是的,我们可以。举个例子,我电脑上PPTV有个.ax文件叫(.ax文件就是DirectShow Filters的DLL文件)CoreAVC.ax。它就将它的导入表放在这段空间里!
看到了?导入表是使用了Kernerl32.dll中的LoadLibraryA和GetProcessAddress两个函数。再仔细看,而除了e_magic和e_lfanew两个字段要保证OK外,其他字段和DOS代码空间都可以被利用!那么不禁有人要问,这样做有什么好处呢?首先,减少了PE文件大小(虽然只是那么一点点)。其次,它可以让一些非常强大的分析工具分析出错,比如我电脑上的PE Explorer,因为它足够“较真”,所以它识别不出来该文件的信息。至于原因,我会在之后介绍导入表的时候给出来。这儿再废话几句,研究完PE文件格式,我发现一个道理:标准是标准,即使标准很严谨,但是如果标准实现不完善,那么也会产生各种有趣的漏洞和利用。
贴一下代码
代码语言:javascript复制#define DOSMAGIC 0x5A4D
BOOL CGetPEInfo::IsMzFile() {
size_t unWordSize = sizeof(WORD);
ULONG ulFileSize =(ULONG)( m_lpFileEnd - m_lpFileStart );
if ( ulFileSize < unWordSize ) {
return FALSE;
}
WORD wMagic = 0;
SafeCopy( &wMagic, m_lpFileStart, unWordSize );
return (DOSMAGIC == wMagic) ? TRUE : FALSE;
}
BOOL CGetPEInfo::GetDOSHeaderInfo() {
if ( FALSE == IsMzFile() ) {
return FALSE;
}
size_t unDosHeader = sizeof(IMAGE_DOS_HEADER);
memset( &m_DosHeader, 0, unDosHeader );
BOOL bSuc = SafeCopy( &m_DosHeader,m_lpFileStart, unDosHeader );
if ( FALSE == bSuc ) {
_ASSERT(FALSE);
}
else {
m_dwInfoMask |= DOSHEADER;
}
return bSuc;
}