Catégories
CTF interIUT 2020 Reverse Write-ups

CTF interIUT – BoC 1 & 2

Ce challenge assez sympa de reverse provient du CTF interIUT. J’ai choisi d’en faire le write-up car celui-ci peut servir de bonne petite introduction à l’analyse de malwares. Vous trouverez ici les deux parties du challenge.

Description des challenges

  • BoC 1 :” Le service forensic de Random Corp. a détecté un fichier étrange. Le responsable, Cristouffe, surchargé comme à son habitude vous confie la lourde tâche d’analyser ce fichier. Récupérez la clé de chiffrement de ce malware.”

    Le flag est au format H2G2{la clé en hexa sans les null bytes}
  • BoC 2 : “Récupérez maintenant le nom de domaine du serveur que le malware contacte.”

    Le flag est au format H2G2{le nom de domaine du serveur}

Auteurs : @Masterfox / @OxNinja

Fichier du challenge

Le binaire à reverse pour ces challenges est disponible ici : BoC.exe.

Première approche

Comme d’habitude, on commence par faire un file sur le binaire.

$ file BoC.exe 
BoC.exe: PE32 executable (console) Intel 80386, for MS Windows

On a donc ici un exécutable windows x86_64 (32 bits) : la commande file ne donne pas beaucoup d’informations sur les exécutables Windows.

Reverse

Imports suspects

On ouvre donc IDA (ou notre désassembleur favoris : Cutter, Ghidra, Binary Ninja, radare2…) et on peut commencer par observer les imports : plusieurs choses nous sautent aux yeux.

Premièrement, on remarque ici des fonction importées de la librairie ADVAPI32.dll qui vont servir à chiffrer des données, ce qui est assez inhabituel dans un programme.

On peut ensuite voir des fonctions importées de la librairie KERNEL32.dll qui sont couramment utilisées dans des malwares (r/w de fichiers, détection de debugger, gestion des threads et processus).

Pour finir, on retrouve des fonctions importées de la libraire WS2_32.dl également couramment associées à des malwares notamment pour la connexion aux CnCs (Command and Control servers) qui sont globalement des machines auxquelles toutes les instances d’un même malware vont se connecter, un serveur appartenant donc à l’attaquant qui va centraliser les différentes informations récoltées par le malware.

Toutes ces informations nous confirment donc que ce programme est en fait un malware (ou en tout cas qu’il en a tout l’air !).

Fonction appelant le main

On trouve rapidement la fonction main, mais une bonne pratique consiste à d’abord à analyser le contexte appelant du main. Pour cela, sélectionner la fonction main ou son adresse et appuyer sur la touche “x” (sur IDA et Cutter en tout cas) pour afficher les “Cross-references” c’est à dire les endroits où cette fonction est appelée.

On voit donc que le main est appelé dans une fonction (que j’ai renommée INITIALISATION). Voyons ce qui se cache dans cette fonction.

On voit ici que 5 noms de fichiers sont mis dans des buffers que j’ai ici appelés file_1, file_2 jusqu’à file_5. Ensuite, on voit une boucle qui va lancer le main avec comme paramètre un des noms de fichiers et ceci pour tous les noms de fichiers.

Après ceci, une tête de mort en ASCII art va être affichée, suivie de différents message à l’attention de la victime.

On peut vérifier ceci en exécutant le programme dans une VM (je vous le conseille TRÈS vivement si vous êtes amenés à analyser de vrais malwares).

On peut maintenant passer à l’analyse du main et partir à la conquête des flagzzz !

Clé de chiffrement – flag du BoC 1

La première partie du challenge consiste à récupérer la clé de chiffrement du malware. Pour ceci on va donc se concentrer sur les call à des fonctions de la librairie ADVAPI32.dll.

On voit ici dans le bloc en bas à droite que si tout se passe bien, chaque fichier va être remplacé par sa version chiffrée dont le nom sera suivit de l’extension “.boc”.

Tout ce qui nous intéresse pour ce premier flag se trouve ici.

Pour généraliser, la fonction CryptAcquireContextW va permettre de récupérer un conteneur de clé nécessaire à l’appel des autres fonctions du CryptoAPI Windows. Ensuite la fonction CryptCreateHash, nécessaire au call des fonctions CryptHashData et CryptDeriveKey, va se charger d’initialiser le hashage d’un flux de données.

Le fonction qui nous intéresse ici va être CryptHashData qui prend en paramètre un handle d’objet de type “hash” généré précédemment comme on l’a vu (paramètre “hHash”), un pointeur vers un buffer d’octets constant contentant les données qui doivent être ajoutées au hash (ce qui correspondrait donc à notre clé, c’est le paramètre “pbData”) et enfin le paramètre “dwDataLen” qui représente le nombre d’octets du buffer à ajouter à l’objet. On voit dans le début du main que ce sera en fait tout le buffer.

.text:00401042   push    offset buf              ; "Ø"
.text:00401047   call    ds:lstrlenW
.text:0040104D   mov     [esp+0Ch+dwDataLen], eax

Ce qui correspond à ceci :

 dwDataLen = strlen(buf))

Notre premier flag correspond donc au paramètre “pbData” soit ici un buffer que j’ai renommé “buf” (IDA détecte que ce buffer contient juste le caractère “Ø” mais c’est à cause des null bytes).

Notre flag ici sera donc en hexa, sans les null bytes comme dit dans l’énoncé et le tout en lowercase : H2G2{d8c1afc13a750ca86e2e8659437553215dcbc13b1f90}

Connexion au CnC – flag du BoC 2

Pour ce deuxième flag il va nous falloir récupérer le nom de domaine du CnC contacté par le malware, et donc se concentrer sur les call aux fonctions de la libraire WS2_32.dl, et notamment sur la fonction “gethostbyname” qui va prendre en seul argument le nom du domaine à contacter.

On voit ici que le programme va boucler afin de décoder une chaine de caractères que j’ai ici appelée “domain encoded”, qui sera ensuite passée en argument à la fonction gesthostbyname (donc contenue dans le registre ESI).

.rdata:00403938 domain_encoded db 'blb.N58c0xXKx@86bF6z.kovdphwu.bvg',0

Cette chaine de caractère est de base mal gérée par IDA, le premier caractère (“b” soit 0x62 en hexa) apparaissant à part. Pour cela, il faut cliquer sur ce “62h” et appuyer la touche “A” pour rattacher ce caractère à la chaine qui suit.

On peut ensuite décomposer le code : l’algorithme est très simple et nous n’allons donc pas nous servir du pseudo-code que peuvent générer des outils comme IDA, Cutter, Ghidra et autres. De plus, on va construire en parallèle un programme en C représentant l’algorithme afin que ce soit plus clair.

Il est important de commencer par repérer le compteur : ici ce sera ECX qui est le registre par défaut de comptage.

On voit ici que le programme va boucler jusqu’à ce que ECX soit égal à 33 (ou 0x21) en s’incrémentant de un à chaque tour de boucle. En chaque fin de boucle, domain[i] prend la valeur contenue dans le registre AL.

Pour le moment on a donc ceci.

#include <stdio.h>

const char* domain_encoded = "blb.N58c0xXKx@86bF6z.kovdphwu.bvg";
//Y'a un truc ici normalement

for (int i = 0; i < strlen(domain_encoded); i++) {
    // Des trucs ici
    domain[i] = ...
    // Des trucs là
}

On continue !

On voit donc ici qu’à chaque tour de boucle une comparaison a lieu entre le caractère du domaine encodé et le caractère “.” (0x46). Si le caractère correspondant est donc un point, alors il sera également un point dans le nom de domaine final. On a donc ceci.

#include <stdio.h>

const char* domain_encoded = "blb.N58c0xXKx@86bF6z.kovdphwu.bvg";
// Toujours rien ?

for (int i = 0; i < strlen(domain_encoded); i++) {
    if(domain_encoded[i] == '.'){
        domain[i] = domain_encoded[i] // = '.'
    }else{
        domain[i] = ...
        // Encore du mystère
    }
}

Super on avance on avance 🙂 ! Voyons ce qu’il se passe dans le cas où le caractère n’est pas un point. Mais d’abord, petit mémo sur la taille des registres 😀

Guide to x86 Assembly

ECX n’étant ici jamais plus grand que 33, notre compteur rentre alors dans CL (qui prend des valeurs de 0 à 255).

mov     al, cl
and     al, 1
inc     al
xor     al, dl

mov, and, inc, xor ? Wow ca à l’air compliqué tout ça…

– Apprenti reverseur, vite découragé

Mais non, pas du tout ! On a donc notre compteur dans CL, que l’on met dans AL. On effectue l’opération AND entre AL et 1, ce qui correspond à faire un modulo 2 sur AL ! Et ensuite on incrémente AL, c’est-à-dire qu’on lui ajoute 1. Pour finir, on effectue un XOR entre AL et DL (qui contient le caractère correspondant de notre chaine domain_encoded) et on stocke le tout dans AL. Facile 🙂 !

On a donc ceci.

#include <stdio.h>

const char* domain_encoded = "blb.N58c0xXKx@86bF6z.kovdphwu.bvg";
// Il manque toujours quelque chose

for (int i = 0; i < strlen(domain_encoded); i++) {
    if(domain_encoded[i] == '.'){
        domain[i] = domain_encoded[i] // = '.'
    }else{
        domain[i] = domain_encoded[i] ^ ((i%2)+1);
    }
}

Hum ok c’est bien beau tout ça mais il sort d’où ce “domain” ?

– Apprenti reverseur, un peu perdu

Le buffer “domain” correspond ici au registre ESI (registre par défaut utilisé comme index source pour les opérations de chaîne). C’est ce registre qui sera passé en argument de la fonction “gethostbyname” et qui contiendra donc le nom de domaine auquel accéder !

WTF ? Il sort d’où lui aussi ?

– Apprenti reverseur, encore plus perdu

Ah oui j’ai oublié de vous dire ! Revenons un peu en arrière…

Voilà c’est ici que ça se passe ! On voit un call à la fonction calloc qui va se charger de créer un buffer de 0x21, soit 33, octets/caractères (en effet il prend 0x22 octets comme taille mais commence à 1 soit 0x21 octets au final). Cette fonction met un pointeur vers le buffer créé dans EAX. Or on voit que ce pointeur est copié de EAX dans ESI juste après le call à la fonction. On a donc ESI qui est un pointeur vers le buffer créé !

On peut donc compléter le code.

#include <stdio.h>

const char* domain_encoded = "blb.N58c0xXKx@86bF6z.kovdphwu.bvg";
char* domain = calloc(0x22, 1);

for (int i = 0; i < strlen(domain_encoded); i++) {
    if(domain_encoded[i] == '.'){
        domain[i] = domain_encoded[i] // = '.'
    }else{
        domain[i] = domain_encoded[i] ^ ((i%2)+1);
    }
}

Super on a maintenant toutes les pièces du puzzle et on peut par exemple utiliser ce code pour trouver notre nom de domaine !

On ajoute quelques librairies pour faire les choses bien, on met tout ça dans un main et on ajoute un printf pour afficher notre flag.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(){
    const char *domain_encoded = "blb.N58c0xXKx@86bF6z.kovdphwu.bvg";
    char *domain = calloc(0x22, 1);

    for(int i = 0; i < strlen(domain_encoded); i++){
        if(domain_encoded[i] == '.'){
            domain[i] = domain_encoded[i]; // = '.'
        }else{
            domain[i] = domain_encoded[i] ^ ((i % 2) + 1);
        }
    }
    printf("[+] FLAG : H2G2{%s}\n", domain);
    return 0;
}

On compile ça et on peut lancer !

$ gcc -o BoC_2 BoC_2.c 
$ ./BoC_2 
[+] FLAG : H2G2{cnc.O79a1zYIyB94cD7x.interiut.ctf}

(Il est aussi possible de faire ça en python pour ceux qui sont allergiques au C …)

domain_enc = "blb.N58c0xXKx@86bF6z.kovdphwu.bvg"
domain = ""

for i, char in enumerate(domain_enc):
    if char == ".":
        domain += char
    else:
        domain += chr(ord(char) ^ ((i%2)+1))

print("[+] FLAG : H2G2{" + domain + "}")