Config Extractor per DanaBot (PARTE 1)

Config Extractor per DanaBot (PARTE 1)

Introduzione

Ciao a tutti, oggi volevo analizzare la sfida bi-settimanale lanciata di Daniel di Zero2Auto che consiste questa volta nel scrivere un Config Extractor che funzioni per le diverse versioni di DanaBot, un malware scritto in Delphi.

Da Malpedia:

Proofpoints describes DanaBot as the latest example of malware focused on persistence and stealing useful information that can later be monetized rather than demanding an immediate ransom from victims. The social engineering in the low-volume DanaBot campaigns we have observed so far has been well-crafted, again pointing to a renewed focus on “quality over quantity” in email-based threats. DanaBot’s modular nature enables it to download additional components, increasing the flexibility and robust stealing and remote monitoring capabilities of this banker.

Ci vengono forniti questi quattro link, che ci permettono di ottenere diverse versioni del sample:

Logicamente possiamo ottenere altri sample classificati come Danabot, ad esempio qui e qui.

In particolare, in questo post analizzeremo tre sample (MD5: 6b448c6851f3235c9b3d0c24353c480f, 5c0be4a5273dec6b3ebb180a90f337f2, 611c2bf7aa7bb62e90f3a92f3682c0b5), realizzando un semplice script per estrarre gli IP del C&C; nei prossimi post analizzeremo come avviene la comunicazione con il C&C, indentificheremo le funzioni di cifratura, estrarremo la chiave RSA e realizzeremo lo script finale che funziona sui diversi sample forniti.

Partiremo da analizzare il primo sample, si tratta del Main Component di DanaBot, successivamente analizzeremo dei sample più complessi che contengono al loro interno il Main Component.

Analisi Main Component Danabot

Partiamo analizzando il primo sample (MD5: 6b448c6851f3235c9b3d0c24353c480f); si tratta del Main Component di DanaBot, sviluppato in Delphi ed esporta diverse funzioni (f0, f1, … , f9):

DIE rileva che si tratta di una DLL scritta in Delphi
Funzioni esportate dalla DLL

Analizzando la funzione F0, dopo la decifratura di diverse stringhe e la creazione di un altro thread, troviamo la creazione di un thread che contiene diverse chiamate per effettuare operazioni con i socket (per chi volesse maggiori informazioni sul funzionamento dei socket può consultare questa ottima guida); tracciando i parametri passati a queste funzioni, riusciamo ad ottenere dove effettivamente avviene la creazione del config.

Creazione del Thread principale che si occupa di comunicare con il C&C

La funzione che ci interessa attualmente è inet_addr, essendo che ha come parametro l’IP in formato dotted-decimal; in realtà non troveremo l’IP direttamente in questo formato, ma l’IP in formato decimale verrà prima convertito con una semplice funzione che ho rinominato IntToIP e poi passato a inet_addr:

Operazioni con i socket e conversione dell’IP da int

Il primo parametro della funzione IntToIP è un parametro a sua volta della funzione padre, quindi analizzo le chiamate a questa funzione (solo una) e traccio tale valore; viene referenziato solo in due funzioni e in particolare una è interessante perché come parametro ha una variabile globale:

Funzione che accede in scrittura al Config

Questa variabile è acceduta da diverse funzioni, in particolare una di queste effettua la scrittura in questa zona di memoria; essendo che non contiene dati, viene quindi popolata in runtime, avvio quindi il debugger e confermo che questa zona di memoria contiene proprio il config (inizia con 3C e termina con 4E):

Config ottenuto con il debugger

Confermata che fosse questa la funzione che costruisce il config, trovo infatti a un certo punto una variabile globale che contiene i diversi IP:

IP in formato int

Vediamo quindi un primo script specifico per questo sample, che poi verrà generalizzato per supportare i vari sample. In questo caso ho effettuato una regex sullo specifico move nella funzione di Config Builder per ottenere l’indirizzo specifico che contiene i diversi IP del C&C.

import pefile, ipaddress, binascii, re, struct

pe = None
imageBase = None


def GetRVA(va):
    return pe.get_offset_from_rva(va - imageBase)


def GetVA(raw):
    return imageBase + pe.get_rva_from_offset(raw)


def main():

    global pe, imageBase

    filename = "sample3"

    with open(filename, 'rb') as sample:
        data = bytearray(sample.read())        
      
    pe = pefile.PE(filename)
    imageBase = pe.OPTIONAL_HEADER.ImageBase
    

    copy_operation = b'\xa1\x68\x57\x54\x00'
    
    for m in re.compile(copy_operation).finditer(data):      
        addrStart = int(hex(struct.unpack("<L", data[m.start() + 1:m.start() + 1 + 4])[0]), 16)

    
    for i in range(10):
        
        start = int(hex(addrStart + i*4),16)
        end = int(hex(addrStart + (i+1)*4),16)
        
        ip = binascii.hexlify(data[GetRVA(start):GetRVA(end)])
        
        print(str(ipaddress.IPv4Address(int(ip, 16))))


if __name__ == "__main__":
    main()

Lo script ci permette di ottenere i diversi IP utilizzati dal malware come C&C:

243.127.43.6
64.126.175.2
130.15.230.152
74.99.136.192
244.14.226.35
95.179.168.37
51.129.76.8
151.210.85.159
45.76.123.177
75.57.14.121

Analisi Loader Danabot

Nel secondo sample (MD5: 611c2bf7aa7bb62e90f3a92f3682c0b5) abbiamo un VBS script molto offuscato:

Dropper VBS offuscato

Una volta avviato si ottengono due messaggi:

Primo messaggio dello script
Secondo messaggio dello script

Lo script salva la DLL yvNdiXKm.txt in TEMP e avvia la funzione F0, che in realtà non viene esportata dalla DLL e quindi viene avviato l’entry:

Il VBS estrae la DLL in temp e la avvia
Rundll32 avvia la funzione F0 della DLL (entry)

Effettuiamo una prima analisi del sample con Resource Hacker, PE Studio e Detect It Easy:

Il sample risulta packed
Le pochi capability trovate da capa confermano sia un packer
La DLL è composta da diversi form
Path con riferimenti alla guerra e Russia

Analizzando questa DLL non trovo le funzioni socket viste in precedenza essendo il packer; metto come breakpoint le funzioni VirtualProtect, VirtualAlloc e CreateThread. Viene raggiunto VirtualProtect e all’indirizzo base_address + 0x115f50 è presente la shellcode, che viene copiata dall’indirizzo base_address + 0xdaaa0:

Copia della shellcode cifrata

In particolare, la decifratura della shellcode è molto semplice, infatti nonostante siano presente molto operazioni, viene solo modificata da una operazione, che aggiunge per ogni 4 byte il valore 0x1828308 e questa somma viene fatta per 0x0135910C volte:

Decifratura della shellcode
Esecuzione della shellcode attraverso RET

Dopo la decifratura viene avviata la shellcode che si occupa di decifrare la restante shellcode:

Il loop di decryption che decifra la restante shellcode e le funzioni della nuova DLL

Control Flow prima e dopo la decifratura

Successivamente avviene qualcosa di molto interessante, la shellcode cambia i i permessi della varie sezioni con VirtualProtect in scrittura (0x4) riscrivendo parte di queste e quella in .data in execution (0x40); questa tecnica si chiama Reflective DLL Loading e consiste nel caricare direttamente la DLL dalla memoria senza passare dal disco.

Permessi iniziali della DLL
Cambio dei permessi in scrittura
Cambio dei permessi in esecuzione
Riferimento a Delphi

Successivamente viene eseguito CreateThread, passando come indirizzo un indirizzo presente in .data, ricordiamo decifrato in precedenza dalla shellcode.

Creazione del thread con la funzione decifrata dalla shellcode

Inseriamo nuovamente come breakpoint la funzione connect e otteniamo la funzione dove si effettua la connessione al C&C:

Anche questa volta vediamo che il config inizia con 3C e termina con 4E ma il terzo e il quarto byte sono differenti rispetto al config precedente:

Config ottenuto con il debugger

Ho iniziato quindi a tracciare le diverse VirtualAlloc per capire dove effettivamente fosse il config; a un certo punto viene allocata una zona di memoria e il suo indirizzo salvato in [ebx+631549] e questo puntatore viene utilizzato per effettuare diverse operazioni sul PE per deoffuscarlo:

VirtualAlloc alloca la zona di memoria per il PE
Deobfuscation del PE tramite decifratura e decompressione

La prima funzione che viene eseguita estrae il PE offuscato copiando il contenuto in EDI; il PE offuscato si trova nell’indirizzo di memoria base_address + 0xbecc.

Funzione che copia il PE offuscato da ESI in EDI
PE Offuscato ottenuto staticamente

Questo PE compresso viene decifrato da una semplice funzione:

Funzione che decifra il PE

Per quanto riguarda la decompressione, la funzione prende un sottoinsieme di byte dalla zona di memoria puntata da ESI e li trasferisce nella zona di memoria puntata da EDI attraverso movsb, stosb e lodsb; per chi volesse approfondire come funziona il trasferimento attraverso queste istruzioni si può leggere l’ottimo articolo presente qui. In particolare, il PE è compresso con APLib (la signature del PE è M8Z) e successivamente possiamo utilizzare quindi l’ottimo tool di herrcore.

Funzione che si occupa di decomprimere il PE
Function Graph della funzione che si occupa della decompressione del PE

Al ritorno della funzione in EDX avremo il puntatore al PE completamente deoffuscato, dove infatti troviamo gli IP estratti in precedenza:

PE completamente deoffuscato zona di memoria puntata da EDX
Header della nuova DLL estratta corrisponde a Delphi 3
Ricerca dell’IP nell’area di memoria puntata da EDX

Questo PE viene poi utilizzato per sovrascrivere le attuali sezioni come visto in precedenza.

Nella seconda parte dell’articolo realizzerò un video per vedere praticamente questa parte tramite debugger per poi scrivere lo script che si occupa in automatico di rimuovere i diversi layer di obfuscation; per ora, a scopo “didattico”, possiamo effettuare il dump della DLL e proseguire; dopo aver effettuato il fixing con Shylla, forzo la decompilazione nella sezione .data ed ecco la funzione di Decryption Config, simile al primo sample che abbiamo ottenuto:

Config Builder
Caratteristiche nuova DLL sovrascritta dalla shellcode

E anche questa volta otteniamo la lista degli IP come variabili globali:

IP del C&C in formato int

Possiamo facilmente cambiare l’espressione regolare dell’operazione di copy (\xa1\x68\x47\x00\x02) per ottenere:

181.63.44.194
207.148.83.108
45.77.40.71
87.115.138.169
24.229.48.7
116.111.206.27
45.196.143.203
218.65.3.199
131.59.110.186
113.81.97.96

Vediamo come adattare ora lo script precedente per farlo funzionare per i due sample visti fino ad ora, iniziamo ad analizzare le due funzioni di Config Builder; dovendo generalizzare nella regex la destination essendo l’indirizzo dove son presenti gli IP diversi, mantenendo solo come statica la source (registro EAX) otteniamo un numero molto elevato di MOV, è necessario quindi rendere più specifica la regex.

Dopo il primo MOV vediamo che è presente un altro MOV che salva il valore di EAX in un’altra variabile globale.

MOV del primo sample
MOV del secondo sample

Inoltre all’inizio della funzione sono presenti delle inizializzazioni di registri quasi uguali (si noti che questa parte non è strettamente necessaria, infatti anche rimuovendo il regex per questa parte lo script funziona comunque essendo che la prima modifica già permette di ottenere solo quell’indirizzo):

Operazioni del primo sample
Operazioni del secondo sample

Dopo queste due considerazioni la regex diventa quindi:

header = b'\x64\xff\x30\x64\x89\x20\x33\xc0\x89\x45.'
copy_operation = b'\xa1\x68...\xa3\xc1...'
regex = header + copy_operation

Provando il nuovo script funziona su entrambi i sample:

IP decifrati

Si noti che questo script funziona solo sul Main Loader di DanaBot; nel prossimo post vedremo come aggiornare lo script per farlo funzionare direttamente sul dropper (vbs, exe) e automatizzare la decifratura effettuata dalla shellcode.

Analisi secondo Loader

Iniziamo ora l’analisi del terzo sample (MD5: 5c0be4a5273dec6b3ebb180a90f337f2), questa volta è un EXE sviluppato in C:

In questo caso l’EXE si occupa di estrarre la DLL nella cartella corrente e avviarla; questa DLL a sua volta avvia la stessa DLL passando un parametro casuale (quindi verrà avviato in realtà l’entry); infatti come possiamo vedere oltre gli export canonici essendo una DLL scritta in Delphi, non abbiamo altro:

Applico la conoscenza precedente cercando riferimenti a socket, non trovando niente.

Sospetto quindi che in realtà sia un packer e tramite il debugger allora analizzo le varie chiamate VirtualAlloc, VirtualProtect e CreateThread si vede come in realtà la DLL abbia al suo interno un’altra DLL, questa volta con un export FunDLLData:

In questa DLL invece si trovano i riferimenti alla comunicazione tramite socket e tracciando i parametri passati riusciamo a raggiungere il Config Builder:

Per questo post è tutto, nei prossimi continueremo l’analisi, analizzeremo le altre informazioni presenti nel config, estrarremo la chiave RSA utilizzata per la comunicazione e generalizzeremo lo script per i restanti sample 🙂 Per qualunque consiglio o richiesta, scrivete pure nei commenti, grazie! 🙂

Si ringrazia bleepingcomputer.com per l’immagine di copertina

Share this content:

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *