在解決了起始目錄的問題之后,就可以從這些起始目錄開始使用FindFirstFile和FindNextFile開始遍歷其下以及其子目錄下的所有文件和目錄了,遍歷方法可采用深度優先或廣度優先搜索算法,較常用的還是深度優先算法。具體實現方式可采用遞歸搜索或非遞歸搜索兩種實現方式。遞歸搜索需要占用棧空間,有可能造成棧空間耗竭而產生異常,不過在現實應用中這種情況很少出現,而非遞歸搜索則不存在此問題,但代碼實現略復雜。在現實應用中,應用最多的還是遞歸遍歷搜索。搜索時,可指定FindFirstFile的第一形參為*.*以搜索所有文件,根據搜索結果WIN32_FIND_DATA結構的dwFileAttributes成員判斷是否為目錄,若為目錄則需要繼續遍歷該子目錄,根據WIN32_FIND_DATA的cFileName中的文件名成員判斷是否具有要感染的文件后綴以采取修改感染動作,以下代碼實現了遞歸搜索某個目錄及其下所有子目錄的功能:
void enum_path(char *cpath){
WIN32_FIND_DATA wfd;
HANDLE hfd;
char cdir[MAX_PATH];
char subdir[MAX_PATH];
int r;
GetCurrentDirectory(MAX_PATH,cdir);
SetCurrentDirectory(cpath);
hfd = FindFirstFile("*.*",&wfd);
if(hfd!=INVALID_HANDLE_VALUE) {
do{
if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if(wfd.cFileName[0] != '.') {
// 合成完整路徑名
sprintf(subdir,"%s\\%s",cpath,wfd.cFileName);
// 遞歸枚舉子目錄
enum_path(subdir);
}
}else{
printf("%s\\%s\n",cpath,wfd.cFileName);
// 病毒可根據后綴名判斷是
// 否要感染相應的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
SetCurrentDirectory(cdir);
}
短短20 多行C 代碼就實現了文件遍歷的功能,Win32 API的強大功能不僅為開發者提供了便利,同時也為病毒敞開了方便之門。用匯編實現則稍微復雜一些,感興趣的讀者可參閱Elkern 中的enum_path部分,原理是一樣的,限于篇幅這里不再給出相應的匯編代碼。
非遞歸搜索不使用堆棧存儲相關的信息,而使用顯式分配的鏈表或棧等結構存儲相關的信息,應用一個迭代循環完成遞歸遍歷同樣的功能,下面是使用鏈表以棧方式處理子目錄列表的一個簡單實現:
void nr_enum_path(char *cpath){
list<string> dir_list;
string cdir,subdir;
WIN32_FIND_DATA wfd;
HANDLE hfd;
int r;
dir_list.push_back(string(cpath));
while(dir_list.size()) {
cdir = dir_list.back();
dir_list.pop_back();
SetCurrentDirectory(cdir.c_str());
hfd = FindFirstFile("*.*",&wfd);
if(hfd!=INVALID_HANDLE_VALUE) {
do{
if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if(wfd.cFileName[0] != '.') {
// 合成完整路徑名
subdir=cdir+"\\"+wfd.cFileName;
cout<<"push subdir: "<<subdir<<endl;
// 遞歸枚舉子目錄
dir_list.push_back(string(subdir));
}
}else{
printf("%s\\%s\n",cpath,wfd.cFileName);
// 病毒可根據后綴名判斷
// 是否要感染相應的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
}//end while
}
?????? 在以匯編語言實現時,需要自己管理鏈表以及分配和釋放相應的結構,因此較為煩瑣,代碼量也稍大,因此病毒多采用遞歸的方式進行搜索。值得注意的是搜索深層次的目錄是很費時的,因此大部分病毒為避免CPU占用率過高,搜索一定數量的文件之后,都會調用Sleep 休眠一會,以避免被敏感的用戶發覺。文件搜索和感染模塊通常是以單獨的線程運行的,在病毒獲得控制權后,創建相應的搜索和感染線程,而將主現成的控制權交給原程序。
PE 文件的修改和感染策略
既然已經能夠搜索磁盤及網絡共享文件中的所有文件,要實現寄生,那么自然下一步就是對搜索到的PE文件進行感染了。感染PE的很重要的一個考慮就是將病毒代碼寫入到PE 文件的哪個位置。讀寫文件一般利用Win32 API CreateFile、CreateFileMapping、MapViewOfFile等API以內存映射文件的方式進行,這樣可以避免自己管理緩沖的麻煩,因而為較多病毒所采用。為了能夠讀寫具有只讀屬性的文
件,病毒在操作前首先利用GetFileAttributes 獲取其屬性并保存,然后用SetFileAttributes將文件的屬性修改為可寫,在
感染完畢后再恢復其屬性值。
一般說來,有如下幾種感染PE文件的方案供選擇:
a)添加一個新的節。將病毒代碼寫入到新的節中,相應修改節表,文件頭中文件大小等屬性值。由于在PE尾部增加了一個節,因此較容易被用戶察覺。在某些情況下,由于原PE頭部沒有足夠的空間存放新增節的節表信息,因此還要對其它數據進行搬移等操作。鑒于上述問
題,PE 病毒使用該方法的并不多。
b)附加在最后一個節上。修改最后一個節節表的大小和屬性以及文件頭中文件大小等屬性值。由于越來越多的殺毒軟件采用了一種尾部掃描的方式,因此很多病毒還要在病毒代碼之后附加隨機數據以逃避該種掃描。現代PE 病毒大量使用該種方式。
c)寫入到PE文件頭部未用空間各個節所保留的空隙之中。PE 頭部大小一般為1024 字節,有5-6 個節的普通PE文件實際被占用部分一般僅為600 字節左右,尚有400 多個字節的剩余空間可以利用。PE文件各個節之間一般都是按照512 字節對齊的,但節中的實際數據常常未完全使用全部的512字節,PE文件的對齊設計本來是出于效率的考慮,但其留下的空隙卻給病毒留下了棲身之地。這種感染方式感染后原PE 文的總長度可能并不會增加,因此自CIH 病毒首次使用該技術以來,備受病毒作者的青睞。?
d)覆蓋某些非常用數據。如一般exe文件的重定位表,由于exe一般不需要重定位,因此可以覆蓋重定位數據而不會造成問題,為保險起見可將文件頭中指示重定位項的DataDirectory 數組中的相應項清空,這種方式一般也不會造成被感染文件長度的增加。因此很多病毒也廣泛使用該種方法。
e)壓縮某些數據或代碼以節約出存放病毒代碼的空間,然后將病毒代碼寫入這些空間,在程序代碼運行前病毒首先解壓縮相應的數據或代碼,然后再將控制權交給原程序。該種方式一般不會增加被感染文件的大小,但需考慮的因素較多,實現起來難度也比較大。用的還不多。?
?????? 不論何種方式,都涉及到對PE頭部相關信息以及節表的相關操作,我們首先研究一下PE的修改,即如何在添加了病毒代碼后使得PE文件仍然是合法的PE文件,仍然能夠被系統加載器加載執行。PE文件的每個節的屬性都是由節表中的一個表項描述的,節表緊跟在IMAGE_NT_HEADERS后面,因此從文件偏移0x3C 處的雙字找到IMAGE_NT_HEADERS 的起始偏移,再加上IMAGE_NT_HEADERS的大小(248字節)就定位了節表的起始位置,每個表項是一個IMAGE_SECTION_HEADER結構:
?
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
// 節的名字
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
// 字節計算的實際大小
} Misc;
DWORD VirtualAddress;
// 節的起始虛擬地址
DWORD SizeOfRawData;
// 按照文件頭FileAlignment
// 對齊后的大小
DWORD PointerToRawData;
// 文件中指向該節起始的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
// 節的屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
節表項的數目由IMAGE_NT_HEADERS的NumberOfSections成員確定。由節表中的起始虛擬地址以及該節在文件中的位置就可以換算加載后內存虛擬地址和文件中地址之間的映射關系。添加一個節則需要修改該節表數組, 在其中增加一個表項, 然后相應修改
NumberOfSections 的數目。值得注意的是,某些PE文件現存節表后面可能緊跟著其它數據,如bound import 數據,這時就不能簡單地增加一個節表項,需要先移動這些數據并修改相應的結構后才能增加節,否則PE文件將不能正常執行。由于很多病毒是自我修改的,因此節屬性通常設置為E000XXXX,表示該節可讀寫執行,否則就需要在病毒的開始處調用VirtualProtect之類的API動態修改內存頁的屬性了。
由上述節表的定義還可以看到每個節的實際數據都是按照文件頭中FileAlignment 對齊的,這個大小一般是512,因此每個節可能有不超過512字節的未用空間(SizeOfRawData-VirtualSize),這恰好給病毒以可乘之機,著名的CIH病毒首先采用了這種技術,不過問題是每個節的空隙大小是不定的,因此就需要將病毒代碼分成若干部分存放,運行時再通過一段代碼組合起來,優點是如果病毒代碼較小則無需增加PE的大小,隱蔽性較強。如果所有節的未用空間仍不足以容納病毒代碼,則可新增節或附加到最后一個節上。
附加到最后一個節上是比較簡單的,只要修改節表中最后一個節的VirtualSize 以及按FileAlignment 對齊后的SizeOfRawData成員即可。當然在上述所有修改節的情況中,如果改變了文件的大小,都要修正文件頭中SizeOfImage這個值的大小,該值是所有節和頭按照SectionAlignment 對齊后的大小。
這里有兩個問題值得注意,第一問題就是對WFP(Windows File Protection)文件的處理,WFP機制是從Windows 2000 開始新增的保護系統文件的機制,若系統發現重要的系統文件被改變,則彈出一個對話框警告用戶該文件已被替換。當然有多種方法繞過WFP 保護,但對病毒而言,更簡單的方法就是不感染在WFP 列表中的系統文件。
可使用sfc.dll的導出函數SfcIsFileProtected判斷一個文件是否在該列表中,該API 的第一個問 匭胛?,第二個參數是要判斷的文件名,若在列表中返回非0值,否則返回0。另外一個問題就是關于PE文件的校驗。大部分PE文件都不使用文件頭中的CheckSum域的校驗和值,不過有些PE文件,如關鍵的系統服務程序文件以及驅動程序文件則該值必須正確,否則系統加載器將拒絕加載。PE 頭部的CheckSum 可以使用Imagehlp.dll的導出函數 CheckSumMappedFile計算,也可以在將該域清0后按照如下簡單的等價算法計算:
如果PE文件大小是奇數字節,則以0補足,使之按偶數字節。將PE文件頭的CheckSum 域清0,然后以兩個字節為單位進行adc運算,最后和將該累加和同文件實際大小進行adc運算即得到校驗和的值。下面的cal_checksum過程假設esi 已經指向PE文件頭,文件頭部CheckSum域已經被清0,CF 標志位已經被復位:
?
;調用示例:
;clc
;push pe_fileseize
;call cal_checksum cal_checksum:
adc bp,word [esi] ;初始esi指向文件頭,ebx 中保
存的是文件大小
inc esi
inc esi
loop cal_checksum
mov ebx,[esp+4]
adc ebp,ebx ;ebp 中存放的就是PE 的校驗和
ret 4
除了PE頭部的校驗和之外,很多程序自身也有校驗模塊,如Winzip 和Winrar 的自解壓文件,如果被感染,將造成無法正常解壓縮。因此對于類似的PE文件,病毒應盡量不予感染。
Elkern 中感染文件修改文件相關的代碼在infect.asm中,該病毒首先盡可能地利用PE 的頭部和節的間隙存儲自身代碼,若所有間隙仍不足以存放病毒代碼,則附加到最后一個節上,限于篇幅相關代碼從略,感興趣的讀者請自行參閱。
事實上,除了在上邊提到的病毒重定位、API地址的獲取、文件搜索、修改感染PE等基本技術之外,關于病毒技術還有很重要的幾個方面沒有提及:病毒的內存駐留感染技術、內核模式病毒技術、抗分析以及隱藏技術(EPO、多態和變形技術等)。
內存駐留感染是前述主動全盤搜索技術的變形,病毒代碼駐留內存被動地等待用戶事件或等待程序代碼執行到指定的路徑被喚醒以執行感染操作。內核態病毒,也稱ring0病毒,是指那些運行在ring0特權級內核模式下的病毒,這類病毒相當特殊,需要調用內核驅動接口實現感染和傳播等操作,由于NT內核的復雜性,這類病毒非常難于編寫,另外由于不同版本的NT系統之間內核的差異,欲令病毒穩定運行編寫者需要付出額外的努力,最后的結果可能還是會由于測試的不充分而很快因藍屏事故而被發現,這將嚴重影響病毒的傳播速度和傳播范圍。ring0 病毒比較少,其中最著名的ring0 病毒當非CIH莫屬了,但由于其巨大的破壞性,格外引人注目。由于ring0病毒數量不多而且非常復
雜,本文篇幅所限,不做深入介紹。對抗殺毒軟件、抗分析以及病毒自身的隱藏技術可以說是病毒技術近年來除利用社會工程學借助網絡快速傳播之外的又一個重要發展方向,其目的在于對抗或逃避殺毒軟件的掃描,最大限度地延長其生存期,主要包括EPO(入口點模糊)技術、加密技術、多態和變形技術等。作者將在后續的文章中陸續向讀者進行介紹。??
內存駐留感染技術
如果讀者曾經使用過MS-DOS的話,對駐留內存、截獲中斷以執行特定操作的程序(TSR)一定不會陌生。在MS-DOS時代,不僅正常的應用程序大量使用TSR技術,病毒同樣也利用TSR 技術駐留內存,監視文件讀寫操作并伺機進行感染。在Windows NT下,各個進程的地址空間被隔離了,不同進程之間不能自由地相互訪問內存,而且對于用戶態代碼有了訪問限制:ring3程序代碼只能讀寫其進程空間中應用專屬的部分(在進程空間為4GB 的情況下,通常是低2GB),對系統內核部分占用的空間是沒有讀寫權限的。這使得內存駐留感染變得困難,不過類似的想法和技術仍然是可能實現的,需要做的不過是一點變通:既然每個進程有其專屬的進程空間,盡管不能做到永久駐留,但病毒代碼至少在進程的生命期內仍然是可以駐留的;既然Windows下仍然有作用和DOS下中斷相同的API,那么病毒自然可以截獲API,從而監視文件讀寫,伺機進行感染。
截獲API 的技術通常被稱為Hook 技術,實現起來也比較簡單:修改API 的入口點代碼,將其修改為指向病毒代碼的跳轉指令,在病毒代碼開始處保存傳遞給API的參數,待病毒代碼執行完畢后再恢復API 的入口代碼和保存的參數,重新跳轉到API 的入口使得程序繼續執行。HOOK API還有其它的幾種變形,比如修改Import 表的API地址指針;修改調用API點的CALL指令;或者是為了防止API Hook檢查修改API 函數的尾部或中間部分的某條指令而獲取控制權,思路都是類似的。
既然前面介紹了全盤搜索感染技術,那么為什么病毒還大量使用內存駐留感染技術呢?
讓我們回頭思考一下,隨著存儲媒質價格的降低,用戶配備的可存儲媒質的容量也越來越大,但是磁盤讀寫速度并沒有大幅提高,普通的全磁盤搜索是很耗時的。試想一下用戶在雙擊了某個程序后,10分鐘之后才出現界面的情況,即使是初次使用計算機的用戶也會暗生疑竇,這對于病毒的隱藏和傳播是極端不利的。因此那些流行的病毒并不在獲得控制權后直接進行全盤搜索感染,而是采用了如下的變通:
A) 僅搜索當前目錄并對其中的PE 文件進行感染,然后迅速將控制權交給宿主程序。
B) 創建單獨執行的線程后,馬上把控制權交給宿主程序。在用戶進行操作的同時進行當前目錄或全盤的搜索感染。
C) 采用Hook 技術監視文件或目錄相關的API操作,對用戶操作的文件或其所在的目錄下的可執行文件進行感染。
搜索當前目錄的技術已經在前面進行了討論。下面主要討論B和C中涉及到線程技術和內存駐留Hook感染技術。
1.創建獨立的線程
如果讀者熟悉Win32 API 程序設計,對CreateThread這個API一定不會陌生。每個線程都是一個獨立的執行單元,也是Windows內核進行調度以及時間片分配的最基本的單位,同一進程內所有線程共享同一地址空間內的資源。病毒可將真正的功能部分放到該線程部分去執行。病毒代碼可以在宿主程序運行的同時得以執行,普通用戶通常難以察覺。
2.內存駐留Hook感染技術
上面提到的線程模型還是有缺陷的,試想一下,如果宿主程序一執行馬上就退出了怎么辦呢?病毒的感染線程可能還沒有開始運行呢。值得注意的是:Win32線程模型中如果一個線程執行了ExitProcess調用,其它正在運行的線程不會自動得到通知。病毒代碼可能還尚未執行完成就退出了,試想一下感染線程在感染某個PE文件時寫入自身代碼時只寫入了一半的情況。盡管類似的情況在現實中極少發生,但要想提高病毒的傳染能力和隱蔽性這卻又是必須要考慮的問題。此外,用單獨線程進行全盤搜索和感染,仍然會造成計算機CPU 資源被大量占用,用戶可能會注意到程序運行緩慢、硬盤指示燈狂閃等現象。當然,可以采用感染少量文件后休眠一會繼續感染或者僅在計算機空閑時進行感染來解決。
讓我們換個角度重新思考一下:真的有必要搜索全盤然后感染所有的PE 文件嗎?或者,全盤搜索感染模型效率高嗎?事實上,用戶計算機硬盤上的PE 文件有很大一部分是很少有機會執行的,除系統程序外,經常運行的程序只有很少的幾個:可能是幾個游戲程序,也可能是用戶的業務程序。如果占用資源去感染那些很少有機會被執行的PE 文件,除演示病毒的感染性的概念之外意義是不大的。相反,如果優先感染那些用戶經常執行的程序,即使每次只感染少量的文件,病毒的傳播速度也會得到大幅提升。鑒于此,現代病毒作者經常使用一條簡單的啟發式規則:優先感染那些經常被用戶或用戶程序訪問的PE 文件或經常訪問的文件夾下的PE 文件。這樣病毒就從被感染文件開始,逐漸擴散感染與該程序相關或在邏輯位置(目錄層次)比較接近的PE文件,最后直至整個磁盤上的文件都被感染。
無論是想要在進程結束時得到通知,還是實現上述的文件感染模型都可借助于Hook 技術來解決。通過Hook 文件或目錄操作相關的API,病毒就能獲取正在操作的PE文件路徑或目錄,從而對正在操作的PE文件或正在操作的目錄下的PE文件進行感染。有關文件或目錄操作的相關API包括:
CopyFile CopyFileEx CreateFile FindFirstFile FindFirstFileEx
FindNextFile GetCurrentDirectory SetCurrentDirectory GetFileAttributes
SetFileAttributes GetFileSize GetFileType GetFullPathName LockFile
LockFileEx MoveFile MoveFileEx SearchPath UnlockFile UnlockFileEx
如果想在進程或線程結束時獲得通知或進行感染操作可Hook 如下API:
ExitProcess TerminateProcess ExitThread TerminateThread
如果不考慮9X 系統,Hook API 還可以考慮Hook Native API。Windows系統內核代碼運行在CPU 保護模式下的ring0特權級,普通應用程序運行在ring3特權級,普通應用程序執行IO 操作、訪問內存資源等都受到嚴格的限制,加上Windows NT系統嚴格的用戶權限審查機制,不同用戶對資源的訪問權限不同,使得病毒在運行時要考慮的因素越來越多。但反觀ring0 特權級的程序,執行時沒有任何限制,因此ring0 病毒對病毒編寫者有著獨特的吸引力。Windows 9X 下普通的Ring3應用程序切入ring0 模式非常容易,但在Windows NT系統下則困難得多,但也不是不可能的:最通用的方法就是采用內核模式驅動的加載機制將病毒自身代碼寫入驅動文件然后加載;另外的一些技巧還包括感染
Windows 內核驅動文件或修改NTLDR 的IDT、GDT表項的訪問限制位使得在ring3 可以訪問從而使得病毒代碼切入ring0;除此之外還可以利用\Device\PhysicalMemory對象的漏洞切入ring0。
最成功的ring0 病毒就是大名鼎鼎的CIH,不過該病毒只能運行在Win9X 系統下。但在Windows NT系統下,盡管仍然可能切入ring0,但很少有大規模流行的ring0 病毒,這是由于NT系統內核處理流程非常復雜,編寫一個在各個版本的NT系統上都能穩定運行的ring0病毒難度不小,需要更加高超的編程技巧并經過大量的測試。因此ring0病毒數量較少,鑒于此本文將不再作深入介紹。 病毒抗分析技術
當今反病毒已經成為一個產業,病毒在被發現之后,為數眾多的殺毒廠商會迅速分析該病毒并升級其病毒庫或推出專殺工具,頗有“老鼠過街,人人喊打”之勢。但這一切都是建立在對病毒代碼和病毒行為分析的基礎上的。
從病毒的角度來講,對抗病毒分析和動態靜態查殺還是有意義的,盡管再狡猾的狐貍也難逃有經驗獵手的追捕,但抗分析和查殺技術至少可以在一定程度上延緩反病毒廠商推出殺毒方案的時間,從而延長病毒的生命周期、擴大傳播范圍。