PE格式是Windows系統下最常用的可執行文件格式,有些應用必須建立在了解PE文件格式的基礎之上,如可執行文件的加密與解密,文件型病毒的查殺等,熟練掌握PE文件結構,有助於軟件的分析.
在PE文件中,代碼,已初始化的數據,資源和重定位信息等數據被按照屬性分類放到不同的Section(節區/或簡稱為節)中,而每個節區的屬性和位置等信息用一個IMAGE_SECTION_HEADER結構來描述,所有的IMAGE_SECTION_HEADER結構組成了一個節表(Section Table),節表數據在PE文件中被放在所有節數據的前面.
在Win32系統中,當我們執行了可執行文件之后,可執行文件會被映射到內存,並且以4kb的粒度進行對齊,這個4kb也就是一個頁面的大小,而每個頁面又分別具有,可執行,可讀寫等屬性.
PE格式中的DOS部分由MZ格式的文件頭和可執行代碼部分組成,可執行代碼被稱為DOS塊(DOS stub).MZ格式的文件頭由IMAGE_DOS_HEADER結構定義,以下就是DOS頭部分的關鍵屬性.
mov esi,lpMemory
assume esi:ptr IMAGE_DOS_HEADER
movzx eax,[esi].e_magic ; 讀取DOS的頭部
movzx eax,[esi].e_ss ; DOS代碼段的初始堆棧段
movzx eax,[esi].e_sp ; DOS代碼段的初始堆棧指針
movzx eax,[esi].e_cs ; DOS代碼的入口地址
movzx eax,[esi].e_ip ; DOS代碼的入口IP
movzx eax,[esi].e_lfanew ; 指向了PE文件的開頭(重要)
第一個字段e_magic被定義為MZ,標志着DOS文件的開頭部分,最后一個字段e_lfanew則指明了PE文件的開頭位置,現在來說除了第一個字段和最后一個字段有些用處,其他的字段幾乎已經廢棄了,這里也不再介紹了.
解析PE頭結構
從DOS文件頭的e_lfanew字段(文件頭偏移003ch),PE文件格式排列在DOS頭的后面,也就是e_lfanew指針所指向的地址,而PE文件的第一個字節就是PE這兩個字符,有了這些信息,我們就可以寫一個小工具,來檢測指定一個程序是否是可執行文件啦.
.data
szFileName db "lyshark.exe",0h
hFile dd ?
hMapFile dd ?
lpMemory dd ?
szText db "這是一個PE可執行文件 !",0h
.code
main PROC
; 打開文件,並創建內存映射鏡像
invoke CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
mov hFile,eax
invoke CreateFileMapping,hFile,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0
mov lpMemory,eax
; -------------------------------------------------------------------
; 檢測PE文件是否有效,是否是一個正常的PE
mov esi,lpMemory
assume esi:ptr IMAGE_DOS_HEADER
; 判斷是否為DOS文件頭部
.if [esi].e_magic == IMAGE_DOS_SIGNATURE
add esi,[esi].e_lfanew ; 遞增指針
assume esi:ptr IMAGE_NT_HEADERS
; 判斷是否為PE可執行文件
.if [esi].Signature == IMAGE_NT_SIGNATURE
invoke MessageBox,NULL,addr szText,0,MB_OK
.endif
.endif
; -------------------------------------------------------------------
invoke UnmapViewOfFile,addr lpMemory
invoke ExitProcess,NULL
main ENDP
END main
上面的核心代碼原理也非常的簡單,過程:讀入文件,判斷第一個字符是不是MZ,如果是MZ,則在判斷e_lfanew指針指向的地址是不是PE如果是,則說明這是PE文件.
解析各區塊信息
下面的代碼,則用於讀取PE文件的一些關鍵區塊信息.
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include masm32.inc
includelib masm32.lib
.data
szFileName db "lyshark.exe",0h
hFile dd ?
hMapFile dd ?
lpMemory dd ?
lpBuffer db 2048 dup(?)
.const
szMsg db "----------------------------------------",0dh,0ah
db "運行平台: 0x%04X",0dh,0ah
db "節區數量: %d",0dh,0ah
db "文件屬性: 0x%04X",0dh,0ah
db "時間標記: %d",0dh,0ah
db "鏡像裝入基址: 0x%08X",0dh,0ah
db "程序的入口RVA: 0x%08X",0dh,0ah
db "代碼節起始RVA: 0x%08X",0dh,0ah
db "數據節起始RVA: 0x%08X",0dh,0ah
db "----------------------------------------",0dh,0ah,0
.code
main PROC
; 打開文件,並創建內存映射鏡像
invoke CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
mov hFile,eax
invoke CreateFileMapping,hFile,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0
mov lpMemory,eax
mov esi,lpMemory
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
invoke wsprintf,addr lpBuffer,addr szMsg,\
[esi].FileHeader.Machine, \ ; 運行平台
[esi].FileHeader.NumberOfSections, \ ; 節區數目
[esi].FileHeader.Characteristics, \ ; 文件屬性
[esi].FileHeader.TimeDateStamp, \ ; 時間標記
[esi].OptionalHeader.ImageBase, \ ; 鏡像基址
[esi].OptionalHeader.AddressOfEntryPoint, \ ; 入口RVA地址
[esi].OptionalHeader.BaseOfCode, \ ; 代碼節起始RVA
[esi].OptionalHeader.BaseOfData
invoke StdOut,addr lpBuffer
invoke ExitProcess,NULL
main ENDP
END main
解析節與節表
系統裝載可執行文件並不等同於內存映射,內存映射是將整個磁盤文件原封不動的搬到內存中去,而PE的加載則會處理一些其他數據,例如預處理,重定位等,裝入以后頁面位置,偏移等都會隨之發生改變,Windows裝載器在裝載DOS部分,PE文件頭部分和節表部分時不進行任何處理,而裝載節的時候將根據節的屬性做不同的處理.
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include masm32.inc
includelib masm32.lib
.data
szFileName db "lyshark.exe",0h
hFile dd ?
hMapFile dd ?
lpMemory dd ?
lpBuffer db 2048 dup(?)
.const
szMsg db "----------------------------------------------------------",0dh,0ah
db "節區名稱 節區大小 虛擬地址 Raw_尺寸 Raw_偏移 節區屬性",0dh,0ah
db "----------------------------------------------------------",0dh,0ah,0
szFmt db "%s %08X %08X %08X %08X %08X",0dh,0ah,0
.code
main PROC
invoke CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
mov hFile,eax
invoke CreateFileMapping,hFile,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0
mov lpMemory,eax
mov esi,lpMemory
assume esi:ptr IMAGE_DOS_HEADER ; 指向DOS開頭
add esi,[esi].e_lfanew ; 遞增指針到PE結構開頭
assume esi:ptr IMAGE_NT_HEADERS
invoke StdOut,addr szMsg ; 輸出提示信息
movzx ecx,[esi].FileHeader.NumberOfSections ; 取出節的數量,作為循環條件
add esi,sizeof IMAGE_NT_HEADERS ; 指向.text節
assume esi:ptr IMAGE_SECTION_HEADER ; 指向節中的SECTION
.repeat
push ecx ; wsprintf影響ecx寄存器,所以這里必須壓棧保存數據
mov eax,[esi].VirtualAddress
invoke wsprintf,addr lpBuffer,addr szFmt,esi, \ ; 節區名稱
[esi].Misc.VirtualSize, \ ; 節區大小
[esi].VirtualAddress, \ ; 虛擬地址
[esi].SizeOfRawData, \ ; Raw_尺寸
[esi].PointerToRawData, \ ; Raw_偏移
[esi].Characteristics ; 節區屬性
invoke StdOut,addr lpBuffer ; 打印節區信息
pop ecx
add esi,sizeof IMAGE_SECTION_HEADER
.untilcxz
invoke ExitProcess,NULL
main ENDP
END main