?? 病毒編程技術(shù).txt
字號:
LPTSTR lpComment;
LPTSTR lpProvider;
} NETRESOURCE;
在解決了起始目錄的問題之后,就可以從這些起始目錄開始使用FindFirstFile和FindNextFile開始遍歷其下以及其子目錄下的所有文件和目錄了,遍歷方法可采用深度優(yōu)先或廣度優(yōu)先搜索算法,較常用的還是深度優(yōu)先算法。具體實現(xiàn)方式可采用遞歸搜索或非遞歸搜索兩種實現(xiàn)方式。遞歸搜索需要占用棧空間,有可能造成棧空間耗竭而產(chǎn)生異常,不過在現(xiàn)實應(yīng)用中這種情況很少出現(xiàn),而非遞歸搜索則不存在此問題,但代碼實現(xiàn)略復(fù)雜。在現(xiàn)實應(yīng)用中,應(yīng)用最多的還是遞歸遍歷搜索。搜索時,可指定FindFirstFile的第一形參為*.*以搜索所有文件,根據(jù)搜索結(jié)果WIN32_FIND_DATA結(jié)構(gòu)的dwFileAttributes成員判斷是否為目錄,若為目錄則需要繼續(xù)遍歷該子目錄,根據(jù)WIN32_FIND_DATA的cFileName中的文件名成員判斷是否具有要感染的文件后綴以采取修改感染動作,以下代碼實現(xiàn)了遞歸搜索某個目錄及其下所有子目錄的功能:
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);
// 病毒可根據(jù)后綴名判斷是否要感染相應(yīng)的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
SetCurrentDirectory(cdir);
}
短短20多行C代碼就實現(xiàn)了文件遍歷的功能,Win32 API的強大功能不僅為開發(fā)者提供了便利,同時也為病毒敞開了方便之門。用匯編實現(xiàn)則稍微復(fù)雜一些,感興趣的讀者可參閱Elkern中的enum_path部分,原理是一樣的,限于篇幅這里不再給出相應(yīng)的匯編代碼。
非遞歸搜索不使用堆棧存儲相關(guān)的信息,而使用顯式分配的鏈表或棧等結(jié)構(gòu)存儲相關(guān)的信息,應(yīng)用一個迭代循環(huán)完成遞歸遍歷同樣的功能,下面是使用鏈表以棧方式處理子目錄列表的一個簡單實現(xiàn):
void nr_enum_path(char *cpath){
list 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: "< //遞歸枚舉子目錄
dir_list.push_back(string(subdir));
}
}else{
printf("%s\\%s\n",cpath,wfd.cFileName);
// 病毒可根據(jù)后綴名判斷是否要感染相應(yīng)的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
}//end while
}
在以匯編語言實現(xiàn)時,需要自己管理鏈表以及分配和釋放相應(yīng)的結(jié)構(gòu),因此較為煩瑣,代碼量也稍大,因此病毒多采用遞歸的方式進(jìn)行搜索。值得注意的是搜索深層次的目錄是很費時的,因此大部分病毒為避免CPU占用率過高,搜索一定數(shù)量的文件之后,都會調(diào)用Sleep休眠一會,以避免被敏感的用戶發(fā)覺。文件搜索和感染模塊通常是以單獨的線程運行的,在病毒獲得控制權(quán)后,創(chuàng)建相應(yīng)的搜索和感染線程,而將主現(xiàn)成的控制權(quán)交給原程序。
* PE文件的修改和感染策略
既然已經(jīng)能夠搜索磁盤及網(wǎng)絡(luò)共享文件中的所有文件,要實現(xiàn)寄生,那么自然下一步就是對搜索到的PE文件進(jìn)行感染了。感染PE的很重要的一個考慮就是將病毒代碼寫入到PE文件的哪個位置。讀寫文件一般利用Win32 API CreateFile、CreateFileMapping、MapViewOfFile等API以內(nèi)存映射文件的方式進(jìn)行,這樣可以避免自己管理緩沖的麻煩,因而為較多病毒所采用。為了能夠讀寫具有只讀屬性的文件,病毒在操作前首先利用GetFileAttributes獲取其屬性并保存,然后用SetFileAttributes將文件的屬性修改為可寫,在感染完畢后再恢復(fù)其屬性值。
一般說來,有如下幾種感染PE文件的方案供選擇:
a)添加一個新的節(jié)。將病毒代碼寫入到新的節(jié)中,相應(yīng)修改節(jié)表,文件頭中文件大小等屬性值。由于在PE尾部增加了一個節(jié),因此較容易被用戶察覺。在某些情況下,由于原PE頭部沒有足夠的空間存放新增節(jié)的節(jié)表信息,因此還要對其它數(shù)據(jù)進(jìn)行搬移等操作。鑒于上述問題,PE病毒使用該方法的并不多。
b)附加在最后一個節(jié)上。修改最后一個節(jié)節(jié)表的大小和屬性以及文件頭中文件大小等屬性值。由于越來越多的殺毒軟件采用了一種尾部掃描的方式,因此很多病毒還要在病毒代碼之后附加隨機數(shù)據(jù)以逃避該種掃描。現(xiàn)代PE病毒大量使用該種方式。
c)寫入到PE文件頭部未用空間各個節(jié)所保留的空隙之中。PE頭部大小一般為1024字節(jié),有5-6個節(jié)的普通PE文件實際被占用部分一般僅為600字節(jié)左右,尚有400多個字節(jié)的剩余空間可以利用。PE文件各個節(jié)之間一般都是按照512字節(jié)對齊的,但節(jié)中的實際數(shù)據(jù)常常未完全使用全部的512字節(jié),PE文件的對齊設(shè)計本來是出于效率的考慮,但其留下的空隙卻給病毒留下了棲身之地。這種感染方式感染后原PE文件的總長度可能并不會增加,因此自CIH病毒首次使用該技術(shù)以來,備受病毒作者的青睞。
d)覆蓋某些非常用數(shù)據(jù)。如一般exe文件的重定位表,由于exe一般不需要重定位,因此可以覆蓋重定位數(shù)據(jù)而不會造成問題,為保險起見可將文件頭中指示重定位項的DataDirectory數(shù)組中的相應(yīng)項清空,這種方式一般也不會造成被感染文件長度的增加。因此很多病毒也廣泛使用該種方法。
e)壓縮某些數(shù)據(jù)或代碼以節(jié)約出存放病毒代碼的空間,然后將病毒代碼寫入這些空間,在程序代碼運行前病毒首先解壓縮相應(yīng)的數(shù)據(jù)或代碼,然后再將控制權(quán)交給原程序。該種方式一般不會增加被感染文件的大小,但需考慮的因素較多,實現(xiàn)起來難度也比較大。用的還不多。
不論何種方式,都涉及到對PE頭部相關(guān)信息以及節(jié)表的相關(guān)操作,我們首先研究一下PE的修改,即如何在添加了病毒代碼后使得PE文件仍然是合法的PE文件,仍然能夠被系統(tǒng)加載器加載執(zhí)行。
PE文件的每個節(jié)的屬性都是由節(jié)表中的一個表項描述的,節(jié)表緊跟在IMAGE_NT_HEADERS后面,因此從文件偏移0x3C處的雙字找到IMAGE_NT_HEADERS的起始偏移,再加上IMAGE_NT_HEADERS的大小(248字節(jié))就定位了節(jié)表的起始位置,每個表項是一個IMAGE_SECTION_HEADER結(jié)構(gòu):
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 節(jié)的名字
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 字節(jié)計算的實際大小
} Misc;
DWORD VirtualAddress; // 節(jié)的起始虛擬地址
DWORD SizeOfRawData; // 按照文件頭FileAlignment
// 對齊后的大小
DWORD PointerToRawData; // 文件中指向該節(jié)起始的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 節(jié)的屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
節(jié)表項的數(shù)目由IMAGE_NT_HEADERS的NumberOfSections成員確定。由節(jié)表中的起始虛擬地址以及該節(jié)在文件中的位置就可以換算加載后內(nèi)存虛擬地址和文件中地址之間的映射關(guān)系。添加一個節(jié)則需要修改該節(jié)表數(shù)組,在其中增加一個表項,然后相應(yīng)修改NumberOfSections的數(shù)目。值得注意的是,某些PE文件現(xiàn)存節(jié)表后面可能緊跟著其它數(shù)據(jù),如bound import數(shù)據(jù),這時就不能簡單地增加一個節(jié)表項,需要先移動這些數(shù)據(jù)并修改相應(yīng)的結(jié)構(gòu)后才能增加節(jié),否則PE文件將不能正常執(zhí)行。由于很多病毒是自我修改的,因此節(jié)屬性通常設(shè)置為E000XXXX,表示該節(jié)可讀寫執(zhí)行,否則就需要在病毒的開始處調(diào)用VirtualProtect之類的API動態(tài)修改內(nèi)存頁的屬性了。由上述節(jié)表的定義還可以看到每個節(jié)的實際數(shù)據(jù)都是按照文件頭中FileAlignment對齊的,這個大小一般是512,因此每個節(jié)可能有不超過512字節(jié)的未用空間(SizeOfRawData- VirtualSize),這恰好給病毒以可乘之機,著名的CIH病毒首先采用了這種技術(shù),不過問題是每個節(jié)的空隙大小是不定的,因此就需要將病毒代碼分成若干部分存放,運行時再通過一段代碼組合起來,優(yōu)點是如果病毒代碼較小則無需增加PE的大小,隱蔽性較強。如果所有節(jié)的未用空間仍不足以容納病毒代碼,則可新增節(jié)或附加到最后一個節(jié)上。附加到最后一個節(jié)上是比較簡單的,只要修改節(jié)表中最后一個節(jié)的VirtualSize以及按FileAlignment對齊后的SizeOfRawData成員即可。當(dāng)然在上述所有修改節(jié)的情況中,如果改變了文件的大小,都要修正文件頭中SizeOfImage這個值的大小,該值是所有節(jié)和頭按照SectionAlignment對齊后的大小。
這里有兩個問題值得注意,第一問題就是對WFP(Windows File Protection)文件的處理,WFP機制是從Windows 2000開始新增的保護(hù)系統(tǒng)文件的機制,若系統(tǒng)發(fā)現(xiàn)重要的系統(tǒng)文件被改變,則彈出一個對話框警告用戶該文件已被替換。當(dāng)然有多種方法繞過WFP保護(hù),但對病毒而言,更簡單的方法就是不感染在WFP列表中的系統(tǒng)文件。可使用sfc.dll的導(dǎo)出函數(shù)SfcIsFileProtected判斷一個文件是否在該列表中,該API的第一個參數(shù)必須為0,第二個參數(shù)是要判斷的文件名,若在列表中返回非0值,否則返回0。
另外一個問題就是關(guān)于PE文件的校驗。大部分PE文件都不使用文件頭中的CheckSum域的校驗和值,不過有些PE文件,如關(guān)鍵的系統(tǒng)服務(wù)程序文件以及驅(qū)動程序文件則該值必須正確,否則系統(tǒng)加載器將拒絕加載。PE頭部的CheckSum可以使用Imagehlp.dll的導(dǎo)出函數(shù)CheckSumMappedFile計算,也可以在將該域清0后按照如下簡單的等價算法計算:
如果PE文件大小是奇數(shù)字節(jié),則以0補足,使之按偶數(shù)字節(jié)。將PE文件頭的CheckSum域清0,然后以兩個字節(jié)為單位進(jìn)行adc運算,最后和將該累加和同文件實際大小進(jìn)行adc運算即得到校驗和的值。下面的cal_checksum過程假設(shè)esi已經(jīng)指向PE文件頭,文件頭部CheckSum域已經(jīng)被清0,CF標(biāo)志位已經(jīng)被復(fù)位:
;調(diào)用示例:
;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頭部的校驗和之外,很多程序自身也有校驗?zāi)K,如Winzip和Winrar的自解壓文件,如果被感染,將造成無法正常解壓縮。因此對于類似的PE文件,病毒應(yīng)盡量不予感染。
Elkern中感染文件修改文件相關(guān)的代碼在infect.asm中,該病毒首先盡可能地利用PE的頭部和節(jié)的間隙存儲自身代碼,若所有間隙仍不足以存放病毒代碼,則附加到最后一個節(jié)上,限于篇幅相關(guān)代碼從略,感興趣的讀者請自行參閱。
其實,完成了上述功能的代碼片斷就已經(jīng)是一個簡單的病毒了,不管是用匯編語言、C語言或是python語言編寫的。但這些遠(yuǎn)不是病毒技術(shù)的全部。在病毒和反病毒對抗的數(shù)十年中,伴隨著反病毒技術(shù)的進(jìn)步,病毒技術(shù)也在不斷進(jìn)步著,Win32下的內(nèi)存駐留感染技術(shù)、抗分析技術(shù)、EPO技術(shù)、多態(tài)技術(shù)、變形技術(shù)等限于篇幅都還沒有介紹,無論如何,那都是下篇的內(nèi)容了。
* 思考與防范
病毒技術(shù)源自編程實踐,但又無所不用其極,包含了相當(dāng)多的編程技巧,如果我們善于借鑒,其中的很多技巧都可用于解決常見的編程難題。此外知己知彼,才能在病毒出現(xiàn)時冷靜沉著應(yīng)對,分析其機制,找到更好的解決之道。作為用戶,了解病毒的機制對于選擇合適的反病毒產(chǎn)品和方案也是非常有幫助的。
防范病毒,從用戶角度除了使用殺毒軟件定期查毒之外,謹(jǐn)慎地下載或執(zhí)行未知的程序,提高警覺也是非常重要的。
病毒已經(jīng)不再單純是一種展示高超編程技巧的手段了,而被越來越多的領(lǐng)域賦予了其它的如經(jīng)濟、有時甚至是政治的含義。防范病毒,作為負(fù)責(zé)的程序員,應(yīng)首先不編寫病毒、傳播病毒,一切從我做起。
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -