29.11.2005

Борьба с EPO вирусами

image

Эта небольшая статья описывает технологии программирования так называемых EPO (Entry-Point Obscuring – неопределенная точка входа) вирусов, главным образом анализируя вирус Win32.CTX.Phage. От читателя данной статьи потребуются базовые знания в IA-32 ассемблировании, а так же знание основных элементов файловой структуры Portable Executable (PE) для более полного понимания статьи.

Эта небольшая статья описывает технологии программирования так называемых EPO (Entry-Point Obscuring – неопределенная точка входа) вирусов, главным образом анализируя вирус Win32.CTX.Phage. От читателя данной статьи потребуются базовые знания в IA-32 ассемблировании, а так же знание основных элементов файловой структуры Portable Executable (PE) для более полного понимания статьи. Автор также советует читателям просмотреть описание вируса Win32.CTX.Phage, вследствие того, что данная статья не описывает всех возможностей вируса.

Почему EPO и Win32.CTX.Phage

Вирусы с неопределенной точкой входа очень интересны, потому что вызывают наибольшие трудности с обнаружением, лечением и удалением. В наши дни техники EPO используются в различных направлениях, однако вирус Win32.CTX.Phage был выбран для этой статьи, потому что он был написан автором таких известных вирусов как Win9x.Margburg (один из первых Windows9x полиморфных вирусов, первым появившимся в каталогах) и Win9x.HPS. CTX.Phage в частности включает в себя много различных техник, что делает процесс его обнаружения и дезинфекции очень трудным, даже после того как вирус полностью распознан.

Постижение техники EPO.

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

Original EXE Infected EXE
Entry-point: 0x1000 (.code section) Entry-point: 0x6000 (.reloc section)

Такую активность вируса очень легко обнаружить, так как она происходит в файлах, у которых точка входа находится вне секции кода, таким образом, эти файлы отмечаются как подозрительные антивирусными сканнерами. Ниже приведен пример кода, который обнаруживает этот тип вирусов:

Проверка является ли секция точки входа последней:

// --- фрагмент кода сканнера ------------------------------------------------
...(snip)...
sections = pPE->FileHeader.NumberOfSections;
pSH = (PIMAGE_SECTION_HEADER)((DWORD)mymap+pMZ->e_lfanew + sizeof(IMAGE_NT_HEADERS));
    
    
    while (sections != 0) {
        if (IsBadReadPtr(&pSH,sizeof(PIMAGE_SECTION_HEADER)) == TRUE) 
              {
            printf("[-] Error: Bad PE file\n");
            goto error_mode4;
        }

        char *secname=(char *) pSH->Name;
        if (secname == NULL) strcpy(secname,"NONAME");

        startrange=(DWORD) pSH->VirtualAddress + pPE->OptionalHeader.ImageBase;
        endrange=(DWORD) startrange + pSH->Misc.VirtualSize;

        ...(snip)...

        if (pSH->VirtualAddress <= pPE->OptionalHeader.AddressOfEntryPoint && \ 
                   pPE->OptionalHeader.AddressOfEntryPoint < pSH->VirtualAddress + 
				       pSH->Misc.VirtualSize) 
                  {
            
                      printf("[+] Checking call/jump requests from %s section (EP)\n",
					      secname);
            pSHC = pSH;
        }

    
        pSH++;
        sections--;
    }

    pSH--;
    
    if (pSHC == NULL) 
       {
        printf("[-] Error: invalid entrypoint\n");
        goto error_mode4;
    }


    printf("[+] Starting heuristics scan on %s section...\n\n",pSHC->Name);

    if (pSHC == pSH) 
       {
        printf("[!] Alert: Entrypoint points to last section (%s) -> 0x%.08x\n", 
			pSH->Name,pPE->OptionalHeader.AddressOfEntryPoint + 
			    pPE->OptionalHeader.ImageBase);

        printf("[!] Alert: The file may be infected!\n");
        printf("[+] No deep-scan action was performed\n");
        goto error_mode4;
    }


...(snip)...
// --- фрагмент кода сканнера ------------------------------------------------

Главным поводом для появления и развития техники EPO послужила попытка укрыться от обнаружения антивирусным сканнером. Вирус с неопределенной точкой входа – это вирус, который не получает управление от родительской программы непосредственно. Обычно вирус искажает операции jump/call родительской программы, и получает, таким образом, контроль над ней. Существует много различных EPO техник, в этой статье мы детально рассмотрим одну из них.

Техника EPO, используемая в Win32.CTX.Phage

Программа Phage не изменяет точку входа инфицируемого файла, вместо этого она сканирует секцию кода хозяина на предмет вызовов API, генерируемых Borland- или Microsoft- линкерами. Когда соответствующий код найден, программа проверяет, чтобы адрес назначения указывал в некоторую точку секции IMPORT. Если вызов является значимым, Phage генерирует случайное число, которое указывает вирусу изменить поток, обрабатывающий инструкцию, или сканирует дальше. Ниже, рисунки 1,2,3 и 4 иллюстрируют несколько схем примеров:

Рисунок 1: Исходное приложение (Точка входа: 0x1000 Линкер – Borland)

Рисунок 2: Инфицированное приложение (Точка входа: 0x1000 Линкер – Borland).

Рисунок 3: Исходное приложение (Точка входа: 0x1039 Линкер: MICROSOFT).

Рисунок 4: Инфицированное приложение (Точка входа: 0x1039 – Линкер: MICROSOFT)

Приведенные схемы показывают, как работает CTX.Phage EPO вирус. Как упоминалось ранее, вирус вставляет случайный вызов, перезаписывая инструкцию вызова . Из-за того, что размер приложения растет (а вставленный случайный вызов колеблется относительно точки входа), становится довольно трудно определить инъекцию вируса. С другой стороны, несмотря на то, что эта ЕРО – техника увеличивает риск выполнения вируса, есть ситуации, при которых «вызов вируса» не будет выполнен вовсе.

Что же, давайте попробуем найти способ нахождения таких инъекций.

Нахождение вирусной инъекции.

Трудно ли найти инъекции CTX.Phage? Во-первых, вирус вставляет инструкцию вызова вида:

E8 ?? ?? ?? ??

CALL XXXXXXXX

Где

  • Е8 машинный код инструкции CALL
  • ?? ?? ?? ?? операнды инструкции (адресат)

Перед тем, как отправиться дальше, вспомним, что мы знаем о ЕРО:

  1. Инъекция всегда производится, где-либо непосредственно после точки входа
  2. Вставленный вызов выполняет код вируса, который всегда находится в последней секции (важно!)

Читатель, вероятно, понимает, что мы можем просто найти все байты 0xE8 (машинные коды вызовов), но при этом велика вероятность обнаружения «лже-подозрительных» вызовов, несвязанных с инструкциями вызова, например:

68 332211E8

PUSH E8112233

Как видно, это push-инструкция, но сканер найдет байт Е8 и может определить её как вызов. Если мы не хотим ваять дизассемблирующий инструмент (что является довольно трудоемким и долгим процессом), нужно придумать другой способ для решения этой задачи. И он есть! При сканировании нужно добавить условие, при котором отбирались бы вызовы (байт Е8) выполняющие код, находящийся в последней секции. Теперь все намного проще, задаем условия, которые нам требуются:

temp_loc = (DWORD)((DWORD)pSHC->VirtualAddress + i + (*(DWORD*)loc)) + 5;
if (temp_loc >= pSH->VirtualAddress && temp_loc <= pSH->VirtualAddress + pSH-
>Misc.VirtualSize) BAD_CALL = 1;

Здесь

  • temp_loc – вычисленный адресат найденного вызова (машинный код Е8)
  • pSH - заголовок последней секции
  • +5 – размер инструкции вызова (машинный код + адресат вызова)

Пример вычисления temp_loc может выглядеть так:

Scanned instruction:
00401025  \. E8 58270000    Вызов  

Calculation:
temp_loc = 1025 (Виртуальный адрес) + 00002758 (адресат вызова) + 5 (размер инструкции вызова)
Если вычисленный адрес temp_loc находится в пределах виртуального адреса начала последней

секции и адреса начала + виртуальный размер последней секции, вызов помечается как подозрительный.

Вот небольшой фрагмент из кода сканнера автора:

Поиск инструкций call и jump и проверка их адресатов

Поиск инструкций call и jump и проверка их адресатов
 
// --- фрагмент кода сканнера ------------------------------------------------
    
printf("[+] Starting from offset: 0x%.08x\n",pPE->OptionalHeader.ImageBase + 
    pSHC->VirtualAddress);

    for (i = 0; (i != pSHC->SizeOfRawData); i++) 
    {
        loc = (DWORD)((DWORD)mymap + pSHC->PointerToRawData) + i;
        
        if ((*(BYTE*)loc) == O_CALL || (*(BYTE*)loc) == O_JMP ) 
        {
            loc++;
            temp_loc = (DWORD)((DWORD)pSHC->VirtualAddress + i + (*(DWORD*)loc)) + 5;
        
            if (temp_loc >= pSH->VirtualAddress && temp_loc <= pSH->VirtualAddress + \ 
                          pSH->Misc.VirtualSize) 
            {
                printf("[!] Alert: Detected request to %s(0x%.08x) section at: 0x%.08x\n",
				    pSH->Name,pPE->OptionalHeader.ImageBase + temp_loc, \ 
                    pSHC->VirtualAddress + pPE->OptionalHeader.ImageBase + i);

                if (where_ctx == NULL) 
                {
                    where_ctx = (DWORD)(pPE->OptionalHeader.ImageBase + temp_loc);
                    caller = (DWORD)(pSHC->VirtualAddress + \ 
                                              pPE->OptionalHeader.ImageBase + i);

                    upa = (DWORD)(pSH->VirtualAddress + pPE->OptionalHeader.ImageBase);

                    sv = loc - 1;
                    
                }
                count++;
            }
            loc--;
        }

    }
    
    printf("[+] Scan finished, %d suspected instruction(s) found.\n",count);

// --- фрагмент кода сканнера ------------------------------------------------

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

Чистка кода, удаление инъекции

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

1. Инъецированный вызов передает выполнение полиморфному дешифратору, который генерирует несколько фаз дешифрации – от 4 до 7

2. Вирус должен восстановить «затертый» вызов, перед тем как вернёт выполнение хозяину. Иначе, инфицированное приложение может завершиться с ошибкой. Исходная инструкция обычно сохраняется в теле самого вируса.

Основная проблема заключается в том, что вирус зашифрован, и полиморфный дешифратор будет обрабатывать все тело вируса несколько раз. Нам нужно надлежащим образом явно получить тело вируса для восстановления исходной инструкции. Мы не можем добраться до неё напрямую (ибо тело вируса зашифровано). Есть несколько решений избегнуть фаз полиморфной дешифрации, например использование эмуляции, однако написание полноценного эмулятора – дело нелегкое, тем более что есть более оптимальное решение. Большинство Windows – вирусов используют GetProcAdress для получения нужных API адресов для их дальнейшего использования. Давайте попробуем установить breakpoint на GetProcAddress (непосредственно для избежания ложных GetProcAddress запросов. Сначала нам потребуется выудить вирусную инъекцию, что, как мы выяснили в ранее, можно легко проделать). Данные действия демонстрирует Рисунок 5

Рисунок 5 GetProcAddress.

Вызов пришел с адреса 0x406AF3, который на самом деле указывает на дешифрованное тело вируса. Действительно, полиморфные фазы обогнуты! Ниже, на Рисунке 6 приведен пример, доказывающий получение дешифрованных строк.

Рисунок 6 Дешифрованная строка

Для создания дезинфектора, способного прерываться на GetProcAddress, нам потребуется написать небольшой дебаггер. Это довольно просто, ибо Windows – платформы содержат Debug API.

В своей основе, приведенный ниже код отлаживает процесс вируса, модифицирует исходный вход GetProcAddress на 0x90 (nop), 0x90 (nop), 0xCC (int 3 – breakpoint), и принимает EXCEPTION_BREAKPOINT только если она приходит от «затертого» вызова.

(отладка процесса, обработка вызова вируса, обход GetProcAddress и получение адреса вируса):

(отладка процесса, обработка вызова вируса, обход GetProcAddress и получение адреса вируса): 

// --- фрагмент кода сканнера ------------------------------------------------
...(snip)...
unsigned char patch[4] = { 0x90, 0x90, 0xCC };
_GetProcAddress = (DWORD) GetProcAddress(LoadLibrary("KERNEL32.DLL"), "GetProcAddress");

GetStartupInfo(&si);
if (!CreateProcess(NULL,temp_name,NULL,NULL,FALSE,DEBUG_PROCESS + 
    DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, π)) 
{    
    printf("[-] Error: cannot create process, error: %d\n",GetLastError());
    goto error_di;
}

printf("\n[+] Process created, pid=0x%.08x\n",pi.dwProcessId);
printf("[+] Starting emulation engine...\n");
    
while (1) 
{
    WaitForDebugEvent(&de,INFINITE);
    if (de.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) {
        printf("[!] Error: ups process exited...\n");
        goto error_term;
    }

    if (de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) 
    {
        if (de.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
            if (de.u.Exception.dwFirstChance == TRUE) 
            {
                printf("[+] Exception occured at: 0x%.08x, passing to
				    program.\n",de.u.Exception.ExceptionRecord.ExceptionAddress);

                ContinueDebugEvent(de.dwProcessId,de.dwThreadId,\ 
                    DBG_EXCEPTION_NOT_HANDLED);
                }
                else 
                {
                    printf("[-] Hard error occured, terminating the program\n");
                    printf("[-] Disinfecting failed\n");
                    goto error_term;
                }

            }

            if (de.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT) 
            {
                if (fe == NULL) 
                {
                    fe = 1;
                    printf("[+] Reached break point at 0x%.08x\n",
					    de.u.Exception.ExceptionRecord.ExceptionAddress);

                    printf("[+] Modifing 4 bytes at host stack\n");
                    

                    tc.ContextFlags = CONTEXT_CONTROL;
                    if (!GetThreadContext(pi.hThread, &tc)) 
                    {
                        printf("[-] Failed to get thread context, error: %d\n", 
							GetLastError());
                        printf("[-] Disinfecting failed\n");
                        goto error_term;
                    }

                    ReadProcessMemory(pi.hProcess, (void*)tc.Esp, &stack_v,4,NULL);

                    if (stack_v == NULL) 
                    {
                        printf("[-] Error: reading from stack failed\n");
                        printf("[-] Disinfecting failed\n");
                        goto error_term;
                    }

                    tc.Esp = tc.Esp - 4;
                    caller += 5;

                    if (!WriteProcessMemory(pi.hProcess, (void*)tc.Esp, &caller, 4, 
						NULL)) 
                    {
                        printf("[-] Error: writing to stack failed\n");
                        printf("[-] Disinfecting failed\n");
                        goto error_term;
                    }
                    printf("[+] Stack modified, 0x%.08x added caller -> 0x%.08x\n", \ 
                        tc.Esp, caller);


                    printf("[+] Redirecting EIP to 0x%.08x...\n",where_ctx);
                    tc.Eip = where_ctx;

                    if (!SetThreadContext(pi.hThread, &tc)) 
                    {
                        
                        printf("[-] Failed to set thread context, error: %d\n", \ 
                            GetLastError());

                        printf("[-] Disinfecting failed\n");
                        goto error_term;
                    
                    }

                    VirtualProtectEx(pi.hProcess, (void*) _GetProcAddress, sizeof(patch), 
						PAGE_READWRITE, &oldp);

                    WriteProcessMemory(pi.hProcess, (void*) _GetProcAddress, &patch, 
						sizeof(patch), NULL);

                    VirtualProtectEx(pi.hProcess, (void*) _GetProcAddress, sizeof(patch), 
						oldp, &oldp);

                    printf("[+] Placed breaker at 0x%.08x\n",_GetProcAddress);

                
                    ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
                }
            
            
                if ((DWORD) de.u.Exception.ExceptionRecord.ExceptionAddress > 
                    _GetProcAddress && (DWORD) de.u.Exception.ExceptionRecord.ExceptionAddress
					    < _GetProcAddress + sizeof(patch)) 
                {

                    printf("[+] Virus reached the breaker at 0x%.08x\n", \ 
                        de.u.Exception.ExceptionRecord.ExceptionAddress);
                    

                    tc.ContextFlags = CONTEXT_CONTROL;
                    if (!GetThreadContext(pi.hThread, &tc)) 
                    {
                        printf("[-] Failed to get thread context, error: %d\n", \ 
                                                   GetLastError());

                        printf("[-] Disinfecting failed\n");
                        goto error_term;
                    }
                    
                    ReadProcessMemory(pi.hProcess, (void*)tc.Esp, &stack_v, 4, NULL);
                    printf("[+] Virus request captured from 0x%.08x\n",stack_v);
                                 ...(snip)...


        ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_EXCEPTION_NOT_HANDLED);

...(snip)...
// --- фрагмент кода сканнера ------------------------------------------------
 

Теперь, когда у нас есть «чистый »код вируса, можно попытаться выбрать исходные инструкции. Вследствие того, что CTX.Phage не меняет биты секции кода хозяина, у него есть только один способ восстановить исходную инструкцию – использовать WriteProcessMemory (на самом деле можно использовать и VirtualProtect для получения возможности записи в секцию кода хозяина, таким образом восстановить оригинальные биты, однако вирус это не использует). Далее показано прерывание на WriteProcessMemory

Рисунок 7 прерывание на WriteProcessMemory.

Можно увидеть, что BytesToWrite равен 5 и Address равен адресу, найденному сканером. Возникает только одна проблема – вызов пришел из локализованной памяти (вирус локализовал её, скопировал сам себя и продолжил выполнение оттуда). Но попробуем проверить адрес, с которого пришел вызов:

Рисунок 8 Проверка адреса, с которого пришел вызов.

Байты с пометкой «const» (возьмем к примеру адрес выделенный на рисунке):

6A 00 6A 05 E8 05 00 00 00 ?? ?? ?? ?? ?? 50

Здесь

  • 6A 00 - push 0
  • 6A 05 - push 5
  • E8 05 00 00 - call $+5
  • ?? ?? ?? ?? ?? исходные байты хозяина (wildcard)
  • 50 - push eax

Далее приведена сигнатура, способная находить исходные байты хозяина, однако, эти байты находятся в локализованной памяти. Резонен вопрос: существуют ли такие же байты где-либо внутри нешифрованного тела вируса, или другими словами, где-либо в последней секции? Сканируем:

Рисунок 9 Сканирование вируса.

Действительно, несколько байтов были найдены в «родной» локации вируса. GetProcAddress была вызвана вирусом из адреса 0x406AF3, и можно заметить исходные байты, находящиеся перед ней. Далее рассмотрим пример кода сканера, в котором производится поиск оригинальных байтов с использованием сигнатуры. То же самое можно сделать, пытаясь восстановить исходный адрес (0x406AF3) с помощью некоторого постоянного размера, но это не стоит особого внимания.

(проверка тела вируса на предмет исходных байтов с использованием сигнатуры, так же восстановление вызова способом считывание исходных байт в отображенный файл.):

 
// --- фрагмент кода сканнера ------------------------------------------------
...(snip)...
unsigned char ctx_sig[15] = { 0x6A, 0x00, 0x6A, 0x05, 0xE8, 0x05, 0x00, 0x00, 0x00, 
	0x90, 0x90, 0x90, 0x90, 0x90, 0x50 };
unsigned char ctx_fly[15];


    ReadProcessMemory(pi.hProcess, (void*)tc.Esp, &stack_v, 4, NULL);
    printf("[+] Virus request captured from 0x%.08x\n",stack_v);
    printf("[+] Scanning backwards to 0x%.08x\n",upa);
                

    while (1) 
    {
        if (!ReadProcessMemory(pi.hProcess, (void*)stack_v, &ctx_fly,
		    sizeof(ctx_sig), NULL)) break;

        if (stack_v <= upa) break;            
        found = 1;
        for (int ii=0; ii < sizeof(ctx_sig); ii++) 
        {
            if (ctx_sig[ii] != ctx_fly[ii]) 
            {
                if (ctx_sig[ii] != 0x90) 
                {
                    found = 0;
                    break;
                }
            }
        }

        if (found == 1) 
        {
            printf("[+] Orginal bytes were found at 0x%.08x\n", stack_v + 9);

            printf("[!] Repairing the broken instruction.\n");
            ReadProcessMemory(pi.hProcess, (void*)(stack_v + 9) ,(void*) sv, 5, NULL);

            printf("[!] The file was disinfected!\n");
            getch();
            goto error_term;
        }

        stack_v--;
    }

    if (found == 0) 
    {
        printf("[-] Error: no signature was found.\n");
        printf("[-] Disinfecting failed\n");
        goto error_term;
    }

...(snip)...
// --- фрагмент кода сканнера ------------------------------------------------

Ссылку на полный эвристический ЕРО сканнер вместе с дезинфектором Win32.CTX.Phage можно найти в конце статьи. Ниже показан скриншот приложения:

Рисунок 10: ЕРО сканнер.

Занавес опускаются, будем завершать.

Я надеюсь, вам понравилась эта небольшая заметка о ЕРО вирусах и техниках. Дезинфектор, обсуждаемый в статье, только исправляет вирусные инъекции, сам вирус продолжает «сидеть» в последней секции, но он вряд ли когда-нибудь будет выполнен. Тем не менее, читателю выпадает благоприятная возможность для «исправления» вируса, тем более что это не представляется невообразимо трудной задачей.

Почитать

Об авторе

Петр Баниа (Piotr Bania mailto:bania.piotr@gmail.com) независимый исследователь

ИТ безопасности/Антивирусов из Польши с более чем пятилетним опытом. Он обнаружил несколько высоко критичных уязвимостей в популярных приложениях (например, Real Player) Более подробную информацию можно найти на персональном сайте (http://pb.specialised.info/).

Код

Исходный код сканнера и дезинфектора можно найти здесь(http://www.securityfocus.com/virus/images/epos.c), если возникли какие-либо проблемы, полный исходный код, а так же предварительно скомпилированные бинарники доступны на SecurityFocus (http://www.securityfocus.com/virus/images/epos.exe) или на авторском сайте (http://pb.specialised.info/)

или введите имя

CAPTCHA