Путеводитель по написанию вирусов под Win32: 7. Оптимизация под Win32 — Архив WASM.RU
Хмм.. Этот раздел должен писать Super, а не я, но так как я все же его ученик, я напишу здесь о том, что изучил за то время, что нахожусь в мире кодинга под Win32. В этой главе я буду писать больше о локальной оптимизации, чем о структурной, потому что последняя сильно зависит от вашего стиля (например, лично очень параноидально отношусь к вычислениям стека и дельта-оффсета, как вы можете видеть в моем коде, особенно в Win95.Garaipena). В этой статье много моих собственных идей и советов, которые Super дал мне во время валенсийских тусовок. Он, вероятно, лучший оптимизатор в VX-мире. Без шуток. Я не буду обсуждать здесь, как оптимизировать так сильно, как это делает он. Нет. Я только расскажу вам о самых очевидных оптимизациях, которые могут быть сделаны при кодинге под Win32. Я не буду комментировать совсем тривиальные методы оптимизации, которые уже были объяснены в моем путеводителе по написанию вирусов под MS-DOS.
Проверка равен ли регистр нулю
Я устал видеть одно и тоже постоянно, особенно в среде Win32-кодеров, и это меня просто убивает, очень медленно и очень болезненно. Например, мой разум не может переварить идею 'CMP EAX, 0'. Давайте посмотрим, почему:
Код (Text):
cmp eax,00000000h ; 5 байтов jz bribriblibli ; 2 байта (если jz короткий)Хех, я знаю, что жизнь - дерьмо, и вы тратите очень много кода на отстойные сравнения. Ок, давайте посмотрим, как решить эту проблему с помощью кода, который делает то же самое, но с меньшим количеством байтов.
Код (Text):
or eax,eax ; 2 байтов jz bribriblibli ; 2 байтов (если jz короткий)Или эквивалент (но быстрее!):
Код (Text):
test eax,eax ; 2 байта jz bribriblibli ; 2 байта (если jz короткий)Есть способ, как оптимизировать это еще большим образом, если неважно содержимое, которое окажется в другом регистре). Вот он:
Код (Text):
xchg eax,ecx ; 1 байт jecxz bribriblibli ; 2 байта (только если короткий)Теперь вы видите? Никаких извинений, что "я не оптимизирую, потому что теряю стабильность", так как с помощью этих советов вы не будете терять ничего, кроме байтов кода . Мы сделали процедуру на 4 байта короче (с 7 до 3)... Как? Что вы скажете об этом?
Проверка, равен ли регистр -1
Так как многие API-функции в Ring-3 возвращают вам значение -1 (OFFFFFFFh), если вызов функции не удался, и вам нужно проверять, удачно ли он прошел, вы часто должны сравнивать полученное значение с -1. Но здесь та же проблема, что и ранее - многие люди делают это с помощью 'CMP EAX, 0FFFFFFFh', хотя то же можно осуществить гораздо более оптимизированно...
Код (Text):
cmp eax,0FFFFFFFFh ; 5 байтов jz insumision ; 2 байта (если короткий)Давайте посмотрим, как это можно оптимизировать:
Код (Text):
inc eax ; 1 байт jz insumision ; 2 байта dec eax ; 1 байтХех, может быть это занимает больше строк, но зато весит меньше байтов (4 байта против 7).
Сделать регистр равным -1
Есть вещь, которую делают почти все виркодеры новой школы:
Код (Text):
mov eax,-1 ; 5 байтовВы поняли, что это худшее, что вы могли сделать? Неужели у вас только один нейрон? Проклятье, гораздо проще установить -1 более оптимизированно:
Код (Text):
xor eax,eax ; 2 байта dec eax ; 1 байтВы видите? Это не трудно!
Очищаем 32-х битный регистр и помещаем что-нибудь в LSW
Самый понятный пример - это то, что делают все вирусы, когда помещают количество секций в PE-файле в AX (так как это значение занимет 1 слово в PE-заголовке).
Код (Text):
xor eax,eax ; 2 байта mov ax,word ptr [esi+6] ; 4 байтаИли так:
Код (Text):
mov ax,word ptr [esi+6] ; 4 байта cwde ; 1 байтЯ все еще удивляюсь, почему все VX-еры используют эти "старую" формулы, особенно, когда у нас есть инструкция 386+, которая делает регистр равным нулю перед помещением слова в AX. Эта инструкция равна MOVZX.
Код (Text):
movzx eax,word ptr [esi+6] ; 4 байтаХех, мы избежали одной лишней инструкции и лишних байтов. Круто, правда?
Вызов адреса, сохраненного в переменной
Если еще одна вещь, которую делают некоторые VX-еры, и из-за которой я схожу с ума и кричу. Давайте я вам ее напомню:
Код (Text):
mov eax,dword ptr [ebp+ApiAddress] ; 6 байтов call eax ; 2 байтаМы можем вызывать адрес напрямую, ребята... Это сохраняет байты и не используется лишний регистр.
Код (Text):
call dword ptr [ebp+ApiAddress] ; 6 байтовСнова мы избавляемся от ненужной инструкции, которая занимает 2 байта, а делаем то же самое.
Веселье с push'ами
Почти то же, что и выше, но с push'ем. Давайте посмотрим, что надо и не надо делать:
Код (Text):
mov eax,dword ptr [ebp+variable] ; 6 байтов push eax ; 1 байтМы можем сделать то же самое, но на 1 байт меньше. Смотрите.
Код (Text):
push dword ptr [ebp+variable] ; 6 байтовКруто, правда? ;) Ладно, если нам нужно push'ить много раз (если значение велико, более оптимизированно, будет более оптимизированно push'ить значение 2+ раза, а если значение мало, более оптимизированно будет push'ить его, когда вам нужно сделать это 3+ раза) одну и ту же переменную, более выгодно будет поместить ее в регистр и push'ить его. Например, если нам нужно заpushить он 3 раза, более правильным будет сксорить регистр сам с собой и затем заpushить регистр. Давайте посмотрим:
Код (Text):
push 00000000h ; 2 байта push 00000000h ; 2 байта push 00000000h ; 2 байтаИ давайте посмотрим, как прооптимизировать это:
Код (Text):
xor eax,eax ; 2 bytes push eax ; 1 byte push eax ; 1 byte push eax ; 1 byteЧасто во время использования SEH нам бывает необходимо запушить fs:[0] и так далее: давайте посмотрим, как это можно оптимизировать:
Код (Text):
push dword ptr fs:[00000000h] ; 6 байтов ; 666? Хахаха! mov fs:[00000000h],esp ; 6 байтов [...] pop dword ptr fs:[00000000h] ; 6 байтовВместо это нам следует сделать следующее:
Код (Text):
xor eax,eax ; 2 байта push dword ptr fs:[eax] ; 3 байта mov fs:[eax],esp ; 3 байта [...] pop dword ptr fs:[eax] ; 3 байтаКажется, что у нас на 7 байтов меньше! Вау!!!
Получить конец ASCIIz-строки
Это очень полезно, особенно в наших поисковых системах API-функций. И, конечно, это можно сделать гораздо более оптимизированно, чем это делается обычно во многих вирусах. Давайте посмотрим:
Код (Text):
lea edi,[ebp+ASCIIz_variable] ; 6 байтов @@1: cmp byte ptr [edi],00h ; 3 байта inc edi ; 1 байт jnz @@1 ; 2 байта inc edi ; 1 байтЭтот код можно очень сильно сократить, если сделать следующим образом:
Код (Text):
lea edi,[ebp+ASCIIz_variable] ; 6 байтов xor al,al ; 2 байта @@1: scasb ; 1 байт jnz @@1 ; 2 байтаХехехе. Полезно, коротко и выглядит красиво. Что еще нужно?
Работа с умножением
Например, в коде, где ищется последняя секция, очень часто встречается следующее (в EAX находится количество секций - 1):
Код (Text):
mov ecx,28h ; 5 байтов mul ecx ; 2 байтаИ это сохраняет результат в EAX, правильно? Ладно, у нас есть гораздо более лучший путь сделать это с помощью всего лишь одной инструкции:
Код (Text):
imul eax,eax,28h ; 3 байтаIMUL сохраняет в первом регистре результат, который получился с помощью умножения второго регистра с третьим операндом, который в данном случае был непосредственным значением. Хех, мы сохранили 4 байта, заменив две инструкции на одну!
UNICODE в ASCIIz
Есть много путей сделать это. Особенно для вирусов нулевого кольца, которые имеют доступ к специальному сервису VxD. Во-первых, я объясню, как сделать оптимизацию, если используется этот сервис, а затем я покажу метод Super'а, который сохраняет огромное количество байтов. Давайте посмотрим на типичный код (предполагая, что EBP - это указатель на структуру ioreq, а EDI указывает на имя файла):
Код (Text):
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 байтаЛадно, похоже, что здесь можно зделать только одно улучшение, заменив третью линию на следующее:
Код (Text):
mov ah,1 ; 2 байтаИли так
Код (Text):
inc ah ; 2 байтаХех, но я уже сказал, что Super произвел очень сильные улучшения. я не стал копировать его, получающий указатель на юникодовое имя файла, потому что его очень трудно понять, но я уловил идею. Предполагаем, что EBP - это указатель на структуру ioreq, а buffer - это буфер длиной 100 байт. Далее идет некоторый код:
Код (Text):
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 - это указатель на заголовок последней секции):
Код (Text):
mov eax,[esi+8] ; 3 байта push eax ; 1 байт add dword ptr [esi+8],virus_size ; 7 байт pop eax ; 1 байтА теперь давайте посмотрим, как это будет с XADD:
Код (Text):
mov eax,virus_size ; 5 байтов xadd dword ptr [esi+8],eax ; 4 байтаС помощью XADD мы сохранили 3 байта ;). Между прочим, XADD - это инструкция 486+.
Установка кадров стека
Давайте посмотрим, как это выглядит неоптимизированно:
Код (Text):
push ebp ; 1 байт mov ebp,esp ; 2 байта sub esp,20h ; 3 байтаА если мы оптимизируем...
Код (Text):
enter 20h,00h ; 4 байтаИнтересно, не правда ли?
Наложение
Эта простая техника была вначале представлена Demogorgon/PS для скрытия кода. Но используя ее таким образом, который я продемонстрирую, она может помочь сэкономить немного байтов. Например, давайте представим, что есть процедура, которая устанавливаем флаг переноса, если происходит ошибка и очищает его, если таковой не произошло.
Код (Text):
noerr: clc ; 1 байт jmp exit ; 2 байта error: stc ; 1 байт exit: ret ; 1 байтНо мы можем уменьшить размер на 1 байт, если содержимое одного из 8 регистров для нас не важно (например, давайте представим, что содержимое ECX не важно):
Код (Text):
noerr: clc ; 1 байт mov cl,00h ; 1 байт \ org $-1 ; > MOV CL,0F9H error: stc ; 1 байт / ret ; 1 байтМы можем избежать CLC, внеся небольшие изменения: используя TEST (с AL, так как это более оптимизированно) очистими флаг переноса, и AL не будет модифицирован
Код (Text):
noerr: test al,00h ; 1 байт \ org $-1 ; > TEST AL,0AAH error: stc ; 1 байт / ret ; 1 байтКрасиво, правда?
Перемещение 8-битного числа в 32-х битный регистр
Почти все делают это так:
Код (Text):
mov ecx,69h ; 5 байтовЭто очень неоптимизированно... Лучше попробуйте так:
Код (Text):
xor ecx,ecx ; 2 байта mov cl,69h ; 2 байтаЕще лучше попробуйте так:
Код (Text):
push 69h ; 2 байта pop ecx ; 1 байтВсе понятно?
Очищение переменных в памяти
Это всегда полезно. Обычно люди делают так:
Код (Text):
mov dword ptr [ebp+variable],00000000h ; 10 байтов (!)Ладно, я знаю, что это дико . Вы можете выиграть 3 байта следующим образом:
Код (Text):
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'. Оптимизации, которые я вам показал, не приведут к потере стабильности. Просто попытайтесь их использовать, ок? Это очень логично, ребята. © Billy Belcebu, пер. Aquila
Путеводитель по написанию вирусов под Win32: 7. Оптимизация под Win32
Дата публикации 30 окт 2002