Оптимизация под Win32

Этот раздел должен писать Super, а не я, но так как я все же его ученик, я напишу здесь о том, что изучил за то время, что нахожусь в мире кодинга под Win32. В этой главе я буду писать больше о локальной оптимизации, чем о структурной, потому что последняя сильно зависит от вашего стиля (например, лично очень параноидально отношусь к вычислениям стека и дельта-оффсета, как вы можете видеть в моем коде, особенно в Win95.Garaipena). В этой статье много моих собственных идей и советов, которые Super дал мне во время валенсийских тусовок. Он, вероятно, лучший оптимизатор в VX-мире. Без шуток. Я не буду обсуждать здесь, как оптимизировать так сильно, как это делает он. Нет. Я только расскажу вам о самых очевидных оптимизациях, которые могут быть сделаны при кодинге под Win32. Я не буду комментировать совсем тривиальные методы оптимизации, которые уже были объяснены в моем путеводителе по написанию вирусов под MS-DOS.

Проверка равен ли регистр нулю

Я устал видеть одно и тоже постоянно, особенно в среде Win32-кодеров, и это меня просто убивает, очень медленно и очень болезненно. Например, мой разум не может переварить идею 'CMP EAX, 0'. Давайте посмотрим, почему:

 

        cmp     eax,00000000h                   ; 5 байтов
        jz      bribriblibli                    ; 2 байта (если jz короткий)

Хех, я знаю, что жизнь - дерьмо, и вы тратите очень много кода на отстойные сравнения. Ок, давайте посмотрим, как решить эту проблему с помощью кода, который делает то же самое, но с меньшим количеством байтов.

 

        or      eax,eax                         ; 2 байтов
        jz      bribriblibli                    ; 2 байтов (если jz короткий)

Или эквивалент (но быстрее!):

 

        test    eax,eax                         ; 2 байта
        jz      bribriblibli                    ; 2 байта (если jz короткий)

Есть способ, как оптимизировать это еще большим образом, если неважно содержимое, которое окажется в другом регистре). Вот он:

 

        xchg    eax,ecx                         ; 1 байт
        jecxz   bribriblibli                    ; 2 байта (только если короткий)

Теперь вы видите? Никаких извинений, что "я не оптимизирую, потому что теряю стабильность", так как с помощью этих советов вы не будете терять ничего, кроме байтов кода :). Мы сделали процедуру на 4 байта короче (с 7 до 3)... Как? Что вы скажете об этом?

Проверка, равен ли регистр -1

Так как многие API-функции в Ring-3 возвращают вам значение -1 (OFFFFFFFh), если вызов функции не удался, и вам нужно проверять, удачно ли он прошел, вы часто должны сравнивать полученное значение с -1. Но здесь та же проблема, что и ранее - многие люди делают это с помощью 'CMP EAX, 0FFFFFFFh', хотя то же можно осуществить гораздо более оптимизировано...

 

        cmp     eax,0FFFFFFFFh                  ; 5 байтов
        jz      insumision                      ; 2 байта (если короткий)

Давайте посмотрим, как это можно оптимизировать:

 

        inc     eax                             ; 1 байт
        jz      insumision                      ; 2 байта
        dec     eax                             ; 1 байт

Хех, может быть это занимает больше строк, но зато весит меньше байтов (4 байта против 7).

Сделать регистр равным -1

Есть вещь, которую делают почти все виркодеры новой школы:

 

        mov     eax,-1                          ; 5 байтов

Вы поняли, что это худшее, что вы могли сделать? Неужели у вас только один нейрон? Проклятье, гораздо проще установить -1 более оптимизированно:

 

        xor     eax,eax                         ; 2 байта
        dec     eax                             ; 1 байт

Вы видите? Это не трудно!

Очищаем 32-х битный регистр и помещаем что-нибудь в LSW

Самый понятный пример - это то, что делают все вирусы, когда помещают количество секций в PE-файле в AX (так как это значение занимает 1 слово в PE-заголовке).

 

        xor     eax,eax                         ; 2 байта
        mov     ax,word ptr [esi+6]             ; 4 байта

Или так:

 

        mov     ax,word ptr [esi+6]             ; 4 байта
        cwde                                    ; 1 байт

Я все еще удивляюсь, почему все VX-еры используют эти "старую" формулы, особенно, когда у нас есть инструкция 386+, которая делает регистр равным нулю перед помещением слова в AX. Эта инструкция равна MOVZX.

 

        movzx   eax,word ptr [esi+6]            ; 4 байта

Хех, мы избежали одной лишней инструкции и лишних байтов. Круто, правда?

Вызов адреса, сохраненного в переменной

Если еще одна вещь, которую делают некоторые VX-еры, и из-за которой я схожу с ума и кричу. Давайте я вам ее напомню:

 

        mov     eax,dword ptr [ebp+ApiAddress]  ; 6 байтов
        call    eax                             ; 2 байта

Мы можем вызывать адрес напрямую, ребята... Это сохраняет байты и не используется лишний регистр.

 

        call    dword ptr [ebp+ApiAddress]      ; 6 байтов

Снова мы избавляемся от ненужной инструкции, которая занимает 2 байта, а делаем то же самое.

Веселье с push'ами

Почти то же, что и выше, но с push'ем. Давайте посмотрим, что надо и не надо делать:

 

        mov     eax,dword ptr [ebp+variable]    ; 6 байтов
        push    eax                             ; 1 байт

Мы можем сделать то же самое, но на 1 байт меньше. Смотрите.

 

        push    dword ptr [ebp+variable]        ; 6 байтов

Круто, правда? ;) Ладно, если нам нужно push'ить много раз (если значение велико, более оптимизированно, будет более оптимизированно push'ить значение 2+ раза, а если значение мало, более оптимизированно будет push'ить его, когда вам нужно сделать это 3+ раза) одну и ту же переменную, более выгодно будет поместить ее в регистр и push'ить его. Например, если нам нужно заpushить он 3 раза, более правильным будет сксорить регистр сам с собой и затем заpushить регистр. Давайте посмотрим:

 

        push    00000000h                       ; 2 байта
        push    00000000h                       ; 2 байта
        push    00000000h                       ; 2 байта

И давайте посмотрим, как прооптимизировать это:

 

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

Часто во время использования SEH нам бывает необходимо запушить fs:[0] и так далее: давайте посмотрим, как это можно оптимизировать:

 

        push    dword ptr fs:[00000000h]        ; 6 байтов ; 666? Хахаха!
        mov     fs:[00000000h],esp              ; 6 байтов
        [...]
        pop     dword ptr fs:[00000000h]        ; 6 байтов

Вместо это нам следует сделать следующее:

 

        xor     eax,eax                         ; 2 байта
        push    dword ptr fs:[eax]              ; 3 байта
        mov     fs:[eax],esp                    ; 3 байта
        [...]
        pop     dword ptr fs:[eax]              ; 3 байта

Кажется, что у нас на 7 байтов меньше! Вау!!!

Получить конец ASCIIz-строки

Это очень полезно, особенно в наших поисковых системах API-функций. И, конечно, это можно сделать гораздо более оптимизировано, чем это делается обычно во многих вирусах. Давайте посмотрим:

 

        lea     edi,[ebp+ASCIIz_variable]       ; 6 байтов
 @@1:   cmp     byte ptr [edi],00h              ; 3 байта
        inc     edi                             ; 1 байт
        jnz     @@1                             ; 2 байта
        inc     edi                             ; 1 байт

Этот код можно очень сильно сократить, если сделать следующим образом:

 

        lea     edi,[ebp+ASCIIz_variable]       ; 6 байтов
        xor     al,al                           ; 2 байта
 @@1:   scasb                                   ; 1 байт
        jnz     @@1                             ; 2 байта

Хехехе. Полезно, коротко и выглядит красиво. Что еще нужно? :)

Работа с умножением

Например, в коде, где ищется последняя секция, очень часто встречается следующее (в EAX находится количество секций - 1):

 

        mov     ecx,28h                         ; 5 байтов
        mul     ecx                             ; 2 байта

И это сохраняет результат в EAX, правильно? Ладно, у нас есть гораздо более лучший путь сделать это с помощью всего лишь одной инструкции:

 

        imul    eax,eax,28h                     ; 3 байта

IMUL сохраняет в первом регистре результат, который получился с помощью умножения второго регистра с третьим операндом, который в данном случае был непосредственным значением. Хех, мы сохранили 4 байта, заменив две инструкции на одну!

UNICODE в ASCIIz

Есть много путей сделать это. Особенно для вирусов нулевого кольца, которые имеют доступ к специальному сервису VxD. Во-первых, я объясню, как сделать оптимизацию, если используется этот сервис, а затем я покажу метод Super'а, который сохраняет огромное количество байтов. Давайте посмотрим на типичный код (предполагая, что EBP - это указатель на структуру ioreq, а EDI указывает на имя файла):

 

        xor     eax,eax                         ; 2 байта 
        push    eax                             ; 1 байт
        mov     eax,100h                        ; 5 байтов
        push    eax                             ; 1 байт
        mov     eax,[ebp+1Ch]                   ; 3 байта
        mov     eax,[eax+0Ch]                   ; 3 байта
        add     eax,4                           ; 3 байта
        push    eax                             ; 1 байт
        push    edi                             ; 1 байт
@@3:    int     20h                             ; 2 байта
        dd      00400041h                       ; 4 байта

Ладно, похоже, что здесь можно сделать только одно улучшение, заменив третью линию на следующее:

 

        mov     ah,1                            ; 2 байта

Или так :)

 

        inc     ah                              ; 2 байта

Хех, но я уже сказал, что Super произвел очень сильные улучшения. я не стал копировать его, получающий указатель на юникодовое имя файла, потому что его очень трудно понять, но я уловил идею. Предполагаем, что EBP - это указатель на структуру ioreq, а buffer - это буфер длиной 100 байт. Далее идет некоторый код:

 

        mov     esi,[ebp+1Ch]                   ; 3 байт
        mov     esi,[esi+0Ch]                   ; 3 байт
        lea     edi,[ebp+buffer]                ; 6 байт
 @@l:   movsb                                   ; 1 байт -¬
        dec     edi                             ; 1 байт  ¦ Этот цикл был
        cmpsb                                   ; 1 байт  ¦ сделан Super'ом ;)
        jnz     @@l                             ; 2 байт --

Хех, первая из всех процедур (без локальной оптимизации) - 26 байтов, та же, но с локальной оптимизацией - 23 байта, а последняя процедура (со структурной оптимизацией) равна 17 байтам. Вау!!!

Вычисление VirtualSize

Это название является предлогом, чтобы показать вам другие странные опкоды, которые очень полезны для вычисления VirtualSize, так как мы должны добавить к нему значение и получить значение, которые было там до добавления. Конечно, опкод, о котором я говорю - это XADD. Ладно, ладно, давайте посмотрим неоптимизированное вычисление VirtualSize (я предполагаю, что ESI - это указатель на заголовок последней секции):

 

        mov     eax,[esi+8]                     ; 3 байта
        push    eax                             ; 1 байт
        add     dword ptr [esi+8],virus_size    ; 7 байт
        pop     eax                             ; 1 байт

А теперь давайте посмотрим, как это будет с XADD:

 

        mov     eax,virus_size                  ; 5 байтов
        xadd    dword ptr [esi+8],eax           ; 4 байта

С помощью XADD мы сохранили 3 байта ;). Между прочим, XADD - это инструкция 486+.

Установка кадров стека

Давайте посмотрим, как это выглядит неоптимизированно:

 

        push    ebp                             ; 1 байт
        mov     ebp,esp                         ; 2 байта
        sub     esp,20h                         ; 3 байта

А если мы оптимизируем...

 

        enter   20h,00h                         ; 4 байта

Интересно, не правда ли? :)

Наложение

Эта простая техника была вначале представлена Demogorgon/PS для скрытия кода. Но используя ее таким образом, который я продемонстрирую, она может помочь сэкономить немного байтов. Например, давайте представим, что есть процедура, которая устанавливаем флаг переноса, если происходит ошибка и очищает его, если таковой не произошло.

 

 noerr: clc                                     ; 1 байт
        jmp     exit                            ; 2 байта
 error: stc                                     ; 1 байт
 exit:  ret                                     ; 1 байт

Но мы можем уменьшить размер на 1 байт, если содержимое одного из 8 регистров для нас не важно (например, давайте представим, что содержимое ECX не важно):

 

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

Мы можем избежать CLC, внеся небольшие изменения: используя TEST (с AL, так как это более оптимизировано) очистим флаг переноса, и AL не будет модифицирован :)

 

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

Красиво, правда?

Перемещение 8-битного числа в 32-х битный регистр

Почти все делают это так:

 

        mov     ecx,69h                         ; 5 байтов

Это очень неоптимизированно... Лучше попробуйте так:

 

        xor     ecx,ecx                         ; 2 байта
        mov     cl,69h                          ; 2 байта

Еще лучше попробуйте так:

 

        push    69h                             ; 2 байта
        pop     ecx                             ; 1 байт

Все понятно? :)

Очищение переменных в памяти

Это всегда полезно. Обычно люди делают так:

 

        mov     dword ptr [ebp+variable],00000000h ; 10 байтов (!)

Ладно, я знаю, что это дико :). Вы можете выиграть 3 байта следующим образом:

 

        and     dword ptr [ebp+variable],00000000h ; 7 байтов

Хехехе :)

Советы и приемы

Здесь я поместил нерасклассифированные приемы оптимизирования и те, которые (как я предполагаю) вы уже знаете ;).

  • Никогда не используйте директиву JUMPS в вашем коде.
  • Используйте строковые операции (MOVS, SCAS, CMPS, STOS, LODS).
  • Используйте 'LEA reg, [ebp+imm32]' вместо 'MOV reg, offset imm32 / add reg, ebp'.
  • Пусть ваш ассемблер осуществляет несколько проходов по коду (в TASM'е /m5 будет достаточно хорошо).
  • Используйте стек и избегайте использования переменных.
  • Многие операции (особенно логические) оптимизированны для регистра EAX/AL
  • Используйте CDQ, чтобы очистить EDX, если EAX меньше 80000000h (т.е. без знака).
  • Используйте 'XOR reg,reg' или 'SUB reg,reg', чтобы сделать регистр равным нулю.
  • Использование EBP и ESP в качестве индекса тратит на 1 байт больше, чем использование EDI, ESI и т.п.
  • Для битовых операций используйте "семейство" BT (BT,BSR,BSF,BTR,BTF,BTS).
  • Используйте XCHG вместо MV, если порядок регистров не играет роли.
  • Во время push'инга все значение структуры IOREQ, используйте цикл.
  • Используйте кучу настолько, насколько это возможно (адреса API-функций, временные переменные и т.д.)
  • Если вам нравится, используйте условные MOV'ы (CMOVs), но они 586+.
  • Если вы знаете как, используйте сопроцессор (его стек, например).
  • Используйте семейство опкодов SET в качестве семафоров.
  • Используйте VxDJmp вместо VxDCall для вызова IFSMgr_Ring0_FileIO (ret не требуется).

В заключение

Я ожидаю, что вы поняли по крайней мере первые приемы оптимизации в этой главе, так как именно пренебрежение ими сводит меня с ума. Я знаю, что я далеко не лучший в оптимизировании. Для меня размер не играет роли. Как бы то ни было, очевидных оптимизаций следует придерживаться, по крайней мере, чтобы продемонстрировать, что вы знаете что-то в этой жизни. Меньше ненужных байт - это в пользу вируса, поверьте мне. И не надо приводить мне аргументов, которые приводил QuantumG в своем вирусе 'Next Step'. Оптимизации, которые я вам показал, не приведут к потере стабильности. Просто попытайтесь их использовать, ок? Это очень логично, ребята. 

  • Автор: admin
  • Комментарии: 0
  • Просмотры: 1621
0

Добавить комментарий

Вы не авторизованы и вам запрещено писать комментарии. Для расширенных возможностей зарегистрируйтесь!