wtorek, 17 maja 2016

Analiza malware z wykorzystaniem DBI

Na naszym portalu nie mogło zabraknąć informacji o narzędziu PIN [1]. PIN to framework do wykonywania instrumentacji programów. Jest to technika polegająca na wstrzykiwaniu do analizowanego kodu instrukcji monitorujących. W efekcie możemy precyzyjnie śledzić wykonywane instrukcje przez program. PIN jest rozwijany przez firmę Intel.
 
W niniejszym artykule pokażemy kilka przykładów wykorzystania jego możliwości. Dla osób, które pierwszy raz czytają o instrumentacji programów, możliwości dobrze zobrazuje przykład dostarczany razem z pakietem PIN a opisany szczegółowo tutaj [2].

D:\pin.exe –t inscount0.dll - - notepad.exe
D:\type inscount.out
Count 13635480

PIN "wstrzykuje" do tworzonego procesu (notepad.exe) bibliotekę inscount0.dll. Jej celem jest zliczenie wykonywanych instrukcji notepad.exe. Jest to analiza dynamiczna co oznacza, że zliczane są instrukcje wykonane przez program notepad.exe. Kod modułu dll jest prosty. Kluczowe jest wywołanie funkcji INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END). Powoduje ona że narzędzie PIN przed wykonaniem każdej instrukcji analizowanego programu (IPOINT_BEFORE) wywoła funkcję docount() - VOID docount() { icount++; }.

Analiza złośliwego oprogramowania za pomocą PIN pozwala nam między innymi na:
  • Identyfikację i przechwytywanie funkcji wewnętrznych malware (nie tylko systemowych funkcji API),
  • Monitorowanie wywołań systemowych (syscall),
  • Sprawniejszą analizę złośliwego kodu, który ma zaimplementowane mechanizmy obfuskacji
W celu pokazania wybranych funkcji PIN użyjemy prostego programu, który zapisuje na dysku plik z danymi. Następnie z pomocą narzędzia PIN będziemy monitorowali i próbowali przechwycić istotne dane.

Podobnie jak w przypadku pierwszego przykładu w main naszej biblioteki użyjemy funkcji INS_AddInstrumentationFunction(funkcja, NULL), która inicjuje tryb śledzenia na poziomie pojedynczych instrukcji – INS_(inne poziomy to image, routine oraz trace). Poniżej przykładowa zawartość main() rejestrująca funkcję – InsCall().

int main(int argc, char *argv[])
{
    if (PIN_Init(argc, argv))
        return -1;

    OutFile.open(KnobOutputFile.Value().c_str());
    INS_AddInstrumentFunction(InsCall, NULL);
    PIN_AddFiniFunction(Fini, 0);
    PIN_StartProgram();
    return 0;
}


W poniższej funkcji InsCall funkcja PIN - INS_IsCall() zwraca true gdy instrukcja jest instrukcją CALL. Wtedy przed nią wywoływana funkcja przygotowana przez nas - CallCallback. Przekazywane są do niej dwa argumenty: IARG_INST_PTR – adres instrukcji CALL oraz IARG_BRANCH_TARGET_ADDR – adres funkcji wywoływanej.

void InsCall(INS ins, void * v)
{
    if (INS_IsCall(ins)) {
        INS_InsertCall(ins,
            IPOINT_BEFORE,
            (AFUNPTR)CallCallback,
            IARG_INST_PTR,
            IARG_BRANCH_TARGET_ADDR,
            IARG_END);
    }
}


Wiemy już, że CallCallback będzie wywołany przed każdą instrukcją CALL. Argument target_address jest użyty do wyszukania nazwy funkcji wywoływanej za pomocą CALL. RTN_FindByAddress zwraca uchwyt a RTN_Name nazwę funkcji. Dodatkowo funkcja PIN_UndecorateSymbolName przetworzy nazwę wygenerowaną przez kompilator na bardziej czytelną (np. @funkcja_test@12 na funkcja_test). 

void CallCallback(ADDRINT instruction_address, ADDRINT target_address)
{
    PIN_LockClient();
    RTN rtn = RTN_FindByAddress(target_address);
    if (RTN_Valid(rtn)) {
        string symbolName = RTN_Name(rtn);
        symbolName = PIN_UndecorateSymbolName(symbolName, UNDECORATION_COMPLETE);
        OutFile << hex << instruction_address << " -> " << symbolName << endl;
    }
    else {
        OutFile << hex << instruction_address << " -> " << target_address << endl;
}
    PIN_UnlockClient();
}


W efekcie otrzymamy wynik zbliżony do poniższego (fragment po prawej stronie). Po lewej stronie mamy fragment kodu monitorowanego programu do porównania – w tym funkcję CreateFile (ze względu na ASLR adresy się różnią). Pamiętamy, że funkcja CallCallback wywoływana jest za każdym razem gdy PIN napotka instrukcje CALL. Wynik to odtworzona ścieżka działania analizowanego programu (w tym większość to funkcje systemowe). Uzupełniając nasz moduł o zapisywanie wartości argumentów funkcji oraz mechanizmy kopiowania niektórych obszarów danych (np. tych rezerwowanych przez analizowany program) możemy uzyskać bardzo wiele informacji o działaniu malware.
W kolejnym przykładzie zajmiemy się zapisywaniem wartości argumentów. Załóżmy, że istotna jest funkcja API CreateFile z nazwą pliku oraz funkcja API WriteFile z adresem zawartości do zapisu oraz ilością bajtów do zapisu.
Mimo że INS_AddInstrumentFunction() oferuje bardzo szczegółowe monitorowanie (ale duży spadek wydajności) teraz wystarczy nam szczegółowość analizy na poziomie Image – obrazu załadowanego programu. W main użyjemy funkcji IMG_AddInstrumentFunction(ApiCallCallback, NULL) wywoływanej w momencie załadowania modułu do pamięci. W celu dostępu do symboli ważne jest zastosowanie PIN_InitSymbols(). Poniżej przykładowa zawartość wywoływanej funkcji ApiCallCallback(). 
W celu wywołania naszych funkcji (przedrostek my_)przed konkretnymi funkcjami API – poniżej CreateFileA|CreateFileW użyjemy RTN_FindByName(). Do naszych funkcji przekazujemy dwie wartości: nazwę funkcji oraz pierwszy argument z funkcji (jest to nazwa tworzonego pliku).

void ApiCallCallback(IMG img, void * v)
{
    if (img == IMG_Invalid())
        return;

    RTN rtn;
    rtn = RTN_FindByName(img, "CreateFileW");
    if (RTN_Valid(rtn)) {
        RTN_Open(rtn);
        RTN_InsertCall(rtn, IPOINT_BEFORE,
            (AFUNPTR)my_CreateFileW,
            IARG_ADDRINT, "CreateFileW",
            IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
            IARG_END);
        RTN_Close(rtn);
    }
    rtn = RTN_FindByName(img, "CreateFileA");
    if (RTN_Valid(rtn)) {
        RTN_Open(rtn);
        RTN_InsertCall(rtn, IPOINT_BEFORE,
            (AFUNPTR)my_CreateFileA,
            IARG_ADDRINT, "CreateFileA",
            IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
            IARG_END);
        RTN_Close(rtn);
    }
}


W przypadku wersji unicode funkcji API CreateFile zawartość my_CreateFileW może wyglądać tak:

VOID my_CreateFileW(CHAR * apiname, wchar_t * filename)
{
    wstring ws(filename);
    string str(ws.begin(), ws.end());
    OutFile << apiname << "(" << str << ")" << endl;

}

Po uruchomieniu narzędzia (pin.exe -t apimonitor01.dll -- malware.exe) otrzymany następujący wynik:

WriteFile (2740128 2)
CreateFileW(plik.txt)
CreateFileW(plik.txt)
WriteFile (2740120 34)
WriteFile (2750844 44)
WriteFile (2750844 44)
WriteFile (2740120 45)

Nazwy niektórych funkcji (np. CreateFileW) występują dwa razy i jest to poprawne działanie. Wynika to z tego, że ta nazwa jest eksportowana przez kernel32.dll oraz kernelbase.dll.

Kolejny przykład dotyczy kopiowania argumentów wywoływanej funkcji za pomocą rejestru. Na platformie 32-bitowej Windows wszystkie argumenty funkcji są odkładane na stos a następnie wykonywana jest instrukcja CALL. Przy obsłudze funkcji WriteFile możemy zauważyć, że przekazujemy zawartość rejestru ESP (góry stosu). Kolejny fragment ApiCallCallback():

rtn = RTN_FindByName(img, "WriteFile");
    if (RTN_Valid(rtn)) {
        RTN_Open(rtn);
        RTN_InsertCall(rtn,
            IPOINT_BEFORE,
            (AFUNPTR)&WriteFileDump,
            IARG_REG_VALUE, REG_ESP,
            IARG_END);
        RTN_Close(rtn);
    }


Za pomocą PIN_SafeCopy możemy skopiować poszczególne wartości argumentów bezpośrednio ze stosu. Należy też pamiętać o stosowanym sposobie wywoływania (calling conventions). Poniższy przykład pokazuje też jak uzyskać dostęp do danych zapisywanych przez monitorowaną aplikację.

VOID WriteFileDump(ADDRINT esp)
{
    ADDRINT base_address;
    ADDRINT length;
   
    PIN_SafeCopy(&base_address, (ADDRINT *)(esp + 0x8), sizeof(ADDRINT));
    PIN_SafeCopy(&length, (ADDRINT *)(esp + 0xC), sizeof(ADDRINT));
   
    OutFile << "WriteFile " << "(" << base_address << " " << length << ")" << endl;
    

    char * tab_dst = (char *)malloc(length);
    memcpy(tab_dst, (const void *) base_address, length);
    destinationfile = fopen("wynikplik.txt", "a+b");
    fwrite(tab_dst, length, 1, destinationfile);
    fclose(destinationfile);
}


Opisane powyżej monitorowanie funkcji API nie zawsze jest skutecznie. Złośliwe oprogramowanie może skopiować fragment oryginalnej funkcji w celu oszukania narzędzi monitorujących. Może też wywoływać funkcje systemowe bezpośrednio z biblioteki ntdll.dll. Kolejną wadą jest to, że należy przechwytywać wiele funkcji związanych z zapisem danych.

Poniżej znajduje się fragment jednej z funkcji systemu Windows, która jest wywoływana przed przełączeniem w tryb uprzywilejowany. Kluczowa jest wartość przenoszona do rejestru EAX. To numer funkcji systemowej (dla Windows 7 SP1 NtWriteFile), która zostanie wywołana z poziomu jądra systemu operacyjnego. 

ntdll.dll:77D300C4 mov     eax, 5h
ntdll.dll:77D300C9 xor     ecx, 1Ah
ntdll.dll:77D300CB lea     edx, [esp+4]
ntdll.dll:77D300CF call    large dword ptr fs:0C0h
ntdll.dll:77D300D6 add     esp, 4
ntdll.dll:77D300D9 retn    24h

Narzędzie PIN za pomocą między innymi funkcji PIN_AddSyscallEntryFunction() i PIN_AddSyscallEntryFunction() umożliwia nam wywoływanie naszego kodu przed lub po wywołaniu funkcji systemowej.

Poniżej fragment main, gdzie rejestrujemy naszą funkcję syscall_entry wywoływaną przed każdą funkcją systemową:

PIN_AddSyscallEntryFunction(&syscall_entry, NULL);

W funkcji syscall_entry najpierw sprawdzamy numer funkcji systemowej PIN_GetSyscallNumber. Następnie za pomocą PIN_GetSyscallArgument kopiujemy argument 5 i 6 licząc od zera czyli Buffer oraz Length.  Mając te dwie wartości możemy skopiować zawartość bufora do pliku.

Dla przypomnienia opis zawartości argumentów funkcji [3]:

NTSTATUS ZwWriteFile(
  _In_     HANDLE           FileHandle,
  _In_opt_ HANDLE           Event,
  _In_opt_ PIO_APC_ROUTINE  ApcRoutine,
  _In_opt_ PVOID            ApcContext,
  _Out_    PIO_STATUS_BLOCK IoStatusBlock,
  _In_     PVOID            Buffer, (5)
  _In_     ULONG            Length, (6)
  _In_opt_ PLARGE_INTEGER   ByteOffset,
  _In_opt_ PULONG           Key
);


void syscall_entry(THREADID thread_id, CONTEXT *ctx, SYSCALL_STANDARD std, void *v)
{
   
    if (PIN_GetSyscallNumber(ctx, std) == 5){
   
        ADDRINT base_address = PIN_GetSyscallArgument(ctx, std, 5);
        ADDRINT length = PIN_GetSyscallArgument(ctx, std, 6);

        PIN_GetLock(&lock, thread_id + 1);

        char * tab_dst = (char *)malloc(length);
        memcpy(tab_dst, (const void *)base_address, length);
        fwrite(tab_dst, length, 1, g_file);
        fclose(g_file);

        PIN_ReleaseLock(&lock);
    }
}


Przykład, jak zaimplementować automatycznie przeszukiwanie nazw funkcji systemowy znajduje się tutaj [4].

W najbliższym czasie kilka przykładów zastosowania PIN umieścimy na portalu GitHub.

Źródła:

[1] https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool
[2] https://software.intel.com/sites/landingpage/pintool/docs/65163/Pin/html/
[3] https://msdn.microsoft.com/en-us/library/windows/hardware/ff567121%28v=vs.85%29.aspx
[4] https://github.com/jbremer/godware/blob/master/godware.cpp
[5] http://jbremer.org/malware-unpacking-level-pintool/
[6] http://tfpwn.com/blog/writing-tools-with-pin.html
[7] https://msdn.microsoft.com/en-us/magazine/dn818497.aspx 



Brak komentarzy:

Publikowanie komentarza