天天看點

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

本節書摘來自異步社群《c++ 黑客程式設計揭秘與防範(第2版)》一書中的第6章6.2節詳解pe檔案結構,作者冀雲,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

6.2 詳解pe檔案結構

c++ 黑客程式設計揭秘與防範(第2版)

psdk的頭檔案winnt.h包含了pe檔案結構的定義格式。pe頭檔案分為32位和64位版本。64位的pe結構是對32位的pe結構做了擴充,這裡主要讨論32位的pe檔案結構。對于64位的pe檔案結構,讀者可以自行查閱資料進行學習。

6.2.1 dos頭部詳解image_dos_header

對于一個pe檔案來說,最開始的位置就是一個dos程式。dos程式包含了一個dos頭部和一個dos程式體。dos頭部是用來裝載dos程式的,dos程式也就是如圖6-1中的那個dos存根。也就是說,dos頭是用來裝載dos存根用的。保留這部分内容是為了與dos系統相相容。當win32程式在dos下被執行時,dos存根程式會有禮貌地輸出“this program cannot be run in dos mode.”字樣對使用者進行提示。

雖然dos頭部是為了裝載dos程式的,但是dos頭部中的一個字段儲存着指向pe頭部的位置。dos頭在winnt.h頭檔案中被定義為image_dos_header,其定義如下:

該結構體中需要掌握的字段隻有2個,分别是第一個字段e_magic和最後一個字段e_lfanew字段。

e_magic字段是一個dos可執行檔案的辨別符,占用2位元組。該位置儲存着的字元是“mz”。該辨別符在winnt.h頭檔案中有一個宏定義,如下:

e_lfanew字段中儲存着pe頭的起始位置。

在vc下建立一個簡單的“win32 application”程式,然後生成一個可執行檔案,用于學習和分析pe檔案結構的組織。

程式代碼如下:

該程式的功能隻是彈出一個messagebox對話框。為了減小程式的體積,使用“win32 release”方式進行編譯連接配接,并把編譯好的程式用c32asm打開。c32asm是一個反彙編與十六進制編輯于一體的程式,其界面如圖6-2所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

在圖6-2上選擇“十六進制模式”單選按鈕,單擊“确定”按鈕,程式就被c32asm程式以十六進制的模式打開了,如圖6-3所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

在圖6-3中可以看到,在檔案偏移為0x00000000的位置處儲存着2位元組的内容0x5a 4d,用ascii碼表示則是“mz”。圖6-3中的前兩個位元組明明寫着“4d 5a”,為什麼說的是0x5a4d呢?到上面看winnt.h頭檔案中定義的那個宏,也寫着0x5a4d,這是為什麼呢?如果讀者還記得前面章節中介紹的位元組順序的内容,那麼就應該明白為什麼這麼寫了。這裡使用的系統是小尾方式存儲,即高位儲存高位元組,低位儲存低位元組。這個概念是很重要的,希望讀者不要忘記。

注意:

在這裡,如果以ascii碼的形式去考察e_magic字段的話,那麼值的确是“4d 5a”兩個位元組,但是為什麼宏定義是“0x5a4d”呢?因為image_dos_header對于e_magic的定義是一個word類型。定義成word類型,在代碼中進行比較時可以直接使用數值比較;而如果定義成char型,那麼比較時就相對不是太友善了。

在圖6-3中0x0000003c的位置處,就是image_dos_header的e_lfanew字段,該字段儲存着pe頭部的起始位置。pe頭部的位址是多少呢?是0xc8000000嗎?如果是,就錯了,原因還是位元組序的問題。是以,e_lfanew的值為0x000000c8。在檔案偏移為0x000000c8處儲存着“50 45 00 00”,與之對應的ascii字元為“pe00”。這裡就是pe頭部開始的位置。

“pe00”和image_dos_header之間的内容是dos存根,就是一個沒什麼太大用處的dos程式。由于這個程式本身沒有什麼利用的價值,是以這裡就不對這個dos程式做介紹了。在免殺技術、pe檔案大小優化等技術中會對該部分進行處理,可以将該部分直接删除,然後将pe頭部整體向前移動,也可以将一些配置資料儲存在此處等。選中dos存根程式,也就是從0x00000040處一直到0x000000c7處的内容,然後單擊右鍵選擇“填充”指令,在彈出的“填充資料”對話框中,選中“使用16進制填充”單選按鈕,在其後的編輯框中輸入“00”,單擊“确定”按鈕,該過程如圖6-4和圖6-5所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

把dos存根部分填充完畢以後,單擊工具欄上的“儲存”按鈕對修改後的内容進行儲存。儲存時會提示“是否進行備份”,選擇“是”,這樣修改後的檔案就被儲存了。找到檔案然後運作,程式中的messagebox對話框依舊彈出,說明這裡的内容的确無關緊要了。dos存根部分經常由于各種需要而儲存其他資料,是以這種填充操作較為常見。具體填充什麼資料,請讀者在今後的學習中自行發揮想象。

6.2.2 pe頭部詳解image_nt_headers

dos頭是為了相容dos系統而遺留的,dos頭中的最後一個位元組給出了pe頭的位置。pe頭部是真正用來裝載win32程式的頭部,pe頭的定義為image_nt_headers,該結構體包含pe辨別符、檔案頭image_file_header和可選頭image_optional_header 3部分。image_nt_headers是一個宏,其定義如下:

該頭分為32位和64位兩個版本,其定義依賴于是否定義了_win64。這裡隻讨論32位的pe檔案格式,來看一下image_nt_headers32的定義,如下:

該結構體中的signature就是pe辨別符,辨別該檔案是否是pe檔案。該部分占4位元組,即“50 45 00 00”。該部分可以參考圖6-3。signature在winnt.h中有一個宏定義如下:

該值非常重要。如果要簡單地判斷一個檔案是否是pe檔案,首先要判斷dos頭部的開始位元組是否是“mz”。如果是“mz”頭部,則通過dos頭部找到pe頭部,接着判斷pe頭部的前四個位元組是否為“pe00”。如果是的話,則說明該檔案是一個有效的pe檔案。

在pe頭中,除了image_nt_signature以外,還有兩個重要的結構體,分别是image _file_header(檔案頭)和image_optional_header(可選頭)。這兩個頭在pe頭部中占據重要的位置,是以需要詳細介紹這兩個結構體。

6.2.3 檔案頭部詳解image_file_header

檔案頭結構體image_file_header是image_nt_headers結構體中的一個結構體,緊接在pe辨別符的後面。image_file_header結構體的大小為20位元組,起始位置為0x000000cc,結束位置在0x000000df,如圖6-6所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

image_file_header的起始位置取決于pe頭部的起始位置,pe頭部的位置取決于image_dos_header中e_lfanew的位置。除了image_dos_header的起始位置外,其他頭部的位置都依賴于pe頭部的起始位置。

imaeg_file_header結構體包含了pe檔案的一些基礎資訊,其結構體的定義如下:

下面介紹該結構的各字段。

machine:該字段是word類型,占用2位元組。該字段表示可執行檔案的目标cpu類型。該字段的取值如圖6-7所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

在圖6-6中,machine字段的值為“4c 01”,即0x014c,也就是支援intel類型的cpu。

numberofsections:該字段是word類型,占用兩個位元組。該字段表示pe檔案的節區的個數。在圖6-6中,該字段的值為“03 00”,即為0x0003,也就是說明該pe檔案的節區有3個。

timedatastamp:該字段表明檔案是何時被建立的,這個值是自1970年1月1日以來用格林威治時間計算的秒數。

pointertosymboltable:該字段很少被使用,這裡不做介紹。

numberofsymbols:該字段很少被使用,這裡不做介紹。

sizeofoptionalheader:該字段為word類型,占用兩個位元組。該字段指定imageoption al header結構的大小。在圖6-6中,該字段的值為“e0 00”,即0x00e0,也就是說image_ optional_header的大小為0x00e0。注意,在計算image_optional_header的大小時,應該從image_file_header結構中的sizeofoptionalheader字段指定的值來擷取,而不應該直接使用sizeof(image_optional_header)來計算。由該字段可以看出,image_optional _header結構體的大小可能是會改變的。

characteristics:該字段為word類型,占用2位元組。該字段指定檔案的類型,其取值如圖6-8所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

從圖6-6中可知,該字段的的值為“0f 01”,即“0x010f”。該值表示該檔案運作的目标平台為32位平台,是一個可執行檔案,且不存在重定位資訊,行号資訊和符号資訊已從檔案中移除。

6.2.4 可選頭詳解image_optional_header

image_optinal_header在幾乎所有的參考書中都被稱作“可選頭”。雖然被稱作可選頭,但是該頭部不是一個可選的,而是一個必須存在的頭,不可以沒有。該頭被稱作“可選頭”的原因是在該頭的資料目錄數組中,有的資料目錄項是可有可無的,資料目錄項部分是可選的,是以稱為“可選頭”。而筆者覺得如果稱之為“選項頭”會更好一點。不管程式如何,隻要讀者能夠知道該頭是必須存在的,且資料目錄項部分是可選的,就可以了。

可選頭緊挨着檔案頭,檔案頭的結束位置在0x000000df,那麼可選頭的起始位置為0x000000e0。可選頭的大小在檔案頭中已經給出,其大小為0x00e0位元組(十進制為224位元組),其結束位置為0x000000e0 + 0x00e0 – 1 = 0x000001bf,如圖6-9所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

可選頭的定位有一定的技巧性,起始位置的定位相對比較容易找到,按照pe辨別開始尋找是非常簡單的。可選頭結束位置其實也非常容易找到。通常情況下(注意這裡是指通常情況下,不是手工構造的pe檔案),可選頭的結尾後面跟的是第一項節表的名稱。觀察圖6-9,檔案偏移0x000001c0處的節名稱為“.text”,也就是說,可選頭的結束位置在0x000001c0偏移的前一位元組,即0x000001bf處。

可選頭是對檔案頭的一個補充。檔案頭主要描述檔案的相關資訊,而可選頭主要用來管理pe檔案被作業系統裝載時所需要的資訊。該頭同樣有32位版本與64位版本之分。image_optional_header是一個宏,其定義如下:

32位版本和64位版本的選擇是根據是否定義了_win64而決定的,這裡隻讨論其32位的版本。image_optional_header32的定義如下:

該結構體的成員變量非常多,為了能夠更好地掌握該結構體,這裡對結構體的成員變量一一進行介紹。

magic:該成員變量指定了檔案的狀态類型,狀态類型部分取值如圖6-10所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

majorlinkerversion:主連接配接版本号。

minorlinkerversion:次連接配接版本号。

sizeofcode:代碼節的大小。如果有多個代碼節的話,該值是所有代碼節大小的總和(通常隻有一個代碼節),該處是指所有包含可執行屬性的節的大小。

sizeofinitializeddata:已初始化資料塊的大小。

sizeofuninitializeddata:未初始化資料塊的大小。

addressofentrypoint:程式執行的入口位址。該位址是一個相對虛拟位址,簡稱ep(entrypoint),這個值指向了程式第一條要執行的代碼。程式如果被加殼後會修改該字段的值。在脫殼的過程中找到了加殼前該字段的值,就說明找到了原始入口點,原始入口點被稱為oep。該字段的位址指向的不是main()函數的位址,也不是winmain()函數的位址,而是運作庫的啟動代碼的位址。對于dll來說,這個值的意義不大,因為dll甚至可以沒有dllmain()函數,沒有dllmain()隻是無法捕獲裝載和解除安裝dll時的4個消息。如果在dll裝載或解除安裝時沒有需要進行處理的事件,可以将dllmain()函數省略掉。

baseofcode:代碼段的起始相對虛拟位址。

baseofdata:資料段的起始相對虛拟位址。

imagebase:檔案被裝入記憶體後的首選建議裝載位址。對于exe檔案來說,通常情況下,該位址就是裝載位址:對于dll檔案來說,可能就不是其裝入記憶體後的位址了。

sectionalignment:節表被裝入記憶體後的對齊值。節表被映射到記憶體中需要對其的機關。在win32下,通常情況下,該值為0x1000,也就是4kb大小。windows作業系統的記憶體分頁一般為4kb。

filealignment:節表在檔案中的對齊值。通常情況下,該值為0x1000或0x200。在檔案對齊值為0x1000時,由于與記憶體對齊值相同,可以加快裝載速度。而檔案對齊值為0x200時,可以占用相對較少的磁盤空間。0x200是512位元組,通常磁盤的一個扇區即為512位元組。

注:程式無論是在記憶體中還是磁盤上,都無法恰好滿足sectionalignment和filealignment值的倍數,在不足的情況下需要補0值,這樣就導緻節與節之間存在了無用的空隙。這些空隙對于病毒之類程式而言就有了可利用的價值。

majoroperatingsystemversion:要求最低作業系統的主版本号。

minoroperatingsystemversion:要求最低作業系統的次版本号。

majorimageversion:可執行檔案的主版本号。

minorimageversion:可執行檔案的次版本号。

win32versionvalue:該成員變量是被保留的。

sizeofimage:可執行檔案裝入記憶體後的總大小。該大小按記憶體對齊方式對齊。

sizeofheaders:整個pe頭部的大小。這個pe頭部泛指dos頭、pe頭、節表的總和大小。

checksum:校驗和值。對于exe檔案通常為0;對于sys檔案,則必須有一個校驗和。

subsystem:可執行檔案的子系統類型。該值如圖6-11所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

dllcharacteristics:指定dll檔案的屬性,該值大部分時候為0。

sizeofstackreserve:為線程保留的棧大小。

sizeofstackcommit:為線程已送出的棧大小。

sizeofheapreserve:為線程保留的堆大小。

sizeofheapcommit:為線程已送出的堆大小。

loaderflags:被廢棄的成員值。mdsn上的原話為“this member is obsolete”。但是該值在某些情況下還是會被用到的,比如針對原始的低版本的od來說,修改該值會起到反調試的作用。

numberofrvaandsizes:資料目錄項的個數。該個數在psdk中有一個宏定義,如下:

datadirectory:資料目錄表,由numberofrvaandsize個image_data_directory結構體組成。該數組包含輸入表、輸出表、資源、重定位等資料目錄項的rva(相對虛拟位址)和大小。image_data_directory結構體的定義如下:

該結構體的第一個變量為該目錄項的相對虛拟位址的起始值,第二個是該目錄項的長度。資料目錄中的部分成員在數組中的索引如圖6-12所示,詳細的索引定義請參考winnt.h頭檔案。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

在資料目錄中,并不是所有的目錄項都會有值,很多目錄項的值都為0。因為很多目錄項的值為0,是以說資料目錄項是可選的。

可選頭的結構體就介紹完了,希望讀者按照該結構體中各成員變量的含義自行學習可選頭中的十六進制值的含義。隻有參考結構體的說明去對照分析pe檔案格式中的十六進制值,才能更好、更快地掌握pe結構。

6.2.5 節表詳解image_section_header

節表的位置在image_optional_header的後面,節表中的每個image_section_header中都存放着可執行檔案被映射到記憶體中所在位置的資訊,節的個數由image_ file_header中的numberofsections給出。節表資料如圖6-13所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

由image_section_header結構體構成的節表起始位置在0x000001c0處,最後一個節表項的結束位置在0x00000237處。image_section_header的大小為40位元組,該檔案有3個節表,是以共占用了120位元組。

這個結構體相對于image_optional_header結構體來說,成員變量少很多。下面介紹image_section_header結構體的各個成員變量。

name:該成員變量儲存着節表項的名稱,節的名稱用ascii編碼來儲存。節名稱的長度為image_sizeof_short_name,這是一個宏,其定義如下:

節名的長度為8位元組,多餘的位元組會被自動截斷。通常情況下,節名“.”為開始。當然,這是編譯器的習慣,并非強制性的約定。下面來看圖6-13中檔案偏移0x000001c0處的前8位元組的内容“2e 74 65 78 74 00 00 00”,其對應的ascii字元為“.text”。

virtualsize:該值為資料實際的節表項大小,不一定是對齊後的值。

virtualaddress:該值為該節表項載入記憶體後的相對虛拟位址。這個位址是按記憶體進行對齊的。

sizeofrawdata:該節表項在磁盤上的大小,該值通常是對齊後的值,但是也有例外。

pointertorawdata:該節表項在磁盤檔案上的偏移位址。

characteristics:節表項的屬性,該屬性的部分取值如圖6-14所示。

《C++ 黑客程式設計揭秘與防範(第2版)》—第6章6.2節詳解PE檔案結構define IMAGE_NUMBEROF_DIRECTORY_ENTRIES  16define IMAGE_SIZEOF_SHORT_NAME       8

image_section_header結構體主要用到的成員變量隻有這6個,其餘不是必須要了解的,這裡不做介紹。關于image_section_header結構體的介紹就到這裡。

本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。

繼續閱讀