PE教程.docx
《PE教程.docx》由会员分享,可在线阅读,更多相关《PE教程.docx(55页珍藏版)》请在冰豆网上搜索。
PE教程
PE文件格式一览
考虑到早期写的PE教程1是自己所有教程中最糟糕的一篇,此番决心彻底重写一篇以飨读者。
PE的意思就是PortableExecutable(可移植的执行体)。
它是Win32环境自身所带的执行体文件格式。
它的一些特性继承自Unix的Coff(commonobjectfileformat)文件格式。
"portableexecutable"(可移植的执行体)意味着此文件格式是跨win32平台的:
即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。
当然,移植到不同的CPU上PE执行体必然得有一些改变。
所有win32执行体(除了VxD和16位的Dll)都使用PE文件格式,包括NT的内核模式驱动程序(kernelmodedrivers)。
因而研究PE文件格式给了我们洞悉Windows结构的良机。
本教程就让我们浏览一下PE文件格式的概要。
DOSMZheader
DOSstub
PEheader
Sectiontable
Section1
Section2
Section...
Sectionn
上是PE文件结构的总体层次分布。
所有PE文件(甚至32位的DLLs)必须以一个简单的DOSMZheader开始。
我们通常对此结构没有太大兴趣。
有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZheader之后的DOSstub。
DOSstub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串"ThisprogramrequiresWindows"或者程序员可根据自己的意图实现完整的DOS代码。
通常我们也不对DOSstub太感兴趣:
因为大多数情况下它是由汇编器/编译器自动生成。
通常,它简单调用中断21h服务9来显示字符串"ThisprogramcannotruninDOSmode"。
紧接着DOSstub的是PEheader。
PEheader是PE相关结构IMAGE_NT_HEADERS的简称,其中包含了许多PE装载器用到的重要域。
当我们更加深入研究PE文件格式后,将对这些重要域耳目能详。
执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOSMZheader中找到PEheader的起始偏移量。
因而跳过了DOSstub直接定位到真正的文件头PEheader。
PE文件的真正内容划分成块,称之为sections(节)。
每节是一块拥有共同属性的数据,比如代码/数据、读/写等。
我们可以把PE文件想象成一逻辑磁盘,PEheader是磁盘的boot扇区,而sections就是各种文件,每种文件自然就有不同属性如只读、系统、隐藏、文档等等。
值得我们注意的是----节的划分是基于各组数据的共同属性:
而不是逻辑概念。
重要的不是数据/代码是如何使用的,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。
不必关心节中类似于"data","code"或其他的逻辑概念:
如果数据和代码拥有相同属性,它们就可以被归入同一个节中。
(译者注:
节名称仅仅是个区别不同节的符号而已,类似"data","code"的命名只为了便于识别,惟有节的属性设置决定了节的特性和功能)如果某块数据想付为只读属性,就可以将该块数据放入置为只读的节中,当PE装载器映射节内容时,它会检查相关节属性并置对应内存块为指定属性。
如果我们将PE文件格式视为一逻辑磁盘,PEheader是boot扇区而sections是各种文件,但我们仍缺乏足够信息来定位磁盘上的不同文件,譬如,什么是PE文件格式中等价于目录的东东?
别急,那就是PEheader接下来的数组结构sectiontable(节表)。
每个结构包含对应节的属性、文件偏移量、虚拟偏移量等。
如果PE文件里有5个节,那么此结构数组内就有5个成员。
因此,我们便可以把节表视为逻辑磁盘中的根目录,每个数组成员等价于根目录中目录项。
以上就是PE文件格式的物理分布,下面将总结一下装载一PE文件的主要步骤:
1. 当PE文件被执行,PE装载器检查DOSMZheader里的PEheader偏移量。
如果找到,则跳转到PEheader。
2. PE装载器检查PEheader的有效性。
如果有效,就跳转到PEheader的尾部。
3. 紧跟PEheader的是节表。
PE装载器读取其中的节信息,并采用文件映射方法将这些节映射到内存,同时付上节表里指定的节属性。
4. PE文件映射入内存后,PE装载器将处理PE文件中类似importtable(引入表)逻辑部分。
上述步骤是基于本人观察后的简述,显然还有一些不够精确的地方,但基本明晰了执行体被处理的过程。
你应该下载LUEVELSMEYER的《PE文件格式》。
该文的描述相当详细,可用作案头的参考手册。
检验PE文件的有效性
本教程中我们将学习如何检测给定文件是一有效PE文件。
下载范例
理论:
如何才能校验指定文件是否为一有效PE文件呢?
这个问题很难回答,完全取决于想要的精准程度。
您可以检验PE文件格式里的各个数据结构,或者仅校验一些关键数据结构。
大多数情况下,没有必要校验文件里的每一个数据结构,只要一些关键数据结构有效,我们就认为是有效的PE文件了。
下面我们就来实现前面的假设。
我们要验证的重要数据结构就是PEheader。
从编程角度看,PEheader实际就是一个IMAGE_NT_HEADERS结构。
定义如下:
IMAGE_NT_HEADERSSTRUCT
Signaturedd?
FileHeaderIMAGE_FILE_HEADER<>
OptionalHeaderIMAGE_OPTIONAL_HEADER32<>
IMAGE_NT_HEADERSENDS
Signature一dword类型,值为50h,45h,00h,00h(PE\0\0)。
本域为PE标记,我们可以此识别给定文件是否为有效PE文件。
FileHeader该结构域包含了关于PE文件物理分布的信息,比如节数目、文件执行机器等。
OptionalHeader该结构域包含了关于PE文件逻辑分布的信息,虽然域名有"可选"字样,但实际上本结构总是存在的。
我们目的很明确。
如果IMAGE_NT_HEADERS的signature域值等于"PE\0\0",那么就是有效的PE文件。
实际上,为了比较方便,Microsoft已定义了常量IMAGE_NT_SIGNATURE供我们使用。
IMAGE_DOS_SIGNATUREequ5A4Dh
IMAGE_OS2_SIGNATUREequ454Eh
IMAGE_OS2_SIGNATURE_LEequ454Ch
IMAGE_VXD_SIGNATUREequ454Ch
IMAGE_NT_SIGNATUREequ4550h
接下来的问题是:
如何定位PEheader?
答案很简单:
DOSMZheader已经包含了指向PEheader的文件偏移量。
DOSMZheader又定义成结构IMAGE_DOS_HEADER。
查询windows.inc,我们知道IMAGE_DOS_HEADER结构的e_lfanew成员就是指向PEheader的文件偏移量。
现在将所有步骤总结如下:
1.首先检验文件头部第一个字的值是否等于IMAGE_DOS_SIGNATURE,是则DOSMZheader有效。
2.一旦证明文件的DOSheader有效后,就可用e_lfanew来定位PEheader了。
3.比较PEheader的第一个字的值是否等于IMAGE_NT_HEADER。
如果前后两个值都匹配,那我们就认为该文件是一个有效的PE文件。
Example:
.386
.modelflat,stdcall
optioncasemap:
none
include\masm32\include\windows.inc
include\masm32\include\kernel32.inc
include\masm32\include\comdlg32.inc
include\masm32\include\user32.inc
includelib\masm32\lib\user32.lib
includelib\masm32\lib\kernel32.lib
includelib\masm32\lib\comdlg32.lib
SEHstruct
PrevLinkdd?
;theaddressoftheprevioussehstructure
CurrentHandlerdd?
;theaddressoftheexceptionhandler
SafeOffsetdd?
;Theoffsetwhereit'ssafetocontinueexecution
PrevEspdd?
;theoldvalueinesp
PrevEbpdd?
;Theoldvalueinebp
SEHends
.data
AppNamedb"PEtutorialno.2",0
ofnOPENFILENAME<>
FilterStringdb"ExecutableFiles(*.exe,*.dll)",0,"*.exe;*.dll",0
db"AllFiles",0,"*.*",0,0
FileOpenErrordb"Cannotopenthefileforreading",0
FileOpenMappingErrordb"Cannotopenthefileformemorymapping",0
FileMappingErrordb"Cannotmapthefileintomemory",0
FileValidPEdb"ThisfileisavalidPE",0
FileInValidPEdb"ThisfileisnotavalidPE",0
.data?
bufferdb512dup(?
)
hFiledd?
hMappingdd?
pMappingdd?
ValidPEdd?
.code
startproc
LOCALseh:
SEH
movofn.lStructSize,SIZEOFofn
movofn.lpstrFilter,OFFSETFilterString
movofn.lpstrFile,OFFSETbuffer
movofn.nMaxFile,512
movofn.Flags,OFN_FILEMUSTEXISTorOFN_PATHMUSTEXISTorOFN_LONGNAMESorOFN_EXPLORERorOFN_HIDEREADONLY
invokeGetOpenFileName,ADDRofn
.ifeax==TRUE
invokeCreateFile,addrbuffer,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
.ifeax!
=INVALID_HANDLE_VALUE
movhFile,eax
invokeCreateFileMapping,hFile,NULL,PAGE_READONLY,0,0,0
.ifeax!
=NULL
movhMapping,eax
invokeMapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
.ifeax!
=NULL
movpMapping,eax
assumefs:
nothing
pushfs:
[0]
popseh.PrevLink
movseh.CurrentHandler,offsetSEHHandler
movseh.SafeOffset,offsetFinalExit
leaeax,seh
movfs:
[0],eax
movseh.PrevEsp,esp
movseh.PrevEbp,ebp
movedi,pMapping
assumeedi:
ptrIMAGE_DOS_HEADER
.if[edi].e_magic==IMAGE_DOS_SIGNATURE
addedi,[edi].e_lfanew
assumeedi:
ptrIMAGE_NT_HEADERS
.if[edi].Signature==IMAGE_NT_SIGNATURE
movValidPE,TRUE
.else
movValidPE,FALSE
.endif
.else
movValidPE,FALSE
.endif
FinalExit:
.ifValidPE==TRUE
invokeMessageBox,0,addrFileValidPE,addrAppName,MB_OK+MB_ICONINFORMATION
.else
invokeMessageBox,0,addrFileInValidPE,addrAppName,MB_OK+MB_ICONINFORMATION
.endif
pushseh.PrevLink
popfs:
[0]
invokeUnmapViewOfFile,pMapping
.else
invokeMessageBox,0,addrFileMappingError,addrAppName,MB_OK+MB_ICONERROR
.endif
invokeCloseHandle,hMapping
.else
invokeMessageBox,0,addrFileOpenMappingError,addrAppName,MB_OK+MB_ICONERROR
.endif
invokeCloseHandle,hFile
.else
invokeMessageBox,0,addrFileOpenError,addrAppName,MB_OK+MB_ICONERROR
.endif
.endif
invokeExitProcess,0
startendp
SEHHandlerprocusesedxpExcept:
DWORD,pFrame:
DWORD,pContext:
DWORD,pDispatch:
DWORD
movedx,pFrame
assumeedx:
ptrSEH
moveax,pContext
assumeeax:
ptrCONTEXT
push[edx].SafeOffset
pop[eax].regEip
push[edx].PrevEsp
pop[eax].regEsp
push[edx].PrevEbp
pop[eax].regEbp
movValidPE,FALSE
moveax,ExceptionContinueExecution
ret
SEHHandlerendp
endstart
分析:
本例程打开一文件,先检验DOSheader是否有效,有效就接着检验PEheader的有效性,ok就认为是有效的PE文件了。
这里,我们还运用了结构异常处理(SEH),这样就不必检查每个可能的错误:
如果有错误出现,就认为PE检测失效所致,于是给出我们的报错信息。
其实Windows内部普遍使用SEH来检验参数传递的有效性。
若对SEH感兴趣的话,可阅读JeremyGordon的文章。
程序调用打开文件通用对话框,用户选定执行文件后,程序便打开文件并映射到内存。
并在有效性检验前建立一SEH:
assumefs:
nothing
pushfs:
[0]
popseh.PrevLink
movseh.CurrentHandler,offsetSEHHandler
movseh.SafeOffset,offsetFinalExit
leaeax,seh
movfs:
[0],eax
movseh.PrevEsp,esp
movseh.PrevEbp,ebp
一开始就假设寄存器fs为空(assumefs:
nothing)。
记住这一步不能省却,因为MASM假设fs寄存器为ERROR。
接下来保存Windows使用的旧SEH处理函数地址到我们自己定义的结构中,同时保存我们的SEH处理函数地址和异常处理时的执行恢复地址,这样一旦错误发生就能由异常处理函数安全地恢复执行了。
同时还保存当前esp及ebp的值,以便我们的SEH处理函数将堆栈恢复到正常状态。
movedi,pMapping
assumeedi:
ptrIMAGE_DOS_HEADER
.if[edi].e_magic==IMAGE_DOS_SIGNATURE
成功建立SEH后继续校验工作。
置目标文件的首字节地址给edi,使其指向DOSheader的首字节。
为便于比较,我们告诉编译器可以假定edi正指向IMAGE_DOS_HEADER结构(事实亦是如此)。
然后比较DOSheader的首字是否等于字符串"MZ",这里利用了windows.inc中定义的IMAGE_DOS_SIGNATURE常量。
若比较成功,继续转到PEheader,否则设ValidPE值为FALSE,意味着文件不是有效PE文件。
addedi,[edi].e_lfanew
assumeedi:
ptrIMAGE_NT_HEADERS
.if[edi].Signature==IMAGE_NT_SIGNATURE
movValidPE,TRUE
.else
movValidPE,FALSE
.endif
要定位到PEheader,需要读取DOSheader中的e_lfanew域值。
该域含有PEheader在文件中相对文件首部的偏移量。
edi加上该值正好定位到PEheader的首字节。
这儿可能会出错,如果文件不是PE文件,e_lfanew值就不正确,加上该值作为指针就可能导致异常。
若不用SEH,我们必须校验e_lfanew值是否超出文件尺寸,这不是一个好办法。
如果一切OK,我们就比较PEheader的首字是否是字符串"PE"。
这里在此用到了常量IMAGE_NT_SIGNATURE,相等则认为是有效的PE文件。
如果e_lfanew的值不正确导致异常,我们的SEH处理函数就得到执行控制权,简单恢复堆栈指针和基栈指针后,就根据safeoffset的值恢复执行到FinalExit标签处。
FinalExit:
.ifValidPE==TRUE
invokeMessageBox,0,addrFileValidPE,addrAppName,MB_OK+MB_ICONINFORMATION
.else
invokeMessageBox,0,addrFileInValidPE,addrAppName,MB_OK+MB_ICONINFORMATION
.endif
上述代码简单明确,根据ValidPE的值显示相应信息。
pushseh.PrevLink
popfs:
[0]
一旦SEH不再使用,必须从SEH链上断开。
FileHeader(文件头)
本课我们将要研究PEheader的fileheader(文件头)部分。
至此,我们已经学到了哪些东东,先简要回顾一下:
DOSMZheader又命名为IMAGE_DOS_HEADER.。
其中只有两个域比较重要:
e_magic包含字符串"MZ",e_lfanew包含PEheader在文件中的偏移量。
比较e_magic是否为IMAGE_DOS_SIGNATURE以验证是否是有效的DOSheader。
比对符合则认为文件拥有一个有效的DOSheader。
为了定位PEheader,移动文件指针到e_lfanew所指向的偏移。
PEheader的第一个双字包含字符串"PE\0\0"。
该双字与IMAGE_NT_SIGNATURE比对,符合则认为PEheader有效。
本课我们继续探讨关于PEheader的知识。
PEheader的正式命名是IMAGE_NT_HEADERS。
再来回忆一下这个结构。
IMAGE_NT_HEADERSSTRUCT
Signaturedd?
FileHeaderIMAGE_FILE_HEADER<>
OptionalHeaderIMAGE_OPTIONAL_HEADER32<>
IMAGE_NT_HEADERSENDS
SignaturePE标记,值为50h,45h,00h,00h(PE\0\0)。
FileHeader该结构域包含了关于PE文件物理分布的一般信息。
OptionalHeader该结构域包含了关于PE文件逻辑分布的信息。
最有趣的东东在OptionalHeader里。
不过,FileHeader里的一些域也很重要。
本课我们将学习FileHeader,下一课研究OptionalHeader。
IMAGE_FILE_HEADERSTRUCT
MachineWORD?
NumberOfSectionsWORD?
TimeDateStampdd?
PointerToSymbolTabledd?
NumberOfSymbolsdd?
SizeOfOptionalHeaderWORD?
CharacteristicsWORD?
IMAGE_FILE_HEADERENDS
FieldnameMeanings
Machine该文件运行所要求的CPU。
对于Intel平台,该值是IMAGE_FILE_MACHINE_I386(14Ch)。
我们尝试了LUEVELSMEYER的