# Tecniche per semplificare l’analisi del malware GuLoader **[cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/](https://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/)** 21/07/2022 [guloader](https://cert-agid.gov.it/tag/guloader/) Gli analisti di CERT-AgID hanno osservato GuLoader in Italia per la prima volta verso la fine mese di marzo 2021. Nell’arco dello scorso anno sono state registrate solo 6 campagne che utilizzavano GuLoader sfruttando il tema “Pagamenti“, “Preventivo” e “Ordine” con lo scopo di veicolare il malware AgentTesla ed in un solo caso si è avuta evidenza del rilascio di **Remcos.** Le campagne GuLoader in Italia sono terminate a fine settembre 2021 per poi ripresentarsi nel 2022, mantenendo gli stessi temi, con 4 nuove campagne: una ad aprile, un’altra a metà giugno e le ultime due – ad un mese esatto di distanza – a metà luglio. GuLoader è un dropper che si caratterizza per l’efficacia delle sue misure anti-debug e anti[vm. Il CERT-AgID aveva già discusso la natura di tali misure, anche se al tempo il packer](https://cert-agid.gov.it/wp-content/uploads/2020/06/CERT-AGID_Tecniche-di-AntiVM-AntiDBG-20200514.pdf) non era stato identificato come GuLoader. Ad oggi, tali tecniche sono state affinate e ve ne sono state aggiunte di nuove, al punto che analizzare GuLoader è diventato un compito abbastanza complesso. ## Due vecchie tecniche migliorano GuLoader disponeva di un controllo anti-debug che, anzichè cercare un comportamento anomalo delle API di Windows, verificava la presenza in memoria di artefatti usati dai **debugger (o loro plugin) per nascondersi dai malware.** Esempio di un’area di memoria contenente codice e dati di Scylla, un plugin per nascondere il debugger, in un processo sotto debug. GuLoader non controllava direttamente la presenza di valori specifici ma utilizza l’hashing [DBJ2 su gli indirizzi di porzioni di memoria determinate empiricamente.](https://theartincode.stanis.me/008-djb2/) ----- **Questa tecnica era totalmente efficace nel rilevare i debugger più usati: la chiamata alla** funzione che effettuava questo controllo era facilmente identificabile per via del fatto che prendeva un gran numero di argomenti (gli hash degli artefatti) terminati dal valore ``` 0xffffffff . In questo caso era sufficiente rimpiazzare la chiamata con dei NOP per ``` superare l’ostacolo. Questa tecnica, oggi, esiste ancora ma qualcosa è stato cambiato. Gli argomenti passati alla funzione non si limitano agli hash ma contengono anche dei numeri. Gli argomenti della funzione che controlla la presenza di debugger. Sono coppie composte da un numero ed un hash, terminate da uno zero. Probabilmente la tecnica di rilevamento è stata aggiornata per essere più resiliente ed adeguarsi alla continua evoluzione dei debugger. **Ancora oggi il rilevamento del debugger è efficace e l’unica opzione per eludere questo** controllo è quello di individuare la chiamata e di rimpiazzarla, oppure disinstallare i plugin nella speranza che il debugger non abbia artefatti propri rilevati da GuLoader. Disabilitare i plugin però comporterà la facile individuazione del debugger tramite le usuali tecniche antidebug (es: tramite NtQueryInformationProcess) che GuLoader non disdegna. Per debuggare GuLoader è quindi necessario procedere passo passo fino all’individuazione di questa chiamata. Tuttavia, gli autori del dropper hanno individuato un metodo per rendere l’analisi passo passo molto tediosa. ## Tecniche di Anti-VM Sfortunatamente per noi, GuLoader ha un ottimo controllo anti-vm che continua ad [ingannare anche le sandbox online. Quindi, eseguirlo in una VM insieme ad uno strumento](https://app.any.run/tasks/02138642-5020-4e81-a3cf-b5d9d4715ebf/) in grado di monitorare il traffico di rete non è sufficiente per ottenere il drop URL ed il payload. ----- La tecnica che veniva usata nel campione analizzato nel bollettino allegato era stata battezzata RDSTC trick e si basava su un assunto molto semplice: l’istruzione `cpuid` causa un VM-exit non condizionale ed il suo risultato deve essere alterato dall’hypervisor (poichè descrive le caratteristiche e le estensioni della CPU): questo comporta che in un **ambiente virtualizzato la sua esecuzione sia più lenta che in uno fisico. Per effettuare** questo genere di misurazioni è necessario un timer molto preciso ed a bassa latenza di accesso, il timestamp counter (detto anche TSC e letto tramite `rdtsc ) presente nelle CPU` Intel e compatibili è l’ideale. La parte complessa è tarare bene le soglie di rilevamento. **Nota tecnica** Nelle CPU moderne il TSC è un contatore slegato dalla frequenza e dallo stato energetico della CPU. Tuttavia, lo stato energetico (si vedano le tecnologie di gestione termica di Intel, da SpeedStep a HWP passando per Turbo Boost) della CPU influenza pesantemente il tempo cronometrato di esecuzione delle istruzioni, per cui misurare la durata delle istruzioni con il TSC non è molto affidabile ma probabilmente sufficiente agli scopi. Nel campione attuale questo controllo è stato stravolto. A sinistra: il controllo sulla durata di cpuid e rdtsc tramite il timer di Windows. A destra: il controllo che rdtsc non ritorni valori fasulli. Un workaround per l’RDTSC trick era quello di emulare un TSC lento. La nuova strategia di temporizzazione utilizza il timer di Windows, accede direttamente ad user space tramite KUSER_SHARED_DATA e misura l’esecuzione di `cpuid e` `rdtsc` ripetutamente. Qualora il valore accumulato superi una certa soglia, GuLoader assume di trovarsi in presenza di una VM. Viene aggiunto anche un controllo esplicito che verifica se rdtsc ritorna valori falsi, ad esempio che siano troppo “lenti”. Questi controlli sono efficaci e portano GuLoader a mostrare una finestra di avviso e terminare o entrare volutamente in un ciclo infinito, prevenendo l’analisi automatica. ----- In aggiunta a questi controlli di temporizzazione è sempre presente la verifica del bit 31 di ``` CPUID.1.ecx, che indica la presenza di un hypervisor con supporto di ``` **paravirtualizzazione. Dato che le VM non tendono a nascondersi, questo controllo risulta** efficace. Disabilitare la paravirtualizzazione ha i suoi costi rendendo l’esecuzione della VM più lenta ed onerosa. GuLoader cerca inoltre di determinare se è eseguito dentro una VM anche tramite metodi più convenzionali. In particolare utilizza `EnumDeviceDrivers e` `EnumServicesStatusA per` enumerare i driver ed i servizi tipicamente installati nelle VM paravirtualizzate (es: _vmmouse.sys)._ Anche queste misure sono piuttosto efficaci nel rilevare le VM. Maggiori dettagli sono [riportati in questa analisi di SonicWall.](https://securitynews.sonicwall.com/xmlpost/guloader-a-fileless-shellcode-based-malware-in-action/) ## Tecnica Anti-Analisi Nonostante la presenza di questi controlli, inizialmente era più semplice riconoscerli e saltarli. Oggi questo è diventato più complesso per via di una tecnica anti-analisi introdotta da qualche mese e piuttosto fastidiosa. L’exception handler che sposta l’instruction pointer dopo ogni istruzione int3. Il numero di byte di cui spostare eip in avanti è ottenuto come xor tra 0x9d ed il byte successivo ad int3. ----- Subito dopo aver decifrato il suo secondo stadio, lo shellcode di GuLoader installa un exception handler tramite `RtlAddVectoredExceptionHandler . Questo handler è invocato` tramite delle istruzioni int3 sparse in tutto il codice. Istruzioni int3 sparse per il codide del dropper. Come si intuisce dalla presenza di istruzioni privilegiate, l’esecuzione non è lineare. Come mostra il codice qui sopra, questo handler ha due funzioni: 1. Verifica che non siano presenti breakpoint software (dopo l’istruzione `int3 ) o` hardware. 2. Legge il valore del byte successivo all’istruzione `int3, effettua uno xor con` `0x9D e` aggiunge questo valore all’instruction pointer, di fatto spostando l’esecuzione in avanti. I controlli anti-debug di cui il punto 1) possono essere disabilitati rimpiazzando i salti condizionali con dei nop. Ma il secondo punto rimane problematico: il debugger decodificando le istruzioni sequenzialmente si confonde e diventa impossibile avere una visione d’insieme del codice, rendendo complesso il riconoscimento delle funzioni. Infine, quando l’handler ritorna con il valore `EXCEPTION_CONTINUE_EXECUTION l’esecuzione torna` al codice interrotto tramite `NtContinue, la quale non da modo al debugger di interrompere` immediatamente il processo, di fatto facendo saltare l’analisi “da int3 in int3“. Per aggirare il problema di non controllo sull’esecuzione è necessario ricorrere a degli script per il proprio debugger. Ad esempio, per x64dbg è possibile usare le seguenti istruzioni (quando `eip è su` `int3 ):` ``` $ec = byte(eip + 1); xor $ec, 0x9d; eip = eip + $ec; ``` ## Estrarre il drop url automaticamente ----- La nuova tecnica anti-analisi di GuLoader rende il debug molto tedioso e la presenza di numerosi controlli anti-vm ed anti-debug non permettono l’esecuzione non controllata del dropper. ### È possibile velocizzare l’analisi? Lo shellcode di GuLoader appena avviato salta ad una procedura che decodifica il secondo stadio. La struttura dello shellcode è la seguente: La funzione che decifra il secondo stadio si trova subito prima di esso. C’è una prima parte, in blu, che non viene decodificata: essa contiene il codice di decodifica stesso. Tale codice è chiamato tramite un’istruzione `call situata subito prima dell’inizio del` secondo stadio. Questo fa sì che dentro tale chiamata l’indirizzo di ritorno punti proprio al **secondo stadio.** ----- Decodifica del secondo stadio tramite xor. La freccia verso sinistra indica l’istruzione call subito prima del secondo stadio. A destra il codice di decodifica. Nel secondo stadio, dopo una piccola pausa implementata con un ciclo che esegue `rdtsc,` GuLoader determina l’inizio del secondo stadio cercando la DWORD 0xE9Ea9011. ----- Guloader determina l’inizio del secondo stadio cercando la DWORD 0xE9Ea9011 e sottraendovi 5 (la lunghezza di un jmp lungo). Possiamo ipotizzare che la chiave di decodifica vari da campione a campione: uno strumento automatico dovrebbe essere in grado di estrarla o calcolarla. Estrarla è complesso perchè è generata tramite istruzioni aritmetiche e richiederebbe l’esecuzione concolica (simbolica + concreta) dello shellcode. Analogo discorso per l’inizio del secondo stadio. Un’alternativa è quella di sfruttare le debolezze della cifratura con xor e chiave piccola. Il secondo stadio probabilmente conterrà delle sequenze di byte nulli: queste sequenze rilevano la chiave ma il tutto sta nel capire dove sono. Piuttosto che utilizzare offset fissi, un approccio ragionevole è quello di considerare lo shellcode come una sequenza di DWORD (interi senza segno a 32 bit) ed ordinarli dal più frequente a quello meno frequente. ----- Ipotizziamo che tra i primi valori sia presente anche la rotazione della chiave. Parliamo di rotazione della chiave perchè lo xor può non iniziare ad indirizzi multipli di 4 bytes, ovvero: non è allineato a DWORD e in questa campione non lo fa. Possiamo verificare velocemente questa ipotesi con un po’ di codice Python. La chiave usata nel sample in analisi è 0xb49be733. ``` def count_dwords(data, skew=0): hist = {} for i in range(0 + skew, (len(data)-skew)//4 * 4, 4): dw = struct.unpack(" len(b1): print("Wrap around1") if dbg and i-i1 < 16: print(hex(i % len(b1)), hex(b1[i % len(b1)]), hex((i2 + i-i1) % len(b2)), hex(b2[(i2 + i-i1) % len(b2)]), hex(b1[i % len(b1)] ^ b2[(i2 + ii1) % len(b2)])) res[i-i1] = b1[i % len(b1)] ^ b2[(i2 + i-i1) % len(b2)] return bytes(res) def find_stage2(data, key): sign = b"\x11\x90\xea\xe9" #Alternative signature: b"\xe9\x4d\x01\x00" sign_off = 5 #Alternative offset: 0 dec = b"" for i in range(0, len(data), 4): dec = (dec + xor(data, i, key, 0, 4))[-8:] if sign in dec: j = dec.index(sign) return i+j-4sign_off def shift(data, val): return data[val:] + data[:val] def decrypt_stage2(data): for k, v in count_dwords(data).items(): print(f"🤞 ``` ----- ``` Possible (rotated) decrypt key: {hex(k)} ) key = struct.pack( = 0x20 and possible[j] <= 0x7f) or possible[j] in [0xa, 0xd, 0x00, 0x07]: s += bytes([possible[j]]) else: break s2 = s.replace(b"\x00", b"") if len(s2) >= 5: strs.append(s2) i += len(s) i += 1 return strs def interesting_str(strs): res = False for s in strs: if b"http" in s or b"://" in s: print(s) res = True return res ### Lo script completo ``` **Lo script seguente è un PoC su come estrarre il drop url da un campione GuLoader.** Potrebbe essere necessario sistemare `get_string_key con una nuova firma o` un’euristica. La nuova firma è ottenibile con una breve analisi: è possibile anche posizionare un breakpoint in `ZwAllocateVirtualMemory e poi seguire le chiamate per arrivare` direttamente alla funzione che decifra le stringhe (come mostrata nelle figure precedenti). Lo script si esegue passandogli lo shellcode di GuLoader: questo va estratto manualmente dal vettore di infezione. Il campione in analisi utilizzava uno script NSIS per questo: ----- ``` import struct from binascii import hexlify import sys def read_shellcode(filename): with open(filename, "rb") as f: data = f.read() return data def count_dwords(data, skew=0): hist = {} for i in range(0 + skew, (len(data)-skew)//4 * 4, 4): dw = struct.unpack(" len(b1): print("Wrap around1") if dbg and i-i1 < 16: print(hex(i % len(b1)), hex(b1[i % len(b1)]), hex((i2 + i-i1) % len(b2)), hex(b2[(i2 + i-i1) % len(b2)]), hex(b1[i % len(b1)] ^ b2[(i2 + i-i1) % len(b2)])) res[i-i1] = b1[i % len(b1)] ^ b2[(i2 + i-i1) % len(b2)] return bytes(res) def find_stage2(data, key): sign = b"\x11\x90\xea\xe9" #Alternative signature: b"\xe9\x4d\x01\x00" sign_off = 5 #Alternative offset: 0 dec = b"" for i in range(0, len(data), 4): dec = (dec + xor(data, i, key, 0, 4))[-8:] if sign in dec: j = dec.index(sign) return i+j-4-sign_off def shift(data, val): return data[val:] + data[:val] def decrypt_stage2(data): for k, v in count_dwords(data).items(): print(f"🤞 Possible (rotated) decrypt key: {hex(k)}") key = struct.pack("= 0x20 and possible[j] <= 0x7f) or possible[j] in [0xa, 0xd, 0x00, 0x07]: s += bytes([possible[j]]) else: break s2 = s.replace(b"\x00", b"") if len(s2) >= 5: strs.append(s2) i += len(s) i += 1 return strs def interesting_str(strs): res = False for s in strs: if b"http" in s or b"://" in s: print(s) res = True return res def extract_info(data): dec_data = decrypt_stage2(data) print("Looking for the string key.") str_key = get_string_key(dec_data) if str_key is None: print("💔 No string key found. Aborted.") return False else: print(f"🥳 String key found: {hexlify(str_key)}") print("Finding strings by bruteforce...") strs = find_strs(dec_data, str_key) print("Interesting strings found:") return interesting_str(strs) # # MAIN # if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} SHELLCODE_FILENAME", file=sys.stderr) sys.exit(1) sys.exit(2 if not extract_info(read_shellcode(sys.argv[1])) else 0) ``` **Esempio** ----- ``` $ python3 gl.py /shared/guloader_shellcode ``` 🤞 Possible (rotated) decrypt key: 0x33e7b49b ``` Stage 2 found at offset 0x20ea Adjusted key to: 0xb49b33e7 Stage 2 decrypted. Looking for the string key. ``` 🥳 String key found: ``` b'0fc5fc4b7eb350b07d046090e4a0b73cb0100ed1063f658e0d43f257ec17708039314398012d065c9e46 Finding strings by bruteforce... Interesting strings found: b'https://91news.in/bcwq_WFnUhj158.bin' ``` Ottenuto il drop url è quindi possibile scaricare il payload. I primi 64 byte sono random e non usati, i restanti sono un PE xorato con una chiave. Non essendo il payload disponibile al momento di questa analisi non abbiamo potuto automatizzare la sua decodifica. Si suggeriscono comunque due approcci: La chiave è solitamente tra i 0x200 e i 0x380 byte, i PE contengono spesso lunghe sequenze di byte nulli che rileverebbero la chiave. Cercando una sequenza ripetuta è possibile estrarre la chiave. Alcuni campi di un PE sono noti, questo rileva parte della chiave. ### Aggiornamento In seguito all’analisi di ulteriori sample è stato notato che la variabilità tra questi è troppo alta affinchè un approccio basato sul riconoscimento di firme (come avviene nello script sopra) possa funzionare. L’alternativa è quella di utilizzare un approccio puramente bruteforce: 1. Enumerare le prime n DWORD più presenti nello shellcode. La speranza è che qualcuna di queste sia la chiave XOR per decodificare il secondo stadio (l’idea è che il secondo stadio contenga un numero elevato di zeri e quindi una volta cifrato un numero elevato di DWORD che corrispondono alla chiave). 2. Per ogni DWORD, usarla come chiave per decodificare il secondo stadio. Contrariamente a prima non sono fatti controlli riguardo la validità del secondo stadio ottenuto. 3. Cercare tutte le chiamate dirette relative all’indietro, il cui offset sia compreso tra **valori negativi piccoli (di default lo script usa -500 e -100). Questo passo identifica** ogni possibile chiamata che delimita la chiave per decifrare le stringhe. Contrariamente a prima non sono fatte verifiche e tutti i candidati sono presi in considerazione. 4. Usare tutte le chiavi candidate ottenute al punto 3 per decifrare le stringhe e mostrare quelle che contengono determinati caratteri (es: http). ----- Oltre a questo approccio puramente bruteforce, è stata aggiunta la possibilità di continuare la ricerca quando viene trovata una stringa di interesse e soprattutto di salvare su file il secondo stadio decodificato. Questo tornerà utile per decodificare il payload. ``` import struct from binascii import hexlify import sys #After this many bytes, stop decoding a (so far valid) string. NOTE: Guloader uses UTF-16, the #no. of chars is halved! MAX_STRING_LEN = 150 #Try this many possible Stage2 decode keys MAX_STAGE2_DECODE_KEYS = 10 #The call before the String key must have an immediate between min and max (a bigger gap find more candidates) DECODE_STRING_MIN_IMMEDIATE = -500 DECODE_STRING_MAX_IMMEDIATE = -100 #Read a binary file def read_shellcode(filename): with open(filename, "rb") as f: data = f.read() return data #Count the DWORD in an array of bytes, not counting DWORD with the MSB equal to zero def count_dwords(data, skew=0): hist = {} for i in range(0 + skew, (len(data)-skew)//4 * 4, 4): dw = struct.unpack("= DECODE_STRING_MAX_IMMEDIATE or imm < DECODE_STRING_MIN_IMMEDIATE: continue keys.append(data[i+5:i+5+0x2b]) return keys #Decode Strings def find_strs(data, skey, mlen=MAX_STRING_LEN): strs = [] i = 0 while i < len(data): possible = xor(data, i, skey, 0, mlen) s = b""; for j in range(len(possible)): if (possible[j] >= 0x20 and possible[j] <= 0x7f) or possible[j] in [0xa, 0xd, 0x00, 0x07]: s += bytes([possible[j]]) else: break s2 = s.replace(b"\x00", b"") if len(s2) >= 5: strs.append(s2) i += len(s) i += 1 return strs def interesting_str(strs): res = False for s in strs: if b"http" in s or b"://" in s: print(s) res = True return res def bruteforce(data): for k, v in count_dwords(data).items(): print(f"🤞 Possible (rotated) decrypt key: {hex(k)}") key = struct.pack("= 10: print(f"A candidate matched") #Find the key key = xor(stage2, i, struct.pack("