開篇我想講一下於本文無關的話題,其實我很想美化一下自己博客園一直沒時間弄,無意間找了博客園李寶亨的博客園里面有一篇分享自己主題的文章,我就將這個模板暫時用作我的blog主題,我要講述一個關於PE文件結構的文章,這篇文章動手能力比較強,希望大家能夠動手進行操作,這邊文章篇幅有可能會長一些,為了方便大家閱讀我可以將其分為幾個部分進行講解,主要分為以下幾個部分:
① PE文件頭
② 導入表
③ 導出表
④ 資源表
下面我來講解下為什么要學PE文件結構,因為了解PE文件結構就會了解到數據字典中第十五存放的就是元數據通過這個可以進一步研究元數據結構,至於.NET的 PE文件結構下一次進行分析
這里我們不講普通程序的PE文件結構,我們只針對當前.NET程序進行分析,了解普通的PE文件結構后,我們會知道.NET的PE結構不同之處在於在PE頭中的IMAGE_OPTIONAL_HEARDER這個結構中的數據目錄DataDirectory這個包括了映像文件中的CLR頭的RVA和大小。這就使我們能夠很快的進行擴展.NET的PE文件結構,下面我們就對文件進行分析,隨便找一個.NET的程序,我這里有一個程序,我們用16進制編輯器打開,找到數據目錄的第十五個,這個對應的2個字節的CLR頭RVA和2個字節的大小。

現在我們來記錄下這個記錄:
CLR頭:RVA:0x2008 size:0x48
既然我們知道了CLR頭的RVA和大小那么我們計算他在磁盤中的RVA也就是定位在磁盤中的位置,這里我們還需要其他幾個區段的RVA和文件中的大小,這里我們就不在16進制編輯器中進行查找了我們直接打開,CFF Explorer將程序載入后我們查看區塊表信息:

那么我們就開始進行定位,定位該區段在文件中的地址,這里我們來看這個CLR頭的RVA落在了那個區段上,CLR的RVA為0x2008,我們首先看的是第一個區段.text,該區段裝在在內存的地址是0x2000,而這個區段的大小事0x12A00,所以這個區段的范圍是0x2000-0x14A00,剛好0x2008落在這個區段上,那么我們來算出他在文件中的偏移,2008-2000=8,200+8=208;也就是0x208的位置是CLR在文件的RVA。下面實例圖將表述算出來的過程:

這張圖已經很清楚的說明了這個RVA的換算公式,也是我們在這里標出來的?號處就是我們要的東西,這里內存中的CLR頭是內存里面的地址0x2008而區段的開始RVA是0x2000這樣就是S=0x2000,R=.0x2008那么差值=8,P=0x200這樣的話?=0x208這樣就算出了文件的RVA;
這樣我們找到了CLR頭的RVA 我們就來16進制編輯器中進行查看CLR頭,下面是CLR頭的結構:
typedef struct IMAGE_COR20_HEADER { ULONG cb; USHORT MajorRuntimeVersion; USHORT MinorRuntimeVersion; //符號表和開始信息
IMAGE_DATA_DIRECTORY MetaData; ULONG Flags; union{ DWORD EntryPointToken; DWORD EntryPointRVA; }; //綁定信息
IMAGE_DATA_DIRECTORY Resource; IMAGE_DATA_DIRECTORY StrongNameSignature; //常規的定位和綁定信息
IMAGE_DATA_DIRECTORY CodeMagagerTable; IMAGE_DATA_DIRECTORY VTableFixups; IMAGE_DATA_DIRECTORY ExprotAddressTableJumps; IMAGE_DATA_DIRECTORY MagageNativeHeader; }IMAGE_COR20_HEADER
下面是對應字段的描述和對應的大小偏移量等等信息:
| 偏移量 |
大小 |
字段名 |
描述 |
| 0 |
4 |
Cb |
頭的字節大小。 |
| 4 |
2 |
MajorRuntimeVersion |
CLR需要運行程序的最小版本的主版本號。 |
| 6 |
2 |
MinorRuntimeVersion |
CLR需要運行程序的最小版本的次版本號。 |
| 8 |
8 |
MetaData |
RVA和元數據的大小。 |
| 16 |
4 |
Flags |
二進制標記,在接下來的章節討論。在ILAsm中,你可以通過顯示地使用指令.corflags <integer value>和/或命令行選項/FLAGS=<integer value>詳細指明這個值。這個命令行選項優先於指令。 |
| 20 |
4 |
EntryPointToken/EntryPointRVA |
這個映像文件的入口點的元數據識別符(符號);對於DLL映像而言可以是0。這個字段識別了屬於這個模塊的一個方法或包括這個入口點方法的一個模塊。在2.0或更新的版本中,這個字段可能包括內嵌的本地入口點方法的RVA |
| 24 |
8 |
Resources |
RVA和托管資源的大小。 |
| 32 |
8 |
StrongNameSignature |
RVA和用於這個PE文件的哈希數據的大小,由加載器在綁定和版本控制中使用。 |
| 40 |
8 |
CodeManagerTable |
RVA和代碼管理表的大小。在現有的CLR發布版本中,這個字段是保留的,並被設置為0。 |
| 48 |
8 |
VTableFixups |
RVA和一個由虛擬表(v-表)修正組成的數組的字節大小。在當前托管的編譯器中,只有VC++連接器和IL編譯器能夠生成這個數組。 |
| 56 |
8 |
ExportAddressTableJumps |
RVA和由jump thunk的地址組成的數組的大小。在托管的編譯器中,只有8.0之前版本的VC++能夠生成這種表,這將允許導出內嵌在托管PE文件中的非托管本地方法。在CLR的2.0版本中,這個入口是廢棄的並且必須被設置為0。 |
| 64 |
8 |
ManagedNativeHeader |
為預編譯映像而保留的,被設置為0。 |
既然我們已經知道了整個CLR頭的結構,那么我們就來對.NET的這個文件進行十六進制查找下:CTRL+G查找0x208

對應這一塊就是CLR頭的數據,我們可以一步一步進行分析,比如cb占2個字節那么他就是00000048這個數據,以此進行分析可以將所有數據進行分析出來。注意是這里面是以小端的形式存放,也就是他要從后面的是高位,前面的是地位。
那么我可以注意到這個字段StrongNameSignature這個字段就是強命名的字段,如果程序加了強命名我們的一種手段就是將這個RVA和大小全部設置為0就去除了強命名。還有就是Flags標志位,標志里面去除COMIMAGE_FLAGEX_STRONGNAMESIGNED=0x00000008//此程序有強命名。
這里我們要強調的是根據表中最重要的MetaData項,來查看元數據在PE文件中的存儲格式,我們可以在上圖中尋找到:

其中元數據(MetaData)的RVA:0000B2D8,元數據的大小為:00009534,通過這個RVA我們可以將其換算成文件地址,那么這個RVA落在了第一個區段上也就是.text段上,這樣的話我們就可以換算出文件中的RVA:0x94D8,那么我們就可以在16進制編輯器中查看元數據頭的結構。首先我們先看一下整體結構是什么:
| 類型 |
字段 |
描述 |
| DWORD |
lSignature |
424A5342h,就是4個固定的ASCII碼,代表.NET四個創始人的首位字母縮寫 |
| WORD |
iMajorVersion |
元數據的主版本,一般為1 |
| WORD |
iMinorVersion |
元數據的副版本,一般為1 |
| DWORD |
iExtraData |
保留,為0 |
| DWORD |
iLength |
接下來版本字符串的長度,包含尾部0,且按4字節對其 |
| BYTE[ ] |
iVersionString |
UTF8格式的編譯環境版本號 |
| BYTE |
fFlags |
保留為0 |
| BYTE |
[padding] |
此字節無意義,對齊用 |
| WORD |
iStreams |
NStream的個數(流的個數) |
既然我們已經了解了元數據頭的結構之后我們就對應的RVA看一下16進制編輯器里面的內容:

這里我們就不將所有的字段的值取出來我們直接用CFF來看一下我們查找的數據是不是正確的;

其實這里面最重要的就是我們要看一下流到底有多少個,這里面最后一個字段就是iStreams這里面顯示的是5,那么就說明有5個流數據,接下來就開始分析幾個流數據,緊接着元數據頭便是幾個流數據的頭,流按存儲結構的不同分為堆(heap)和表(Table),在元數據中堆是用來存儲字符串和二進制對象。堆分為以下三種:
#Strings:UTF8格式的字符串堆,包含各種元數據的名稱(比如類名,方法名,成員名,參數等),以0開始以0結尾。
#Blob:二進制數據堆,存儲程序中非字符串信息,比如常量值,方法的signature、pubicKey等。每個數據的長度由該數據的前1-3為決定:0表示長度1字節,10表示長度2字節,110表示長度4字節。
#GUID:存儲所有的全局唯一標識
#US:用戶自定義字符串
#~:元數據表流,重要的流,幾乎所有元數據的信息都以表的形式存在
上面我們已經提及到了,MetaData Root緊接着就是流數據,那么我們先看一下流數據的結構,方便我們對其進行分析:
| 大小 |
字段 |
描述 |
| DWORD |
iOffset |
該流的存儲位置相對於MetaData Root的偏移 |
| DWORD |
iSize |
該流占多少字節 |
| char[] |
rcName |
流的名稱,與4字節對齊 |
既然我們看到流數據頭的結構我們可以發現iOffset這個字段是關於流存儲的位置,也就是流數據頭里面存放的是真正流數據的位置,那么我們上面找到的元數據頭的地址是RVA:0x94D8這樣的話我們就可以找到真正的對應的流數據了!那么我們先看一下整體的流數據,我們已經知道一共有5個流數據。

其中的紅色“|”標示着下一個流數據結構的開始,相應對應的結果我用CFF更直觀的展現給大家看,這樣我們就可以進行一個詳細的對比;

經過我們上下數據的比較數據完全符合那么,就說明我們流數據頭找的是正確的。
既然我們將流數據頭找出來,我們就對這5個流數據進行分析,這里我們就單純的講一下#~流,因為這個是.NET都要存在的!上面我們可以看到#~流相對於MetaData的偏移量是0x6C,0x94D8+0x6C就是真正該流數據的存儲位置:0x9544,好的,既然已經尋找到了這個地址那么先來了解下#~內部存儲結構是什么樣的?請看下表:
| 大小 |
字段 |
描述 |
| 4 bytes |
Reserved |
保留,為0 |
| 1 byte |
Major |
元數據表的主版本號,於.NET主版本號一致 |
| 1 byte |
Minor |
元數據的副版本號,一般為0 |
| 1 byte |
Heaps |
Heap中定義數據時的索引的大小,為0表示16位索引值,若堆中數據超出16位數據表示范圍,則使用32位索引值。01代表strings堆,02h代表GUID堆04h代表blob堆 |
| 1 byte |
Rid |
所有元數據表中記錄最大索引值,在運行時有.NET計算,文件中通常為1 |
| 8 bytes |
MaskValid |
8字節長度的掩碼,每個為代表一個表,為1表示該表有效,為0表示該表無效 |
| 8 bytes |
Sorted |
8字節長度的掩碼,每個為代表一個表,為1表示該表已排序,反之為0 |
下面我們來看一下該程序的#~元數據表流的存儲內容,將程序載入到16進制編輯器中,CTRL+G進行搜索0x9544,這個地址就是元數據表流的開始位置:如下所示:

紅色地方代表的是Vaild,其中的數據是0XF0929B69D57,那么將其換算成二進制,看一下哪一些表是有效的,二進制數據如下圖所示:

其中紅色部分表示表數據是有效的一共有24個表,元數據中所有的表:
| 00-Module |
01-TypeRef |
02-TypeDef |
| 03-FiledPtr |
04-Filed |
05-MethodPtr |
| 06-MethodDef |
07-ParamPtr |
08-Param |
| 09-MethodImpl |
10-MemberRef |
11-Constant |
| 12-CustomAttribute |
13-FieldMarshal |
14-DeclSecurity |
| 15-ClassLayout |
16-FieldLayout |
17-StandAloneSig |
| 18-EventMap |
19-EventPtr |
20-Event |
| 21-PropertyMap |
22-PropertyPtr |
23-Property |
| 24-MethodSemantics |
25-MethodImpl |
26-ModuleRef |
| 27-TypeSpec |
28-ImplMap |
29-FiledRVA |
| 30-ENCLog |
31-ENCMap |
32-AssemblyRef |
| 33-AssemblyProcessor |
34-AssemblyOS |
35-Assembly |
| 36- AssemblyRefProcessor |
37- AssemblyRefOS |
38- File |
| 39-ExportedType |
40-ManifestResource |
41- NestedClass |
| 42-GenericParam |
43-MethodSpec |
44-GenericParamConstraint |
緊接着元數據表頭的是一串4字節數組,每個雙字節代表該表中有多少項紀錄(record),本程序中存在24個表那么就是,24*4=144個字節。那么我們就從元數據頭結尾處進行查找:

我們來驗證一下正確性使用CFF來看一下:

經過我們的驗證確實是Module里面只有一條紀錄。點開就可以看到內部結構是什么!這里我們不去講所有表的結構。
這樣我們已經知道了元數據是描述數據的數據,那么這句話要怎么理解呢?那么就來用一個例子來解釋下這個說明的含義:比如該程序我們將其反編譯成IL代碼,查看IL代碼的元數據.

這里我要不去講這個Token的由來,我只講一下這個Token怎么去索引,前面比如這個02000002,前面的02代表在元數據表中的第二個表也就是TypeDef表,至於表內部的結構自己可以再進行研究。那么后面的02代表的是什么呢?代表的是表里面的第二條紀錄。截圖說明下:

和IL圖中描述一致:

至於剩下的#Strings堆都是一些二進制形式存在的數據。為了節省篇幅就到此了!其他的自行分析!
有可能這分析當中會存在一些問題,希望各位能人指出,我將其該正。抽時間將這篇文章整理出來!
