?? readme.txt
字號:
在《軟件加密技術》這本書里看過PE文件各部分的詳細解釋之后,我也有了一個自己寫PE文件分析器的的想法。雖然好的分析器不在少數,但對于一堆十六進制數,有些朋友可能不明白它代表什么意思。如果在程序里就可以將這些01序列轉換成可以直接看懂得信息,那至少用戶可以省去以后去查表的麻煩。懷著這樣的想法,我仔細的研究了書中分析器PEInfo的源代碼,我發現它沒有提供信息轉換的功能。
通過研究發現,PEInfo是通過PE文件在內存中的映象來獲取文件信息的,我在想是否還有別的方法可以繞過將文件映象到內存這一步,直接讀取文件信息。這樣的方法只有直接讀取磁盤上的PE文件,在磁盤上尋找所需要的文件信息。
在這里暫且不說這樣的做法和內存映象法有什么優劣,我在此僅僅只是想找尋另一條解決問題的道路,并實現之??赐晡业姆治龊驮闯绦?,大家自然知道孰優孰劣。
為了避免引起混淆,程序中采用了與PE標準種類似變量名來定義關鍵的數據,如文件頭,可選文件頭,節表,導入表和導出表,具體名稱定義細節可以在winnt.h里查到。整個程序是以面向過程的方式寫的,適當結合了面向對象的特征。我將讀取的PE文件信息封裝在一個對象DataDump里,這樣是為了方便數據的管理和最后輸出分析報告。而對文件的分析則分別有一系列的子程序來完成?,F將子程序說明如下:
//-------------------------------------------------------------------------------------------------------------------
BOOL Is_EXE_file( ifstream& PE_file ) //判斷是否是合法的PE文件,是則返回true,否則返回false
BOOL OutReady( CHAR filename[], ofstream& fout ) //輸出準備,包括輸出流和輸出文件,是則返回true,否則返回false
VOID WriterInfo( ofstream& fout ) //輸出程序版本信息
BOOL Load_EXE_Info( ifstream& PE_file ) //讀取PE文件信息,成功返回true,否則返回false
VOID Decode_EXE_Info(CHAR filename[], BOOL IsEXE, ifstream& PE_file, ofstream& fout) //分析PE文件信息
VOID ToNumeric( LPDWORD ptr, CHAR buf[], INT start, INT size ) //將字符數組從start位開始,轉換size位為數值,放入ptr指向的DWORD類型變量中
VOID ToString( LPSTR ptr, CHAR buf[], INT start, INT size) //從字符數組從start位開始,取出其后的size位,放入一個ptr指向的的字符數組中
//-------------------------------------------------------------------------------------------------------------------
class DataDump
{
private :
IMAGE_FILE_HEADER FILE_HEADER; // IMAGE_FILE_HEADER
IMAGE_OPTIONAL_HEADER32 OPTIONAL_HEADER32; // IMAGE_OPTIONAL_HEADER32
PIMAGE_SECTION_HEADER SECTION_HEADER; // PIMAGE_SECTION_HEADER
IMAGE_IMPORT_DESCRIPTOR IMPORT_DESCRIPTOR; // IMAGE_IMPORT_DESCRIPTOR
PIMAGE_EXPORT_DIRECTORY EXPORT_DIRECTORY; // PIMAGE_EXPORT_DIRECTORY
DWORD ExVRk, ImVRk; // 輸出表和輸入表在磁盤文件的偏移和RVA的差值
public : // You can get the functions of these member functions below by their names.
DataDump();
~DataDump();
BOOL Set_FILE_HEADER( CHAR [], INT );
BOOL Set_OPTIONAL_HEADER32( CHAR [], INT );
BOOL Set_SECTION_HEADER32( CHAR [], INT );
BOOL Set_EXPORT_TABLE( CHAR [], INT );
VOID GetReady( CHAR [] );
DWORD Get_OPTIONAL_HEADER_SIZE( VOID ) const;
DWORD Get_SECTION_NUMBER( VOID ) const;
DWORD Get_EXPORT_TABLE_RAW( VOID ) const;
DWORD Get_IMPORT_TABLE_RAW( VOID ) const;
VOID Set_Export_VRk( VOID );
VOID Set_Import_VRk( VOID );
BOOL Export_Table_Existed( VOID ) const;
BOOL Import_Table_Existed( VOID ) const;
BOOL Show_FILE_HEADER( ofstream& ) const;
BOOL Show_OPTIONAL_HEADER32( ofstream& ) const;
BOOL Show_SECTION_HEADER32( ofstream& ) const;
BOOL Show_EXPORT_TABLE( ifstream&, ofstream& ) const;
BOOL Show_IMPORT_TABLE( ifstream&, ofstream& ) const;
};
DataDump pool;
//-------------------------------------------------------------------------------------------------------------------
DataDump類的實例是全局對象,這樣做是方便子程序對該對象的訪問。程序的基本思路是,在磁盤上打開PE文件,判斷其是否為合法的PE文件,否則輸出錯誤信息,退出;是則進行分析,包括讀取文件頭,可選文件頭,節表,導入表和導出表,將信息儲存在DataDump類中,最后以txt文件的形式輸出一份文件的分析報告。
程序的關鍵在于文件信息的“定位讀取”上。文件頭,可選文件頭和節表在磁盤上是順序存放的,跳過開始的PE標志段,就可以輕松找到上述幾段,而且每一部分的確切大小都在它們的相關屬性里描述了,在程序運行時可以知道的,讀取信息的工作很容易就可以完成。而輸入表和輸出表的大小是不確定的,有的時候會存在沒有輸入表或沒有輸出表的情況,再加上輸入輸出表的出現位置也不固定,這會給讀取輸入輸出表的工作帶來一些困難。
我們知道,一般在PE文件里某一項給的都是相對虛擬地址RVA,并不能直接和磁盤文件的物理地址相對應。在以內存映象為基礎的方法中,只需要取得RVA,和ImageBase作簡單的運算以后就可以定位到某一項數據在內存中的保存地址。而在以直接讀取磁盤文件的方法里,必然要涉及到RVA到真實物理地址RAW的轉換。所以對輸入輸出表的讀取的關鍵轉換到對輸入輸出表在磁盤文件上的定位了。
以下是一個通用的轉換方法及示例:
+---------+---------+---------+---------+---------+---------+
| 段名稱 虛擬地址 虛擬大小 物理地址 物理大小 標志 |
+---------+---------+---------+---------+---------+---------+
| Name VOffset VSize ROffset RSize Flags |
+---------+---------+---------+---------+---------+---------+
| .text 00001000 00000092 00000400 00000200 60000020|
| .rdata 00002000 000000F6 00000600 00000200 40000040|
| .data 00003000 0000018E 00000800 00000200 C0000040|
| .rsrc 00004000 000003A0 00000A00 00000400 C0000040|
+---------+---------+---------+---------+---------+---------+
文件虛擬偏移地址和文件物理偏移地址的轉換公式如下:
FileOffset = VA - ImageBase - VRk (VRk是文件虛擬地址和文件物理址之間的差值)
= RVA - VRk
>>>>>>>VaToFileOffset(虛擬地址轉文件偏移地址)
如VA = 00401000 (虛擬地址)
ImageBase = 00400000 (基地址)
VRk = VOffset - ROffset = 00001000 - 00000400 = C00 (得出文件虛擬地址和文件物理址之間的VRk值)
FileOffset = VA - ImageBase - VRk = 00401000 - 00400000 - C00 = 400(文件物理地址的偏移地址)
這樣看來,關鍵就在于如何求這個VRk上。其實很簡單,就用節表數據項里面的VirtualAddress減去PointerToRawData,就可以得到。VirtualAddress從字面上看,似乎是虛擬地址,但其實它也是個RVA,是相對于內存映象后的首地址的偏移,而PointerToRawData,嚴格的說,是相對與磁盤文件開始處的物理偏移,那對輸出表的VRk來說,計算公式應該是這樣:
ExVRk = SECTION_HEADER[i].VirtualAddress - SECTION_HEADER[i].PointerToRawData;
而此時,內存和磁盤文件有相同的基址,即ImBase = RawBase。輸入表的VRk也是相同的計算方法。
ImVRk = SECTION_HEADER[i].VirtualAddress - SECTION_HEADER[i].PointerToRawData;
我們首先要找到輸入輸出表所處的區段。雖然一般以.idata和.edata命名的就是輸入輸出表數據區段,但一旦更改了區段名稱,就無從查起了。在這應該把IMAGE_OPTIONAL_HEADER32中DataDirectory數組里輸入表和輸出表的VirtualAddress定位到節表中VirtualAddress劃分出來的區間里,就可以找到輸入輸出表所處的區段。代碼如下:
for ( INT i=1; i<FILE_HEADER.NumberOfSections; i++ )
{
if ( SECTION_HEADER[i].VirtualAddress>OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress )
{
ExVRk = SECTION_HEADER[i-1].VirtualAddress - SECTION_HEADER[i-1].PointerToRawData;
break;
}
}
接下來對于和輸入表相關的數據,只需要用對應項減去ImVRk就是這一項在磁盤文件里的偏移。如輸入表的OriginalFirstThunk的RVA是00318140,只要用這個值減去ImVRk,就可以得到OriginalFirstThunk在磁盤文件的偏移。So is Export Table!
其他就只剩下怎么處理讀取的數據了。我用的是C++的文件輸入流fstream,以二進制的形式讀進一批數據,通常都是以相應塊的大小讀入數據,如以sizeof(IMAGE_FILE_HEADER),然后通過 ToNumeric( LPDWORD ptr, CHAR buf[], INT start, INT size )函數將字符形式的變量轉換為數值型,有時有需要一些字符型的數據,如函數名,就要用ToString( LPSTR ptr, CHAR buf[], INT start, INT size)取出特定的某幾位字符,這些在源代碼里都可以看到。我在寫的過程中,發現C++的輸入流不是太穩定,有的時候會讀不進數據。我在每一個涉及到讀入數據的地方都加了輸入流的clear()函數,它重置了流的狀態,讓流始終處于穩定的狀態下。輸出分析報告到txt文件,我用的是C++的輸出流,為了保證輸出的穩定性,我也調用了輸出流的clear()函數。最后的報告會保存在和用戶輸入的可執行文件同名的文本文件里。
最后有一點申明,這個程序是在Visual C++ 6.0環境下編譯的,在其他的C++環境下好像不能編譯通過,因為winnt.h的版本問題,不同編譯環境,所帶的winnt.h內容不盡相同,在這些環境下編譯會出錯。而且這個程序可以在32位和64位環境下運行,但還不能分析64位的應用程序??赡苁?4位的PE32+格式和32位的PE格式不同引起的,因為我用PEid0.94和stud_PE也不能分析64位應用程序。
讀到這里,如果你看完源代碼,應該可以得到自己的結論了。哪種方法更好,一目了然的,但仔細斟酌,每種方法都有他自己的優點和弊病。但這不是我所關心的事情,關鍵是我在這過程中,更加深入理解了PE的結構,鍛煉了自己的編程能力。歡迎大家發表意見,關于程序的,關于PE的,or something else。程序寫得倉促,在代碼可讀性上敬請原諒。有什么好的建議,歡迎大家和我聯系。
E-mail : fahrenheit871116@163.com
寫完之后,就像高考結束在等待成績到來的那一段時間,放松,悠閑,別人怎么評價已不重要,盡力就好!^_^
HQ(Fahrenheit) 04CS, NJU
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -