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:
- https://tria.ge/220730-18sgmaafg4
- https://tria.ge/220731-np187sfcej
- https://tria.ge/220731-lj42ssaac3
- Hatching Triage | Malware sandboxing report by Hatching Triage
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):
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.
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:
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:
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):
Confermata che fosse questa la funzione che costruisce il config, trovo infatti a un certo punto una variabile globale che contiene i diversi IP:
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:
Una volta avviato si ottengono due messaggi:
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:
Effettuiamo una prima analisi del sample con Resource Hacker, PE Studio e Detect It Easy:
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:
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:
Dopo la decifratura viene avviata la shellcode che si occupa di decifrare la restante shellcode:
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.
Successivamente viene eseguito CreateThread, passando come indirizzo un indirizzo presente in .data, ricordiamo decifrato in precedenza 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:
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:
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.
Questo PE compresso viene decifrato da una semplice funzione:
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.
Al ritorno della funzione in EDX avremo il puntatore al PE completamente deoffuscato, dove infatti troviamo gli IP estratti in precedenza:
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:
E anche questa volta otteniamo la lista degli IP come variabili globali:
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.
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):
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:
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:
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