摘要:PE文件格式是Windows操作系統引入的可執行文件格式。論文介紹windows平臺下PE文件的基本結構。重點闡述了在不重編譯源碼的前提下實現代碼插入技術所涉及的基本任務:把代碼插入到PE文件中可用的空閑空間或者在文件尾部添加一個新的節來插入代碼;如何用一般方法鉤住程序的控制和和重定位插入代碼;插入代碼如何獲取對其有用的windows API函數的地址。向PE文件插入外部代碼技術的研究是很有價值的,它對反PE型病毒和軟件加殼技術的研究都很有用。
關鍵詞:PE文件格式;結構化異常處理;導出表
中圖分類號:TP311 文獻標識碼: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平臺是當今最為流行的桌面系統。其可執行文件(普通的用戶程序、共享庫以及NT系統的驅動文件)采用的是PE(Portable Executable)文件格式。為了研究代碼插入技術,那么首先得先理解這種可執行文件的內部結構,但PE文件是一種復雜的文件格式,下面僅作簡單的介紹。
2 PE文件結構簡介
PE文件基本結構如圖1所示。在PE文件中,代碼、已初始化的數據、資源和重定位信息等數據被按照屬性不同放到了不同的節中,而每個節的屬性和位置等信息用IMAGE_SECTION_HEADER結構來描述,所有的IMAGE_SECTION_HEADER結構組成一個節表,節表數據放在所有節數據的前面。因為各種數據是按照屬性放置在節中,不同用途但屬性相同的數據(如導入表導出表以及.const段指定的數據)可能被放在同一個節中,所以PE文件用一系列的目錄結構IMAGE_DATA_DIRECTORY來分別指明這些關鍵數據的位置。目錄結構組成的數據目錄表和其它描述文件屬性的數據一起放置在PE文件頭中。所有PE文件必須以一個簡單的DOS MZ header開始,在偏移0處有DOS下可執行文件的“MZ標志”。有了它,一旦程序在DOS環境下執行,DOS就能識別出這是有效的執行體,然后運行緊隨MZ header之后的DOS stub。在不支持PE文件格式的操作系統中,它將簡單顯示一個錯誤提示,類似于字符串“This program cannot run in DOS mode”。
3 PE文件的代碼插入技術
代碼插入技術包含三個重要的任務:1)把外部代碼插入PE文件中;2)在程序啟動之前鉤住程序;3)獲取對其非常重要的API函數的地址。
3.1 插入位置的選擇
1) 插入到PE文件頭中
PE頭部大小一般為1024字節,有5-6個節的普通PE文件實際被占用部分一般僅為600字節左右,尚有400多個字節的剩余空間可以利用。這些字節可以用于插入全部的代碼或者只是一個很小的裝載程序。PE文件中用于植入代碼的空閑空間有限,為了插入更多的代碼,可以把代碼分成一個很小的裝載程序和一個較長的尾部。裝載程序可以放在PE文件頭內,或者以常規序列的形式放在文件內部,尾部可以放在覆蓋中。覆蓋不會被映射到內存,所以不能把全部代碼插入到覆蓋中。
2) 插入到文件中節的尾部
磁盤文件對齊的單位一般為512個字節,所以PE文件的節之間一般存在一些空閑的空間。在插入之前必須找到一個具有合適的屬性和在其尾部具有足夠空間的節。如果一個節的尾部空間不夠,也可以將代碼分散到幾個節中。候選節的物理長度(SizeOfRawData)要大于虛擬長度(VirtualSize),但其差值不要超過節在文件中的對齊單位,因為它很可能包含一個覆蓋。候選節的屬性應該被設置成IMAGE_SCN_MEM_READ 或者IMAGE_SCN_MEM_ EXECUTE,并且同時設置了IMAGE_SCN_CNT_CODE和IMAGE_SCN_CNT_INITIALIZED。
3)添加一個新的節
創建一個新的節附到原文件的尾部,將插入代碼寫入到新的節中,并修改節表和文件頭中文件的屬性信息。在節表尾部添加一個新的節表項,并設置節表項中的各個字段,其中Characteristics字段設置可執行和可讀寫,其它字段的求法:① PointerToRawData =上一節的PointerToRawData+ 上一節的SizeOfRawData;② SizeOfRawData=插入代碼的長度按FileAlignment值對齊;③ VirtualAddress=上一節的VirtualAddress+上一節的VirtualSize按SectionAlignment值對齊。由于新的節部分的改變了文件的結構,所以必須對PE 文件頭的相關字段進行調整,如NumberOfSection,SizeOfCode和 SizeOfImage。在文件尾部的節中寫入插入的代碼后,最后還得用函數SetFileEnd設置文件的新結尾。
3.2 鉤住程序和重定位
插入的代碼首先必須是完全可重定位的,而不受它可能具有的映像基址的影響。有兩種方法可以解決重定位的問題:1)在PE文件重定位表中添加相應的表項;2)利用Intel X86體系結構的特殊指令,用call或浮點指令fnstenv等指令動態獲取當前指令的運行時地址,計算該地址與編譯時預定義地址的差值,再將該差值加到原編譯時預定的地址上,得到的就是運行時數據的正確地址,演示代碼如下:
var dd?
_Entry:
call delta
delta:
popebp
subebp, offset delta
; 計算出的差值用 ebp保存
…
mov eax, [ebp+var]
…
_ToOldEntry:
db0e9h
; 此處定義了jmp指令的機器碼
_dwOldEntry:
dd?
; 這里保存下一條指令地址與原入口地址的差值
在程序執行之前鉤住控制,將PE文件入口指針改為被插入代碼的入口,這樣在系統加載PE文件后,插入代碼就首先獲取了控制權。在執行完插入代碼后,設置 jmp指令再將控制權轉移給原來程序的代碼。
3.3 API函數地址的獲取
插入的代碼沒有自己的導入表,那么可以在PE文件導入表中搜尋所需要的API函數。從數據目錄表中定位到文件的導入表,如果發現要使用的函數未被引入,則修改導入表來增加該函數的引入表項,并將對該API的調用指向新增加的引入函數地址。這樣在宿主程序啟動的時候,系統加載器已經把正確的API函數地址填好了,代碼即可正確地直接調用該函數。
另一種使用windows動態庫中API函數的方法是調用Kernel32.DLL動態庫中LoadLibrary和GetProcAddress。Kernel32.DLL幾乎在所有的Win32進程中都要被加載,只要獲取了它在進程中加載的基址,然后解析其導出表得到LoadLibrary和GetProcAddress。獲取Kernel32.DLL基址的方法很多,最常見的一種是搜索法。如果已知Kernel32.DLL加載的大致地址(Kernel32.dll模塊下方的某個地址),那么按照內存頁對齊的邊界一頁一頁地,由從該地址開始向低地址搜索其基址。在Win32程序執行過程中,FS段寄存器的基址總是指向進程的TEB(線程環境塊),而TEB的第一個成員指向SHE(結構化異常處理)鏈。該鏈表的節點是一個EXCEPTION_REGISTRATION結構,該結構定義如下:
struct EXCEPTION_REGISTRATION{
struct EXCEPTION_REGISTRATION*prev;
void* handler;
};
在Windows下SEH鏈表最后一個成員的handler指向Kernel32.DLL動態庫中函數UnhandledExceptionFilter的起始地址,該地址為Kernel32.DLL模塊下方的某個地址。如果成員是最后一個SEH節點,那么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動態庫被映射到每一個進程的地址空間,而且其映像基址總是通過64KB邊界來對齊的。因此將得到的Kernel32.dll模塊下方的地址按64K對齊,然后以每次一個頁的間隔在內存中尋找DOS MZ文件標識和PE文件頭標識,如果滿足標識條件的話,表示這個頁的起始地址就是Kernel32.dll的基址。然后用得到的的基地址來定位Kernel32.dll的導出表,在導出表中查找函數地址的過程:
1) 在導出表中獲取NumberOfNames的值以及數組AddressOfNames、AddressOfNameOrdinals和AddressOfFunctions的地址。
2) 字段NumberOfNames是已命名函數的總數,用這個值作為循環次數構造一個循環。
3) 在AddressOfNames數組中,用上面的循環搜索要查找的函數名,若沒有,表明文件中沒有指定的函數。
4) 若找到,獲取當前函數名字指針在AddressOfNames數組中的索引值,在AddressOfNameOrdinals數組中取出以該值索引的函數序號,以該序號值作為AddressOfFunctions數組的索引,在AddressOfFunctions數組中取出導出函數的RVA值,加上基址就得到了運行時導出函數的地址。
得到了LoadLibrary和GetProcAddress的地址之后,就可以利用這兩個函數來獲取任意動態庫中導出的任意函數的地址。
4 結束語
向PE文件中插入外部代碼的技術不僅僅對軟件安全方面的專業人士(與病毒做斗爭)有用,對加密程序和壓縮程序的開發人員也是很有用處的。插入技術的實現需要對操作系統內核及其 PE可執行文件加載機制有深入了解,因此本文限于篇幅不能詳細介紹全部的設計過程,只能從實現的機制上闡述了插入技術的基本原理、方法和應該注意的一些問題。
參考文獻:
[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].北京:電子工業出版社,2003.
[3] 段鋼.軟件加密技術內幕[M].北京:電子工業出版社,2004.
[4] 方旺盛,邵利平,張克俊.基于PE文件格式的信息隱藏技術研究[J].微計算機信息,2006,22(11):77-80.