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

看雪資料發表於2015-11-15

 

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, 00hPE\0\0)。 本域為PE標記,我們可以此識別給定檔案是否為有效PE檔案。
FileHeader
該結構域包含了關於PE檔案物理分佈的資訊, 比如節數目、檔案執行機器等。
OptionalHeader
該結構域包含了關於PE檔案邏輯分佈的資訊,雖然域名有"可選"字樣,但實際上本結構總是存在的。

我們目的很明確。如果IMAGE_NT_HEADERSsignature域值等於"PE\0\0",那麼就是有效的PE檔案。實際上,為了比較方便,Microsoft已定義了常量IMAGE_NT_SIGNATURE供我們使用。

IMAGE_DOS_SIGNATURE equ 5A4Dh
IMAGE_OS2_SIGNATURE equ 454Eh
IMAGE_OS2_SIGNATURE_LE equ 454Ch
IMAGE_VXD_SIGNATURE equ 454Ch
IMAGE_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 的檔案偏移量。

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

  1. 首先檢驗檔案頭部第一個字的值是否等於 IMAGE_DOS_SIGNATURE是則 DOS MZ header 有效。
  2. 一旦證明檔案的 DOS header 有效後,就可用e_lfanew來定位 PE header 了。
  3. 比較 PE header 的第一個字的值是否等於 IMAGE_NT_HEADER。如果前後兩個值都匹配,那我們就認為該檔案是一個有效的PE檔案。

Example:

.386
.model flat,stdcall
option casemap: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

SEH struct
PrevLink dd ?    ; the address of the previous seh structure
CurrentHandler dd ?    ; the address of the exception handler
SafeOffset dd ?    ; The offset where it's safe to continue execution
PrevEsp dd ?      ; the old value in esp
PrevEbp dd ?     ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.2",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                 db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileValidPE db "This file is a valid PE",0
FileInValidPE db "This file is not a valid PE",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start proc
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke 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
             .endif
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
             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
.endif
invoke ExitProcess, 0
start endp

SEHHandler 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
    ret
SEHHandler endp
end 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處理函式地址和異常處理時的執行恢復地址,這樣一旦錯誤發生就能由異常處理函式安全地恢復執行了。同時還儲存當前espebp的值,以便我們的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鏈上斷開。

翻譯:iamgufeng [Iczelion's Win32 Assembly Homepage][LuoYunBin's Win32 ASM Page]

.

相關文章