Billy Belceb 病毒編寫教程for Win32 ----Win32優化

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

【Win32 優化】
~~~~~~~~~~~~~
 Ehrm...Super應該做這個而不是我,因為我是他的學生,我就在這裡寫一下我在Win32程式設計世界裡所學到的東西。我將在這一章裡討論本地優化而不是結構優化,因為這個取決於於你和你的風格(例如,我個人非常熱衷於堆疊和delta offset計算,正如你在我的程式碼裡可以看到的,特別是在Win95.Garaipena裡)。這篇文章充滿了我自己的觀點和在Valencian(瓦倫西亞)會議上Super給我的建議。他可能在病毒編寫領域裡優化得最後得人了。我沒有撒謊。這裡我不討論象他那樣怎麼進行最大優化了。我只是想要使你看到在編寫Win32程式的時候一些最明顯的優化。我就不對非常明顯的優化花招註釋了,已經在我的《MS-DOS病毒編寫教程》裡解釋了。

%檢測一個暫存器是否為0%
~~~~~~~~~~~~~~~~~~~~~~~
    我很討厭看到,特別在Win32程式設計師中,這些相同的方法,這個使得我非常慢而且非常痛苦。不,不,我得大腦不能吸收CMP EAX,0的主意,例如。OK,讓我們看看為什麼:

        cmp     eax,00000000h                   ; 5 bytes
        jz      bribriblibli                    ; 2 bytes (if jz is short)

    嗨,我知道生活就是就是狗屎,而且你正在把許多程式碼浪費在一些狗屎比較上。OK,讓我們看看怎麼來解決這個問題,利用一個程式碼來做同樣的事情,但是用更少的位元組。

        or      eax,eax                         ; 2 bytes
        jz      bribriblibli                    ; 2 bytes (if jz is short)

    或者等價的(但更安全!):

        test    eax,eax                         ; 2 bytes
        jz      bribriblibli                    ; 2 bytes (if jz is short)

    而且還有一個甚至更優化的方法來做這個,如果對EAX的內容不是關心的話(在我打算放到這裡之後,EAX的內容將在ECX中完成)。下面你得到:

        xchg    eax,ecx                         ; 1 byte
        jecxz   bribriblibli                    ; 2 bytes (only if short)

    你看到了嗎?對"我不優化因為我失去了穩定性"沒有託詞,因為利用這個,你將不會失去除了程式碼的位元組數的任何東西;)嗨,我使得一個7位元組的例程減到了3位元組...嗨?對此你還有什麼好說的?哈哈哈。

%檢查一個暫存器的值是否為-1%

    因為許多Ring-3 API會返回你一個-1(0FFFFFFFFh)值,如果函式失敗的話,而且當你比較它是否失敗的時候,你必須對那個值進行比較。但是和以前一樣有同樣的問題,許多人通過使用CMP EAX,0FFFFFFFFh來做這個,而且它可以更優化...

        cmp     eax,0FFFFFFFFh                  ; 5 bytes
        jz      insumision                      ; 2 bytes (if short)

    讓我們這麼做來使它更優化:

        inc     eax                             ; 1 byte
        jz      insumision                      ; 2 bytes
        dec     eax                             ; 1 byte

    嗨,可能它佔了更多的行,但是佔了更少的位元組(4比7)。

%使得一個暫存器為-1%
~~~~~~~~~~~~~~~~~~~~
    這是一個幾乎所有的初學病毒編寫者面對的問題:

        mov     eax,-1                          ; 5 bytes

    你難道沒有意識到你的選擇很糟糕?你只要一根神經嗎?該死,用一個更優化的方法來把它置-1非常簡單:

        xor     eax,eax                         ; 2 bytes
        dec     eax                             ; 1 byte
   
    你看到了嗎?它不難!

%清除一個32bit暫存器並對它的LSW賦值%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    最明顯的例子是所有的病毒在把PE檔案的節的個數裝載到AX中(因為這個值在PE頭中佔一個word)。好了,讓我們看看大多數病毒編寫者所做的:

        xor     eax,eax                         ; 2 bytes
        mov     ax,word ptr [esi+6]             ; 4 bytes

或者這樣:


        mov     ax,word ptr [esi+6]             ; 4 bytes
        cwde                                    ; 1 byte

    我還在想為什麼所有的病毒編寫者還用這個"老"公式呢,特別地是在你有一個386+指令使得我們避免在把word放到AX中之前把暫存器清0。這個指令是MOVZX。

        movzx   eax,word ptr [esi+6]            ; 4 bytes

    嗨,我們避免了一個2位元組的指令。Cool,哈?

%呼叫一個儲存在一個變數中的地址%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    呵呵,這是一些病毒編寫者所做的另外一件事,使我快瘋了,放聲大哭。讓我提醒你記住:

        mov     eax,dword ptr [ebp+ApiAddress]  ; 6 bytes
        call    eax                             ; 2 bytes

    我們可以直接呼叫一個地址...它節約了位元組而且不用其它的任何可以用來做其它事情的暫存器。

        call    dword ptr [ebp+ApiAddress]      ; 6 bytes

    而且,我節約了一個沒有用的,不需要的佔了兩個位元組的指令,而且我們做的是完全一樣的事情。

%關於push的趣事%
~~~~~~~~~~~~~~~~
    幾乎和上面一樣,但是是push。讓我們看看什麼該做什麼不該做:

        mov     eax,dword ptr [ebp+variable]    ; 6 bytes
        push    eax                             ; 1 byte

    我們可以少用一個位元組來做這個。看:

        push    dword ptr [ebp+variable]        ; 6 bytes

    Cool,哈?;)好了,如果我們需要push很多次(如果這個值很大,如果你把那個值push 2+次就更優化,而如果這個值很小把那個值push 3+次)同樣的變數把它先放到一個暫存器中,然後push暫存器將更優化。例如,如果我們需要把0 push 3次,把一個暫存器和它本身xor,然後push這個暫存器更優化。讓我們看:

        push    00000000h                       ; 2 bytes
        push    00000000h                       ; 2 bytes
        push    00000000h                       ; 2 bytes

 讓我們看看怎麼來優化它:

        xor     eax,eax                         ; 2 bytes
        push    eax                             ; 1 byte
        push    eax                             ; 1 byte
        push    eax                             ; 1 byte

同樣的在使用SEH的時候,當我們需要push fs:[0]之類的時候。讓我們看看怎樣來優化:

        push    dword ptr fs:[00000000h]        ; 6 bytes ; 666? Mwahahahaha!
        mov     fs:[00000000h],esp              ; 6 bytes
        [...]
        pop     dword ptr fs:[00000000h]        ; 6 bytes

代之我們應該這麼做:

        xor     eax,eax                         ; 2 bytes
        push    dword ptr fs:[eax]              ; 3 bytes
        mov     fs:[eax],esp                    ; 3 bytes
        [...]
        pop     dword ptr fs:[eax]              ; 3 bytes

    呵呵,看起來有點傻,但是我們少用了7個位元組!哇!!!

%獲取一個ASCII字串的結尾%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
    這個非常有用,特別在我們的API搜尋引擎中。而且毫無疑問,它應該在所有的病毒中比傳統的方法更優化。讓我們看看:

        lea     edi,[ebp+ASCIIz_variable]       ; 6 bytes
 @@1:   cmp     byte ptr [edi],00h              ; 3 bytes
        inc     edi                             ; 1 byte
        jnz     @@1                             ; 2 bytes
        inc     edi                             ; 1 byte

 這個相同的程式碼可以非常簡化,如果你用這個方法來編寫它:

        lea     edi,[ebp+ASCIIz_variable]       ; 6 bytes
        xor     al,al                           ; 2 bytes
 @@1:   scasb                                   ; 1 byte
        jnz     @@1                             ; 2 bytes

    呵呵呵。有用,簡單,好看。你還需要什麼呢?;)

%關於乘法%
~~~~~~~~~~
    例如,當要從程式碼中得到最後一節的時候,這個程式碼大多數是這麼用的(我們在EAX中是節數-1):
       
        mov     ecx,28h                         ; 5 bytes
        mul     ecx                             ; 2 bytes

    它把結果儲存在EAX中,對嗎?好了,我們有一個好得多的方法來做這個,僅僅用一個指令:

        imul    eax,eax,28h                     ; 3 bytes

    IMUL指令把結果儲存在第一個暫存器中,這個結果是把第二個暫存器和第三個運算元相乘得到的在這裡,它是一個立即數。呵呵,我們減少了2個指令還節約了4個位元組!

%UNICODE 轉成 ASCII%
~~~~~~~~~~~~~~~~~~~~
    這裡有許多事情要做。對於Ring-0病毒特別的是,有一個VxD服務來做那個,首先我要解釋基於這個服務怎麼來做優化,最終我將給出Super的方法,那個方法節約了大量的位元組。讓我們看看經典的程式碼(假設EBP是一個指向ioreq結構的指標,而EDI指向檔名):

        xor     eax,eax                         ; 2 bytes
        push    eax                             ; 1 byte
        mov     eax,100h                        ; 5 bytes
        push    eax                             ; 1 byte
        mov     eax,[ebp+1Ch]                   ; 3 bytes
        mov     eax,[eax+0Ch]                   ; 3 bytes
        add     eax,4                           ; 3 bytes
        push    eax                             ; 1 byte
        push    edi                             ; 1 byte
@@3:    int     20h                             ; 2 bytes
        dd      00400041h                       ; 4 bytes

    特別指出的是對那個程式碼只有1個改進,把第3行替代成這樣:

        mov     ah,1                            ; 2 bytes

    或者這樣 ;)

        inc     ah                              ; 2 bytes

    呵呵,但是我要說的是Super把這個進行了最大的優化。我沒有複製他的獲取指向檔名unicode的指標的程式碼,因為,幾乎無法看懂,但是我理解了他的理念。假設EBP是指向一個ioreq結構的指標,buffer是一個100h位元組的緩衝區。下面是一些程式碼:

        mov     esi,[ebp+1Ch]                   ; 3 bytes
        mov     esi,[esi+0Ch]                   ; 3 bytes
        lea     edi,[ebp+buffer]                ; 6 bytes
 @@l:   movsb                                   ; 1 byte  目
        dec     edi                             ; 1 byte   ?This loop was 
        cmpsb                                   ; 1 byte   ?made by Super ;)
        jnz     @@l                             ; 2 bytes 餒

    呵呵,最主要的是所有例程(沒有本地優化)是26個位元組,用同樣的方法進行本地優化後是23位元組,而最後的例程,結構優化後是17個位元組。哇哈哈哈!!!

%虛擬大小(VirtualSize)計算%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
    這個標題是一個給你顯示另外一個奇怪的程式碼的理由,對於VirtualSize計算非常有用,因為我們不得不把它加上一個值,在我們加之前是獲得這個值。當然了,我將要討論的操作符是XADD。Ok,ok,讓我們看看沒有優化的VirtualSize計算(我假設ESI是一個指向最後一節的頭部的指標):

        mov     eax,[esi+8]                     ; 3 bytes
        push    eax                             ; 1 byte
        add     dword ptr [esi+8],virus_size    ; 7 bytes
        pop     eax                             ; 1 byte

    讓我們看看用XADD該是什麼樣:

        mov     eax,virus_size                  ; 5 bytes
        xadd    dword ptr [esi+8],eax           ; 4 bytes

    用XADD我們節約了3個位元組;)Btw,XADD是一個486+指令。

%設定堆疊結構%
~~~~~~~~~~~~~~

    讓我們看看沒有優化的:

        push    ebp                             ; 1 byte
        mov     ebp,esp                         ; 2 bytes
        sub     esp,20h                         ; 3 bytes

    而如果我們優化了...

        enter   20h,00h                         ; 4 bytes

    很迷人,不是嗎?;)

%重疊%
~~~~~~
    這個簡單的東西最初是由Demogorgon/PS為了隱藏程式碼而使用的。但是正如我要顯示給你看的,它可以節約一些位元組。例如,讓我們想象一個如果有一個錯誤就會設定進位標誌(carry flag)而如果沒有錯誤就清除的例程。

 noerr: clc                                     ; 1 byte
        jmp     exit                            ; 2 bytes
 error: stc                                     ; 1 byte
 exit:  ret                                     ; 1 byte

    但是如果任何8位元暫存器不重要的話(例如,讓我們假設ECX暫存器的內容不重要),我們可以減少一個位元組:


 noerr: clc                                     ; 1 byte
        mov     cl,00h                          ; 1 byte \
        org     $-1                             ;         > MOV CL,0F9H
 error: stc                                     ; 1 byte /
        ret                                     ; 1 byte

    我們可以用一個小小的改變來避免CLC:使用TEST(用AL的話,它會更加優化)來清除進位標誌,而且AL不會改變:)

 noerr: test    al,00h                          ; 1 byte \
        org     $-1                             ;         > TEST AL,0AAH
 error: stc                                     ; 1 byte /
        ret                                     ; 1 byte

    很美妙,哈?

%把一個8位元立即數賦給一個32位元暫存器%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    幾乎所有人都是這麼做的:

        mov     ecx,69h                         ; 5 bytes

    這是一個真正沒優化的東西...試試這個:

        xor     ecx,ecx                         ; 2 bytes
        mov     cl,69h                          ; 2 bytes

    試試這個甚至更好:

        push    69h                             ; 2 bytes
        pop     ecx                             ; 1 byte

    所有人都還好嗎? :)

%清除記憶體中的變數%
~~~~~~~~~~~~~~~~~~
    OK,這個總是很有用的。通常人們這麼做:

        mov     dword ptr [ebp+variable],00000000h ; 10 bytes (!)

    OK,我知道這是一件很原始的事情:)OK,用這個你將贏得3個位元組:

        and     dword ptr [ebp+variable],00000000h ; 7 bytes

    呵呵呵呵 :)

%花招和訣竅%
~~~~~~~~~~~~
    這裡我將給出一些不經典的優化訣竅,我假設你讀過這篇文章之後你就知道了這個 ;)

-不要在你的程式碼中直接使用JUMP。
-使用字串操作(MOVS, SCAS, CMPS, STOS, LODS)。
-使用LEA reg,[ebp+imm32]而不是使用MOV reg,offset imm32 / add reg,ebp。 
-使你的彙編編譯器對程式碼多掃描幾遍(在TASM中,/5就很好了)。
-使用堆疊,儘量避免使用變數。
-試圖避免使用AX,BX,CX,DX,SP,SI,DI 和 BP,因為他們多佔一個位元組。
-許多操作(特別使邏輯操作)是為EAX/AL暫存器優化的。
-如果EDX比80000000h小(也就是說沒有符號),使用CDQ來清除EDX
-使用XOR reg,reg或者SUB reg,reg來使得暫存器為0。
-使用EBP和ESP作為索引將比EDI,ESI等等多浪費1個位元組。
-對於位操作使用BT家族的指令(BT,BSR,BSF,BTR,BTF,BTS)。
-如果暫存器的順序不重要的話使用XCHG代替MOV。
-在push一個IOREQ結構的所有的值的時候,使用一個迴圈。
-儘可能地使用堆(API地址,臨時感染變數,等等)
-如果你願意,使用條件MOV(CMOVS),但是它們是586+才能用的。
-如果你知道怎麼用,使用協處理器(例如它的堆疊)。
-使用SET族的操作符。
-為了呼叫IFSMgr_Ring0_FileIO(不需要ret),使用VxDJmp而不是VxDCall。

%最後的話%
~~~~~~~~~~
    我希望你至少理解了這一章的開始幾個優化,因為它們是那些使我變瘋的一些優化。我知道我不是優化得最後得人,也不是那些人之一。對我來說,大小沒有關係。無論如何,最明顯的優化是必須要做的,至少表明你知道一些事情。更少的無用的位元組就意味著一個更好的病毒,相信我。我這裡顯示的優化不會使你的病毒失去穩定性。只要試著去使用它們,OK?它是很有邏輯性的,同志們。

相關文章