# Realizziamo un HTTP C&C in Python (Bankshot) **malverse.it/analisi-bankshot-copperhedge** ## Introduzione Ciao a tutti! Oggi vedremo l’analisi di Bankshot (conosciuto anche come CopperHedge); Bankshot è un RAT semplice che implementa 15 comandi, scritto in C++ e utilizza RC4 per effettuare parzialmente API Hashing e per cifrare/decifrare la comunicazione il C&C; il config è presente in chiaro. _Bankshot is a remote access tool (RAT) that was first reported by the Department of_ _Homeland Security in December of 2017. In 2018, Lazarus Group used_ _the Bankshot implant in attacks against the Turkish financial sector._ [Per maggiori dettagli si può visionare il report del CISA dove sono presenti le 6 varianti e la](https://www.cisa.gov/uscert/ncas/analysis-reports/ar20-133a) [collection di Virustotal. Altri riferimenti utili: IOC di ESET e correlazione tra i sample di](https://www.virustotal.com/gui/collection/alienvault_5ebaee4cb570b4824f773f44) [Reversing Lab.](https://blog.reversinglabs.com/blog/hidden-cobra) In particolare oggi analizzeremo un sample della Variante B, MD5: **[667cf9e8ec1dac7812f92bd77af702a1 che può essere ottenuto qui o](https://app.any.run/tasks/a3cf2f13-3495-4062-b332-3622f7e3aace/#)** [qui. Partiamo!](https://app.any.run/tasks/e272b8d1-019c-405a-a1da-30b0398b7795/) ## Introduzione Come sempre utilizziamo alcuni tool per velocizzare le successive analisi: ----- Esecuzione di capa Questa volta, a differenza di Danabot, è molto più semplice ottenere il config in quanto i tre server C&C sono presenti in chiaro: URL memorizzati in chiaro Con queste informazioni aggiuntive proseguiamo con l’analisi; il malware avvia immediatamente un Thread: Main che avvia il Thread principale Il Thread inizia risolvendo le diverse API dinamicamente. In particolare, l’algoritmo utilizzato per l’API Hashing è RC4. All’inizio di ogni funzione che vedremo successivamente avremo la risoluzione dell’API attraverso questa funzione. ----- Operazioni effettuate dal Thread Principale ----- Funzione di API Hashing con RC4 Continuiamo effettuando la decifratura delle API con un semplice script Python utilizzando le **API Ghidra; il funzionamento è il seguente:** Si ottengono tutte le chiamate alla funzione che si occupa di effettuare API Hashing (in questo caso è rinominata in ApiHashingViaRC4) tramite getReferencesTo(). Ottengo le istruzioni precedenti fino a trovare MOV EDX, **indirizzoNomeFunzioneCifrata tramite getInstructionBefore().** Il primo byte contenuto in questo indirizzo contiene la lunghezza della stringa cifrata e poi la stringa cifrata; con getBytes(addrEncrypted, 1)[0] ottengo il primo byte; ottengo quindi il byte array (nome funzione cifrata) partendo dall’indirizzo contenuto in EDX + 1 (addrEncrypted.add(1)) essendo che il primo byte contiene la lunghezza e da questo indirizzo leggo la lunghezza che ho ottenuto in precedenza. Effettuo la decifratura tramite RC4 della stringa cifrata ottenuta. ----- ``` def rc4Decrypt(key, data): S = list(range(256)) j = 0 for i in list(range(256)): j = (j + S[i] + ord(key[i % len(key)])) % 256 S[i], S[j] = S[j], S[i] j = 0 y = 0 out = [] for char in data: j = (j + 1) % 256 y = (y + S[j]) % 256 S[j], S[y] = S[y], S[j] out.append(unichr(ord(char) ^ S[(S[j] + S[y]) % 256])) return ''.join(out) def main(): key = '78292e4c5da3b5d067f081b736e5d593'.decode('hex') for ref in getReferencesTo(toAddr("ApiHashingViaRC4")): fromAddr = ref.getFromAddress() while True: instr = getInstructionBefore(fromAddr) if instr.getMnemonicString().lower() == 'mov' and instr.getOpObjects(0) [0].toString().lower() == 'edx': addrEncrypted = toAddr(instr.getOpObjects(1)[0].getValue()) print("Indirizzo API cifrata: " + str(addrEncrypted)) encryptedName = str(bytearray(getBytes(addrEncrypted.add(1), getBytes(addrEncrypted, 1)[0]))) print("0x" + str(instr.getAddress()) + " " + rc4Decrypt(key, encryptedName)) break fromAddr = instr.getAddress() if __name__ == '__main__': main() ``` ----- Esecuzione dello script Curioso come non tutte le API sono offuscate, ad esempio quelle riguardanti la comunicazione HTTP: Funzioni non offuscate per la comunicazione HTTP Dopo aver effettuato la risoluzione delle API, avviene la creazione del CONFIG che viene salvato in una variabile globale; in questo config vengono salvati i 3 URL insieme a un valore casuale compreso tra 65535 e 16777215: ----- Funzione Config Builder Successivamente vengono chiamate le diverse funzioni che si occupano di comunicare con il C&C; vediamo ora come è possibile sfruttare le informazioni presenti su any.run per velocizzare l’analisi successiva. ## Analisi Dinamica Su any.run sono presenti diversi sample che ci permettono di avere una prima Overview di come avviene la comunicazione con il server C&C: ----- Sample 1: invio del primo pacchetto Sample 2: invio del primo pacchetto ----- Sample 3: invio del primo pacchetto Le risposte a questa richiesta sono tutte dei redirect essendo il C&C offline in quel determinato momento; cercando altri sample però abbiamo una richiesta che questa volta fornisce una risposta e ci fornisce nuovi dettagli sul protocollo challenge-response, possiamo vedere infatti che il C&C risponde solo con il board_id (in questo caso 1838): Sample 4: invio del primo pacchetto Sample 4: risposta al primo pacchetto che ritorna uno dei parametri inviati (board_id) ----- Da queste diversi sample possiamo iniziare ed effettuare delle supposizioni su come funziona il protocollo di comunicazione, che verranno poi approfondite con le successive analisi: **board_id: numero differente tra le diverse richieste, potrebbe essere l’ID della richiesta** **user_id: conviso tra le varie richieste, potrebbe essere un valore di autenticazione** **file1: nome di file differente tra le diverse richieste, che non corrisponde a un file** presente sulla macchina any.run; potrebbe essere utilizzato per cambiare la signature di ogni richiesta Continuiamo ora con l’analisi per confermare/smentire le prime supposizioni. Essendo un malware scritto in C++, approfondiamo ora la classe WebPacket che si occupa di comunicare con il C&C. ## Classe WebPacket Il malware presenta una classe di nome WebPacket che si occupa di inizializzare l’oggetto (dimensione 3872 byte) con diversi attributi riguardi la comunicazione e dispone di diverse funzioni che si occupano di comunicare con il C&C. ----- Costruttore classe WebPacket Come possiamo vedere dal costruttore, i primi 4 byte contengono il puntatore alla vftable e dopo abbiamo la zona di memoria che contiene i membri dell’oggetto. Per ulteriori info su [come effettuare reverse di programmi C++: QUI,](https://p.ost2.fyi/courses/course-v1:OpenSecurityTraining2+RE3011_re_cpp+2022_v1/course/) [QUI e](https://www.youtube.com/watch?v=ir2B1trR0fE) [QUI.](https://www.blackhat.com/presentations/bh-dc-07/Sabanal_Yason/Paper/bh-dc-07-Sabanal_Yason-WP.pdf) ----- Struttura di un oggetto C++ in memoria (Fonte: Gal Zaban) I membri principali presenti in questa classe sono: ``` HINTERNET hSession; HINTERNET hInternet; HINTERNET hRequest; String URL; String Path; int port; .... char substitutionBox1[256] char substitutionBox2[256] int keyLength; int rc4Key[4]; ``` La vtable invece contiene solo una funzione che viene chiamata al termine per effettuare il reset delle variabili e chiamare la funzione WinHttpCloseHandle: vftable che contiene il puntatore alla funzione FreeAndCloseHandle Queste info vengono estratte da Ghidra da diverse strutture presenti nei programmi C++: ----- Struct che ci fornisce info sulla classe e il puntatore alla VFTable Struct che contiene informazioni sull’ereditarietà della classe Dopo aver fatto una piccola degressione su C++, passiamo al funzionamento; questa classe si occupa di inviare il primo pacchetto di autenticazione, con board_id casuale (minore di 10000), user_id uguale a *dJU!JE&!M@UNQ@ e filename casuale scelto tra happy.pdf, star.avi, hp01.avi, dream.avi, example.dat, pratice.pdf, my.doc e img01_29.jpg. ----- Generazione del valore casuale (board_id) e invio del pacchetto di autenticazione Successivamente viene ricevuta la risposta e viene confrontato il valore casuale generato (board_id) con quello ricevuto; questo conferma la supposizione che avevamo fatto precedentemente tramite analisi dinamica. Controllo dell’autenticazione attraverso il campo board_id ----- Vediamo ora quali metodi esporta questa classe che permettono di effettuare delle operazioni C&C; per tracciare quali sono i metodi di questa classe sfruttiamo il registro ECX che contiene l’indirizzo a questa classe appena definita. Copia del puntatore che contiene l’indirizzo della classe WebPacket Da queste informazioni riusciamo ad ottenere i seguenti metodi: **C&CSendRequest: invia la richiesta di tipo POST al C&C attraverso** WinHttpSendRequest; esegue la funzione C&CConnectAndOpenRequest. **C&CConnectAndOpenRequest: si occupa di chiamare le funzioni WinHttpOpen,** WinHttpConnect, WinHttpOpenRequest. **C&CReceiveAndDecryptDataRC4: si occupa di ottenere i dati con** WinHttpReceiveResponse, WinHttpReadData e opzionalmente decifrarli con RC4. **C&CEncryptCodeResult: effettua l’encryption tramite RC4 dello status code (0x1836,** 0x1837, ecc) ed esegue WinHttpWriteData con input i dati cifrati. **C&CEncryptCommandResult: effettua l’encryption tramite RC4 del risultato del** comando eseguito ed esegue WinHttpWriteData con input i dati cifrati. **C&CSendRequestAndExecuteCommand: si occupa di inviare il command packet,** ricevere ulteriori dati, eseguire il comando e ritornare il risultato al C&C. Per capire bene le successive analisi è necessario conoscere le WinHTTP API; per chi non [conoscesse il flow può approfondirlo tramite degli esempi presenti qui o](https://www.codeproject.com/Articles/4586/Web-Data-Extraction-by-Crawling-using-WINHTTP-and) [qui.](https://learn.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpwritedata) ----- Communication Flow con il C&C ## Cifratura RC4 L’algoritmo di cifratura RC4 viene utilizzato come visto in precedenza per l’API hashing ma anche per la comunicazione con il C&C. In particolare si hanno tre SBox che vengono utilizzate per la cifratura, una per l’API hashing e due per la comunicazione C&C (una per la ricezione dei dati e una per l’invio). Essendo che la funzione PRGA utilizza l’SBox come input per generare il valore random successivo per poi effettuare la cifratura/decifratura, è necessario avere due dichiarazioni differenti quando realizzeremo il server C&C. Un’altra differenza è che la funzione riguardante l’API Hashing è una funzione locale, mentre quelle riguardanti la comunicazione fanno parte della classe WebPacket e utilizzano le SBox e la chiave salvate all’interno di questa classe. ----- Le due fasi KSA con due SBox per la comunicazione C&C ## Funzione C&CSendRequest La funzione C&CSendRequest dopo aver effettuato la risoluzione delle API si occupa: Chiamare la funzione C&CConnectAndOpenRequest. Costruire l’header della richiesta HTTP. Costruire il body della richiesta HTTP. Inviare la richiesta attraverso WinHttpSendRequest. La richiesta POST ha questa forma: ----- ``` POST /URI HTTP/1.1 Cache-Control: max-age=0 Connection: keep-alive Accept: */* Content-Type: multipart/form-data; boundary=----FormBoundaryCaratteri casuali User-Agent: Ottenuto da ObtainUserAgentString o Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729) Content-Length: Lunghezza Host: Dominio ------FormBoundaryCaratteri casuali Content-Disposition: form-data; name="board_id" Casuale ------FormBoundaryCaratteri casuali Content-Disposition: form-data; name="user_id" *dJU!*JE&!M@UNQ@ se autentication packet altrimenti vuoto se è command packet ------FormBoundaryCaratteri casuali Content-Disposition: form-data; name="file1"; filename=Casuale tra happy.pdf, star.avi, hp01.avi, dream.avi, example.dat, pratice.pdf, my.doc e img01_29.jpg. Content-Type: application/octet-stream .... ``` I riferimenti al body non sono cifrati ----- Funzioni che utilizzato i riferimenti alle stringhe del body ----- Function Graph C&CSendRequest Aggiunta dei diversi header attraverso WinHttpAddRequestHeaders Il pacchetto di autenticazione viene differenziato da quello per la richiesta dei comandi attraverso il board_id, che è minore di 10000 se si tratta del primo caso, maggiore nel secondo; oltre a questo il secondo tipo di pacchetto non contiene user_id con la stringa ***dJU!*JE&!M@UNQ@.** Aggiunta di 10000 se il pacchetto è per la richiesta di un comando ----- Aggiunta di user_id con dJU!*JE&!M@UNQ@ se il pacchetto non è di autenticazione Al termine della costruzione del pacchetto HTTP viene inviato tentando l’invio tre volte con uno sleep di 300 millisecondi tra un invio e l’altro: ----- ## Funzione C&CConnectAndOpenRequest Questa funzione viene chiamata immediatamente dalla funzione C&CSendRequest e si occupa di: Ottenere l’user agent corrente tramite ObtainUserAgentString Ottenere le configurazioni proxy correnti tramite **WinHttpGetIEProxyConfigForCurrentUser** Chiamare la funzione WinHttpOpen, WinHttpConnect, WinHttpOpenHttp. Nella conversione dell’User Agent si utilizza due volte MultiByteToWideChar; questo avviene spesso con l’utilizzo di determinate API Windows, come si può vedere dall’esempio sotto, per ottenere prima la dimensione del buffer da ricevere (in questo caso UserAgent) per poi richiamare MultiByteToWideChar con il valore di size corretto. Utilizzo di MultiByteToWideChar per la conversione dell’User Agent ----- Esempio di utilizzo di ObtainUserAgentString e MultiByteToWideChar (Source: cpp.hotexamples.com) ## Funzione C&CReceiveAndDecryptDataRC4 Dopo aver inviato la richiesta con C&CSendRequest questa funzione si occupa di: Ricevere i dati tramite WinHttpReceiveResponse e WinHttpReadData Decifrare i dati con RC4 È presente una flag come parametro che stabilisce se i dati devono essere decifrati: ----- ## Funzioni C&CEncryptCodeResult e C&CEncryptCommandResult Queste due funzioni si occupano di effettuare la cifratura RC4 dei dati in input e aggiungerli alla richiesta HTTP tramite WinHttpWriteData: **C&CEncryptedCodeResult: si occupa di cifrare il result code (0x1836, 0x1837,** 0x1838, 0x1839) del comando eseguito. **C&CEncryptCommandResult: si occupa di cifrare la risposta del comando eseguito.** Come si può evidenziare dal Call Graph, ci sono funzioni che ritornano solo il result code (es. KeepAlive, TerminateProcessByPID), altre che ritornano solo il risultato del comando (es. GetSystemInfo, GetDriverinfo) e altri comandi più complessi (es. WriteFile) che ritornano entrambi. ----- Call Graph delle due funzioni Cifratura del buffer in input e scrittura dei dati cifrati tramite WinHttpWriteData ## Funzione C&CSendRequestAndExecuteCommand Infine dopo aver ricevuto la richiesta e averla decifrata, viene eseguita l’operazione in base al codice del comando specificato. ----- (OBBLIGATORIO) 4 BYTE numero comando (OBBLIGATORIO) 2 BYTE lunghezza parametro opzionale (OPZIONALE) 4 BYTE parametro opzionale Struttura del comand packet Altri comandi invece richiedono l’invio di altri dati, ad esempio la funzione WriteFile o **DownloadAndMapFile; per ulteriori info vedere lo script Python per la realizzazione del** C&C. Vengono ricevuti i primi 6 Byte e se gli ultimi 2 byte sono diversi da zero, si richiama la funzione per ricevere i dati restanti di dimensione variabile. Ricezione e decifratura dei comandi e dei parametri opzionali ----- Switch per l’esecuzione del comando ricevuto Abbiamo anche dei result code che vengono inviati come risultato di alcuni comandi: **0x1836: esecuzione avvenuta con successo (es. comando KeepAlive, processo creato** con successo) ----- **0x1837: errore nell esecuzione del comando (es. file da leggere non esistente)** **0x1838: invio metadati di un file/directory o scrittura avvenuta correttamente (es. prima** risposta a WriteFile o ReadFile) **0x1839: termine esecuzione comando (es. ultima risposta a WriteFile)** Il RAT supporta 15 comandi: **NUMERO COMANDO** **FUNZIONE** 0x1827 **GetSystemInfo** 0x1828 **GetDriverInfo** 0x1829 **SetConfig** 0x182A **GetConfig** 0x182B **KeepAlive** 0x182C **WriteFile** 0x182D **ReadFile** 0x182E **CreateProcessByName** 0x182F **ExecuteCMD** 0x1830 **GetMetadataFile** 0x1831 **GetProcessList** 0x1832 **TerminateProcessByPID** 0x1834 **Disconnect** 0x1835 **DeleteTempFile** 0x183C **DownloadAndMapFile** Funzionalità del RAT Per implementare correttamente il server C&C è necessario capire anche quali funzioni rimangono in attesa di ricevere ulteriori dati per essere eseguite correttamente; questo può essere velocemente rilevato con la funzione Function Call Graph di Ghidra: ----- Funzione che richiede ulteriori dati per essere eseguita correttamente ## Implementazione Server C&C Implementiamo ora un server HTTP, che risponde ad alcuni dei comandi ricevuti dal malware; si lascia come compito al lettore di implementare i restanti tre comandi e gli error code non gestiti 🙂 Importante ricordarsi che è necessario avere due cipher per la cifratura e decifratura dei dati; inoltre alcuni comandi non richiedono ulteriori interazioni con il C&C, mentre alti richiedono l’interazione con l’operatore che deve inserire ulteriori dati (es. nome del processo da creare). ----- ``` #!/usr/bin/env python3 import sys, struct, cgi, Crypto.Cipher.ARC4, time, hexdump from http.server import BaseHTTPRequestHandler, HTTPServer # Dominio e porta dove il server deve essere in ascolto DOMAIN = '0.0.0.0' PORT = 80 class httpHandler(BaseHTTPRequestHandler): key = bytes.fromhex('271a16ab6d7a900ef3fa677dce8ab268') rc4Receive = Crypto.Cipher.ARC4.new(key) rc4Send = Crypto.Cipher.ARC4.new(key) lastCommand = None def unpack10(x): x1, x2, x3, x4 = struct.unpack(' bytes of file, write): '))) pass # Set Config if commandToExecute == "0x1829": # TODO: implementare set config pass # Download e eseguire file if commandToExecute == "0x183c": # TODO: implementare download and execute file pass cmdLen = struct.pack(' 10000: print('[C&C - RECEIVE] Command Package') cmd = self.__class__.sendCommand() buffer = self.__class__.rc4Send.encrypt(cmd) # Risultato Command Packet (3 FASE) else: print('[C&C - RECEIVE] CMD Execution Result') cmdResponse = self.__class__.rc4Receive.decrypt(fields['file1'][0]) hexdump.hexdump(cmdResponse) global lastCommand if(lastCommand == 0x182d): if(len(cmdResponse) == 10): result, blank, size = self.__class__.unpack10(cmdResponse) print("[C&C - RECEIVE] File size: " + str(size)) cmdCode = struct.pack('