Decodificare e crackare Agile.NET 6.6

29/10/2020

Agile.NET

Agile.NET è un offuscatore commerciale per assembly .NET già conosciuto in letteratura ma che ha recentemente1 cambiato o introdotto la modalità di cifratura dei metodi.

Agile.NET 6.6 usa un duplice fronte di azione: da un lato compie le usuali manipolazioni delle istruzioni CIL, come ad esempio l’utilizzo di CFF, l’utilizzo di delegate per nascondere il chiamato e la sostituzione delle stringhe con una loro versione cifrata e la relativa chiamata per decifrarle; l’altro fronte è quello dell’occultamento del codice CIL (offuscato) tramite la usuale tecnica di CIL-injection, in cui il codice dei metodi salvato nell’assembly non è quello realmente eseguito.


1 Non siamo in grado di stabilire quando il cambiamento sia avvenuto o se sia effettivamente recente. Le ultime versioni dell’apposito deoffuscatore presente in de4dot non sono in grado di deoffuscare gli assembly creati con le ultime versioni di Agile.NET.

Bootstrap

Il codice di bootstrap ha il compito di impostare l’hook del compilatore JIT, l’esecuzione inizia dall’inizializzatore di modulo dell’assembly offuscato e presenta l’utilizzo di delegate come forma di offuscamento:

Il codice di bootstrap offuscato

I delegate hanno tutti la stessa forma, qui esposta:

Un tipico delegate di Agile.NET 6.6

In particolare:

  • Sono tutti MulticastDelegate.
  • Il costruttore invoca sempre la funzione dau con un valore che è il proprio RID – 1.
  • Contengono campi statici il cui nome, tolti eventuali %, è un base64 valido.

La funzione dau è abbastanza semplice:

La funzione di risoluzione dei target dei delegate

tale funzione può essere riassunta nel seguente modo (le conversioni da decimale ad esadecimale sono lasciate al lettore):

  1. Enumera tutti i campi della classe identificata dal RID passato in input.
  2. Il nome di ogni campo è un base64 che codifica il RID del metodo target.
  3. Inizializza il campo per “puntare” al target.

Dato che un delegate invoca dau con il proprio RID, i delegate Agile.NET sono facili da decodificare con strumenti come dnlib, infatti:

Ogni campo di un delegate Agile.NET chiama il metodo con RID pari al numero indicato, in base64, nel nome stesso del campo.

Risulta banale scrivere un programma per rimuovere questi delegate:

Il codice di bootstrap, senza delegate

Il codice di bootstrap carica una DLL nativa, a 32 o 64 bit a seconda del bitness dell’interprete, e ne chiama la funzione _Initialize.
La DLL è presa dalle risorse, è in chiaro, e viene salvata in un percorso temporaneo.
Questa DLL ha il compito di redirottare l’esecuzione di compileMethod del JIT .NET, in modo da fornirgli al volo il codice CIL reale da compilare.
I metodi dell’assembly offuscato sono infatti, per la maggior parte, vuoti:

L’entry-point dell’assembly offuscato, il metodo contiene solo il bytecode ret.

L’iniezione del vero codice

Modificare il codice CIL al volo è un procedimento ormai “standard” usato da molti offuscatori.
Non reitereremo i dettagli di come questo sia possibile, basti sapere che considerando che il JIT è un’oggetto COM, cambiare la sua vtable è banale.

Con un semplice breakpoint al caricamento delle librerie è possibile analizzare la DLL nativa direttamente dal processo dell’assembly offuscato: una volta caricata è possibile mettere un breakpoint in _Initialize.

La DLL è scritta in C++, compilata con Visual Studio 2017 (15.9.1) stando al Rich header presente.
Il reverse engineering del codice C++ è in generale complesso per via di alcune caratteristiche di questo linguaggio, in particolare l’ampia libreria che ha disposizione, combinata con l’estensivo uso dei template e l’aggressività con cui i compilatori inlinea le funzioni rendono difficile distinguere il codice “utente” da quello delle librerie.
A questo si aggiunge che C++ promuove l’astrazione risultato in chiamate annidate ed oggetti il cui layout impone la conoscenza perfetta dell’ABI di competenza e la navigazione tra le istruzioni di aggiustamento e calcolo dei vari offset. Analizzare codice C++ è difficile e tedioso.

Risulta fondamentale per l’analisi della DLL realizzare che viene fatto ampio uso di questa libreria per le tabelle hash (dizionari): http://web.mit.edu/sage/export/tachyon-0.98~beta.dfsg/src/hash.c.
Non proprio la stessa, una versione modificata, probabilmente templetizzata ed inlineata dato che si presenta duplicata in varie parti del codice ma anche leggermente diversa a seconda del tipo della chiave.

Il seguente codice è uno spot-on per l’identificazione della suddetta libreria:

Il fattore, se cercato in decimale su Google, rileva la provenienza del codice

Da qui è possibile nominare le principali funzioni della libreria e cominciare a fare chiarezza sugli oggetti usati dal codice.

Con pazienza si trovano le tre funzioni, scelte in base all’ambiente, di hook:

Le tre funzioni di hook, facile quando le abbiamo già rinominate noi, individuarle.

Trovate le funzioni di hook, che rimandano sempre alla solita implementazione, è utile tenere a portata di mano un foglio con i vari offset dei campi PE e della struttura dei metadati .NET perchè la DLL navigherà questi dati estensivamente.
Con la dovuta pazienza è possibile arrivare a decodificare le strutture usate da Agile.NET 6.6, ovvero i dati della licenza, il codice reale dei metodi (cifrato) e l’algoritmo di decifratura.

Eccolo qui:

L’algoritmo di decifratura, non complesso ma il codice assembly generato in configurazione di debug è molto verboso.

Le strutture di Agile.NET 6.6 e l’algoritmo di decifratura

Analizzare la struttura dati usata da Agile.NET non è semplice e per questo non saranno mostrati i passi intermedi ma solo il risultato finale.

In particolare Agile.NET salva i propri dati subito dopo i metadati .NET (indicati dagli appositi campi nella relativa directory PE):

La posizione dei dati di Agile.NET

I dati sono divisi in tre parti:

  1. Header (minimo 0x30 byte)
  2. CIL codificato (variabile)
  3. Tabella per il patching (variabile)

Header

L’header dei dati

L’header è composto da una firma di 16 byte del valore di

08 44 65 E1 8C 82 13 4C 9C 85 B4 17 DA 51 AD 25

Il secondo paragrafo contiene i dati della licenza. Il primo byte di questo contiene il tipo di licenza:

  • 0x00 – Limitata ad alcuni dispositivi.
  • 0x03 – Trial
  • Altro – Full

Nel caso di licenze di tipo Trial, la QWORD successiva al byte di tipo è una struttura FILETIME che indica il tempo di scadenza della licenza.
Il sample ottenuto scadeva nel 2099 ma è utile per sapere il momento esatto in cui il sample è stato creato (dato che si presuppone la licenza sia in multipli dell’anno):

Nel caso la licenza sia limitata ad alcuni dispositivi, i MAC address della macchina sono confrontati con la lista contenuta nei dati dalla licenza.
Questa lista è composta da un primo byte che indica la dimensione dei dati della lista e poi dai MAC address.

La licensa full non ha restrinzioni, probabilmente il valore da usare per il suo tipo è 0x02 ma gli sviluppatori hanno gestito il caso con un else semplice, per cui ogni valore va bene.

La licensa è usata per la decodifica dei metodi ma è possibile decodificare il CIL, cambiare la licenza e codificarlo di nuovo.
Considerando che il costo di Agile.NET è sui 700$, questo permette ai criminali di risparmiare un bel po’ di soldi per offuscare il proprio codice.

Dopo la lincenza è presente un paragrafo con le informazioni operative:

  • Quanto è lungo il codice CIL cifrato. Questo si trova subito dopo l’header e questa dimensione ci permette di raggiungere la tabella per il patching.
  • Quanti metodi ci sono nei metadai. Questi indica indirettamente la dimensione della tabella di patching (16 byte per riga, una riga per metodo).
  • La posizione della tabella dei metadati .NET dei metodi. Agile.NET deve patchare l’header dei metodi per preparare il JIT alla compilazione (si faccia riferimento a come funziona l’hooking del JIT) ma per la decodifica dei metodi non è necessario, usiamo direttamente l’header corretto contenuto nel blob del CIL cifrato.
  • La dimensione di ogni riga della tabella di prima. Non è necessaria per la decodifica, vedi sopra.

CIL codificato

Questo campo è un blob binario che contiene, per ogni metodo occultato, il suo header (quello nel corpo del metodo, si faccia riferimento al formato .NET) ed il suo CIL.

Per la decodifica del CIL di un metodo servono:

  • I primi 16 byte della licenza
  • L’offset “relativo” del CIL del metodo. Con questo indichiamo la differenza tra l’offset del CIL del metodo, come indicata nella tabella di patching, e l’offset del CIL del primo metodo, non nullo, nella suddetta tabella.

L’algoritmo di decifratura è mostrato sotto.

int k1 = license_type == 2 ? 7 : 9;
int k2 = 0;
for (int i = 0; i < cil_size; i++)
{
   cil[cil_offset + i] ^=
	license[(rel_offset + i) % 0x10] ^
	license[(rel_offset + i + k1) % 0x10];

   cil[cil_offset + i] |= k2;
}

Il parametro k2 dovrebbe essere 0, è settato a zero tramite una gestione di eventi con un thread secondario che controlla la presenza di alcuni processi (non abbiamo analizzato questa funzionalità a fondo).
Se il thread non setta un evento, il valore di k2 finisce per essere 1 altrimenti è 0.
Ipotiziamo che si tratti di una tecnica antidebug, nel caso sono solo due tentativi da effettuare.

La tabella di patching

Il formato della tabella di patching, evidenziate due righe, di cui la prima (arancione) da saltare.

Questa tabella indica, in ordine per ogni metodo nei metadati .NET, la posizione del CIL di questi all’interno del blob cifrato e la sua dimensione.

  • La prima DWORD (verde) indica la posizione all’interno del blob. Questo è un offset relativo all’header (e non al blob del CIL).
  • La seconda DWORD (blu) è la dimensione del CIL da decodificare.
  • La restante QWORD (giallo) indica due campi da patchare nell’header di corpo dei metodi .NET, prima della compilazione del CIL. Per la decodifica non è necessario dato che l’header reale è contenuto nello stesso CIL (di fatto abbiamo abusato del termine “CIL”).

Non tutti i metodi sono cifrati, quelli che hanno la prima DWORD nulla vanno saltati (riga arancione sopra).
Inoltre i metodi con header tiny non hanno la seconda QWORD popolata (poichè non vi è niente da patchare, oltre la dimensione).

Nell’esempio sopra il primo metodo codificato ha offset 0x30, il suo offset relativo sarà ovviamente 0. Il metodo successivo ha offset 0x48, quindi l’offset relativo è 0x18, e così via.
Questo è necessario per la decodifca.

Risultato

L’entry-point della DLL offuscata era prima vuoto, adesso si presenta con il suo vero codice:

Il vero codice dell’entry-point

Possiamo rimuovere, di nuovo, i delegate, deoffuscare le stringe e rimuovere la CFF.
Tutte queste tecniche sono già state mostrate in precedenti articoli, per cui l’analisi di assembly offuscati con Agile.NET non dovrebbe più presentare problemi.

Taggato  Agile.NET