漢字最好--2008.1
看雪論壇首發
幾年前曾打算學習PE文件格式,但滿篇的英語和c語言令人聲畏。一直到上個月因為需要才硬著頭皮學下來。於是參考了幾篇1.9版譯文,同時對數個文件反編譯、修改,最後自己組裝了一個小exe,總算對PE文件有了一個初步的認識。
本文僅僅研究的是win32GUI程序中重要的內容,對dll,驅動,控制台沒有涉及,適合最初學的、不懂c語言和英語的學習者學習。
工具:ollice,超級工具,本文用來PE文件在內存中的情況。
Winhex,二進制編輯工具,本文用來查看、編輯、創建PE文件。
Restool,資源工具,本文用來驗證資源數據。
以上工具都有前輩的漢化修正版,對不懂鳥語學習者來說是天大的福音。譯者易也,我能理解翻譯者的難處,給計算機英語取個好中文名有點難度。是他們給我節約了學習一門外語的時間,讓我不懂英語一樣也學到了知識。
本文中不正確的地方望指正!
一 序言
PE文件是微軟弄出來的可執行文件格式。Pe文件中的數據是一節節整齊的排列下去的,所以我們把一節數據稱為‘節’。PE文件的第一個節叫頭節,但為了與其他節區別開,我把頭節還是叫文件頭。文件頭依次包括DOS頭、pe頭、選項頭、節頭表四個部分。
執行pe文件實際上是系統的pe載入器把PE文件的各節映射到一個虛擬的進程空間後再執行代碼的。這個虛擬的進程空間在32位下有$FFFFFFFF($表示十六進制,相當於c的0x,匯編的H),即4GB大小,即所謂的虛擬內存,裡面的地址也就是虛擬地址.這個pe文件在虛擬空間裡稱為映象文件,映象文件開始的地方叫映象文件基址.而本文後面提到的虛擬地址都是相對虛擬地址,即相對於映象文件基址的偏移量.而pe文件中的偏移量稱為物理地址.
本章給出了很多重要定義,有一些和1.9版譯文不一樣.其實起什麼名不重要,重要的是能不能理解.另外,本文中稱的’**表’除特別說明外指**的數組.如:節頭表,就是節頭的數組.
下面用winhex打開附件中的st.exe對照學習.
二 DOS頭
Pe文件的開始是文件頭,文件頭的開始是DOS頭,DOS頭的開始是’MZ’.如下:
物理地址
占用字節
含義
常用值
在文件中形式
0
2
DOS頭開始標志
‘MZ’
40 5A
2
58
DOS下的東西
Windows下都置0吧
$3C
4
PE頭的物理地址
如果後面沒有DOS根就取$40,否則還要加上DOS根大小.下文將這項取值用ppe表示.
(數值和字符串在文件中的形式不用我提了吧?)
下面一般有一個DOS根結構,是一小段DOS程序,對我們初學者沒有用,本文就不提了.
三 PE頭
PE頭是本文的第二部分,共$18字節.
物理地址
占用字節
含義
常用值
在文件中形式
Ppe+0
4
PE頭開始的標志
‘PE’
50 45 00 00
Ppe+4
2
Cpu要求
4C 01
Ppe+6
2
節數目
後面節頭表聲明了幾個就是幾
Ppe+8
4
日期時間
多半是亂填
Ppe+$C
8
沒什麼用
都置0
Ppe+$14
2
選項頭大小
$E0
E0 00
Ppe+$16
2
PE屬性
$10F或$818E,互改了都能運行
0F 01
PE頭中最重要的是節數目,其他值都用常用值就可以.
四 選項頭
選項頭是文件頭的第三部分.選項頭中的許多選項是告知系統如何在虛擬內存中執行的數據.共$E0字節
物理地址
占用字節
含義
常用值
在文件中形式
Ppe+$18
2選項頭標志
$10B
0B 01
Ppe+$1A
2
鏈接器版本號
可隨便寫
Ppe+$1C
4
代碼段長度
用不上
Ppe+$20
4
初始化數據長度
用不上
Ppe+$24
4
未初始化數據長度
可能有用吧
Ppe+$28
4
程序入口(OEP),程序開始執行的地方.這是虛擬地址
如果加了殼,就是殼的入口
Ppe+$2C
4
代碼段基址
好像用不上
Ppe+$30
4
數據段基址
Ppe+$34
4
映象文件基址.改成$200000沒影響.改成$100000就與棧沖突了.如果載入這個基址出了問題,系統就會從基址重定位表中找.
$400000
00 00 40 00
Ppe+$384
節對齊.節被映射到虛擬內存後,占用節對齊的整數倍.如:節有$2400字節,在虛擬內存中占$3000.
$1000也就是4KB
00 10 00 00
Ppe+$3C
4
文件對齊.節在文件中占用文件對齊的整數倍,不足的補0,包括文件頭(頭節)
$200也就是512字節
00 02 00 00
Ppe+$40
4
這三項是操作系統及子系統版本號
4
04 00 00 00
Ppe+$44
4
4
04 00 00 00
Ppe+$48
4
4
04 00 00 00
Ppe+$4C
4
沒用的
0
Ppe+$50
4
映象文件大小.是所有節映射到虛擬內存後的大小.別忘了計算文件頭和未初始化數據節.文件頭一般占一個節對齊,節映射到虛擬內存後是節對齊的整數倍,所以映象文件大小也是節對齊的整數倍.
Ppe+$54
4
文件頭大小.隨便寫也沒問題
$400
00 04 00 00
Ppe+$58
4
校驗和.可能用得上.
0
Ppe+$5C
2
NT子系統(控制台選3)
2
02 00
2
Dll狀態
0
00 00
Ppe+$60
4
保留棧
1MB
00 00 10 00
Ppe+$64
4
初始棧
4KB
00 10 00 00
Ppe+$68
4
保留堆
1MB
00 00 10 00
Ppe+$6C
4
初始堆
4KB
00 10 00 00
Ppe+$70
4
載入風格
0
Ppe+$74
數據索引表的索引數
16
10 00 00 00
上面共占用$60字節.下面緊跟一個數據索引表,占用$80字節.
數據索引表由16條索引組成,索引的前4字節是某種數據的虛擬地址,後4字節是數據的大小.數據的類型由索引在表中的位置決定,各位置的意義如下:
0:輸出表(DLL必用)
1:輸入表
2:資源數據
3:異常
4:安全
5:基址重定位表
6:調試
7:描述文字
8;機器值
9:線程存儲地址(TLS)
10:載入配置
11:綁定輸入表
12:輸入地址表
13-15未見定義
St.exe定義了第1,2,12項,沒有用到的都置0,
選項頭到這裡就結束了.其實也只有程序入口,映象文件基址,數據索引表讓你斟酌一下,其他都有默認值.
五 節頭表
節頭表由一串節頭組成.每個節頭都聲明如何把一個節映射到虛擬內存中去.一個節頭有$28字節,結構如下:
占用字節
含義
說明
8
節的名稱
Asni字符.可不寫
4
物理地址或大小
既然‘或’了,那隨便吧
4
節的虛擬地址
別定義到已經定義了地方.如$300,顯然已經被文件頭占有.
4
節數據大小
注意:肯定是文件對齊的整數倍,即使你的一個節實際使用2個字節也要占一個文件對齊.未初始化數據是文件對齊0倍
4
節的物理地址
12
無用的,都置0
4
節屬性
是個32位數據,具體查1.9版譯文
常見節屬性:
1:代碼$00000002
2:已初始化數據$00000004
3:未初始化數據$00000008
4:可共享$10000000
5:可執行$20000000
6:可讀$40000000
7:可寫$80000000
$60000020 1 和 5 和 6 (算術或運算) 通常是代碼段
$C0000040 2 和 6 和 7 通常是輸入表
$40000040 2 和 6 通常是資源數據
我們一起看st.exe,紅色標記虛擬地址:
00000060 00 00 00 00 00 00 00 00 30 10 00 00 00 00 00 00 ........0.......
00000070 00 00 00 00 00 00 40 00 00 10 00 00 00 02 00 00 ......@.........
$6C處是入口=$1030,$70處是映象文件基址=$400000.
00000130 2E 74 65 78 74 00 00 00 .text...
00000140 00 01 00 00 00 10 00 00 00 02 00 00 00 02 00 00 ................
00000150 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60 ............ ..`
00000160 2E 72 64 61 74 61 00 00 6A 01 00 00 00 20 00 00 .rdata..j.... ..
00000170 00 02 00 00 00 04 00 00 00 00 00 00 00 00 00 00 ................
00000180 00 00 00 00 40 00 00 40 2E 64 61 74 61 00 00 00 ....@[email protected]...
00000190 04 00 00 00 00 30 00 00 00 00 00 00 00 00 00 00 .....0..........
000001A0 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 C0 ............@..?
000001B0 2E 72 73 72 63 00 00 00 60 02 00 00 00 40 00 00 .rsrc...`....@..
000001C0 00 04 00 00 00 06 00 00 00 00 00 00 00 00 00 00 ................
000001D0 00 00 00 00 40 00 00 40 ....@..@
上面就是節頭表,
名稱
虛擬地址
大小
物理地址
屬性
.text
$1000
$200
$200
$60000020
.rdata
$2000
$200
$400
$40000040
.data
$3000
$0
0$400000C0
.rsrc
$4000
$400
$600
$40000040
看這些節都沒有超過節對齊,所以都只占一個節對齊..data只是預定了空間,適合變量用.
虛擬地址轉物理地址:
我們看程序入口正好落到了.text節的+$30($1030-$1000)處,來到$200+$30:
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000230 6A 00 E8 35 01 00 00 A3 00 30 40 00 E8 4F 01 00 j.?...?0@.鐿..
00000240 00 6A 00 68 5E 10 40 00 6A 00 6A 65 FF 35 00 30 .j.h^[email protected]5.0
可在OLLICE中查看此處代碼含義($00401030).
在節頭表部分,每個節頭的虛擬地址,大小,物理地址,屬性都要仔細設定.
文件頭到這裡就結束了,不足文件對齊整數倍的要補足0.文件後面緊跟節頭表定義的各種節.
當然後面也可能只有一個節.
六 輸入表
輸入表在虛擬內存中的地址和大小由選項頭的數據索引表中第二條索引指定.輸入表有點回調函數的味道,是給系統調用的.通過輸入表,系統了解程序需要哪些動態庫的函數,在加載PE文件時把這些動態庫也映射到進程虛擬空間,並把這些函數的地址寫到程序指定的地方供調用.
輸入表實際上是一個‘輸入說明結構’的數組.該數組的最後一個成員置0以表結束.
一個輸入說明結構由5個雙字組成,占用20字節,對應一個動態庫.如下:
占用字節
名稱
含義
1
4
地址一
指向函數索引地址表的地址
2
時間戳
用於驗證dll或綁定輸入
3
4
中轉鏈
4
4
動態庫名
指向動態庫名的指針(即Pchar或char*),動態庫名是標准字符串
5
4
地址二
指向地址表的指針.這個地址表可能與其他輸入說明結構指向的地址表組成一個連續的輸入地址表.(見數據索引表)
標准字符串指以空字符結束的ansi字符串.
上述結構中第2,3項沒有找到實例所以跳過,呵呵.
動態庫名也好理解,關鍵是第1,5項.地址一是個指針,指向函數索引地址表.這個表裡都是函數索引的地址,以空地址(0)表示結束.函數索引由一個2個字節的索引號和進跟其後的一個標准字符串(即函數名)組成.
系統的工作就是首先獲得動態庫名,然後順著地址一找到函數索引地址表,再一條條地讀函數索引,將找到的函數的地址依次寫到地址二指定的地址表中,直到讀到空地址,再讀取下一條輸入說明結構,直到為0.
有點難於理解?再次用winhex和ollice打開st.exe:
000000C0 30 20 00 00 50 00 00 00 00 40 00 00 60 02 00 00 0 ..P....@..`...
來到$C0處,輸入表的虛擬地址是$2030,大小$50.換成物理地址在$430,在.rdata節中.
來到$430處,可以讀倒條輸入說明結構:
00000430 88 20 00 00 00 00 00 00 00 00 00 00 F2 20 00 00 ?..........?..
00000440 08 20 00 00 9C 20 00 00 00 00 00 00 00 00 00 00 . ..?..........
00000450 3A 21 00 00 1C 20 00 00 80 20 00 00 00 00 00 00 :!... ..€ ......
00000460 00 00 00 00 5C 21 00 00 00 20 00 00 00 00 00 00 ....\!... ......
00000470 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
上面都是虛擬地址,轉成物理地址時-$2000+$400.
以第一條為例,地址一為$2088->$488,地址二為$2008->$408,動態庫名為$20F2->$4F2.
000004F0 00 00 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00 00 ..kernel32.dll..
$4F2原來是kernel32.dll.
$488處:
00000480 E2 20 00 00 CE 20 00 00 ?..?..
00000490 BE 20 00 00 B0 20 00 00 00 00 00 00 ?..?......
有四個地址,也就是說有四個函數.來到$4E2($2012):
000004E0 00 00 3C 02 53 65 74 4C 61 73 74 45 72 72 6F 72 ..<.SetLastError
000004F0 00 00 ..
找到了一個函數,其它的就這麼找.
在地址二指向的$408好像看不到什麼,到OLLICE中,點’M’圖標,,在列表中雙擊
輸入表項(Memory map, 條目 21),在彈出的窗口中看$400000+$2008處已經寫入了函數地址,紅色字體表示是載入時寫入的.
多數情況下程序並不是直接CALL這個地址,而是先CALL一個跳轉表,然後JMP到這裡:
00401030 >/$ 6A 00 push 0 ; /pModule = NULL
00401032 |. E8 35010000 call <jmp.&kernel32.GetModuleHandleA> ; \GetModuleHandleA
0040116C $- FF25 0C204000 jmp dWord ptr [<&kernel32.GetModuleH>; kernel32.GetModuleHandleA
00401172 $- FF25 08204000 jmp dWord ptr [<&kernel32.SetLastErr>; ntdll.RtlSetLastWin32Error
一些編譯器將地址一都置0,這時系統用地址二代替地址一,此時,地址二指向的地址表要符合函數索引地址表的規范,即以空地址表結束.以Project1.exe(Delphi編寫的)為例:
輸入表
00002C00 00 00 00 00 00 00 00 00 00 00 00 00 08 61 00 00 .............a..
00002C10 78 60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 x`..............
00002C20 90 62 00 00 D0 60 00 00 00 00 00 00 00 00 00 00 恇..衊..........
00002C30 00 00 00 00 BC 62 00 00 DC 60 00 00 00 00 00 00 ....糱..躟......
00002C40 00 00 00 00 00 00 00 00 FC 62 00 00 EC 60 00 00 ........黚..靈..
00002C50 00 00 00 00 00 00 00 00 00 00 00 00 48 63 00 00 ............Hc..
00002C60 00 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .a..............
00002C70 00 00 00 00 00 00 00 00
地址二指向的地址表
00002CD0 9C 62 00 00 AE 62 00 00 00 00 00 00 CA 62 00 00 渂..産......蔮..
00002CE0 DE 62 00 00 EE 62 00 00 00 00 00 00 0A 63 00 00 辀..頱.......c..
00002CF0 18 63 00 00 26 63 00 00 34 63 00 00 00 00 00 00 .c..&c..4c......
00002D00 54 63 00 00 00 00 00 00 Tc......
再看看ollice中相應的地方,明白了嗎?
多打開幾個文件看看就會加深映象。
七 資源數據
其實有些程序除了程序圖標並不使用其他的標准資源數據,標准的資源數據在鏈接到PE文件中前是一個單獨的資源文件,通常以.res為後綴.
資源數據的虛擬地址和大小由數據索引表第三項指定.
標准的資源數據是一個樹狀結構,共分5層:
一層
_______|_______________
| | |
二層
_____________|__________________
| | | |
三層
_____________|__________________
| | | |
四層
_____________|__________________
| &nbs | | |
五層
一、二、三層是相似的結構,四層是資源數據的描敘,五層是具體的資源數據(如光標、位圖).
一、二、三層都由一個或幾個資源干組成.當然一層只能有一個資源干(是這個資源樹的主干嘛).一個資源干由16字節的項目干和數目不等的項目組成,項目干決定項目的數目.另外,標准資源中的字符都是unicode字符.
資源干=項目干+項目1+……+項目n.
項目干結構如下:
4字節
特征
4字節
時間
4字節
版本
2字節
已命名項目數.使用名稱標識資源的項目數目
2字節
ID項目數.使用ID(數字編號)標識資源的項目數目
項目的結構如下:
占用字節
最高位
含義
第1個32位數據
4
1
剩下31位是資源名稱的偏移量
0
ID
第2個32位數據
4
1
剩下31位是下層某資源干的偏移量
0
四層某資源描敘的偏移量
下面的內容很重要:項目中的偏移量指相對於資源數據起始位置(一層的開頭)的偏移.
項目中的ID在一層指資源類型,二層指具體資源的ID號,在三層指語言ID(如04 09是美國英語).
一層中資源類型ID如下:
1: 光標 2: 位圖3: 圖標 4: 菜單5: 對話框6: 字串表 7: 字體目錄8: 字體 9: 快捷鍵
10: 未格式化資源數據11: 信息表12: 組光標 14: 組圖標 16: 版本信息
ID為10時可以用來導入任何文件
程序可以利用二層的ID或資源名稱來調用相應的資源。如例程中st.exe用函數調用了ID號為$65的對話框資源.(在$00401052調用dialogboxparama時)
三層的項目給出了四層某資源描敘的偏移量.資源描敘的結構如下:
占用字節
含義
4
具體資源的虛擬地址(當然是相對映象文件基址的偏移量)
4
具體資源的大小
4
代碼頁(不知道有什麼用)
4
未用.
具體資源就是五層了.具體資源的格式就不在本文討論范圍內了.太陽的,俺連對話框格式還沒有完全弄明白呢.本來還想說說數據索引表指定的其他幾種數據(如重定位,TLS),可實在找不到有價值的資料,遺憾.
還是以st.exe為例:
$C8處指定資源數據開始於$4000,$1B0處.rsrc節也開始於$4000,所以這一節就是資源.
來到其物理地址$600處,讀資源干,原來有兩個項目一個是對話框(5),一個是版本($10).下面我用顏色標識對話框這條分支.
00000600 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 ................00000610 05 00 00 00 20 00 00 80 10 00 00 00 38 00 00 80 .... ..€....8..€
00000620 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 ................
00000630 65 00 00 00 50 00 00 80 00 00 00 00 00 00 00 00 e...P..€........
00000640 00 00 00 00 00 00 01 00 01 00 00 00 68 00 00 80 ............h..€
00000650 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 ................
00000660 09 04 00 00 80 00 00 00 00 00 00 00 00 00 00 00 ....€...........
00000670 00 00 00 00 00 00 01 00 04 08 00 00 90 00 00 00 ............?..
00000680 A0 40 00 00 82 00 00 00 00 00 00 00 00 00 00 00 燖..?..........
00000690 28 41 00 00 38 01 00 00 00 00 00 00 00 00 00 00 (A..8...........
上面紅色標出ID($65就是上面提到的資源ID號),紫色標出偏移量(注意($80就是最高位為1 ),藍色標出具體資源的虛擬地址,換成物理地址就是$6A0.有興趣到$6A0處研究下對話框格式.
八 組裝PE文件
學習到這裡就告一段落了,這章我們一起來組裝一個PE文件.不知道為什麼1.9版譯文中組裝的控制台程序在我的xp下總報錯.重新弄個例子吧.
用winhex新建一個文件,大小位$600字節,命名為’測試1’,開工:
偏移
寫入值
0
4D 5A//’MZ’
$3C
40 00//PE頭偏移
$40
50 45//’PE’
$44
4C 01//cpu
$46
02 00//節數目
$54
E0 00//選項頭大小
$56
0F 01//PE屬性
0B 01//標志
$68
00 10 00 00//入口
$74
00 00 04 00//映象文件基址
$78
00 10//節對齊
$7C
00 02//文件對齊
$80
04 00
$84
04 00
$88
04 00//版本號
$90
00 30//映象文件大小(兩個節加一文件頭,各占一個節對齊)
$94
00 02//文件頭大小
$9C
02 00//NT子系統
$A4
00 00 10 00//棧
$A8
00 10
$AC
00 00 10 00//堆
$B0
00 10
$B4
10 00//數據索引數
數據索引表只有輸入表索引(這裡把輸入表放到第二節)
$C0
00 20 00 00//輸入表虛擬地址
$C4
28 00 00 00//輸入表大小面定義第一個節,包含代碼和兩條字符串
$138
63 6F 64 65//節名
$144
00 10 00 00//虛擬地址
$148
00 02 00 00//節大小
$14C
00 02//物理地址
$15C
20 00 00 60//節屬性,
下面定義第二個節,是輸入表
$160
64 61 74 61//節名
$16C
00 20 00 00//虛擬地址
$170
00 02//大小
$174
00 04//物理地址
$184
40 00 00 C0//屬性
文件頭寫完了.先完成第二個節的具體數據:在$460處
00000460 75 73 65 72 33 32 2E 64 6C 6C 00 00 00 00 4D 65 user32.dll....Me
00000470 73 73 61 67 65 42 6F 78 41 00 ssageBoxA
只輸入一個函數,索引號沒有寫.記下庫名和函數名的地址,換成虛擬地址分別是$2060和$206C.現在可以填函數地址索引表了,只有一個,在$430處寫入6C 20($206C).然後填輸入表了,也只有一條輸入說明結構:在$400處寫地址一30 20($2030->$430),$40C處寫庫文件名60 20($2060),$410處寫地址二30 20(呵呵跟地址一一樣哦).載入後messageboxa的地址就會寫到地址二,我們在代碼中就可以調用這個函數了.
下面完成第一個節,先寫兩條字符:
00000220 B3 AC D0 A1 B3 CC D0 F2 00 00 00 00 00 00 00 00 超小程序........
00000230 50 45 CE C4 BC FE D1 A7 CF B0 B3 C9 B9 FB 00PE文件學習成果..
記下它們的虛擬地址$1020和$1030.
在$200處開始寫代碼:
55 push ebp
8BEC mov ebp, esp
6A 00 push 0
68 20104000 push 00401020
68 30104000 push 00401030
6A 00 push 0
FF15 30204000 call dWord ptr [$00402030] ; user32.MessageBoxA
61 popad
保存收工.運行看看.不對頭?對照附件中的測試1.exe看看哪裡寫錯了.
有興趣看看測試2.exe,代碼和輸入表寫到一個節裡,節屬性要改,用的是跳轉方式.
完了,請斧正!
參考資料: “PE文件格式”1.9版 完整譯文(附注釋)
感謝俺老婆打了一半的文字