Путеводитель по написанию вирусов под Win32: 7. Оптимизация под Win32

Дата публикации 30 окт 2002

Путеводитель по написанию вирусов под Win32: 7. Оптимизация под Win32 — Архив WASM.RU

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

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

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

Код (Text):
  1.  
  2.         cmp     eax,00000000h                   ; 5 байтов
  3.         jz      bribriblibli                    ; 2 байта (если jz короткий)

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

Код (Text):
  1.  
  2.         or      eax,eax                         ; 2 байтов
  3.         jz      bribriblibli                    ; 2 байтов (если jz короткий)

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

Код (Text):
  1.  
  2.         test    eax,eax                         ; 2 байта
  3.         jz      bribriblibli                    ; 2 байта (если jz короткий)

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

Код (Text):
  1.  
  2.         xchg    eax,ecx                         ; 1 байт
  3.         jecxz   bribriblibli                    ; 2 байта (только если короткий)

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

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

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

Код (Text):
  1.  
  2.         cmp     eax,0FFFFFFFFh                  ; 5 байтов
  3.         jz      insumision                      ; 2 байта (если короткий)

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

Код (Text):
  1.  
  2.         inc     eax                             ; 1 байт
  3.         jz      insumision                      ; 2 байта
  4.         dec     eax                             ; 1 байт

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

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

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

Код (Text):
  1.  
  2.         mov     eax,-1                          ; 5 байтов

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

Код (Text):
  1.  
  2.         xor     eax,eax                         ; 2 байта
  3.         dec     eax                             ; 1 байт

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

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

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

Код (Text):
  1.  
  2.         xor     eax,eax                         ; 2 байта
  3.         mov     ax,word ptr [esi+6]             ; 4 байта

Или так:

Код (Text):
  1.  
  2.         mov     ax,word ptr [esi+6]             ; 4 байта
  3.         cwde                                    ; 1 байт

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

Код (Text):
  1.  
  2.         movzx   eax,word ptr [esi+6]            ; 4 байта

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

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

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

Код (Text):
  1.  
  2.         mov     eax,dword ptr [ebp+ApiAddress]  ; 6 байтов
  3.         call    eax                             ; 2 байта

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

Код (Text):
  1.  
  2.         call    dword ptr [ebp+ApiAddress]      ; 6 байтов

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

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

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

Код (Text):
  1.  
  2.         mov     eax,dword ptr [ebp+variable]    ; 6 байтов
  3.         push    eax                             ; 1 байт

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

Код (Text):
  1.  
  2.         push    dword ptr [ebp+variable]        ; 6 байтов

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

Код (Text):
  1.  
  2.         push    00000000h                       ; 2 байта
  3.         push    00000000h                       ; 2 байта
  4.         push    00000000h                       ; 2 байта

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

Код (Text):
  1.  
  2.         xor     eax,eax                         ; 2 bytes
  3.         push    eax                             ; 1 byte
  4.         push    eax                             ; 1 byte
  5.         push    eax                             ; 1 byte

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

Код (Text):
  1.  
  2.         push    dword ptr fs:[00000000h]        ; 6 байтов ; 666? Хахаха!
  3.         mov     fs:[00000000h],esp              ; 6 байтов
  4.         [...]
  5.         pop     dword ptr fs:[00000000h]        ; 6 байтов

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

Код (Text):
  1.  
  2.         xor     eax,eax                         ; 2 байта
  3.         push    dword ptr fs:[eax]              ; 3 байта
  4.         mov     fs:[eax],esp                    ; 3 байта
  5.         [...]
  6.         pop     dword ptr fs:[eax]              ; 3 байта

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

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

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

Код (Text):
  1.  
  2.         lea     edi,[ebp+ASCIIz_variable]       ; 6 байтов
  3.  @@1:   cmp     byte ptr [edi],00h              ; 3 байта
  4.         inc     edi                             ; 1 байт
  5.         jnz     @@1                             ; 2 байта
  6.         inc     edi                             ; 1 байт

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

Код (Text):
  1.  
  2.         lea     edi,[ebp+ASCIIz_variable]       ; 6 байтов
  3.         xor     al,al                           ; 2 байта
  4.  @@1:   scasb                                   ; 1 байт
  5.         jnz     @@1                             ; 2 байта

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

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

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

Код (Text):
  1.  
  2.         mov     ecx,28h                         ; 5 байтов
  3.         mul     ecx                             ; 2 байта

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

Код (Text):
  1.  
  2.         imul    eax,eax,28h                     ; 3 байта

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

UNICODE в ASCIIz

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

Код (Text):
  1.  
  2.         xor     eax,eax                         ; 2 байта
  3.         push    eax                             ; 1 байт
  4.         mov     eax,100h                        ; 5 байтов
  5.         push    eax                             ; 1 байт
  6.         mov     eax,[ebp+1Ch]                   ; 3 байта
  7.         mov     eax,[eax+0Ch]                   ; 3 байта
  8.         add     eax,4                           ; 3 байта
  9.         push    eax                             ; 1 байт
  10.         push    edi                             ; 1 байт
  11. @@3:    int     20h                             ; 2 байта
  12.         dd      00400041h                       ; 4 байта

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

Код (Text):
  1.  
  2.         mov     ah,1                            ; 2 байта

Или так :smile3:

Код (Text):
  1.  
  2.         inc     ah                              ; 2 байта

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

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

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

Вычисление VirtualSize

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

Код (Text):
  1.  
  2.         mov     eax,[esi+8]                     ; 3 байта
  3.         push    eax                             ; 1 байт
  4.         add     dword ptr [esi+8],virus_size    ; 7 байт
  5.         pop     eax                             ; 1 байт

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

Код (Text):
  1.  
  2.         mov     eax,virus_size                  ; 5 байтов
  3.         xadd    dword ptr [esi+8],eax           ; 4 байта

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

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

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

Код (Text):
  1.  
  2.         push    ebp                             ; 1 байт
  3.         mov     ebp,esp                         ; 2 байта
  4.         sub     esp,20h                         ; 3 байта

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

Код (Text):
  1.  
  2.         enter   20h,00h                         ; 4 байта

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

Наложение

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

Код (Text):
  1.  
  2.  noerr: clc                                     ; 1 байт
  3.         jmp     exit                            ; 2 байта
  4.  error: stc                                     ; 1 байт
  5.  exit:  ret                                     ; 1 байт

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

Код (Text):
  1.  
  2.  noerr: clc                                     ; 1 байт
  3.         mov     cl,00h                          ; 1 байт \
  4.         org     $-1                             ;         > MOV CL,0F9H
  5.  error: stc                                     ; 1 байт /
  6.         ret                                     ; 1 байт

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

Код (Text):
  1.  
  2.  noerr: test    al,00h                          ; 1 байт \
  3.         org     $-1                             ;         > TEST AL,0AAH
  4.  error: stc                                     ; 1 байт /
  5.         ret                                     ; 1 байт

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

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

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

Код (Text):
  1.  
  2.         mov     ecx,69h                         ; 5 байтов

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

Код (Text):
  1.  
  2.         xor     ecx,ecx                         ; 2 байта
  3.         mov     cl,69h                          ; 2 байта

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

Код (Text):
  1.  
  2.         push    69h                             ; 2 байта
  3.         pop     ecx                             ; 1 байт

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

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

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

Код (Text):
  1.  
  2.         mov     dword ptr [ebp+variable],00000000h ; 10 байтов (!)

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

Код (Text):
  1.  
  2.         and     dword ptr [ebp+variable],00000000h ; 7 байтов

Хехехе :smile3:

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

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

  • Никогда не используйте директиву 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'. Оптимизации, которые я вам показал, не приведут к потере стабильности. Просто попытайтесь их использовать, ок? Это очень логично, ребята. © Billy Belcebu, пер. Aquila


0 871
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532