病毒編程技術-5

來源:互聯網
上載者:User

網際網路共用資源也是按樹狀組織的,非分葉節點稱為容器(container),對容器需要進一步搜尋直到到達葉子節點為止,葉子節點才是共用資源的根路徑。共用資源一般分成兩種:共用列印裝置和共用資料夾。對於網際網路共用檔案的搜尋,採用WNetOpenEnum和WNetEnumResource(由mpr.dll匯出)進行遞迴枚舉。其函數原型及參數含義請參閱MSDN,使用如下代碼enumshare.cpp將顯示所有的網路磁碟機共用資料夾的路徑:
  
  #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,    // 範圍:所有網路資源
                            RESOURCETYPE_DISK,// 類型:僅枚舉可儲存介質
                            RESOURCEUSAGE_ALL,// 使用狀態:所有
                            lpnr,             // 初次調用時為NULL
                            &hEnum);          // 成功後返回的網路資源控制代碼
  
      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;
  }

  遍曆開始時WNetOpenEnum第4形參為0,在發現共用容器進行遞迴調用時候,該參數將為共用容器的NETRESOURCE結構指標。從NETRESOURCE結構中可以找到我們感興趣的lpRemoteName,該指標不為0則表示是有效共用容器或共用資料夾。

typedef struct _NETRESOURCE {
  DWORD dwScope;
  DWORD dwType;
  DWORD dwDisplayType;
  DWORD dwUsage;
  LPTSTR lpLocalName;
  LPTSTR lpRemoteName;
  LPTSTR lpComment;
  LPTSTR lpProvider;
} NETRESOURCE;

        在解決了起始目錄的問題之後,就可以從這些起始目錄開始使用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部分,原理是一樣的,限於篇幅這裡不再給出相應的彙編代碼。
 非遞迴搜尋不使用堆棧儲存相關的資訊,而使用顯式分配的鏈表或棧等結構儲存相關的資訊,應用一個迭代迴圈完成遞迴遍曆同樣的功能,下面是使用鏈表以棧方式處理子目錄列表的一個簡單實現:

在以組合語言實現時,需要自己管理鏈表以及分配和釋放相應的結構,因此較為煩瑣,代碼量也稍大,因此病毒多採用遞迴的方式進行搜尋。值得注意的是搜尋深層次的目錄是很費時的,因此大部分病毒為避免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值,否則返回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的頭部和節的間隙儲存自身代碼,若所有間隙仍不足以存放病毒代碼,則附加到最後一個節上,限於篇幅相關代碼從略,感興趣的讀者請自行參閱。
         其實,完成了上述功能的代碼片斷就已經是一個簡單的病毒了,不管是用組合語言、C語言或是python語言編寫的。但這些遠不是病毒技術的全部。在病毒和反病毒對抗的數十年中,伴隨著反病毒技術的進步,病毒技術也在不斷進步著,Win32下的記憶體駐留感染技術、抗分析技術、EPO技術、多態技術、變形技術等限於篇幅都還沒有介紹,無論如何,那都是下篇的內容了。
* 思考與防範
  病毒技術源自編程實踐,但又無所不用其極,包含了相當多的編程技巧,如果我們善於借鑒,其中的很多技巧都可用於解決常見的編程難題。此外知己知彼,才能在病毒出現時冷靜沉著應對,分析其機制,找到更好的解決之道。作為使用者,瞭解病毒的機制對於選擇合適的反病毒產品和方案也是非常有協助的。
  防範病毒,從使用者角度除了使用殺毒軟體定期查毒之外,謹慎地下載或執行未知的程式,提高警覺也是非常重要的。
  病毒已經不再單純是一種展示高超編程技巧的手段了,而被越來越多的領域賦予了其它的如經濟、有時甚至是政治的含義。防範病毒,作為負責的程式員,應首先不編寫病毒、傳播病毒,一切從我做起。

* 參考文獻
[1] The PE file format ,LUEVELSMEYER
[2] Microsoft Portable Executable and Common Object File Format Specification ,Microsoft Corp.
[3] An In-Depth Look into the Win32 Portable Executable File Format , Matt Pietrek
[4] 29A issue7

作者簡介
溫玉傑,男,現從事網路安全工作。主要研究領域為惡意代碼、逆向工程、人工智慧、編譯理論、底層安全技術等。曾與羅雲彬合譯《intel組合語言程式設計》,與人合著《軟體加密技術內幕》等。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.