解析PE檔案的匯出函數表
PE檔案的函數匯出機制是進行模組間動態調用的重要機制,對於正常的程式,相關操作是由系統載入器在程式載入前自動完成的,對使用者程式是透明的。但要想在病毒代碼中實現函數地址的動態解析以取代載入器,那就有必要瞭解函數匯出表的結構了。在圖1中可以看到在PE頭結構IMAGE_OPTIONAL_HEADER32結構中包含一個DataDirectory數組結構,該結構包含16個成員,每個成員都是一個IMAGE_DATA_DIRECTORY結構:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory數組的每個結構都指向一個重要的資料結構,第一個成員指嚮導出函數表(索引0),第2個成員指向PE檔案的引入函數表(索引1)。DataDirectory中的第一個成員指嚮導出函數表的IMAGE_EXPORT_DIRECTORY結構:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
AddressOfFunctions是一個雙字數組,包含了所有匯出函數的RVA,另外兩個成員AddressOfNames也是一個雙字數組,包含了指嚮導出函數名字的字串的RVA,AddressOfNameOrdinals是一個字數組(16bit),和AddressOfNames數組是並行的,和AddressOfNames數組一起確定了相應引出函數的序號,該序號可直接用於索引AddressOfFunctions數組擷取匯出函數的地址。因此病毒搜尋指定的API就包含了如下步驟:
a)擷取NumberOfNames的值以及AddressOfNames、AddressOfNameOrdinals和AddressOfFunctions的數組的地址。
b)搜尋AddressOfNames數組,按字串對比,若找到相應的API,轉d
c)若NumberOfNames名字尚未全部搜尋完畢,轉b繼續搜尋,若搜尋完畢,則表明未找到進行錯誤處理,這一步通常可以省略,因為我們已經知道相應的DLL中肯定匯出了相應的函數。
d)擷取當前函數名字指標在AddressOfNames數組中的索引,在AddressOfNameOrdinals數組中取出以該值索引的函數序號,以該序號值作為AddressOfFunctions數組的索引,在AddressOfFunctions數組中取出匯出函數的RVA值,加上基址就得到了運行時匯出函數的地址。
看起來似乎比較羅嗦,實際上這是PE設計時為考慮靈活性而做出的犧牲。不過實現起來還是比較簡單的,通常彙編代碼編譯後不到100位元組。以下是在Kernel32搜尋GetProcAddress的完整代碼:
push esi
;esi=VA Kernel32.BASE
;edi=RVA K32.pehdr
mov ebp,esi
mov edi,[ebp+edi+peh.DataDirectory]
push edi esi
mov eax,[ebp+edi+peexc.AddressOfNames]
mov edx,[ebp+edi+peexc.AddressOfNameOrdinals]
call @F
db "GetProcAddress",0
@@:
pop edi
mov ecx,15
sub eax,4
next_:
add eax,4
add edi,ecx
sub edi,15
mov esi,[ebp+eax]
add esi,ebp
mov ecx,15
repz cmpsb ;進行字串比較,判斷是否為要尋找的函數
jnz next_
pop esi edi
sub eax,[ebp+edi+peexc.AddressOfNames]
shr eax,1
add edx,ebp
movzx eax,word [edx+eax]
add esi,[ebp+edi+peexc.AddressOfFunctions]
add ebp,[esi+eax*4] ;ebp=Kernel32.GetProcAddress.addr
;use GetProcAddress and hModule to get other func
pop esi ;esi=kernel32 Base
在前面解析匯出函數表擷取API地址的時候,採用的是直接比較字串的方法判斷是不是找到了相應的API,其實還可以計算函數名字的hash,然後同預計算的hash進行比對,現代的PE病毒更多採用的hash的方法,其原因在於一般的函數名字長度都大於4位元組,而用hash只要佔用4個位元組或2個位元組,可以節省空間的,此外還有抗病毒分析的作用,因為hash要比字串名字費解得多。hash演算法的設計只要能保證無衝突即可,可以用crc等成熟演算法,也可以設計自己的簡單演算法。在Elkern中就使用了crc16演算法。
* 檔案搜尋
檔案搜尋是病毒的重要功能模組之一,也是實現感染和傳播的關鍵。現代Windows和各種移動介質的檔案系統可能採用多種複雜格式,因此象一些Dos病毒一樣試圖直接存取檔案系統(讀寫扇區)是不大現實的。通常利用Win32 API的FindFirstFile和FindNextFile來實目前的目錄下所有目錄和檔案的搜尋,通過判斷搜尋到的檔案屬性,可區分是否為目錄或可執行檔,對於可執行檔則根據預先設計好的感染策略進行感染;對於目前的目錄下的所有子目錄以及特殊的..父目錄,可以使用遞迴或非遞迴的方式利用上述兩個API全部進行遍曆,因此從某個磁碟機或網際網路共用檔案夾的任意一個子目錄開始,都可以遍曆當前磁碟機或網際網路共用檔案夾內的所有檔案和目錄。一般地,搜尋檔案從磁碟機或共用資料夾的根目錄開始,那麼如何得到當前系統中存在的所有磁碟機或所有的共用資料夾列表呢?對於前一個問題,我們知道Windows下可劃分A:~Z:共26個邏輯盤符,因此可以從A:開始遞增搜尋所有的磁碟機,使用Win32 API GetDriveType判斷當前搜尋的盤符是否存在,以及是否是固定硬碟、抽取式存放裝置介質、是否可寫或是網路磁碟機等。一般病毒只感染固定硬碟或網路磁碟機。由於組合語言在表述演算法時顯得過於冗長,因此演算法部分使用C語言描述,當然將C演算法轉換成組合語言是很簡單的過程。
下面的代碼enumdisk.cpp將顯示A-Z各個磁碟機的相關屬性:
#include
#include
#define MAX_DRIVENAME_LENGTH 64
void __cdecl main(int argc,char *argv[])
{
char DriveName[MAX_DRIVENAME_LENGTH];
char *p;
unsigned int drv_attr;
p = DriveName;
strncpy(DriveName,"A:",MAX_DRIVENAME_LENGTH);
for(;*p<'Z';++*p) {
drv_attr = GetDriveType(p);
switch(drv_attr)
{
case DRIVE_UNKNOWN: // 未知類型
printf("drive %s type %s/n",p,"DRIVE_UNKNOWN");break;
case DRIVE_NO_ROOT_DIR: // 該磁碟機不存在
printf("drive %s type %s/n",p,"DRIVE_NO_ROOT_DIR");break;
case DRIVE_REMOVABLE: // 可移動盤,磁碟片或隨身碟或移動硬碟等
printf("drive %s type %s/n",p,"DRIVE_REMOVABLE");break;
case DRIVE_FIXED: // 固定硬碟
printf("drive %s type %s/n",p,"DRIVE_FIXED");break;
case DRIVE_REMOTE: // 一般是映射網路磁碟機
printf("drive %s type %s/n",p,"DRIVE_REMOTE");break;
case DRIVE_CDROM: // 光碟片
printf("drive %s type %s/n",p,"DRIVE_CDROM");break;
case DRIVE_RAMDISK: // RAM DISK
printf("drive %s type %s/n",p,"DRIVE_RAMDISK");break;
}
}
}
與僅僅顯示一條資訊不同的是,病毒此時將調用檔案枚舉函數(如後面給出的enum_path函數)從當前根目錄開始遍曆DRIVE_FIXED的磁碟機上的所有檔案,根據預定義策略進行檔案感染。