Depuis quelques temps j’accordais une grande attention à l’idée de faire un blog, et les sujets ne me manquent pas…
Je dois avouer que je ne suis pas très à l’aise lorsqu’il s’agit de faire de la rédaction, et certains sujets sont visiblement censurés par notre chère démocratie (cf. Condamné en justice pour un tuto Aircrack).
Et, une obsession en chasse une autre…
Alors, en rangeant un peu de contenu numérique, je suis tombé par hasard sur une version de Bozok-RAT
. Quoi ? un rat ?
Commençons par le commencement…
Un RAT
est comme son acronyme l’indique, un outil d’administration à distance.
Mais contrairement à certains outils du marché (Teamviewer
, …) un RAT
dispose d’un panel d’options généralement plus attractif.
keylogger
),Parmi ces autres fonctionnalités dans son édition Ultimate le logiciel dispose de l’option form grabbing
. Une capacité qui permet l’extraction de données de formulaires Web avant que ceux-ci soient transmis à un serveur sécurisé.
Si l’on ajoute les fonctionnalités de keylogger
, password recovery
, rootkit
, … pour ma part (cela n’engage que moi) ce n’est plus un simple outil d’administration à distance, mais un malware.
Pour essayer de satisfaire ma curiosité, j’ai téléchargé une version plus récente du logiciel et visiblement, le développement c’est arrêté à la version 1.5.1, marketing oblige ! C’est une version d’évaluation que j’ai obtenu en glanant sur Internet.
Business is business, le développeur de l’application proposait l’édition Ultimate de son logiciel pour 480€ annuel.
Par défaut, le port d’écoute sur le C2C
(Centre de Contrôle) est tcp/1515
et le mot de passe de connexion est … mypass
.
La génération d’un agent se déroule comme la plupart des autres outils de ce type.
Ci-dessous, la simulation d’un accès en Remote Shell
.
Notez au passage que pour ne pas m’infecter inutilement, je travaille en environnement complètement cloisonné (sur une machine virtuelle dédiée pour ce type de travail).
J’ai pris l’habitude de commencer mes analyses par des opérations très basiques. Par exemple, en utilisant des outils comme PEiD
et PE Studio
.
Le logiciel PEiD
m’indique que l’agent généré par le centre de contrôle (serveur.exe
), est un fichier binaire pour les systèmes 32-bits
de Microsoft Windows
(codé en Delphi
).
Ici, je cherche à savoir si le programme est protégé par un packer
. Me rappelant, lors d’une précédente analyse que j’avais sans doute commencé avec trop de hâte en voyant la chaîne UPX
dans le debugger.
J’utilise également le plugin KANAL
pour rechercher de la cryptographie (sait on jamais) mais rien.
Aussi, PE Studio
me fourni des données facilement interprétables afin d’obtenir un aperçu général de ce fichier PE
(Portable Executable).
Ci-dessous, les différents indicateurs parlent d’eux-même.
Je regarde dans les entêtes et dans la table d’importation.
Dans optional-header
j’obtiens, entre autre, des informations sur les options utilisées lors de la compilation (SEH
, ASLR
, DEP
, …)
Dans la table d’importation j’observe les APIs
appelées par ce programme.
La liste des sections laisse apparaitre une section nommée .rsrc
.
Des données de toutes sortes peuvent être stockées dans un fichier PE
, une façon de réaliser ceci, est d’utiliser les ressources comme espace de stockage.
Ci-dessous, la liste des ressources. L’une d’elle s’appelle CFG
.
Liste des chaines de caractères ANSII
et Unicode
codée en dur dans le programme.
Donc pour résumer, à première vue le code ne semble pas être obfuscé
, ce qui me laisse penser ceci ; tout simplement, des appels à certaines API
sensibles de Windows
sont vus dans la table d’importation.
Certaines informations suggèrent un accès en lecture/écriture dans la base de registre de Windows
:
La chaine de caractères Unicode Software\Microsoft\Windows\CurrentVersion\Run
est également présente. C’est un PATH
de la base de registre Windows
bien connu pour la persistance (basique) d’un programme au démarrage de l’ordinateur (ou de la session utilisateur).
Généralement, il est préférable de ne pas laisser fuiter ces informations aussi simplement. D’autres
RAT
sont bien plus subtiles ou … évolués.
Le programme semble contenir des ressources (rcdata
), l’une de ces ressources s’appelle CFG
. S’agirait-il de la configuration du RAT ?
La chaine de caractères BILGE:TONYUKUK:BEN:M:TABGAC:ILINGE:KILINDIM: TRK:BODUNU:TABGACKA:KK:ERTI
est plutôt suspecte, je décide de la googeler et … oh surprise.
En bref, si on demande à son interprète Turc favori (Google), on obtient la traduction approximative suivante :
Le sage Tonyukuk était la solution au problème, et le buddog turc était aveugle. (cf. Wikipédia)
Il y a également une référence au Khan dans les écrits, ce qui laisse penser que cette histoire ne doit pas dater d’hier :
Les inscriptions Orhun concernent aussi Tonyukuk, une personne politique qui, contrairement à Bilge Kagan et Ash Tigin, devrait, en fait, être généralement écrite pour le Khan.
Comment dire … je me suis laissé distraire là !
Je jette un petit coup d’œil aux ressources du programme, j’utilise Resource Hacker
comme explorateur.
What The Fuck! Je m’attendais à rencontrer un algorithme de chiffrement ou à minima une substitution mono-alphabétique (rot-13
, …). Mais, il n’en est rien.
La configuration de l’agent est accessible, en clair, dans les ressources.
Je trouve également d’autres éléments dans les ressources, sans doute liés aux commandes du RAT
.
Au passage, j’en profite pour coder un outil en Python
capable d’extraire des données contenues dans les ressources d’un PE
.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def read_pe_rsrc(file_in, rsrc_name) :
from pefile import PE
try :
pe = PE(file_in)
except Exception, error :
print("warning: pe_fail (with exception from pefile module) on file: %s" % (file_in))
return(None, "(Exception):, %s" % (str(error)))
offset = 0x0
size = 0x0
for rsrc in pe.DIRECTORY_ENTRY_RESOURCE.entries :
for entry in rsrc.directory.entries :
if(entry.name is not None) :
if(entry.name.__str__() == rsrc_name) :
offset = entry.directory.entries[0].data.struct.OffsetToData
size = entry.directory.entries[0].data.struct.Size
return((pe.get_memory_mapped_image()[offset : offset + size]), None)
def main() :
import sys
from argparse import ArgumentParser
parser = ArgumentParser(prog=sys.argv[0])
parser.add_argument('-f', '--filename', type=unicode, help='Specify target filename.')
parser.add_argument('-r', '--rsrcname', type=str, help='specify resource name.')
args = parser.parse_args()
print read_pe_rsrc(args.filename, args.rsrcname)[0]
if(__name__ == '__main__') :
main()
En jouant avec le builder
j’arrive assez vite à me faire une idée sur la structure de la configuration.
Je poursuis mon analyse statique à l’aide d’un désassembleur (IDA
), mon objectif est d’analyser le comportement (focus sur le chargement de la configuration) du RAT
.
L’agent Bozok-RAT
n’a pas été compilé avec l’option ASLR
, l’analyse sera d’autant plus simple.
Ci-dessous, le point d’entrée du programme (l’agent) à l’adresse mémoire 0x004071F0
.
Le premier appel call nullsub_1
fait référence à une fonction qui ne sert à rien.
Je décide alors de suivre le second appel call sub_404514
.
Dans cette fonction, j’observe (encore une fois) cette référence à Tonyukuk. Je suppose, avec le recul, que cette string sert de point de repère au développeur du logiciel pour des besoins de débogage.
Le quatrième appel de cette fonction call sub_4040f0
sert vraisemblablement à l’initialisation du processus de l’agent :
Je poursuis sur le cinquième appel call sub_401f9c
et j’observe des références sur la configuration du RAT
.
J’observe également la version de l’agent (1.5.1) sans doute en dur dans le binaire.
Dans le premier appel de cette fonction call sub_402300
je trouve des références aux fonctions permettant de charger des ressources contenues dans un PE
(FindRessourceA, LoadResource, …).
Le chargement de la configuration est donc réalisé dans cette fonction.
Si je continu à dérouler, la création d’un mutex
.
Un thread
est créé si l’option Visible Mode
est activée pour afficher une boîte de dialogue avec le texte suivant : Server is running in visible mode, click OK to uninstall
.
Puis, le troisième appel de la fonction principale commence par initialiser le protocole de communication.
Je dois dire … que je suis encore surpris par le manque de protection concernant la configuration.
Je décide alors de laisser tomber le désassembleur pour une analyse réseau avec Wireshark
. Toujours avec l’idée d’avoir un aperçu global avant de rentrer dans les détails si besoin.
Ca commence fort … sur la même VM
(de sandbox) j’exécute le client et le serveur. Ma première difficulté est d’obtenir une analyse de trames.
Visiblement, dans ces conditions je n’observe rien dans l’analyseur, pourtant, le RAT
fonctionne correctement, lui.
Pour éliminer tout malentendu, je positionne une seconde VM
. Toujours en étant cloisonné (mais les deux VM
peuvent communiquer entre-elles).
Je me sers de l’une des VM
pour héberger le C2C
et, sur la seconde j’exécute l’agent. Je fini par obtenir une analyse de trames, enfin.
En résumé, j’ai procédé de la façon suivante. J’ai mis en écoute Wireshark
sur une des deux VM
et j’ai ouvert le centre de contrôle du RAT
sur la première, puis, j’ai exécuté l’agent sur la seconde … Je suis parti boire un café.
Un instant plus tard, constatant que le dialogue ne s’est limité qu’à une pseudo authentification et le push
de quelques informations concernant l’hôte cible. Je décide alors d’ouvrir un shell
à distance et de réaliser un simple dir
.
J’obtiens ainsi timidement quelques informations qu’il faut désormais étudier.
Ci-dessous, l’authentification entre le C2C
et l’agent (nan mais … c’est quoi cette merde ???). Le mot de passe est annoncé cleartext par le centre de contrôle !
Et puis l’agent… qui envoi au C2C
quelques informations d’usages…
Je ne sais pas quoi dire, sauf, que ce n’était pas très brillant de payer presque 500€ annuel pour un outil aussi … pourri !
L’agent commence par initialiser une connexion TCP/IP inversée avec le centre de contrôle.
Une parenthèse, sur le nom des binaires, choisie par le développeur :
client.exe
,serveur.exe
.Ici, la notion de client et serveur est un peu particulière :
RAT
, généralement chargé de se connecter à un ou plusieurs centres de contrôles, et d’attendre des instructions (ce qui sous-entend qu’il joue un rôle de client).Donc, si on se place côté réseau, le port en écoute est sur le centre de contrôle.
Mais alors pourquoi ?
Peut-être que … tout simplement, il fut une époque ou les ordinateurs disposaient pour la plupart d’une adresse IP
publique. Ainsi le serveur s’installait sur l’hôte ciblé, et, l’attaquant utilisait un client pour se connecter à distance.
Aujourd’hui, les firewalls
et le NAT
ne permettent plus ce type de connexion. C’est alors l’hôte, contenant la charge utile (l’agent), qui initialise la connexion avec son centre de contrôle.
En réalisant que le protocole de communication est aussi sécurisé que le stockage de la configuration, je décide dans un premier temps de coder un outils qui serait capable d’émuler une communication avec le centre de contrôle.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
def create_socket(hostname, port) :
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
try :
s.connect((hostname, port))
except(socket.error), message :
print(message)
s.close()
return(0)
return(s)
def recv_socket(socket) :
data_stream = ""
try :
data_stream = socket.recv(4096)
except(Exception), message :
if(message == "timed out") :
sleep(1)
finally :
if(len(data_stream) != 0) :
return(data_stream)
else :
return("")
def format_str(str) :
s = ""
i = 0
for i in range(0, len(str)) :
s = s + str[i]
if(i < (len(str) - 1)) :
s = s + chr(int('0x00', 16))
return(s)
def unformat_str(str) :
s = ""
i = 0
for i in range(0, len(str) - 1) :
if not (i % 2) :
s = s + str[i]
return(s)
def bozok_client_register(socket, hostname, username, server_id, password) :
from binascii import unhexlify
client_fake_str = format_str(hostname + "|" + username + "|" + server_id + "|FRA|3|1.5.1|0|2|" + password + "|1|Program Manager|0|1|1023|592|Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz|2699")
try :
socket.send(unhexlify("1401000000") + client_fake_str + unhexlify("0000"))
except(socket.error), message :
print(message)
def main() :
from binascii import hexlify
from time import sleep
c2c_hostname = "192.168.XXX.XXX"
c2c_port = 1515
# initialisation d'une connexion vers le centre de controle.
socket = create_socket(c2c_hostname, c2c_port)
if(socket == 0) : exit()
print(" [*] Connected to %s:%d" % (c2c_hostname, c2c_port))
# le centre de controle envoi son mot de passe (en clair).
data_stream = recv_socket(socket)
if(hexlify(data_stream[:5]).upper() == "0E00000000") :
password = unformat_str(data_stream[5:])
else :
exit(1)
bozok_client_register(socket, "SBOX-XXXXXXXXXX", "Administrator", "PIRATES.RE", password)
print(" [*] Registering to c2c completed")
while(True) :
data_stream = ""
command_id = ""
data_stream = recv_socket(socket)
if(len(data_stream) == 6) :
command_id = hexlify(data_stream).upper()
elif(len(data_stream) > 6) :
command_id = hexlify(data_stream[5:]).upper()
else :
sleep(1)
if(len(command_id) != 0) :
if(command_id == "020000006200") : # stop server
print("[C2C] request to stop the server.")
socket.close()
exit(0)
else :
print("[C2C] Command not implemented : %s" % (command_id))
socket.close()
if(__name__ == "__main__") :
main()
Dans un second temps, je fais une copie de mon script et je l’ai modifié légèrement pour faire du fuzzing
. Mais je ne me suis pas trop attardé sur le sujet étant donné que je n’ai pas regardé le centre de contrôle. Et aussi … que ce projet est mort.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
def create_socket(hostname, port) :
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
try :
s.connect((hostname, port))
except(socket.error), message :
print(message)
s.close()
return(0)
return(s)
def recv_socket(socket) :
data_stream = ""
try :
data_stream = socket.recv(4096)
except(Exception), message :
if(message == "timed out") :
sleep(1)
finally :
if(len(data_stream) != 0) :
return(data_stream)
else :
return("")
def format_str(str) :
s = ""
i = 0
for i in range(0, len(str)) :
s = s + str[i]
if(i < (len(str) - 1)) :
s = s + chr(int('0x00', 16))
return(s)
def unformat_str(str) :
s = ""
i = 0
for i in range(0, len(str) - 1) :
if not (i % 2) :
s = s + str[i]
return(s)
def bozok_client_register(socket, hostname, username, server_id, password, fuzz_var) :
from binascii import unhexlify
client_fake_str = format_str(fuzz_var + "|" + username + "|" + server_id + "|FRA|3|1.5.1|0|2|" + password + "|1|Program Manager|0|1|1023|128|Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz|2699")
try :
socket.send(unhexlify("1401000000") + client_fake_str + unhexlify("0000"))
except(socket.error), message :
print(message)
def fuzz_c2c(str_fuzz) :
from binascii import hexlify
from time import sleep
c2c_hostname = "192.168.XXX.XXX"
c2c_port = 1515
# initialisation d'une connexion vers le centre de controle.
socket = create_socket(c2c_hostname, c2c_port)
if(socket == 0) : exit()
print(" [*] Connected to %s:%d" % (c2c_hostname, c2c_port))
# le centre de controle envoi son mot de passe (en clair).
# data_stream = socket.recv(4096)
data_stream = recv_socket(socket)
if(hexlify(data_stream[:5]).upper() == "0E00000000") :
password = unformat_str(data_stream[5:])
else :
exit(1)
bozok_client_register(socket, "SBOX-XXXXXXXXXX", "Administrator", "PIRATES.RE", password, str_fuzz)
print(" [*] Registering to c2c completed")
sleep(2)
socket.close()
def main() :
F = "A" * 1
i = 0
while(True) :
i = i + 1
F = F + 'A'
fuzz_c2c(F)
print("[*] Count: %s" % (i))
if(__name__ == "__main__") :
main()
Touver un buffer overflow
sans qu’une opération soit demandée par le C2C
aurait été intéressant. Aussi, ce passage me permet d’avoir une idée un peu plus précise sur la longueur des données qui sont envoyées au centre de contrôle.
Alors, je reste concentré sur l’objectif de créer plusieurs instances sur le centre de contrôle (en modifiant légèrement le script de départ).
Et pour terminer le protocole de communication, à ce stade, le script est capable d’interpréter la commande Stop Server
envoyée par le centre de contrôle et se stopper. Toutes les autres commandes sont ignorées, il est aussi possible de créer n instance(s) sur le centre de contrôle.
J’ai essayer de créer un maximum d’instances (5000) et puis le script m’a refusé un nouveau thread
à 882 instances, le centre de contrôle reste stable (pas de crash).
J’ai fais quelques recherches sur le concepteur, le compte twitter @slayer_616 servait à communiquer sur ses différents projets. Dans l’ordre :
J’ai téléchargé une version de Schwarze Sonne
et Umbra loader
et j’ai accordé un peu d’attention à trois points.
IDA
), voir si il y a une correspondance avec Bozok-RAT
,Vous l’aurez compris … c’est la même merde (mais en plus vieux).
Du coté du code des agents :
Schwarze Sonne
tout semble complètement différent, ce n’est apparement pas le même code.Umbra Loader
on retrouve un code un peu similaire à Bozok-RAT
, du coup, je n’exclus pas une réutilisation de portions de codes. Et comme par hasard, je retrouve aussi des références à Tonyukuk.A propos de la configuration :
Schwarze Sonne
est également servie cleartext dans les ressources du PE
.
Umbra loader
SURPRISE ! la configuration est cryptée. En même temps, je me dis que ce ne sera pas plus évolué qu’un simple XOR
.
Avec IDA
, j’ai regardé le code en partant de la fonction principale. Quand je suis arrivé à la création du mutex
je me suis dit que j’étais allé un peu trop loin. Du coup, je suis allé voir la fonction placée juste avant.
Cette fonction semble jouer un rôle dans le chargement de la configuration, je regarde dans celle-ci un call
au hasard et … Un XOR
.
Je modifie le script précédent (pour la lecture de la configuration de Bozok-RAT
) et j’adapte un XOR 0x0D
pour le décryptage de la configuration de Umbra loader
.
Notez : La clé XOR
est codée en dur dans l’agent. Elle ne change donc pas à chaque génération.
D’autre part 0x00 ^ 0x0D = 0x0D
, on retrouve ainsi la clé de cryptage un peu partout dans la ressources.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def read_pe_rsrc(file_in, rsrc_name) :
from pefile import PE
try :
pe = PE(file_in)
except Exception, error :
print("warning: pe_fail (with exception from pefile module) on file: %s" % (file_in))
return(None, "(Exception):, %s" % (str(error)))
offset = 0x0
size = 0x0
for rsrc in pe.DIRECTORY_ENTRY_RESOURCE.entries :
for entry in rsrc.directory.entries :
if(entry.name is not None) :
if(entry.name.__str__() == rsrc_name) :
offset = entry.directory.entries[0].data.struct.OffsetToData
size = entry.directory.entries[0].data.struct.Size
return((pe.get_memory_mapped_image()[offset : offset + size]), None)
def main() :
import sys
from argparse import ArgumentParser
from binascii import hexlify
parser = ArgumentParser(prog=sys.argv[0])
parser.add_argument('-f', '--filename', type=unicode, help='Specify target filename.')
parser.add_argument('-r', '--rsrcname', type=str, help='specify resource name.')
args = parser.parse_args()
cfg = read_pe_rsrc(args.filename, args.rsrcname)[0]
plain = ""
for c in cfg :
plain += chr(ord(c) ^ 0x0d)
print("config: %s" % (plain))
if(__name__ == '__main__') :
main()
Ci-dessous, une représentation d’une partie de la configuration cryptée.
Et pour ce qui est du protocole de communication, je n’ai pas pris le temps de regarder Umbra loader
mais juste jeter un oeil sur Schwarze Sonne
.
Je n’ai pas compris à quoi sert le mot de passe, il n’est pas possible de le configurer sur le C2C
. Par contre, il est facile de deviner à quoi il ne sert pas. 😛
Le script ci-dessous est une version modifiée de celui que j’ai codé pour l’émulation d’un hôte avec Bozok-RAT
, adapté pour Schwarze Sonne
.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
def create_socket(hostname, port) :
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
try :
s.connect((hostname, port))
except(socket.error), message :
print(message)
s.close()
return(0)
return(s)
def recv_socket(socket) :
data_stream = ""
try :
data_stream = socket.recv(4096)
except(Exception), message :
if(message == "timed out") :
sleep(1)
finally :
if(len(data_stream) != 0) :
return(data_stream)
else :
return("")
def ss_client_register(socket, hostname, username, server_id) :
from binascii import unhexlify
client_fake_str = server_id + "|" + username + "@" + hostname + "|Windows XP|0|2.0 Beta 1|Program Manager|FRA|"
try :
socket.send(client_fake_str)
except(socket.error), message :
print(message)
def main() :
from binascii import unhexlify, hexlify
from time import sleep
c2c_hostname = "192.168.XXX.XXX"
c2c_port = 1515
# Initialisation d'une connexion vers le centre de controle.
socket = create_socket(c2c_hostname, c2c_port)
if(socket == 0) : exit()
print(" [*] Connected to %s:%d" % (c2c_hostname, c2c_port))
# L'agent se signial a son centre de controle.
try :
socket.send(unhexlify("015200000001"))
except(socket.error), message :
print(message)
exit(-1)
sleep(1)
ss_client_register(socket, "SBOX-XXXXXXXXXX", "Administrator", "PIRATES.RE")
print(" [*] Registering to c2c completed")
while(True) :
data_stream = ""
command_id = ""
data_stream = recv_socket(socket)
if(len(data_stream) == 6) :
command_id = hexlify(data_stream).upper()
elif(len(data_stream) > 6) :
command_id = hexlify(data_stream[5:]).upper()
else :
sleep(1)
if(len(command_id) != 0) :
if(command_id == "010000000003") : # Stop server
print("[C2C] request to stop the server.")
socket.close()
exit(0)
else :
print("[C2C] Command not implemented : %s" % (command_id))
socket.close()
if(__name__ == "__main__") :
main()
Le RE
(Reverese Engineering
) est une pratique intéressante lorsqu’il s’agit de comprendre comment une technologie fonctionne. Ceci s’applique bien sûr aux logiciels.
Dans l’étude de Bozok-RAT
, enfin, plus précisément son agent, on peut se rendre compte que rien n’est mis en oeuvre pour ralentir le reverse engineering
.
Egalement, la sécurité n’a pas été intégrée au produit.
PE
.C2C
envoit son mot de passe à n’importe qui voulant initialiser une connextion.C2C
ne sont pas chiffrés.Ces trois outils (Schwarze Sonne
, Umbra loader
et Bozok
) sont de très bons cas d’études pour apprendre le RE
. Le coté plus concret, réel, fait un peu sortir de l’univers des CTF
(Capture The Flag
).
A savoir tout de même, très généralement, les agents sont packer
avant d’être déployés. Ce qui permet de cacher le binaire exécuté aux anti-virus (d’une certaine façon).
Je pense que le sujet des packers
sera abordé dans un prochain article 🙂
Je crois désormais que j’ai passé beaucoup trop de temps à étudier ceci alors, je ferme ce dossier.
J’èspère que cet article vous aura intéressé.