病毒編程技術-3

來源:互聯網
上載者:User

* API函數地址的擷取

        在能夠正確重定位之後,病毒就可以運行自己代碼了。但是這還遠遠不夠,要搜尋檔案、讀寫檔案、進行進程枚舉等操作總不能在有Win32 API的情況下自己用彙編完全重新實現一套吧,那樣的編碼量過大而且相容性很差。Win9X/NT/2000/XP/2003系統都實現了同一套在各個不同的版本上都高度相容的Win32 API,因此調用系統提供的Win32 API實現各種功能對病毒而言就是自然而然的事情了。
  所以接下來要解決的問題就是如何動態擷取Win32 API的地址。最早的PE病毒採用的是預編碼的方法,比如Windows 2000中CreateFileA的地址是0x7EE63260,那麼就在病毒代碼中使用call [7EE63260h]調用該API,但問題是不同的Windows版本之間該API的地址並不完全相同,使用該方法的病毒可能只能在Windows 2000的某個版本上運行。因此病毒作者自然而然地回到PE結構上來探求解決方案,我們知道系統載入PE檔案的時候,可以將其引入的特定DLL中函數的運行時地址填入PE的引入函數表中,那麼系統是如何為PE引入表填入正確的函數地址的呢?答案是系統解析引入DLL的匯出函數表,然後根據名字或序號搜尋到相應引出函數的的RVA(相對虛擬位址),然後再和模組在記憶體中的實際載入地址相加,就可以得到API函數的運行時真正地址。

在研究作業系統是如何?動態PE檔案連結的過程中,病毒作者找到了以下兩種解決方案:

A)在感染PE檔案的時候,可以搜尋宿主的函數引入表的相關地址,如果發現要使用的函數已經被引入,則將對該API的調用指向該引入表函數地址,若未引入,則修改引入表增加該函數的引入表項,並將對該API的調用指向新增加的引入函數地址。這樣在宿主程式啟動的時候,系統載入器已經把正確的API函數地址填好了,病毒代碼即可正確地直接調用該函數。

B)系統可以解析DLL的匯出表,自然病毒也可以通過這種手段從DLL中擷取所需要的API地址。要在運行時解析搜尋DLL的匯出表,必須首先擷取DLL在記憶體中的真實載入地址,只有這樣才能解析從PE的頭部資訊中找到匯出表的位置。應該首先解析哪個DLL呢?我們知道Kernel32.DLL幾乎在所有的Win32進程中都要被載入,其中包含了大部分常用的API,特別是其中的LoadLibrary和GetProcAddress兩個API可以擷取任意DLL中匯出的任意函數,在迄今為止的所有Windows平台上都是如此。只要擷取了Kernel32.DLL在進程中載入的基址,然後解析Kernel32.DLL的匯出表擷取常用的API地址,如需要可進一步使用Kernel32.DLL中的LoadLibrary和GetProcAddress兩個API更簡單地擷取任意其他DLL中匯出函數的地址並進行調用。

* 擷取Kernel32.DLL基址

  擷取Kernel32.DLL基址的方法很多,最常見的一種是搜尋法,如果已知Kernel32.DLL載入的大致地址,那麼可由該地址向高地址或低地址進行搜尋可以找到其基址。另外一種方法是搜尋NT PEB結構中的模組列表擷取Kernel32.DLL的準確載入基址。下面看一下具體的實現代碼:

方法1:暴力搜尋擷取Kernel32.DLL的基址

         最初的病毒是指定一個大致的載入地址,比如根據實驗在9X下其載入地址是0xBFF70000;在Windows 2000下載入基址是0x77E80000;在XP和2003下其載入基址是0x77E60000,因此在NT系統下就可以從0x77e00000開始向高地址搜尋,在9X下可以從0xBFF00000開始向高地址搜尋,如果搜尋到Kernel32.DLL的載入地址,其頭部一定是“MZ”標誌,由模組起始位移0x3C的雙字確定的PE頭部標誌必然是“PE”標誌,因此可根據這兩個標誌判斷是否找到了模組載入地址,也許有人認為該方法不可靠,因為如果恰好有某段資料符合這兩個特徵,那麼找到的基址可能就是錯誤的,但經實驗證明,該判斷方法非常可靠,基本不會出現錯誤。有一點需要注意的是,在所有版本的Windows系統下Kernel32.DLL的載入基址都是按照0x10000對齊的,根據這一特點可以不必逐位元組搜尋,按照64K對齊的邊界地址搜尋即可。
       從大致的一個地址開始搜尋Kernel32.DLL基址可能會出現讀寫到未映射記憶體地區的情況,因此需要和SEH配合使用。如果有在各個版本下準確擷取Kernel32.DLL中某地址的通用方法,那麼就可以更可靠地從該地址開始向低地址搜尋,顯然會更加通用。事實上,這種方法是存在的。在系統載入PE檔案跳轉到PE進入點第一條指令的時候,堆棧頂儲存的就是Kernel32.DLL中的某個地址,Elkern中採用的就是這種方法:
_start:
        pushfd ;If some flags,especial DF,changed,some APIs can crash down!!!
        pushad
_start_@1 equ $
        ;......
        mov ebx,[esp+9*4]   ;前面已經由pushfd和pushad壓入了9個雙字
        and ebx,0ffe00000h  ;該地址為Kernel32.dll模組下方的某個地址
                ;先減去0x100000確保該地址處於Kernel32.dll的下方
                ;向高地址搜尋如果將來Windows的發行版本中Kernel32.dll
                ;大小和代碼結構發生變化,該方法可能無效

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

         在有的病毒直接以0x7FFDE000作為TEB的指標值,其原因在於在Windows 2003 SP1、Windows XP SP2以前的NT類系統上,該值是固定的,這樣的確可以節省一兩個位元組。但是在Windows 2003 SP1、Windows XP SP2中,情況已經發生了變化,出於安全性的考慮,Windows系統開始動態映射TEB了,也就是說,指向TEB的指標值不再固定,因此這種硬式編碼方法也就走到了盡頭。此時可以按照前面的方法向低地址搜尋判斷直到找到Kernel32.dll的基址為止。Elkern中判斷是否找到了Kernel32.dll基址的相關代碼如下:
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的相關結構擷取Kernel32.DLL的基址

        前述TEB位移0x30處,亦即FS:[0x30]地址處儲存著一個重要的指標,該指標指向PEB(進程環境塊),PEB成員很多,這裡並不介紹PEB的詳細結構。我們只需要知道PEB結構的位移0xC處儲存著另外一個重要指標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      
  該結構的後三個成員是指向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指向按照初始化順序排序的第一個LDR_MODULE節點的InInitializationOrderModuleList成員的指標,在WinNT平台(不包含Win9X)下,該鏈表前端節點的LDR_MODULE結構包含的是NTDLL.DLL的相關資訊,而鏈表的下一個節點所包含的就是Kernel32.dll相關的資訊了,該節點LDR_MODULE結構中的BaseAddress不正是我們所苦苦尋找的嗎。注意InInitializationOrderModuleList是LDR_MODULE的第3個成員,因此要擷取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鏈表頭第一個LDR_MODULE節點
     InInitializationOrderModuleList成員的指標
    lodsd                 ;擷取雙向鏈表當前節點後繼的指標
  mov ebx, dword ptr [eax+08h] ;取其基地址,該結構當前包含的是
                     ;kernel32.dll相關的資訊

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

圖6 利用PEB搜尋kernel32.dll基地址的過程

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.