(軟體加密解密技術內幕)PE檔案結構?

PE教程2: 檢驗PE檔案的有效性

本教程中我們將學習如何檢測給定檔案是一有效PE檔案。下載範例

理論:

如何才能校驗指定檔案是否為一有效PE檔案呢?這個問題很難回答,完全取決於想要的精準程度。您可以檢驗PE檔案格式裡的各個資料結構,或者僅校驗一些關鍵資料結構。大多數情況下,沒有必要校驗檔案裡的每一個數據結構,只要一些關鍵資料結構有效,我們就認為是有效的PE檔案了。下面我們就來實現前面的假設。

我們要驗證的重要資料結構就是PE
header。從程式設計角度看,PE
header實際就是一個IMAGE_NT_HEADERS結構。定義如下:

IMAGE_NT_HEADERS STRUCT Signature
dd ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader
IMAGE_OPTIONAL_HEADER32 <>IMAGE_NT_HEADERS ENDS

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_SIGNATURE equ
5A4DhIMAGE_OS2_SIGNATURE
equ 454EhIMAGE_OS2_SIGNATURE_LE equ 454ChIMAGE_VXD_SIGNATURE equ 454ChIMAGE_NT_SIGNATURE equ 4550h

接下來的問題是:如何定位PE header?答案很簡單: DOS MZ header已經包含了指向PE header的檔案偏移量。DOS MZ
header又定義成結構IMAGE_DOS_HEADER。查詢windows.inc,我們知道IMAGE_DOS_HEADER結構的e_lfanew成員就是指向PE header的檔案偏移量。

現在將所有步驟總結如下:

首先檢驗檔案頭部第一個字的值是否等於IMAGE_DOS_SIGNATURE,是則DOS MZ header有效。

一旦證明檔案的DOS header有效後,就可用e_lfanew來定位PE header了。

比較PE header的第一個字的值是否等於IMAGE_NT_HEADER。如果前後兩個值都匹配,那我們就認為該檔案是一個有效的PE檔案。

Example:

.386.model flat,stdcalloption casemap:noneinclude \masm32\include\windows.incinclude \masm32\include\kernel32.incinclude \masm32\include\comdlg32.incinclude \masm32\include\user32.incincludelib \masm32\lib\user32.libincludelib \masm32\lib\kernel32.libincludelib \masm32\lib\comdlg32.libSEH structPrevLink dd ?
; the address of the previous seh structureCurrentHandler dd ? ; the
address of the exception handlerSafeOffset dd ? ; The offset where it's
safe to continue executionPrevEsp dd ? ; the old value in espPrevEbp dd ? ; The old value in ebpSEH ends.dataAppName db "PE tutorial no.2",0ofn OPENFILENAME <>FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0 db "All Files",0,"*.*",0,0FileOpenError db "Cannot
open the file for reading",0FileOpenMappingError db "Cannot open the file
for memory mapping",0FileMappingError db "Cannot map the file into
memory",0FileValidPE db "This file is a valid PE",0FileInValidPE db
"This file is not a valid PE",0.data?buffer db 512 dup(?)hFile dd ?hMapping dd ?pMapping dd ?ValidPE dd ?.codestart procLOCAL seh:SEHmov ofn.lStructSize,SIZEOF
ofnmov ofn.lpstrFilter, OFFSET FilterStringmov ofn.lpstrFile, OFFSET
buffermov ofn.nMaxFile,512mov ofn.Flags, OFN_FILEMUSTEXIST or
OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLYinvoke GetOpenFileName, ADDR ofn.if eax==TRUE invoke
CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL .if eax!=INVALID_HANDLE_VALUE mov
hFile, eax invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0 .if eax!=NULL mov hMapping, eax invoke
MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0 .if eax!=NULL mov pMapping,eax assume fs:nothing push fs:[0] pop seh.PrevLink
mov seh.CurrentHandler,offset SEHHandler mov
seh.SafeOffset,offset FinalExit lea eax,seh
mov fs:[0], eax mov seh.PrevEsp,esp mov
seh.PrevEbp,ebp mov edi, pMapping assume
edi:ptr IMAGE_DOS_HEADER .if [edi].e_magic==IMAGE_DOS_SIGNATURE add edi, [edi].e_lfanew assume edi:ptr
IMAGE_NT_HEADERS .if [edi].Signature==IMAGE_NT_SIGNATURE mov ValidPE, TRUE .else mov ValidPE, FALSE .endif .else mov ValidPE,FALSE
.endifFinalExit: .if ValidPE==TRUE
invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .else invoke MessageBox, 0, addr
FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .endif push seh.PrevLink pop fs:[0]
invoke UnmapViewOfFile, pMapping .else invoke
MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR .endif invoke CloseHandle,hMapping .else invoke MessageBox, 0, addr FileOpenMappingError, addr AppName,
MB_OK+MB_ICONERROR .endif invoke CloseHandle, hFile .else invoke MessageBox, 0, addr FileOpenError, addr AppName,
MB_OK+MB_ICONERROR .endif.endifinvoke ExitProcess, 0start
endpSEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD,
pContext:DWORD, pDispatch:DWORD mov edx,pFrame assume edx:ptr
SEH mov eax,pContext assume eax:ptr CONTEXT push
[edx].SafeOffset pop [eax].regEip push [edx].PrevEsp pop
[eax].regEsp push [edx].PrevEbp pop [eax].regEbp mov
ValidPE, FALSE mov eax,ExceptionContinueExecution retSEHHandler endpend start

分析:

本例程開啟一檔案,先檢驗DOS
header是否有效,有效就接著檢驗PE header的有效性,ok就認為是有效的PE檔案了。這裡,我們還運用了結構異常處理(SEH),這樣就不必檢查每個可能的錯誤:如果有錯誤出現,就認為PE檢測失效所致,於是給出我們的報錯資訊。其實Windows內部普遍使用SEH來檢驗引數傳遞的有效性。若對SEH感興趣的話,可閱讀Jeremy Gordon的文章。

程式呼叫開啟檔案通用對話方塊,使用者選定執行檔案後,程式便開啟檔案並對映到記憶體。並在有效性檢驗前建立一SEH:

assume fs:nothing push fs:[0] pop
seh.PrevLink mov seh.CurrentHandler,offset SEHHandler mov
seh.SafeOffset,offset FinalExit lea eax,seh mov fs:[0], eax mov seh.PrevEsp,esp mov seh.PrevEbp,ebp

一開始就假設暫存器fs為空(assume
fs:nothing)。記住這一步不能省卻,因為MASM假設fs暫存器為ERROR。接下來儲存Windows使用的舊SEH處理函式地址到我們自己定義的結構中,同時儲存我們的SEH處理函式地址和異常處理時的執行恢復地址,這樣一旦錯誤發生就能由異常處理函式安全地恢復執行了。同時還儲存當前esp及ebp的值,以便我們的SEH處理函式將堆疊恢復到正常狀態。

mov edi, pMapping assume edi:ptr
IMAGE_DOS_HEADER .if [edi].e_magic==IMAGE_DOS_SIGNATURE

成功建立SEH後繼續校驗工作。置目標檔案的首位元組地址給edi,使其指向DOS header的首位元組。為便於比較,我們告訴編譯器可以假定edi正指向IMAGE_DOS_HEADER結構(事實亦是如此)。然後比較DOS header的首字是否等於字串"MZ",這裡利用了windows.inc中定義的IMAGE_DOS_SIGNATURE常量。若比較成功,繼續轉到PE
header,否則設ValidPE值為FALSE,意味著檔案不是有效PE檔案。

add edi, [edi].e_lfanew assume edi:ptr
IMAGE_NT_HEADERS .if [edi].Signature==IMAGE_NT_SIGNATURE mov ValidPE, TRUE .else mov ValidPE, FALSE .endif

要定位到PE
header,需要讀取DOS
header中的e_lfanew域值。該域含有PE header在檔案中相對檔案首部的偏移量。edi加上該值正好定位到PE header的首位元組。這兒可能會出錯,如果檔案不是PE檔案,e_lfanew值就不正確,加上該值作為指標就可能導致異常。若不用SEH,我們必須校驗e_lfanew值是否超出檔案尺寸,這不是一個好辦法。如果一切OK,我們就比較PE header的首字是否是字串"PE"。這裡在此用到了常量IMAGE_NT_SIGNATURE,相等則認為是有效的PE檔案。如果e_lfanew的值不正確導致異常,我們的SEH處理函式就得到執行控制權,簡單恢復堆疊指標和基棧指標後,就根據safeoffset的值恢復執行到FinalExit標籤處。

FinalExit: .if ValidPE==TRUE invoke
MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION .else invoke MessageBox, 0, addr FileInValidPE, addr AppName,
MB_OK+MB_ICONINFORMATION .endif

上述程式碼簡單明確,根據ValidPE的值顯示相應資訊。

push seh.PrevLink pop fs:[0]

一旦SEH不再使用,必須從SEH鏈上斷開。

相關問題答案