{
	"id": "ce925715-770f-4040-b0d3-e2a5c6fe914c",
	"created_at": "2026-04-06T00:18:04.595366Z",
	"updated_at": "2026-04-10T03:20:03.344842Z",
	"deleted_at": null,
	"sha1_hash": "5defb8d1ca6281e415c12f22a790436f5274300c",
	"title": "Tecniche per semplificare l’analisi del malware GuLoader",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1803317,
	"plain_text": "Tecniche per semplificare l’analisi del malware GuLoader\r\nArchived: 2026-04-05 14:11:08 UTC\r\nGli analisti di CERT-AgID hanno osservato GuLoader in Italia per la prima volta verso la fine mese di marzo\r\n2021. Nell’arco dello scorso anno sono state registrate solo 6 campagne che utilizzavano GuLoader sfruttando il\r\ntema “Pagamenti“, “Preventivo” e “Ordine” con lo scopo di veicolare il malware AgentTesla ed in un solo caso si\r\nè avuta evidenza del rilascio di Remcos.\r\nLe campagne GuLoader in Italia sono terminate a fine settembre 2021 per poi ripresentarsi nel 2022, mantenendo\r\ngli stessi temi, con 4 nuove campagne: una ad aprile, un’altra a metà giugno e le ultime due – ad un mese esatto di\r\ndistanza – a metà luglio.\r\nGuLoader è un dropper che si caratterizza per l’efficacia delle sue misure anti-debug e anti-vm. Il CERT-AgID\r\naveva già discusso la natura di tali misure, anche se al tempo il packer non era stato identificato come GuLoader.\r\nAd oggi, tali tecniche sono state affinate e ve ne sono state aggiunte di nuove, al punto che analizzare GuLoader è\r\ndiventato un compito abbastanza complesso.\r\nDue vecchie tecniche migliorano\r\nGuLoader disponeva di un controllo anti-debug che, anzichè cercare un comportamento anomalo delle API di\r\nWindows, verificava la presenza in memoria di artefatti usati dai debugger (o loro plugin) per nascondersi dai\r\nmalware.\r\nEsempio di un’area di memoria contenente codice e dati di Scylla, un plugin per nascondere il\r\ndebugger, in un processo sotto debug.\r\nGuLoader non controllava direttamente la presenza di valori specifici ma utilizza l’hashing DBJ2 su gli indirizzi\r\ndi porzioni di memoria determinate empiricamente.\r\nQuesta tecnica era totalmente efficace nel rilevare i debugger più usati: la chiamata alla funzione che\r\neffettuava questo controllo era facilmente identificabile per via del fatto che prendeva un gran numero di\r\nargomenti (gli hash degli artefatti) terminati dal valore 0xffffffff . In questo caso era sufficiente rimpiazzare la\r\nchiamata con dei NOP per superare l’ostacolo.\r\nQuesta tecnica, oggi, esiste ancora ma qualcosa è stato cambiato. Gli argomenti passati alla funzione non si\r\nlimitano agli hash ma contengono anche dei numeri.\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 1 of 16\n\nGli argomenti della funzione che controlla la presenza di debugger. Sono coppie composte da un\r\nnumero ed un hash, terminate da uno zero.\r\nProbabilmente la tecnica di rilevamento è stata aggiornata per essere più resiliente ed adeguarsi alla continua\r\nevoluzione dei debugger.\r\nAncora oggi il rilevamento del debugger è efficace e l’unica opzione per eludere questo controllo è quello di\r\nindividuare la chiamata e di rimpiazzarla, oppure disinstallare i plugin nella speranza che il debugger non abbia\r\nartefatti propri rilevati da GuLoader. Disabilitare i plugin però comporterà la facile individuazione del debugger\r\ntramite le usuali tecniche anti-debug (es: tramite NtQueryInformationProcess) che GuLoader non disdegna. Per\r\ndebuggare GuLoader è quindi necessario procedere passo passo fino all’individuazione di questa chiamata.\r\nTuttavia, gli autori del dropper hanno individuato un metodo per rendere l’analisi passo passo molto tediosa.\r\nTecniche di Anti-VM\r\nSfortunatamente per noi, GuLoader ha un ottimo controllo anti-vm che continua ad ingannare anche le sandbox\r\nonline. Quindi, eseguirlo in una VM insieme ad uno strumento in grado di monitorare il traffico di rete non è\r\nsufficiente per ottenere il drop URL ed il payload.\r\nLa tecnica che veniva usata nel campione analizzato nel bollettino allegato era stata battezzata RDSTC trick e si\r\nbasava su un assunto molto semplice: l’istruzione cpuid causa un VM-exit non condizionale ed il suo risultato\r\ndeve essere alterato dall’hypervisor (poichè descrive le caratteristiche e le estensioni della CPU): questo comporta\r\nche in un ambiente virtualizzato la sua esecuzione sia più lenta che in uno fisico. Per effettuare questo genere\r\ndi misurazioni è necessario un timer molto preciso ed a bassa latenza di accesso, il timestamp counter (detto anche\r\nTSC e letto tramite rdtsc ) presente nelle CPU Intel e compatibili è l’ideale. La parte complessa è tarare bene\r\nle soglie di rilevamento.\r\nNota tecnica\r\nNelle CPU moderne il TSC è un contatore slegato dalla frequenza e dallo stato energetico della CPU.\r\nTuttavia, lo stato energetico (si vedano le tecnologie di gestione termica di Intel, da SpeedStep a HWP\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 2 of 16\n\npassando per Turbo Boost) della CPU influenza pesantemente il tempo cronometrato di esecuzione\r\ndelle istruzioni, per cui misurare la durata delle istruzioni con il TSC non è molto affidabile ma\r\nprobabilmente sufficiente agli scopi.\r\nNel campione attuale questo controllo è stato stravolto.\r\nA sinistra: il controllo sulla durata di cpuid e rdtsc tramite il timer di Windows. A destra: il controllo\r\nche rdtsc non ritorni valori fasulli.\r\nUn workaround per l’RDTSC trick era quello di emulare un TSC lento.\r\nLa nuova strategia di temporizzazione utilizza il timer di Windows, accede direttamente ad user space tramite\r\nKUSER_SHARED_DATA e misura l’esecuzione di cpuid e rdtsc ripetutamente. Qualora il valore\r\naccumulato superi una certa soglia, GuLoader assume di trovarsi in presenza di una VM. Viene aggiunto anche un\r\ncontrollo esplicito che verifica se rdtsc ritorna valori falsi, ad esempio che siano troppo “lenti”.\r\nQuesti controlli sono efficaci e portano GuLoader a mostrare una finestra di avviso e terminare o entrare\r\nvolutamente in un ciclo infinito, prevenendo l’analisi automatica.\r\nIn aggiunta a questi controlli di temporizzazione è sempre presente la verifica del bit 31 di CPUID.1.ecx , che\r\nindica la presenza di un hypervisor con supporto di paravirtualizzazione. Dato che le VM non tendono a\r\nnascondersi, questo controllo risulta efficace. Disabilitare la paravirtualizzazione ha i suoi costi rendendo\r\nl’esecuzione della VM più lenta ed onerosa.\r\nGuLoader cerca inoltre di determinare se è eseguito dentro una VM anche tramite metodi più convenzionali. In\r\nparticolare utilizza EnumDeviceDrivers e EnumServicesStatusA per enumerare i driver ed i servizi tipicamente\r\ninstallati nelle VM paravirtualizzate (es: vmmouse.sys).\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 3 of 16\n\nAnche queste misure sono piuttosto efficaci nel rilevare le VM. Maggiori dettagli sono riportati in questa analisi di\r\nSonicWall.\r\nTecnica Anti-Analisi\r\nNonostante la presenza di questi controlli, inizialmente era più semplice riconoscerli e saltarli. Oggi questo è\r\ndiventato più complesso per via di una tecnica anti-analisi introdotta da qualche mese e piuttosto fastidiosa.\r\nL’exception handler che sposta l’instruction pointer dopo ogni istruzione int3. Il numero di byte di\r\ncui spostare eip in avanti è ottenuto come xor tra 0x9d ed il byte successivo ad int3.\r\nSubito dopo aver decifrato il suo secondo stadio, lo shellcode di GuLoader installa un exception handler tramite\r\nRtlAddVectoredExceptionHandler . Questo handler è invocato tramite delle istruzioni int3 sparse in tutto il\r\ncodice.\r\nIstruzioni int3 sparse per il codide del dropper. Come si intuisce dalla presenza di istruzioni\r\nprivilegiate, l’esecuzione non è lineare.\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 4 of 16\n\nCome mostra il codice qui sopra, questo handler ha due funzioni:\r\n1. Verifica che non siano presenti breakpoint software (dopo l’istruzione int3 ) o hardware.\r\n2. Legge il valore del byte successivo all’istruzione int3 , effettua uno xor con 0x9D e aggiunge questo\r\nvalore all’instruction pointer, di fatto spostando l’esecuzione in avanti.\r\nI controlli anti-debug di cui il punto 1) possono essere disabilitati rimpiazzando i salti condizionali con dei nop.\r\nMa il secondo punto rimane problematico: il debugger decodificando le istruzioni sequenzialmente si confonde e\r\ndiventa impossibile avere una visione d’insieme del codice, rendendo complesso il riconoscimento delle funzioni.\r\nInfine, quando l’handler ritorna con il valore EXCEPTION_CONTINUE_EXECUTION l’esecuzione torna al codice\r\ninterrotto tramite NtContinue , la quale non da modo al debugger di interrompere immediatamente il processo, di\r\nfatto facendo saltare l’analisi “da int3 in int3“.\r\nPer aggirare il problema di non controllo sull’esecuzione è necessario ricorrere a degli script per il proprio\r\ndebugger. Ad esempio, per x64dbg è possibile usare le seguenti istruzioni (quando eip è su int3 ):\r\n$ec = byte(eip + 1); xor $ec, 0x9d; eip = eip + $ec;\r\nEstrarre il drop url automaticamente\r\nLa 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.\r\nÈ possibile velocizzare l’analisi?\r\nLo shellcode di GuLoader appena avviato salta ad una procedura che decodifica il secondo stadio. La struttura\r\ndello shellcode è la seguente:\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 5 of 16\n\nLa funzione che decifra il secondo stadio si trova subito prima di esso.\r\nC’è una prima parte, in blu, che non viene decodificata: essa contiene il codice di decodifica stesso. Tale codice è\r\nchiamato tramite un’istruzione call situata subito prima dell’inizio del secondo stadio. Questo fa sì che dentro\r\ntale chiamata l’indirizzo di ritorno punti proprio al secondo stadio.\r\nDecodifica del secondo stadio tramite xor. La freccia verso sinistra indica l’istruzione call subito\r\nprima del secondo stadio. A destra il codice di decodifica.\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 6 of 16\n\nNel secondo stadio, dopo una piccola pausa implementata con un ciclo che esegue rdtsc , GuLoader determina\r\nl’inizio del secondo stadio cercando la DWORD 0xE9Ea9011.\r\nGuloader determina l’inizio del secondo stadio cercando la DWORD 0xE9Ea9011 e sottraendovi 5\r\n(la lunghezza di un jmp lungo).\r\nPossiamo ipotizzare che la chiave di decodifica vari da campione a campione: uno strumento automatico dovrebbe\r\nessere in grado di estrarla o calcolarla. Estrarla è complesso perchè è generata tramite istruzioni aritmetiche e\r\nrichiederebbe l’esecuzione concolica (simbolica + concreta) dello shellcode. Analogo discorso per l’inizio del\r\nsecondo stadio.\r\nUn’alternativa è quella di sfruttare le debolezze della cifratura con xor e chiave piccola.\r\nIl secondo stadio probabilmente conterrà delle sequenze di byte nulli: queste sequenze rilevano la chiave ma il\r\ntutto sta nel capire dove sono. Piuttosto che utilizzare offset fissi, un approccio ragionevole è quello di considerare\r\nlo shellcode come una sequenza di DWORD (interi senza segno a 32 bit) ed ordinarli dal più frequente a quello\r\nmeno frequente.\r\nIpotizziamo che tra i primi valori sia presente anche la rotazione della chiave. Parliamo di rotazione della chiave\r\nperchè lo xor può non iniziare ad indirizzi multipli di 4 bytes, ovvero: non è allineato a DWORD e in questa\r\ncampione non lo fa.\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 7 of 16\n\nPossiamo verificare velocemente questa ipotesi con un po’ di codice Python. La chiave usata nel sample in analisi\r\nè 0xb49be733.\r\ndef count_dwords(data, skew=0): hist = {} for i in range(0 + skew, (len(data)-skew)//4 * 4, 4): dw =\r\nstruct.unpack(\"\u003cI\", data[i:i+4])[0] if dw \u003c= 0xffffff: continue if dw not in hist: hist[dw] = 1 else:\r\nhist[dw] += 1 return {x[0]:x[1] for i, x in enumerate(sorted(hist.items(), key=lambda x: -x[1])) if i\r\n\u003c 10}\r\nIl risultato di count_dwords mostra che la (rotazione) della chiave è il primo risultato:\r\n0x33e7b49b 53\r\n0xff000000 39\r\n0xbae7b49a 37\r\n0xffe7b49a 35\r\n0xfbfbfbfb 26\r\n0x78787878 24\r\n0xbae7b499 24\r\n0x49494949 23\r\n0xe5e5e5e5 23\r\n0x74747474 23\r\nPer ogni possibile chiave, possiamo fare lo xor con lo shellcode, includendo la parte blu, visto che non sappiamo\r\ndove finisce, e verificare la presenza del valore 0xE9EA9011, esattamente come fa GuLoader. Questo ci permette\r\nnon solo di confermare che la chiave è giusta ma anche di determinare dove inizia il secondo stadio in modo\r\nda decifrare soltanto quello.\r\ndef count_dwords(data, skew=0): hist = {} for i in range(0 + skew, (len(data)-skew)//4 * 4, 4): dw =\r\nstruct.unpack(\"\u003cI\", data[i:i+4])[0] if dw \u003c= 0xffffff: continue if dw not in hist: hist[dw] = 1 else:\r\nhist[dw] += 1 return {x[0]:x[1] for i, x in enumerate(sorted(hist.items(), key=lambda x: -x[1])) if i\r\n\u003c 10} def xor(b1, i1, b2, i2, l, dbg=False): res = [0] * l for i in range(i1, i1+l): if dbg and i \u003e\r\nlen(b1): print(\"Wrap around1\") if dbg and i-i1 \u003c 16: print(hex(i % len(b1)), hex(b1[i % len(b1)]),\r\nhex((i2 + i-i1) % len(b2)), hex(b2[(i2 + i-i1) % len(b2)]), hex(b1[i % len(b1)] ^ b2[(i2 + i-i1) %\r\nlen(b2)])) res[i-i1] = b1[i % len(b1)] ^ b2[(i2 + i-i1) % len(b2)] return bytes(res) def\r\nfind_stage2(data, key): sign = b\"\\x11\\x90\\xea\\xe9\" #Alternative signature: b\"\\xe9\\x4d\\x01\\x00\"\r\nsign_off = 5 #Alternative offset: 0 dec = b\"\" for i in range(0, len(data), 4): dec = (dec + xor(data,\r\ni, key, 0, 4))[-8:] if sign in dec: j = dec.index(sign) return i+j-4-sign_off def shift(data, val):\r\nreturn data[val:] + data[:val] def decrypt_stage2(data): for k, v in count_dwords(data).items():\r\nprint(f\"🤞 Possible (rotated) decrypt key: {hex(k)}\") key = struct.pack(\"\u003cI\", k) offset =\r\nfind_stage2(data, key) if offset is None: print(f\"😐 No stage found for this key, trying next one.\")\r\ncontinue else: print(f\"Stage 2 found at offset {hex(offset)}\") key = shift(key, offset \u0026 0x3)\r\nprint(f\"Adjusted key to: {hex(struct.unpack('\u003cI', key)[0])}\") dec_data = data[:offset] + xor(data,\r\noffset, key, 0, len(data)-offset) print(f\"🦾 Stage 2 decrypted.\") return dec_data\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 8 of 16\n\nNel campione analizzato il secondo stadio inizia a 0x20ea. Il risultato dello script Python conferma che la\r\ndecifratura è corretta:\r\n🤞 Possible (rotated) decrypt key: 0x33e7b49b\r\nStage 2 found at offset 0x20ea\r\nAdjusted key to: 0xb49b33e7\r\n🦾 Stage 2 decrypted.\r\nCome possiamo usare il codice del secondo stadio per velocizzare l’analisi?\r\nIl drop url è contenuto in una stringa codificata. Fortunatamente la prima azione di GuLoader, dopo aver\r\ndeterminato l’inizio del secondo stadio, è decodifcare la stringa L”ntdll” per cui possiamo subito analizzare come\r\navviene questo processo.\r\nTenere traccia degli indirizzi è tedioso per via del codice superfluo: le stringhe sono salvate xorate con una chiave\r\ndi 0x2b byte e precedute da una DWORD che indica la lunghezza, anch’essa è xorata con una costante. Gli offset\r\ndove trovare queste stringhe codificate sono probabilmente fissi e generati tramite istruzioni aritmetiche per cui\r\nottenerli è complicato.\r\nDa sinistra a destra: La funzione che ottiene l’indirizzo della chiave tramite il proprio indirizzo di\r\nritorno, la funzione intermedia che passa i parametri alla vera procedura di decifratura, il codice di\r\ndecifratura.\r\nTuttavia, se avessimo la chiave, potremmo provare un bruteforce alla ricerca di stringhe stampabili e, tra\r\nqueste, quelle che iniziano per http o contengono ://.\r\nCon un po’ di pazienza si trova facilmente che GuLoader ottiene l’indirizzo della chiave per decifrare le stringhe\r\nin modo analogo a come ottiene l’indirizzo del secondo stadio: ovvero tramite una chiamata posizionata subito\r\nprima della chiave. Con un po’ di debug si scopre che la lunghezza di questa chiave è 0x2b byte.\r\nCome trovare la chiave nel secondo stadio decifrato?\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 9 of 16\n\nL’idea è di cercare tutte le chiamate con opcode 0xe8 e offset negativo (salto all’indietro) e considerare i byte\r\nsuccessivi come la chiave. La speranza è che non ve ne siano molte. In realtà possiamo provare a cercare\r\nesattamente i byte 0xe8, 0xd9, 0xfe, 0xff se ipotizziamo che la distanza tra la chiave e la funzione di decifratura\r\nnon cambi ed eventualmente tornare ad un metodo bruteforce nel caso questo fallisca.\r\nOttenuta la chiave è possibile fare un bruteforce su ogni offset e prendere le stringhe stampabili di almeno n\r\ncaratteri. Si deve porre attenzione al fatto che le stringhe sono, o potrebbero essere, in UTF-16.\r\nNello script di seguito riportato vengono ricercate tutte le stringhe ma mostrate solo quelle con http o :// ed è\r\npossibile ottimizzarlo per cercare solo quelle di interesse:\r\ndef get_string_key(data): call_strdec = b\"\\xe8\\xd9\\xfe\\xff\\xff\" #The string key are the 0x2b bytes\r\nafter this call. #If this fails, we can try looking for all E8 (relative) calls if call_strdec in\r\ndata: i = data.index(call_strdec)+5 return data[i:i+0x2b] def find_strs(data, skey, mlen=100): strs =\r\n[] i = 0 while i \u003c len(data): possible = xor(data, i, skey, 0, mlen) s = b\"\"; for j in\r\nrange(len(possible)): if (possible[j] \u003e= 0x20 and possible[j] \u003c= 0x7f) or possible[j] in [0xa, 0xd,\r\n0x00, 0x07]: s += bytes([possible[j]]) else: break s2 = s.replace(b\"\\x00\", b\"\") if len(s2) \u003e= 5:\r\nstrs.append(s2) i += len(s) i += 1 return strs def interesting_str(strs): res = False for s in strs:\r\nif b\"http\" in s or b\"://\" in s: print(s) res = True return res\r\nLo script completo\r\nLo script seguente è un PoC su come estrarre il drop url da un campione GuLoader. Potrebbe essere\r\nnecessario sistemare get_string_key con una nuova firma o un’euristica. La nuova firma è ottenibile con una\r\nbreve analisi: è possibile anche posizionare un breakpoint in ZwAllocateVirtualMemory e poi seguire le chiamate\r\nper arrivare direttamente alla funzione che decifra le stringhe (come mostrata nelle figure precedenti).\r\nLo script si esegue passandogli lo shellcode di GuLoader: questo va estratto manualmente dal vettore di infezione.\r\nIl campione in analisi utilizzava uno script NSIS per questo:\r\nimport struct from binascii import hexlify import sys def read_shellcode(filename): with\r\nopen(filename, \"rb\") as f: data = f.read() return data def count_dwords(data, skew=0): hist = {} for i\r\nin range(0 + skew, (len(data)-skew)//4 * 4, 4): dw = struct.unpack(\"\u003cI\", data[i:i+4])[0] if dw \u003c=\r\n0xffffff: continue if dw not in hist: hist[dw] = 1 else: hist[dw] += 1 return {x[0]:x[1] for i, x in\r\nenumerate(sorted(hist.items(), key=lambda x: -x[1])) if i \u003c 10} def xor(b1, i1, b2, i2, l, dbg=False):\r\nres = [0] * l for i in range(i1, i1+l): if dbg and i \u003e len(b1): print(\"Wrap around1\") if dbg and i-i1\r\n\u003c 16: print(hex(i % len(b1)), hex(b1[i % len(b1)]), hex((i2 + i-i1) % len(b2)), hex(b2[(i2 + i-i1) %\r\nlen(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\r\nsignature: b\"\\xe9\\x4d\\x01\\x00\" sign_off = 5 #Alternative offset: 0 dec = b\"\" for i in range(0,\r\nlen(data), 4): dec = (dec + xor(data, i, key, 0, 4))[-8:] if sign in dec: j = dec.index(sign) return\r\ni+j-4-sign_off def shift(data, val): return data[val:] + data[:val] def decrypt_stage2(data): for k, v\r\nin count_dwords(data).items(): print(f\"🤞 Possible (rotated) decrypt key: {hex(k)}\") key =\r\nstruct.pack(\"\u003cI\", k) offset = find_stage2(data, key) if offset is None: print(f\"😐 No stage found for\r\nthis key, trying next one.\") continue else: print(f\"Stage 2 found at offset {hex(offset)}\") key =\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 10 of 16\n\nshift(key, offset \u0026 0x3) print(f\"Adjusted key to: {hex(struct.unpack('\u003cI', key)[0])}\") dec_data =\r\ndata[:offset] + xor(data, offset, key, 0, len(data)-offset) print(f\"🦾 Stage 2 decrypted.\") return\r\ndec_data def get_string_key(data): call_strdec = b\"\\xe8\\xd9\\xfe\\xff\\xff\" #The string key are the 0x2b\r\nbytes after this call. #If this fails, we can try looking for all E8 (relative) calls if call_strdec\r\nin data: i = data.index(call_strdec)+5 return data[i:i+0x2b] def find_strs(data, skey, mlen=100): strs\r\n= [] i = 0 while i \u003c len(data): possible = xor(data, i, skey, 0, mlen) s = b\"\"; for j in\r\nrange(len(possible)): if (possible[j] \u003e= 0x20 and possible[j] \u003c= 0x7f) or possible[j] in [0xa, 0xd,\r\n0x00, 0x07]: s += bytes([possible[j]]) else: break s2 = s.replace(b\"\\x00\", b\"\") if len(s2) \u003e= 5:\r\nstrs.append(s2) i += len(s) i += 1 return strs def interesting_str(strs): res = False for s in strs:\r\nif b\"http\" in s or b\"://\" in s: print(s) res = True return res def extract_info(data): dec_data =\r\ndecrypt_stage2(data) print(\"Looking for the string key.\") str_key = get_string_key(dec_data) if\r\nstr_key is None: print(\"💔 No string key found. Aborted.\") return False else: print(f\"🥳 String key\r\nfound: {hexlify(str_key)}\") print(\"Finding strings by bruteforce...\") strs = find_strs(dec_data,\r\nstr_key) print(\"Interesting strings found:\") return interesting_str(strs) # # MAIN # if len(sys.argv)\r\n!= 2: print(f\"Usage: {sys.argv[0]} SHELLCODE_FILENAME\", file=sys.stderr) sys.exit(1) sys.exit(2 if not\r\nextract_info(read_shellcode(sys.argv[1])) else 0)\r\nEsempio\r\n$ python3 gl.py ~/shared/guloader_shellcode\r\n🤞 Possible (rotated) decrypt key: 0x33e7b49b\r\nStage 2 found at offset 0x20ea\r\nAdjusted key to: 0xb49b33e7\r\n🦾 Stage 2 decrypted.\r\nLooking for the string key.\r\n🥳 String key found: b'0fc5fc4b7eb350b07d046090e4a0b73cb0100ed1063f658e0d43f257ec17708039314398012d065c9e4663'\r\nFinding strings by bruteforce...\r\nInteresting strings found:\r\nb'https://91news.in/bcwq_WFnUhj158.bin'\r\nOttenuto il drop url è quindi possibile scaricare il payload. I primi 64 byte sono random e non usati, i restanti sono\r\nun PE xorato con una chiave.\r\nNon essendo il payload disponibile al momento di questa analisi non abbiamo potuto automatizzare la sua\r\ndecodifica. Si suggeriscono comunque due approcci:\r\nLa chiave è solitamente tra i 0x200 e i 0x380 byte, i PE contengono spesso lunghe sequenze di byte nulli\r\nche rileverebbero la chiave. Cercando una sequenza ripetuta è possibile estrarre la chiave.\r\nAlcuni campi di un PE sono noti, questo rileva parte della chiave.\r\nAggiornamento\r\nIn seguito all’analisi di ulteriori sample è stato notato che la variabilità tra questi è troppo alta affinchè un\r\napproccio basato sul riconoscimento di firme (come avviene nello script sopra) possa funzionare.\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 11 of 16\n\nL’alternativa è quella di utilizzare un approccio puramente bruteforce:\r\n1. Enumerare le prime n DWORD più presenti nello shellcode. La speranza è che qualcuna di queste sia la\r\nchiave XOR per decodificare il secondo stadio (l’idea è che il secondo stadio contenga un numero elevato\r\ndi zeri e quindi una volta cifrato un numero elevato di DWORD che corrispondono alla chiave).\r\n2. Per ogni DWORD, usarla come chiave per decodificare il secondo stadio. Contrariamente a prima non\r\nsono fatti controlli riguardo la validità del secondo stadio ottenuto.\r\n3. Cercare tutte le chiamate dirette relative all’indietro, il cui offset sia compreso tra valori negativi\r\npiccoli (di default lo script usa -500 e -100). Questo passo identifica ogni possibile chiamata che delimita\r\nla chiave per decifrare le stringhe. Contrariamente a prima non sono fatte verifiche e tutti i candidati sono\r\npresi in considerazione.\r\n4. Usare tutte le chiavi candidate ottenute al punto 3 per decifrare le stringhe e mostrare quelle che\r\ncontengono determinati caratteri (es: http).\r\nOltre a questo approccio puramente bruteforce, è stata aggiunta la possibilità di continuare la ricerca quando viene\r\ntrovata una stringa di interesse e soprattutto di salvare su file il secondo stadio decodificato. Questo tornerà utile\r\nper decodificare il payload.\r\nimport struct from binascii import hexlify import sys #After this many bytes, stop decoding a (so far\r\nvalid) string. NOTE: Guloader uses UTF-16, the #no. of chars is halved! MAX_STRING_LEN = 150 #Try this\r\nmany possible Stage2 decode keys MAX_STAGE2_DECODE_KEYS = 10 #The call before the String key must have\r\nan immediate between min and max (a bigger gap find more candidates) DECODE_STRING_MIN_IMMEDIATE =\r\n-500 DECODE_STRING_MAX_IMMEDIATE = -100 #Read a binary file def read_shellcode(filename): with\r\nopen(filename, \"rb\") as f: data = f.read() return data #Count the DWORD in an array of bytes, not\r\ncounting DWORD with the MSB equal to zero def count_dwords(data, skew=0): hist = {} for i in range(0 +\r\nskew, (len(data)-skew)//4 * 4, 4): dw = struct.unpack(\"\u003cI\", data[i:i+4])[0] if dw \u003c= 0xffffff:\r\ncontinue if dw not in hist: hist[dw] = 1 else: hist[dw] += 1 return {x[0]:x[1] for i, x in\r\nenumerate(sorted(hist.items(), key=lambda x: -x[1])) if i \u003c MAX_STAGE2_DECODE_KEYS} #XOR b1[i1, i1+l]\r\nwith b2[i2:i2+l] and return the result (which has length l!) def xor(b1, i1, b2, i2, l): res = [0] * l\r\nfor i in range(i1, i1+l): res[i-i1] = b1[i % len(b1)] ^ b2[(i2 + i-i1) % len(b2)] return bytes(res)\r\n#Find all calls with negative offset, not too big nor too small def get_string_keys(data): keys = []\r\nfor i in range(0, len(data)-4-0x2b): if data[i] != 0xe8: continue imm = struct.unpack(\"\u003ci\",\r\ndata[i+1:i+5])[0] if imm \u003e= DECODE_STRING_MAX_IMMEDIATE or imm \u003c DECODE_STRING_MIN_IMMEDIATE: continue\r\nkeys.append(data[i+5:i+5+0x2b]) return keys #Decode Strings def find_strs(data, skey,\r\nmlen=MAX_STRING_LEN): strs = [] i = 0 while i \u003c len(data): possible = xor(data, i, skey, 0, mlen) s =\r\nb\"\"; for j in range(len(possible)): if (possible[j] \u003e= 0x20 and possible[j] \u003c= 0x7f) or possible[j] in\r\n[0xa, 0xd, 0x00, 0x07]: s += bytes([possible[j]]) else: break s2 = s.replace(b\"\\x00\", b\"\") if len(s2)\r\n\u003e= 5: strs.append(s2) i += len(s) i += 1 return strs def interesting_str(strs): res = False for s in\r\nstrs: if b\"http\" in s or b\"://\" in s: print(s) res = True return res def bruteforce(data): for k, v in\r\ncount_dwords(data).items(): print(f\"🤞 Possible (rotated) decrypt key: {hex(k)}\") key = struct.pack(\"\r\n\u003cI\", k) print(f\"👽 Decoding the shellcode...\") dec_data = xor(data, 0, key, 0, len(data)) print(f\"🔍\r\nFinding the possible string keys...\") keys = get_string_keys(dec_data) print(f\"️ Found {len(keys)}\r\nkeys. Brute forcing...\") for k in keys: print(f\"🤞 Trying key {hexlify(k)}\") strs =\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 12 of 16\n\nfind_strs(dec_data, k) if interesting_str(strs): while True: action = input(\"Type c to continue the\r\nsearch, q to quit, s FILENAME to save the decoded stage and exit: \").split(\" \", 2) cmd =\r\naction[0].lower() if cmd == \"q\": return True elif cmd == \"c\": break elif cmd == \"s\": with\r\nopen(action[1].strip(), \"wb\") as f: f.write(dec_data) return True return False # # MAIN # if\r\nlen(sys.argv) != 2: print(f\"Usage: {sys.argv[0]} SHELLCODE_FILENAME\", file=sys.stderr) sys.exit(1)\r\nsys.exit(2 if not bruteforce(read_shellcode(sys.argv[1])) else 0)\r\nIl payload scaricabile dal drop URL è codificato tramite XOR con una chiave la cui lunghezza (e valore) cambia\r\nda sample a sample.\r\nLa chiave non è salvata nel secondo stadio in chiaro ma è a sua volta XORata con una WORD (intero di 16 bit)\r\nche guloader calcola a runtime tramite bruteforcing. L’algoritmo che usa Guloader è il seguente.\r\n//Chiave (sconosciuta per noi)\r\nuint16_t key = { ... };\r\n//Payload scaricato\r\nuint16_t* payload = ...;\r\n//Calcolo della WORD da xorare con la chiave\r\nuint16_t index;\r\nfor (index = 0; (uint32_t)index \u003c 0x10000; index++)\r\n if (key[0] ^ index ^ payload[32] == 0x4d5a)\r\n break;\r\n//Calcolo della chiave\r\nfor (int j = 0; j \u003c sizeof(key)/sizeof(key[0]); j++)\r\n key[j] ^= index;\r\nGuloader può calcolare index perchè sà che la prima WORD di un PE è 0x4d5a (MZ) e perchè conosce dove è la\r\nchiave codificata e la sua lunghezza.\r\nNoi non conosciamo dove si trova la chiave nè la sua lunghezza ma possiamo di nuovo sfruttare della\r\nCrittoanalisi 101 per montare un attacco bruteforce.\r\nL’idea è che conosciamo i primi due byte della chiave grazie alla firma MZ (chiamiamoli k0), k0 può comparire\r\nall’interno del PE sia perchè la WORD corrispondente nel PE era zero (0 ^ k0 = k0) sia perchè per coincidenza\r\nl’operazione di XOR l’ha data come risultato (word_nel_pe_originale ^ ki = k0).\r\nSupponiamo che troviamo solo instanze del primo tipo, la distanza in byte tra due di queste WORD di valore k0 è\r\nun multiplo della chiave. E’ un multiplo e non la lunghezza esatta perchè non possiamo garantire che tutte le\r\nistanze di k0 compaiano nel PE codificato. Per cui prendendo la più piccola lunghezza abbiamo buona probabilità\r\ndi trovare la lunghezza effettiva della chiave.\r\nIstanze del secondo tipo (che, con un po’ di forzatura, assunto una distribuzione uniforme di chiave e payload si\r\nverificano con probabilità 2^-16) possono indurre falsi positivi. Per cui si ottiene una lista di possibili lunghezze\r\n(tutte quelle che non sono multiple di altre).\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 13 of 16\n\nAlla fine si ha un insieme di possibili lunghezze di chiavi, se si è fortunati se ne ha una solo.\r\nSe le lunghezze ottenute sono tutte multiple della lunghezza effettiva l’attacco sotto fallisce, si può pensare in\r\nquesto caso di fattorizzare le lunghezze trovare e considerare tutti i possibili divisori (questo è ancora da\r\nimplementare).\r\nPer trovare la chiave stessa verrebbe da provare a cercare una sequenza di byte che inizia con k0 e che si ripete\r\n(almeno in parte). Ma le chiavi usate possono essere troppo lunghe perchè questo succeda, ad esempio nel sample\r\nusato la chiave era di 0x606 byte, troppo lunga affinchè il PE avesse tutti questi zeri consecutivi.\r\nUn altro approccio è utilizzare il secondo stadio decodificato e tentare un bruteforce.\r\nScorriamo ogni singola WORD s0 nel secondo stadio e la consideriamo come l’inizio della chiave, che ricordiamo\r\nè XORata con la quantità index. Dato che conosciamo per certo k0, possiamo calcolare index = s0 ^ k0 visto che\r\ns0 = k0 ^ index. Se davvero s0 è l’inizio della chiave codificata nel secondo stadio, calcolando si ^ ki, dove si sono\r\nle WORD successive a s0 nel secondo stagio e ki quelle successive a k0 nel payload cifrato, per ogni i fino a\r\nraggiungere la lunghezza stimata, la maggior parte di questi valori sarà pari ad index.\r\nNon tutti saranno uguale ad index perchè i ki sono in realtà WORD che vengono dal payload codificato e\r\ncorrispondono alla WORD ki chiave solo se e solo se in quella posizione il PE conteneva una WORD nulla.\r\nTuttavia prendendo come candidati le chiavi che danno almeno m valori uguali ad index (di default m=10) si ha\r\nuna buona probabilità di trovare dove inizia la chiave nel secondo stadio e il valore index.\r\nTrovati index, la chiave nel secondo stadio e la sua lunghezza, è possibile emulare la decodifica di Guloader ed\r\nottenere il payload.\r\nLo script seguente prende da linea di comando il percorso del secondo stadio decodificato (generato dallo script\r\nsopra ad esempio) e del payload cifrato (così come scaricato) e prova un attacco bruteforce per ottenere il payload.\r\nimport binascii import struct import pefile import sys #Xor b1[i1:i1+l] with b2[i2:i2+l] and return a\r\nbyte array of length l def xor(b1, i1, b2, i2, l): res = [0] * l for i in range(i1, i1+l): res[i-i1] =\r\nb1[i % len(b1)] ^ b2[(i2 + i-i1) % len(b2)] return bytes(res) #Find the possible lengths of the key\r\nand the possible keys (only those with a \"primitive\" length will be effetively used) def\r\nfind_keys_and_lens(data, min_len=0x100): #We know the first two bytes of the PE, so we know the first\r\ntwo bytes of the key key_start = xor(b\"MZ\", 0, data, 0, 2) print(f\"Key start is\r\n{binascii.hexlify(key_start)}\") #Where was the last WORD with value key_start last_start = None #The\r\nkeys found keys = [] #The lengths found (# of these is \u003c= # keys as two or more keys can share a\r\nlength) lens = [] #Scan all the payload WORDs for i in range(0, len(data), 2): #If not a key start,\r\nskip if data[i:i+2] != key_start: continue #If this is the second key start, save the key and the\r\nlength if not already present if last_start is not None : pkey = data[last_start:i] if pkey not in\r\nkeys: print(f\"Found a possible key at offset {hex(i)} with (possible multiple) len {hex(len(pkey))}\")\r\nkeys.append(pkey) last_start = i #We remove all the length that are multiple of other lengths or too\r\nlow for k in sorted(keys, key = lambda k: len(k)): l = len(k) if l \u003c min_len: continue #First (and\r\nsmallest) length if len(lens) == 0: lens.append(l) #No multiples? Add elif len([ol for ol in lens if l\r\n% ol == 0]) == 0: lens.append(l) return keys, lens # # M A I N # if len(sys.argv) != 3: print(f\"Usage:\r\n{sys.argv[0]} DECODED_STAGE2_FILENAME PAYLOAD_FILENAME\") sys.exit(1) #Read the data with\r\nopen(sys.argv[2], \"rb\") as f: data = f.read()[64:] with open(sys.argv[1], \"rb\") as f: stage2 =\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 14 of 16\n\nf.read() #Get keys and lengths keys, lens = find_keys_and_lens(data) print(f\"Found {len(lens)}\r\npossible key length(s)\") #TODO: Show the key lengths and ask if we should add each divisor (if greater\r\nthan a threshold) of these length to the list (and the relative key prefixes to keys) before\r\nbruteforcing. # if the script fails to find the payload, try implementing this, even manually. #For\r\neach key length... for kl in lens: #Get the possibly partially coded keys from the payload candidates\r\n= [k for k in keys if len(k) == kl] print(f\"Trying keys with len {hex(kl)} ({len(candidates)}\r\ncandidate(s) found)\") #For each candidate n = 0 for c in candidates: #This WORD is known to be the\r\nvalid (it's the first WORD of the key) k0 = struct.unpack(\"\u003cH\", c[0:2])[0] print(\"Looking for a match\r\nin the decoded stage2...\") #For each WORD in the second stage... for i in range(0, len(stage2)-kl):\r\n#Progress if i % 10000 == 0: print(f\"Still looking... ({i*100//(len(stage2)-kl)}% of stage2 checked)\")\r\n#Calculate the possible index s0 = struct.unpack(\"\u003cH\", stage2[i:i+2])[0] index = s0 ^ k0 #Count how\r\nmany times index comes up when decodind the subsequent words count_matches = 0 for j in range(2, kl,\r\n2): si = struct.unpack(\"\u003cH\", stage2[i+j:i+j+2])[0] ki = struct.unpack(\"\u003cH\", c[j:j+2])[0] if si ^ ki ==\r\nindex: count_matches += 1 #If we have at least 10 matches, consider this a possible key if\r\ncount_matches \u003e= 10: print(f\"A candidate matched\") #Find the key key = xor(stage2, i, struct.pack(\"\r\n\u003cH\", index), 2, kl) #Decode the PE pe = xor(data, 0, key, 0, len(data)) #Try parsing the PE try:\r\npefile.PE(data=pe) name = f\"Payload{n}.exe\" print(f\"Possible key found, saving payload to {name}\")\r\nwith open(name, \"wb\") as f: f.write(pe) except Exception as e: continue\r\n$ python3 gl3.py gu_s2.bin ~/Malwares/20220727/gumabelt_DNCAoUwjFj89.bin\r\nKey start is b'5848'\r\nFound a possible key at offset 0x5c3a with (possible multiple) len 0x3d7c\r\nFound a possible key at offset 0x10862 with (possible multiple) len 0xac28\r\nFound a possible key at offset 0x114ae with (possible multiple) len 0xc4c\r\nFound a possible key at offset 0x120fa with (possible multiple) len 0xc4c\r\nFound a possible key at offset 0x12d46 with (possible multiple) len 0xc4c\r\nFound a possible key at offset 0x1522a with (possible multiple) len 0x24e4\r\nFound a possible key at offset 0x1770e with (possible multiple) len 0x24e4\r\nFound a possible key at offset 0x20a9e with (possible multiple) len 0x9390\r\nFound a possible key at offset 0x28bbc with (possible multiple) len 0x811e\r\nFound a possible key at offset 0x2b6c6 with (possible multiple) len 0x2b0a\r\nFound a possible key at offset 0x2c312 with (possible multiple) len 0xc4c\r\nFound a possible key at offset 0x33e0a with (possible multiple) len 0x7af8\r\nFound a possible key at offset 0x34430 with (possible multiple) len 0x626\r\nFound 1 possible key lengths\r\nTrying keys with len 0x626 (1 candidate(s) found)\r\nLooking for a match in the decoded stage2...\r\nStill looking... (0% of stage2 checked)\r\nStill looking... (11% of stage2 checked)\r\nStill looking... (22% of stage2 checked)\r\nStill looking... (33% of stage2 checked)\r\nStill looking... (44% of stage2 checked)\r\nStill looking... (55% of stage2 checked)\r\nStill looking... (66% of stage2 checked)\r\nA candidate matched\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 15 of 16\n\nPossible key found, saving payload to Payload0.exe\r\nStill looking... (78% of stage2 checked)\r\nStill looking... (89% of stage2 checked)\r\n$ file Payload0.exe\r\nPayload0.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows\r\nAggiornamento\r\nE’ disponibile anche la versione C degli stessi script. I parametri di ricerca sono definiti in config.h.\r\nL’utilizzo è il seguente :\r\ngudecoder url FILE_STAGE2_CIFRATO\r\n...\r\ngudecoder payload FILE_STAGE2_DECIFRATO PAYLOAD_CIFRATO\r\nSource: https://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nhttps://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/\r\nPage 16 of 16",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://cert-agid.gov.it/news/malware/tecniche-per-semplificare-lanalisi-del-malware-guloader/"
	],
	"report_names": [
		"tecniche-per-semplificare-lanalisi-del-malware-guloader"
	],
	"threat_actors": [],
	"ts_created_at": 1775434684,
	"ts_updated_at": 1775791203,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/5defb8d1ca6281e415c12f22a790436f5274300c.pdf",
		"text": "https://archive.orkl.eu/5defb8d1ca6281e415c12f22a790436f5274300c.txt",
		"img": "https://archive.orkl.eu/5defb8d1ca6281e415c12f22a790436f5274300c.jpg"
	}
}