摘要:PE文件格式是Windows操作系統(tǒng)引入的可執(zhí)行文件格式。論文介紹windows平臺下PE文件的基本結(jié)構(gòu)。重點闡述了在不重編譯源碼的前提下實現(xiàn)代碼插入技術(shù)所涉及的基本任務(wù):把代碼插入到PE文件中可用的空閑空間或者在文件尾部添加一個新的節(jié)來插入代碼;如何用一般方法鉤住程序的控制和和重定位插入代碼;插入代碼如何獲取對其有用的windows API函數(shù)的地址。向PE文件插入外部代碼技術(shù)的研究是很有價值的,它對反PE型病毒和軟件加殼技術(shù)的研究都很有用。
關(guān)鍵詞:PE文件格式;結(jié)構(gòu)化異常處理;導(dǎo)出表
中圖分類號:TP311 文獻(xiàn)標(biāo)識碼:A 文章編號:1009-3044(2008)28-0127-03
Research on Injecting your Code to a PE File
YANG Jian1, QIN Jing2
(1. College of Information Science and Engineering,Shenyang University of Technology ,Shenyang 110178,China;2. College of Information Science and Engineering,Northeastern University, Shenyang 110004,China)
Abstract: Portable Executable File Format is a kind of executable file formats of windows operation system. Portable executable file format is introduced in this article, and base tasks to inject your code in a PE file without recompiling source code are demonstrated: Injecting your code to PE file's redundant space or adding a new section at the file's tail to save your code; How to get control of program generally and Relocation of the code; How to get some necessary windows API entrance address. Code injecting is very useful work, it benefits anti-PE virus and software packing technology.
Key words: PE file format; structured exception handling; export directory table
1 引言
Windows平臺是當(dāng)今最為流行的桌面系統(tǒng)。其可執(zhí)行文件(普通的用戶程序、共享庫以及NT系統(tǒng)的驅(qū)動文件)采用的是PE(Portable Executable)文件格式。為了研究代碼插入技術(shù),那么首先得先理解這種可執(zhí)行文件的內(nèi)部結(jié)構(gòu),但PE文件是一種復(fù)雜的文件格式,下面僅作簡單的介紹。
2 PE文件結(jié)構(gòu)簡介
PE文件基本結(jié)構(gòu)如圖1所示。在PE文件中,代碼、已初始化的數(shù)據(jù)、資源和重定位信息等數(shù)據(jù)被按照屬性不同放到了不同的節(jié)中,而每個節(jié)的屬性和位置等信息用IMAGE_SECTION_HEADER結(jié)構(gòu)來描述,所有的IMAGE_SECTION_HEADER結(jié)構(gòu)組成一個節(jié)表,節(jié)表數(shù)據(jù)放在所有節(jié)數(shù)據(jù)的前面。因為各種數(shù)據(jù)是按照屬性放置在節(jié)中,不同用途但屬性相同的數(shù)據(jù)(如導(dǎo)入表導(dǎo)出表以及.const段指定的數(shù)據(jù))可能被放在同一個節(jié)中,所以PE文件用一系列的目錄結(jié)構(gòu)IMAGE_DATA_DIRECTORY來分別指明這些關(guān)鍵數(shù)據(jù)的位置。目錄結(jié)構(gòu)組成的數(shù)據(jù)目錄表和其它描述文件屬性的數(shù)據(jù)一起放置在PE文件頭中。所有PE文件必須以一個簡單的DOS MZ header開始,在偏移0處有DOS下可執(zhí)行文件的“MZ標(biāo)志”。有了它,一旦程序在DOS環(huán)境下執(zhí)行,DOS就能識別出這是有效的執(zhí)行體,然后運行緊隨MZ header之后的DOS stub。在不支持PE文件格式的操作系統(tǒng)中,它將簡單顯示一個錯誤提示,類似于字符串“This program cannot run in DOS mode”。
3 PE文件的代碼插入技術(shù)
代碼插入技術(shù)包含三個重要的任務(wù):1)把外部代碼插入PE文件中;2)在程序啟動之前鉤住程序;3)獲取對其非常重要的API函數(shù)的地址。
3.1 插入位置的選擇
1) 插入到PE文件頭中
PE頭部大小一般為1024字節(jié),有5-6個節(jié)的普通PE文件實際被占用部分一般僅為600字節(jié)左右,尚有400多個字節(jié)的剩余空間可以利用。這些字節(jié)可以用于插入全部的代碼或者只是一個很小的裝載程序。PE文件中用于植入代碼的空閑空間有限,為了插入更多的代碼,可以把代碼分成一個很小的裝載程序和一個較長的尾部。裝載程序可以放在PE文件頭內(nèi),或者以常規(guī)序列的形式放在文件內(nèi)部,尾部可以放在覆蓋中。覆蓋不會被映射到內(nèi)存,所以不能把全部代碼插入到覆蓋中。
2) 插入到文件中節(jié)的尾部
磁盤文件對齊的單位一般為512個字節(jié),所以PE文件的節(jié)之間一般存在一些空閑的空間。在插入之前必須找到一個具有合適的屬性和在其尾部具有足夠空間的節(jié)。如果一個節(jié)的尾部空間不夠,也可以將代碼分散到幾個節(jié)中。候選節(jié)的物理長度(SizeOfRawData)要大于虛擬長度(VirtualSize),但其差值不要超過節(jié)在文件中的對齊單位,因為它很可能包含一個覆蓋。候選節(jié)的屬性應(yīng)該被設(shè)置成IMAGE_SCN_MEM_READ 或者IMAGE_SCN_MEM_ EXECUTE,并且同時設(shè)置了IMAGE_SCN_CNT_CODE和IMAGE_SCN_CNT_INITIALIZED。
3)添加一個新的節(jié)
創(chuàng)建一個新的節(jié)附到原文件的尾部,將插入代碼寫入到新的節(jié)中,并修改節(jié)表和文件頭中文件的屬性信息。在節(jié)表尾部添加一個新的節(jié)表項,并設(shè)置節(jié)表項中的各個字段,其中Characteristics字段設(shè)置可執(zhí)行和可讀寫,其它字段的求法:① PointerToRawData =上一節(jié)的PointerToRawData+ 上一節(jié)的SizeOfRawData;② SizeOfRawData=插入代碼的長度按FileAlignment值對齊;③ VirtualAddress=上一節(jié)的VirtualAddress+上一節(jié)的VirtualSize按SectionAlignment值對齊。由于新的節(jié)部分的改變了文件的結(jié)構(gòu),所以必須對PE 文件頭的相關(guān)字段進(jìn)行調(diào)整,如NumberOfSection,SizeOfCode和 SizeOfImage。在文件尾部的節(jié)中寫入插入的代碼后,最后還得用函數(shù)SetFileEnd設(shè)置文件的新結(jié)尾。
3.2 鉤住程序和重定位
插入的代碼首先必須是完全可重定位的,而不受它可能具有的映像基址的影響。有兩種方法可以解決重定位的問題:1)在PE文件重定位表中添加相應(yīng)的表項;2)利用Intel X86體系結(jié)構(gòu)的特殊指令,用call或浮點指令fnstenv等指令動態(tài)獲取當(dāng)前指令的運行時地址,計算該地址與編譯時預(yù)定義地址的差值,再將該差值加到原編譯時預(yù)定的地址上,得到的就是運行時數(shù)據(jù)的正確地址,演示代碼如下:
var dd?
_Entry:
call delta
delta:
popebp
subebp, offset delta
; 計算出的差值用 ebp保存
…
mov eax, [ebp+var]
…
_ToOldEntry:
db0e9h
; 此處定義了jmp指令的機(jī)器碼
_dwOldEntry:
dd?
; 這里保存下一條指令地址與原入口地址的差值
在程序執(zhí)行之前鉤住控制,將PE文件入口指針改為被插入代碼的入口,這樣在系統(tǒng)加載PE文件后,插入代碼就首先獲取了控制權(quán)。在執(zhí)行完插入代碼后,設(shè)置 jmp指令再將控制權(quán)轉(zhuǎn)移給原來程序的代碼。
3.3 API函數(shù)地址的獲取
插入的代碼沒有自己的導(dǎo)入表,那么可以在PE文件導(dǎo)入表中搜尋所需要的API函數(shù)。從數(shù)據(jù)目錄表中定位到文件的導(dǎo)入表,如果發(fā)現(xiàn)要使用的函數(shù)未被引入,則修改導(dǎo)入表來增加該函數(shù)的引入表項,并將對該API的調(diào)用指向新增加的引入函數(shù)地址。這樣在宿主程序啟動的時候,系統(tǒng)加載器已經(jīng)把正確的API函數(shù)地址填好了,代碼即可正確地直接調(diào)用該函數(shù)。
另一種使用windows動態(tài)庫中API函數(shù)的方法是調(diào)用Kernel32.DLL動態(tài)庫中LoadLibrary和GetProcAddress。Kernel32.DLL幾乎在所有的Win32進(jìn)程中都要被加載,只要獲取了它在進(jìn)程中加載的基址,然后解析其導(dǎo)出表得到LoadLibrary和GetProcAddress。獲取Kernel32.DLL基址的方法很多,最常見的一種是搜索法。如果已知Kernel32.DLL加載的大致地址(Kernel32.dll模塊下方的某個地址),那么按照內(nèi)存頁對齊的邊界一頁一頁地,由從該地址開始向低地址搜索其基址。在Win32程序執(zhí)行過程中,F(xiàn)S段寄存器的基址總是指向進(jìn)程的TEB(線程環(huán)境塊),而TEB的第一個成員指向SHE(結(jié)構(gòu)化異常處理)鏈。該鏈表的節(jié)點是一個EXCEPTION_REGISTRATION結(jié)構(gòu),該結(jié)構(gòu)定義如下:
struct EXCEPTION_REGISTRATION{
struct EXCEPTION_REGISTRATION*prev;
void* handler;
};
在Windows下SEH鏈表最后一個成員的handler指向Kernel32.DLL動態(tài)庫中函數(shù)UnhandledExceptionFilter的起始地址,該地址為Kernel32.DLL模塊下方的某個地址。如果成員是最后一個SEH節(jié)點,那么prev的值為0xFFFFFFFF,演示代碼如下:
assume fs: nothing
movesi, fs:[0]
lodsd
_before:
inceax
je _forward
deceax
xchg esi, eax
lodsd
jmp_before
_forward:
lodsd
;eax保存的就是Kernel32.dll模塊下方的某個地址
Kernel32.dll動態(tài)庫被映射到每一個進(jìn)程的地址空間,而且其映像基址總是通過64KB邊界來對齊的。因此將得到的Kernel32.dll模塊下方的地址按64K對齊,然后以每次一個頁的間隔在內(nèi)存中尋找DOS MZ文件標(biāo)識和PE文件頭標(biāo)識,如果滿足標(biāo)識條件的話,表示這個頁的起始地址就是Kernel32.dll的基址。然后用得到的的基地址來定位Kernel32.dll的導(dǎo)出表,在導(dǎo)出表中查找函數(shù)地址的過程:
1) 在導(dǎo)出表中獲取NumberOfNames的值以及數(shù)組AddressOfNames、AddressOfNameOrdinals和AddressOfFunctions的地址。
2) 字段NumberOfNames是已命名函數(shù)的總數(shù),用這個值作為循環(huán)次數(shù)構(gòu)造一個循環(huán)。
3) 在AddressOfNames數(shù)組中,用上面的循環(huán)搜索要查找的函數(shù)名,若沒有,表明文件中沒有指定的函數(shù)。
4) 若找到,獲取當(dāng)前函數(shù)名字指針在AddressOfNames數(shù)組中的索引值,在AddressOfNameOrdinals數(shù)組中取出以該值索引的函數(shù)序號,以該序號值作為AddressOfFunctions數(shù)組的索引,在AddressOfFunctions數(shù)組中取出導(dǎo)出函數(shù)的RVA值,加上基址就得到了運行時導(dǎo)出函數(shù)的地址。
得到了LoadLibrary和GetProcAddress的地址之后,就可以利用這兩個函數(shù)來獲取任意動態(tài)庫中導(dǎo)出的任意函數(shù)的地址。
4 結(jié)束語
向PE文件中插入外部代碼的技術(shù)不僅僅對軟件安全方面的專業(yè)人士(與病毒做斗爭)有用,對加密程序和壓縮程序的開發(fā)人員也是很有用處的。插入技術(shù)的實現(xiàn)需要對操作系統(tǒng)內(nèi)核及其 PE可執(zhí)行文件加載機(jī)制有深入了解,因此本文限于篇幅不能詳細(xì)介紹全部的設(shè)計過程,只能從實現(xiàn)的機(jī)制上闡述了插入技術(shù)的基本原理、方法和應(yīng)該注意的一些問題。
參考文獻(xiàn):
[1] Microsoft Corporation.Microsoft Portable Executable and Common Object File Format Specification[EB/OL]. [2008-02-15]http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx.
[2] 段鋼.加密與解密[M].北京:電子工業(yè)出版社,2003.
[3] 段鋼.軟件加密技術(shù)內(nèi)幕[M].北京:電子工業(yè)出版社,2004.
[4] 方旺盛,邵利平,張克俊.基于PE文件格式的信息隱藏技術(shù)研究[J].微計算機(jī)信息,2006,22(11):77-80.