Pirates Corporation & Co.


> Hop là ho ! une bouteille de rhum...

#

Dealing with a RAT


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…


Remote Administration Tool (RAT) - Bozok v1.5.1

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.

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).


Analyse statique

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.


Analyse dynamique

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 :

Ici, la notion de client et serveur est un peu particulière :

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).


Autres projets (même artiste)

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.

  1. Le fonctionnement général des agents (avec IDA), voir si il y a une correspondance avec Bozok-RAT,
  2. Dans les ressources à la recherche d’une trace de configuration,
  3. Une observation du protocole de communication.

Vous l’aurez compris … c’est la même merde (mais en plus vieux).

Du coté du code des agents :

A propos de la configuration :


Umbra loader

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.


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()


Conclusions

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.

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é.