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