• 标 题:Billy Belceb 病毒编写教程for Win32 ----Win32优化
  • 作 者:onlyu
  • 时 间:2004-05-28,12:35
  • 链 接:http://bbs.pediy.com

【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?它是很有逻辑性的,同志们。