poniedziałek, 15 sierpnia 2016

Obfuskacja funkcji API czyli jak utrudnić analizę złośliwego oprogramowania – część 1

Jedną z podstawowych technik przy analizie złośliwego oprogramowania jest monitorowanie i rejestrowanie odwołań do funkcji API bibliotek systemu operacyjnego. Oprócz informacji o nazwie funkcji często rejestrowane są też argumenty przekazywane do funkcji. Jest to bardzo przydatne do zrozumienia działania złośliwego kodu.
Programy antywirusowe, framework-i (np. Detours) czy debugger-y mogą to zadanie realizować na kilka sposobów. Jedną z nich jest inline API hooking. Ta technika stosowana jest często przez dwa pierwsze typy narzędzi a breakpoint-y stosowane są przy manualnej analizie z użyciem narzędzi typu debugger.
Część złośliwego oprogramowania ale i narzędzia „zabezpieczające” malware mają zaimplementowane mechanizmy omijające te metody monitorowania.
Zaczniemy od bardzo prostego przykładu bazującego na funkcji ze standardowym prologiem, czyli początkiem funkcji która jest taka sama dla wielu API funkcji eksportowanych przez biblioteki systemowe. Ten prolog dla 32 bitowej wersji to:

mov edi, edi
push ebp
mov ebp, esp


Mov edi,edi jest wykorzystywany do tzw. hot-patching-u. Instrukcja ta może być traktowana jako 2 bajtowy no operation (NOP). Inne aplikacje mogą nadpisać ją instrukcjami JMP lub CALL w celu przejęcia kontroli po wywołaniu jej.

Poniżej przykład takiej modyfikacji:

Kolejny przykład pokazujący ustawiony breakpoint programowy 0xCC na pierwszym bajcie monitorowanej funkcji.
Push ebp, mov ebp, esp to standardowy 3 bajtowy prolog związany z utworzeniem nowej ramki stosu dla funkcji.

Trywialną metodą "oszukania" narzędzi monitorujących jest zaimplementowanie własnego prologu a następnie wywołanie kolejnej instrukcji znajdującej się zaraz po 5 bajtowym prologu.
Zanim jednak to zrobimy, prześledźmy standardowe wywołanie funkcji. Dla uproszczenia skorzystamy z funkcji LoadLibraryA() oraz GetProcAddress() (w kolejnych wpisach zajmiemy się omówieniem innych metod identyfikacji adresów funkcji API bez korzystania z funkcji API).

typedef UINT(CALLBACK *FUNKCJA)(HWND, LPCSTR, LPCSTR, UINT);

HINSTANCE dllhandle = NULL;
FUNKCJA funkcja1;

dllhandle = LoadLibraryA("User32.dll");

funkcja1 = (FUNKCJA) GetProcAddress(dllhandle,"MessageBoxA");

char *title = "Przyklad 01";
char *body = "Prevenity 2016";
__asm
{
    pushad;    

    mov eax, dword ptr ds:[funkcja1];
    push 0;
    push title;
    push body;
    push 0;
    call eax;
    popad;
}


Adres funkcji którą chcemy wywołać zapisywany jest w zmiennej funkcja1. Następnie ten adres zapisywany jest w rejestrze eax. Przed wywołaniem tej instrukcji wywołujemy instrukcje pushad, która na stosie umieszcza zawartość rejestrów ogólnego zastosowania. Chodzi o zapisanie stanu rejestrów przed manualną modyfikacją rejestrów. Często złośliwe oprogramowanie odkodowujące/odpakowujące inne fragmenty swojego kodu korzysta z rejestrów pushad, popad.

W kolejnym kroku na stos odkładane są argumenty funkcji, którą wywołujemy, po czym za pomocą instrukcji CALL wywołujemy funkcję. W rejestrze eax jest wskazanie na adres wywoływanej funkcji więc narzędzia monitorujące zarejestrują jej wywołanie.

Teraz zamiast zastosowania instrukcji CALL użyjmy instrukcji JMP. Różnica jest taka, że przy instrukcji CALL na stos automatycznie odkładany jest adres powrotu. JMP tego nie robi więc musimy zapisać na stosie adres powrotu używając instrukcji push offset.

        …
        push 0;
        push title;
        push body;
        push 0;
        push offset label1
        jmp eax;
    label1:
        popad;


Poniżej gotowa implementacja, która wywoła funkcje z pominięciem pierwszych instrukcji znajdujących się w eksportowanych funkcjach API a tym samym ominie część z narzędzi do monitorowania.

        …
        push 0;
        push title;
        push body;
        push 0;
        push offset label1
        mov edi, edi;
        push ebp;
        mov ebp, esp;
        lea eax, [eax + 5];
        jmp eax;
    label1:
        popad;


Możemy jeszcze usprawnić nasz kod dodając sprawdzenia wykrywające modyfikacje wykonane przez system operacyjny. Dla przykładu instrukcja JMP (0xe8 lub 0xe9)  wskazująca na adres z przestrzeni adresowej kernelbase.dll czy JMP a następnie 4 bajty NOP mogą wskazywać na domyślne przejęcie kontroli przez system operacyjny.

Brak komentarzy:

Publikowanie komentarza