Хмм.. Этот раздел должен писать 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 байтов
Хехехе :)
Советы и приемы
Здесь я поместил нерасклассифированные примемы оптимизирования и те, которые (как я предполагаю) вы уже знаете ;).
В заключение
Я ожидаю, что вы поняли по крайней мере первые приемы оптимизации в этой главе, так как именно пренебрежение ими сводит меня с ума. Я знаю, что я далеко не лучший в оптимизировании. Для меня размер не играет роли. Как бы то ни было, очевидных оптимзаций следует придерживаться, по крайней мере, чтобы продемонстрировать, что вы знаете что-то в этой жизни. Меньше ненужных байт - это в пользу вируса, поверьте мне. И не надо приводить мне аргументов, которые приводил QuantumG в своем вирусе 'Next Step'. Оптимизации, которые я вам показал, не приведут к потере стабильности. Просто попытайтесь их использовать, ок? Это очень логично, ребята.
[C] Billy Belcebu, пер. Aquila