Skoči na vsebino

SI-CERT TZ014 / Analiza prenašalnika GuLoader

V tokratnem tehničnem zapisu bomo analizirali in opisali delovanje prenašalnika GuLoader. Prenašalnik (angl. “Downloader”) je vrsta škodljive kode, katere namen je prenos tovora na sistem in zagon le-tega. Tovor je običajno zašifrirana škodljiva koda, ki jo pred zagonom potrebno tudi odšifrirati. Prenašalniki lahko vsebujejo veliko različnih metod, ki otežijo tako statično kot dinamično analizo.

GuLoader je lupinska koda, zato lahko začnemo statično analizo z razstavljavcem Ghidra, pri čemer je potrebno ročno nastavit tip procesorja na x86 32-bit.

Slika prikazuje 4 sklope kode v zbirniku
Prikaz začetka izvajanja kode v pogledu grafa funkcije

Na začetku kode opazimo dve nenavadni zadevi. Prva je veliko število neobičajnih inštrukcij, ki so povezane z operacijam s plavajočo vejico (angl. floating point). Druga nenavadna zadeva je veliko število brezpogojnih skokov oz. JMP inštrukcij. Obe zadevi otežita branje zbirne kode. Omenjene neobičajne inštrukcije lahko v tem primeru povsem ignoriramo, saj se rezultati operacij nikoli ne uporabijo in ne vplivajo na delovanje programa. Pri branju kode z veliko brezpogojnih skokov si lahko v pomagamo s pogledom grafa funkcije (angl. Function Graph).

Z nadaljnjo analizo ugotovimo, da koda na začetku odšifrira preostalo kodo programa in nato skoči na neko lokacijo znotraj odšifrirane kode. Opazimo, da je koda šifrirana s preprostim XOR šifrom. Inštrukcija, ki opravi odšifriranje dela kode je:

00000c44 XOR      dword ptr [EAX + EDI*0x1],ESI

in se izvaja znotraj zanke. Register EAX vsebuje naslov začetka zašifrirane kode, EDI trenutni indeks oz. odmik od začetka šifrirane kode in se vsako iteracijo poveča za 4, ESI pa vsebuje ključ za XOR šifer. Ključ v ESI registru se nastavi pred zanko z naslednjimi inštrukcijami:

00000579 MOV      ESI,0x1a9cfa2a00000652 XOR      ESI,0xaf3f49d40000072f XOR      ESI,0x711b3a55000007f7 XOR      ESI,0x5f081f5f000008a2 XOR      ESI,0xcb9ceb7a00000957 ADD      ESI,0xf47e4c60

Po izvedbi zgornjih inštrukcij bo v ESI registru vrednost 0x44aac9ee. Za nadaljevanje analize in odšifriranje lahko uporabimo enega izmed mnogih orodij, ki podpirajo XOR šifer (npr. CyberChef) ali pa s pomočjo razhroščevalca (npr. x64dbg) izvršimo kodo do konca zanke in nato zapišemo območje pomnilnika, ki vsebuje odšifrirano kodo, v novo datoteko. V tem primeru smo se odločili za drugo opcijo in si pri tem pomagali z orodjem BlobRunner.

Slika prikazuje delovanje orodja BlobRunner v ukazni vrstici
Orodje BlobRunner

Metode oteževanja razhroščevanja in analiziranja kode

Kot smo omenili na začetku, GuLoader vsebuje veliko metod, ki otežijo analizo. Pomembnejši nizi so šifrirani z XOR šifrom, s ključem velikim preko 64 bajtov, in se odšifrirajo samo ob uporabi (program ne odšifrira vseh nizov na enkrat). Poleg tega se zašifrirani podatki generirajo dinamično. Spodaj je primer funkcije, ki generira zašifrirane podatke za samo en niz:

void FUN_04f53803(uint *param_1)
{
  uint *puVar2;

  *param_1 = 0x629e7180;
  *param_1 = *param_1 ^ 0x704747f0;
  *param_1 = *param_1 + 0xe0229335;
  *param_1 = *param_1 + 0xd043664;
  puVar2 = param_1 + 1;
  *puVar2 = 0x74aff9e7;
  *puVar2 = *puVar2 + 0xdf1ae5f0;
  *puVar2 = *puVar2 + 0x9bd927d9;
  *puVar2 = *puVar2 + 0x1e43ec35;
  puVar2 = param_1 + 2;
  *puVar2 = 0x9e148c95;
  *puVar2 = *puVar2 + 0xf4519c83;
  *puVar2 = *puVar2 + 0xe5dca25;
  *puVar2 = *puVar2 + 0x445e3510;
  param_1 = param_1 + 3;
  *param_1 = 0xe6d89253;
  *param_1 = *param_1 ^ 0x8dc3b562;
  *param_1 = *param_1 + 0x8d75f54e;
  *param_1 = *param_1 ^ 0xf8911cd2;
  return;
}

Obfuskirani so tudi klici do Windows API funkcij. Vsakič, ko GuLoader potrebuje neko Windows API funkcijo, dinamično pridobi njen naslov in po potrebi naloži knjižnico, v kateri se ta funkcija nahaja. Posamezno funkcijo poišče na podlagi zgoščene vrednosti imena funkcije in na ta način skrije imena funkcij. Zaradi tega se pri statični analizi ne vidi, katere Windows API funkcije progam uporablja.

Obfuskacija poteka izvajanja z uporabo izjem

GuLoader skozi celoten program izkorišča inštrukcijo INT3. Ta inštrukcija sproži izjemo za prekinitveno točko (angl. breakpoint), ki jo običajno procesira oz. obravnava razhroščevalnik, ampak v tem primeru se ta inštrukcija uporablja za izvedbo obfuskacije poteka izvajanja programa (angl. control flow obfuscation). GuLoader z uporabo Windows API funkcije AddVectoredExceptionHandler doda svojo proceduro za obravnavo izjem, ki se izvede ob vsaki izjemi.

S pomočjo razhroščevalnika pridobimo funkcijo za obravnavo izjem, ki je podana kot parameter funkciji AddVectoredExceptionHandler. V dokumentaciji najdemo tudi podpis (angl. signature) te funkcije in z uporabo povratnega prevajalnika (angl. decompiler) znotraj Ghidre pridobimo naslednjo kodo:

long GuLoader_ExHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
  char *index;
  uint continue_offset;
  PCONTEXT context;
  
  if (((((ExceptionInfo->ExceptionRecord->ExceptionCode == 0x80000003) && (context = ExceptionInfo->ContextRecord, context->Dr0 == 0)) && (context->Dr1 == 0)) && ((context->Dr2 == 0 && (context->Dr3 == 0)))) && ((context->Dr6 == 0 && ((context->Dr7 == 0 && (*(char *)context->Eip == 0xcc)))))) {
    continue_offset = (uint)(byte)(((char *)context->Eip)[1] ^ 0x60);
    index = (char *)(context->Eip + continue_offset);
    do {
      index -= 1;
      if ((char *)(context->Eip + 2) == index) {
        context->Eip = context->Eip + continue_offset;
        return -1;
      }
    }
    while (index != 0xcc);
  }
  return 0;
}

Koda najprej preveri, ali gre za pravilno izjemo (0x80000003 – EXCEPTION_BREAKPOINT), in ali je na naslovu, ki je sprožil izjemo (context->Eip), res inštrukcija INT3 (0xcc). Poleg tega preveri tudi vrednosti v DR oz. registrih za razhroščevanje, ki se uporabljajo za strojne prekinitvene točke (angl. hardware breakpoints). Nato pride glavni del funkcije, ki nam pove, kje bo program nadaljeval izvajanje po procesiranju izjeme. Vidimo, da pridobi vrednost bajta za naslovom, ki je sprožil izjemo, in to vrednost XORa z 0x60 ter rezultat shrani v spremenljivko continue_offset. Zatem sledi zanka, ki preveri, da v regiji med naslovom, ki je sprožil izjemo, in izračunanim odmikom, ni drugih INT3 inštrukcij. Če jih ni, potem poveča context->Eip za izračunan odmik. Po obravnavi izjeme se zgodi neke vrste zamenjava konteksta (angl. context switch), ki v trenutno stanje procesorja zapiše spremenjeno stanje iz spremenljivke context. Tako bo program nadaljeval svoje izvajanje na naslovu, ki je zapisan v context->Eip.

Analizo programa s to vrsto obfuskacije si lahko olajšamo z uporabo skripte. x86 arhitektura vsebuje inštrukcijo JMP, ki skoči na odmaknjen naslov. Poleg tega lahko za odmike v razponu od -128 do 127 to inštrukcijo kodiramo s samo dvema bajtoma, kar nam omogoča, da lahko vse INT3 inštrukcije (velike 1 bajt + 1 bajt zašifriranega odmika) zamenjamo z JMP inštrukcijam. Taka zamenjava bi nam močno olajšala analizo, vendar pa pri tem lahko nastane težava. Odmik oz. razlika med naslovom, ki sproži izjemo, in naslovom na katerem program nadaljuje izvajanje po obravnavi izjeme, je v razponu od 0 do 255, kar vidimo v izračunu spremenljivke continue_offset, ki rezultat pretvori v tip byte (unsigned char). Zato v primerih, kjer je ta razlika večja od 127, zamenjave z JMP inštrukcijo ne bi bile možne oz. bi vzele več kot 2 bajta. Vendar se izkaže, da v vseh do sedaj obravnavanih primerih GuLoader-ja je bil ta odmik vedno manjši od 32, zato lahko zamenjavo naredimo brez težav (* glej opombo na koncu).

Skeniranje pomnilnika

Veliko peskovnikov in ostalih orodij, ki jih uporabljamo pri analizi, pri svojem delovanju v analiziran proces vstavi različne module ali v njem dodeli pomnilniški prostor. Takšne sledi lahko zlonameren program zazna in spremeni svoje delovanje. GuLoader v namen preprečevanja dinamične analize v svojem pomnilniškem prostoru išče znakovne nize, ki so povezani z nekaterimi orodji, in v primeru najdbe zaključi izvajanje.

Najprej pridobi bazni naslov alocirane regije v pomnilniku. To stori tako, da v zanki povečuje naslov za velikost strani (PAGE_SIZE) in kliče Windows API funkcijo NtQueryVirtualMemory (dokumentacija) s parametrom BaseAddress nastavljenim na ta naslov ter preveri, da je rezultat klica 0 oz. STATUS_SUCCESS. S tem pridobi naslov strani v pomnilniku, do katere ima program dostop. Zatem preveri še zaščito te regije. Če je nastavljena na PAGE_EXECUTE, PAGE_EXECUTE_READ ali PAGE_EXECUTE_READWRITE, potem v tej regiji poišče nize. Teh pa ne išče direktno, ampak najprej izračuna zgoščeno vrednost in nato preveri, ali sta zgoščena vrednost in dolžina niza v seznamu blokiranih vrednosti.

Funkcija, ki izračuna zgoščeno vrednost, je preprosta. S pomočjo povratnega prevajalnika in malo optimizacije jo lahko zapišemo v spodnji obliki:

uint GuLoader_CalcHash(byte *input_str)
{
  byte cur_char;
  uint hash;
  hash = 0x1505;
  while( true ) {
    cur_char = *input_str;
    input_str = input_str + 1;
    if (cur_char == '@') {
      return 0;
    }
    if (cur_char == '?') {
      return 0;
    }
    if (cur_char == '$') {
      return 0;
    }
    if (cur_char > 0xa3) {
      break;
    }
    hash = (hash * 0x21 + cur_char) ^ 0xbbae3246;
    if (*input_str == '\0') {
      return hash;
    }
  }
  return 0;
}

Ta algoritem je rahlo spremenjena različica DJB2 algoritma. GuLoader algoritmu doda še XOR operacijo, konstanta oz. ključ pri tej operaciji pa je za vsak GuLoader primer drugačen (v tem primeru 0xbbae3246), zato vsak primer GuLoader-ja izračuna in primerja različne zgoščene vrednosti. V spodnji tabeli so zapisane zgoščene vrednosti, ki jih analizirani GuLoader išče v pomnilniku in nekateri dejanski nizi, ki se zgostijo v to vrednost (pridobljeni z dinamično analizo in orodji za iskanje zgoščenih vrednosti, npr. hashcat):

Zgoščena vrednostDolžina iskanega nizaIskan nizOpomba
0x5496BF480x15 (21)SbieApi_EnumProcessExSandboxie
0xEF572C7A0x0B (11)SbieDll.pdbSandboxie
0xA646764C0x16 (22)apimonitor-drv-x86.sysrohitab API Monitor
0x697F8E9A0x22 (34)http://www.rohitab.com/apimonitor/rohitab API Monitor
0x808B752F0x12 (18)HookLibraryx86.dllScyllaHide

Detekcija navideznih strojev

GuLoader vsebuje več načinov s katerimi preveri ali se izvaja na navideznih sistemih. Prvi način je najbolj zanimiv in izkoristi razliko med navideznm in fizičnim sistemom v času izvajanja nekaterih inštrukcij. Bolj specifično uporablja inštrukciji CPUID in RDTSC, ki jih hipernadzornik (angl. hypervisor) pogosto emulira povsem programsko in pri tem nastanejo večje razlike v času izvajanja teh inštrukcij.

GetSystemTime:LFENCEMOV        EDX,0xb0bbcce0XOR        EDX,0xfb0b613dXOR        EDX,0x3ad82980XOR        EDX,0xe968449MOV        EDX=>DAT_7ffe0014,dword ptr [EDX]LFENCERET

Zgornja funkcija pridobi trenutni čas sistema oz. čas, od kar je sistem prižgan. Vidimo, da je vsebuje zelo malo inštrukcij, veliko jih je celo odveč. Kako program pridobi sistemski čas brez uporabe sistemskih klicev, posebnih funkcij ali inštrukcij (LFENCE se uporablja samo za sinhronizacijo pomnilniških operacij)? Odgovor se skriva v strukturi na naslovu 0x7ffe0000. Na tem naslovu se nahaja posebna sistemska struktura KUSER_SHARED_DATA (opis), ki se uporablja za deljenje nekaterih vrednosti iz pomnilnika v jedru (angl. kernel) in pospeševanje bolj pogostih sistemskih funkcij. Če pogledamo zgornjo funkcijo, vidimo, da prebere vrednost iz naslova 0x7ffe0014 oziroma odmika 0x14 v strukturi KUSER_SHARED_DATA. Tam se nahaja vrednost SystemTime, ki vsebuje sistemski čas.

UINT32 GuLoader_TimingCalc(void)
{
  UINT32 startTime;
  UINT32 endTime;
  startTime = GetSystemTime();
  rdtsc();
  cpuid(1);
  endTime = GetSystemTime();
  return endTime - startTime;
}

Funkcija GuLoader_TimingCalc izračuna in vrne skupni čas izvajanja RDTSC in CPUID inštrukcij.

void GuLoader_TimingCheck(void)
{
  UINT32 count;
  UINT32 timeDelta;
  UINT32 timeSum;
  do {
    timeSum = 0;
    count = 11100000;
    do {
      timeDelta = GuLoader_TimingCalc();
      timeSum = timeSum + timeDelta;
      count = count - 1;
   } while (count != 0);
 } while (14999999 < timeSum);
}

V funkciji GuLoader_TimingCheck znotraj zanke kliče funkcijo GuLoader_TimingCalc in sešteva čas izvajanja. To stori 11100000 krat  in na koncu preveri, ali je skupni čas izvajanja večji od 14999999. Če je, zanko ponovi. Na fizičnih sistemih ta funkcija obnaša kot neka čakajoča oz. Sleep funkcija, na navideznih sistemih se pa ne bo zaključila, saj bo skupen čas izvajanja vedno večji od 14999999.

int GuLoader_HypervisorCheck(void)
{
  int startCycles;
  int endCycles;
  UINT32 ECX;
  do {
    startCycles = ReadTimeStampCounter();
    ECX = cpuid(1);
    if ((ECX >> 0x1F) & 0x1) {
      goto programEnd;
    }
    endCycles = ReadTimeStampCounter();
  } while (endCycles – startCycles <= 0);
  return endCycles - startCycles;
}

Na podoben način deluje tudi funkcija GuLoader_HypervisorCheck, ki preveri rezultat CPUID inštrukcije, namesto razlike v času pa računa razliko v CPE ciklih.

ReadTimeStampCount:LFENCERDTSCLFENCESHL      EDX,0x20OR       EDX,EAXRET

Število CPE ciklov pridobi s funkcijo ReadTimeStampCount. Ta uporablja inštrukcijo RDTSC, ki vrne število CPE ciklov v 64 bitni vrednosti tako, da uporabi dva 32 bitna registra (EDX in EAX). Implementacija, ki jo uporablja GuLoader, sicer vsebuje napako in izgleda, kot da je namenjena za 64 bitni program. SHL inštrukcija zaradi specifike delovanja (odmik je maskiran na spodnjih 5 bitov) ne naredi nič in zato je končna vrednost samo OR operacija med spodnjimi in zgornjimi 32biti vrnjenega števila CPE ciklov, kar pa nima preveč smisla.

Če gremo malo nazaj in si bolj natančno pogledamo funkcijo GuLoader_HypervisorCheck, opazimo, da uporabi CPUID inštrukcijo in preveri rezultat. CPUID inštrukcija, glede na vrednost v EAX registru, vrne specifično informacije o CPE. V tem primeru inštrukcijo izvede z EAX vrednostjo 1, kar pomeni, da vrne informacijo o verziji in sposobnostih CPE. Funkcija zatem preveri vrednost ECX registra, ki vsebuje del rezultata CPUID inštrukcije. Bolj specifično, preveri, če je 31. bit nastavljen in v takem primeru zaključi izvajanje programa. x86 32bit (IA-32) ISA določa, da je po klicu CPUID inštrukcije z EAX vrednostjo 1, 31. bit v ECX registru vedno nastavljen na 0. Vendar ta bit hipernadzorniki pogosto nastavijo na 1, z namenom, da lahko preverimo, ali se program izvaja na navideznem sistemu.

Celotno funkcijo, ki z uporabo CPUID in RDTSC inštrukcij preverja, ali se GuLoader izvaja na navideznem sistemu, lahko prikažemo v spodnji obliki:

void GuLoader_CheckVM(void)
{
  UINT32 count;
  UINT32 count2;
  int cycleDelta;
  int cycleTotal;
  
  GuLoader_TimingCheck();
  do {
    do {
      do {
      cycleTotal = 0;
      count = 100000;
      count2 = 0;
      do {
        cycleDelta = GuLoader_HypervisorCheck();
        if (cycleDelta <= 49) {
          count2 = count2 + 1;
        }
        count = count - 1;
        cycleTotal = cycleTotal + cycleDelta;
        } while (count != 0);
      } while (60000 < count2);
    } while (cycleTotal < 0);
  } while (110000000 <= cycleTotal);
  return;
}

Podobno kot v GuLoader_TimingCheck, se tudi tu v zanki kliče GuLoader_HypervisorCheck in na koncu preverja seštevek ciklov.

GuLoader uporablja tudi nekaj bolj enostavnih tehnik za detekcijo navideznega sistema. Preveri ali na sistemu obstajajo datoteke:

  • C:\Program Files\Qemu-ga\qemu-ga.exe
  • C:\Program Files\qga\qga.exe

Te so povezane s Qemu Guest Additions, ki navideznemu stroju omogočijo ali izboljšajo nekatere funkcionalnosti.

GuLoader išče tudi gonilnike, povezane z nekaterimi navideznimi stroji. To stori s klicem funkcije psapi.EnumDeviceDrivers, ki pridobi seznam gonilnikov, nato pa s funkcijo psapi.GetDeviceDriverBaseNameA pridobi ime posameznega gonilnika. Podobno kot pri skeniranju pomnilnika, tudi tu izračuna zgoščeno vrednost imena gonilnika in nato primerja zgoščene vrednosti.

Zgoščena vrednostIskano ime gonilnikaOpomba
0x495B667Avioser.sysKVM/Virtio
0x96C83D93netkvm.sysKVM/Virtio
0x4350AFB2viostor.sysKVM/Virtio
0x6AF32BF8vmmouse.sysVMware
0xB907A4FCvmusbmouse.sysVMware
0x0068A981vm3dmp_loader.sysVMware
0xAF8202FDvm3dmp.sysVMware

Zatem preveri seznam nameščenih programov in išče imena razvijalcev oz. izdajateljev programske opreme za navidezne sisteme ali orodij za analizo programske opreme. Podobno kot pri iskanju gonilnikov to naredi s klicem dveh funkcij: msi.MsiEnumProductsA in msi.MsiGetProductInfoA. Z njimi pridobi ime izdajatelja posameznega nameščenega programa, nato izračuna zgoščeno vrednost izdajatelja in primerja zgoščene vrednosti.

Zgoščena vrednostIskano ime izdajateljaOpomba
0xEA6FCB7DQEMUQemu Guest Tools
0x0ACE7453Red Hat, Inc.KVM/Virtio
0x7F838509RedHatKVM/Virtio

Na koncu preveri še imena nameščenih Windows storitev. S funkcijama advapi32.OpenSCManager in advapi32.EnumServicesStatusA pridobi storitve in njihova imena ter nato izračuna in primerja zgoščene vrednosti imen.

Zgoščena vrednostIskano ime storitveOpomba
0x0864A1CDVirtualBox Guest Additions ServiceVirtualBox
0x0C40EB54VMware ToolsVMware
0x7B9F5DC2VMware Snapshot ProviderVMware
0x67537454QGAQemu
0xEC72DBD8QEMU Guest AgentQemu
0x9C671930QEMU-GAQemu
0xEDFD5E4CSPICE VDAgentQemu/SPICE

V primeru, da najde katero od zgoraj omenjenih datotek ali pa, da se katera od zgoščenih vrednosti ujema, prikaže okno s sporočilom in nato zaključi izvajanje.

Slika prikazuje windows okno z napako: This program cannot be run under virtual environment or debugging software
Napaka, ki jo vrne GuLoader, če zazna analizo

Replikacija

Če se po vseh zgornjih preverjanih program še vedno izvaja, potem replicira svojo kodo v drug, novo ustvarjen proces. To naredi z uporabo le nekaj sistemskih klicev in Windows API funkcij:

  • CreateProcessInternalW: Nedokumentirana Windows API funkcija s katero ustvari nov proces. Zastavice za kreacijo postavi tako, da se proces kreira v zaustavljenem stanju;
  • NtAllocateVirtualMemory: Sistemski klic s katerim dodeli pomnilnik v novo kreiranem procesu;
  • NtWriteVirtualMemory: Sistemski klic s katerim v dodeljen pomnilnik replicira svojo kodo;
  • GetThreadContext: Windows API funkcija s katero pridobi kontekst glavne niti v novo kreiranem procesu;
  • SetThreadContext: Windows API funkcija s katero nastavi nov kontekst v kreiranem procesu (EIP register nastavi na nek začetni naslov v replicirani kodi);
  • NtResumeThread: Sistemski klic, ki zaustavljeno nit oz. proces postavi v izvrševalno stanje.

Novo kreiran proces gre potem spet čez vsa preverjanja in na koncu doda vztrajnost (angl. persistency) in prenese škodljiv tovor. V nekaterih primerih se GuLoader večkrat replicira preden prenese škodljiv tovor.

Prenos, odšifriranje in izvršitev škodljivega tovora

Spletni naslov, s katerega se prenese škodljiv tovor, je šifriran na enak način kot ostali nizi v programu, prvih nekaj znakov je pa še dodatno zašifriranih, zato da se izogne napadu z znanim golim besedilom (angl. known-plaintext attack). Škodljiv tovor prenese s klicem naslednjih funkcij:

  • InternetOpenA: Inicializacija WinINet funkcij in nastavitev user agent;
  • InternetSetOptionA: Nastavi timeout za poslane zahteve in prejete odgovore;
  • InternetOpenUrlA: Odpre povezavo in pošlje zahtevo za določen naslov oz. URL;
  • InternetReadFile: Prebere podatke iz prejetega odgovora;
  • InternetCloseHandle: Zapre povezavo.

GuLoader pogosto izkorišča legitimne storitve za deljenje datotek, npr. Google Drive, OneDrive, Dropbox. V analiziranem primeru je tovor prenesel iz naslova: hxxps://drive[.]google.com/uc?export=download&id=1guMzobutL4U2E6GWe0p9b3eV5_L2pkva (naslov je namenoma okvarjen).

Prenesen tovor je šifriran, zato ga je pred izvršitvijo potrebno odšifrirati. Prvih 64 bajtov tovora so smeti (angl. garbage data), preostali del so pa XOR zašifrirani podatki. Ključ za odšifriranje je, podobno kot podatki nizov, generiran dinamično in je običajno velik okoli 850 bajtov. Poleg tega je tudi ključ dodatno XOR šifriran z še enim ključem velikim 2 bajta. Ta drugi ključ pridobi na podlagi prenesenega tovora. Ker GuLoader vedno prenese neko izvršljivo datoteko, bo ta na začetku vsebovala niz “MZ”, ki označuje začetek PE izvršilnih datotek. To GuLoader izkoristi in iz tega znanega niza ter podatkov tovora izračuna manjši ključ, s katerim odšifrira celotni glavni ključ. Poenostavljeno kodo odšifriranja tovora vidimo v funkciji GuLoader_PayloadDecrypt:

void GuLoader_PayloadDecrypt(uint8_t *payload, size_t payloadSize, uint8_t *key, size_t keySize)
{
  uint8_t *payloadEnd = payload + payloadSize;
  uint8_t *pPtr = payload + 0x40; // Preskoči smeti
  uint16_t kkey = *((uint16_t *)pPtr) ^ 0x5A4D ^ *((uint16_t *)key); // Pridobi XOR ključ glede na znani niz "MZ" in prva 2 bajta tovora
                                                                     // "MZ" == 0x5A4D; little-endian
  uint16_t *kPtr = (uint16_t *)key;
  uint16_t *keyEnd;
  if (keySize % 2 != 0) // Če je velikost ključa liho (se ne ujema z velikostjo uint16_t), potem spusti zadnji bajt ključa
    keyEnd = (uint16_t *)(key + keySize - 1);
  else
    keyEnd = (uint16_t *)(key + keySize);  
  
  // Odšifriranje ključa
  while (kPtr < keyEnd) {
  *kPtr ^= kkey;
    kPtr++;
  }

  // Odšifriranje tovora
  size_t kIndex = 0;
  while (pPtr < payloadEnd) {
    if (kIndex == keySize) {
      kIndex = 0;
    }
    *pPtr ^= key[kIndex];
    pPtr++;
    kIndex++;
  }
}

Po odšifriranju tovora sledi le še izvršitev. Odšifriran tovor oz. program ročno naloži v trenutni proces in nato ustvari novo nit na začetnem naslovu (angl. entry point) odšifriranega programa. Program naloži na obstoječ bazni naslov trenutnega procesa in ker program nalaga ročno, mora narediti še naslednje korake:

  • premik sekcij na prave navidezne naslove,
  • postavitev pravilnih zaščit za premaknjene sekcije,
  • kreiranje uvozne tabele (angl. import table),
  • obravnava premestitev (angl. relocation).

Po nalaganju programa ustvari novo nit in zaključi izvajanje trenutne niti. Pred zaključkom pa še šifrira velik del GuLoader kode in počisti ter sprosti dodeljen pomnilnik. Kot zanimivost lahko povemo, da predno naloži tovor, trenutnemu procesu s funkcijo NtSetInformationProcess onemogoči DEP (Data Execution Prevention), ki bi lahko vplival na ali onemogočil izvajanje nekaterih programov.

* Opomba: Novejše verzije GuLoader-ja vsebujejo bolj kompleksno obfuskacijo poteka izvajanja programa in šifriranja celotnega programa. Izjeme za obfuskacijo poteka izvajanja se v novi verziji generirajo dinamično in jih ni več zlahka zaznati s statično analizo, zato preprosta skripta, s katero smo si pomagali v zgornjem primeru, ne deluje več. V takih primerih si lahko pomagamo z močnejšimi metodami analize, kot so npr. simbolično izvajanje (angl. symbolic execution) ali dinamična instrumentacija (angl. dynamic instrumentation).

Preberite tudi

Konferenca o ozaveščanju o kibernetski varnosti

Agencija EU za kibernetsko varnost ENISA v partnerstvu s SI-CERT organizira prvo mednarodno konferenco o ozaveščanju o kibernetski varnosti. Dogodek bo potekal 27. novembra 2024, v Klubu Cankarjevega doma, s …
Več

Statistika SI-CERT za prvo polovico leta 2024

Za trend prve polovice 2024 smo izbrali ciljanje na mobilne naprave, najbolj očitno gre za lažna SMS sporočila (smishing) v imenu bank in dostavnih služb. Na te se usmerjajo tudi vedno bolj napredni trojanci za krajo podatkov za dostop do mobilnih denarnic.
Več

CrowdStrike izpad in posledice v Sloveniji

Ob pospešeni digitalizaciji različnih vidikov družbe postajamo vedno bolj odvisni od informacijskih sistemov. Kako odporni so le-ti na izpade?
Več