Solution de tacorabane pour Vault

intro reverse linux x86/x64

5 décembre 2023

Introduction

Ce challenge est étiquetté comme reverse. Donc nous allons devoir nous attaquer à un fichier exécutable.

Commencez par télécharger le fichier exécutable et nous allons commencer méthodiquement à la résolution du challenge.

Analyse du fichier

Une fois téléchargé, commencez par analyser le fichier que les équipes Hackropole nous ont mis à disposition.

N.B: Pour les pro-Linux, n’oubliez pas de modifier le mode du fichier en le rendant exécutable chmod +x vault.

$ file vault
vault: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=29f72609e9d07b048545e63694738662fea248da, not stripped

À ce sade, je ne vais pas détailler la ligne retour car vous êtes censés avoir déjà passé l’épreuve précédente de reverse engineering.

Commençons par lancer le programme et regardons ce qu’il nous dit.

$ ./vault
=-=-=-= Very secure vault =-=-=-=
Please enter you very secure password: 

Mettez-y ce que vous voulez.

=-=-=-= Very secure vault =-=-=-=
Please enter you very secure password: 
Wrong password: authorities have been alerted!

Bon ! Nous avons alerté les autorités… Nous devons faire vite afin de démonter notre PC et mettre le disque dur dans le micro-onde… C’est une blague, hein ! Ne faites surtout pas ça ! ^^’

Nous avons donc deux informations :

  1. Le prompt de début que j’appellerai banière.
  2. Le prompt d’erreur.

C’est à ce moment là que notre rôle de rétro-ingénieur intervient !

Désassemblage du programme

Dans mon exemple, j’utiliserai l’outil Cutter. Cependant, vous êtes libres d’utiliser n’importe quel outil avec lequel vous êtes le plus confortable.

Avertissement : Les adresses mémoire mentionnées dans cette solution peuvent être différentes aux vôtres. Ne paniquez pas et copiez bien ce que vous voyez sur votre écran ;).

Commençons par parcourir le code désassemblé pour trouver la chaine de caractère banière qui suit =-=-=-= Very secure vault =-=-=-=.

Allez dans le code désassemblé du main. Classique dans tous les programmes, plus bas, nous trouvons deux lignes qui nous intéressent.

call_banner

Dans l’exemple, nous nous aperçevons que la chaine de caractère est stockée à l’adresse 0xcb8. En y accédant, nous tombons sur toutes les chaines de caractère du programme. Intéressant !

list_strings

Bien sûr, nous y voyons le format du flag à donner mais, évidemment, les développeurs sont malins et ne l’ont pas programmé en brut dans le code source.

Plus haut, on voit une suite alpha-numérique qui me semble importante. Mon petit doigt me l’a dit.

Essayons de voir à quoi elle correspond.

Comme nous avons vu dans le retour d’analyse du fichier que les objets sont dynamiquements liés, il existe une table des relation dynamique plus haut.

dynamic_relations

Nous pouvons voir qu’il y a un objet nommé password. Accédons à l’adresse 0x202010.

password

Donc la suite du mot de passe est bien ce que nous devons récupérer. Si vous tentez de mettre la chaine de caractère password en tant que mot de passe dans votre programme, vous aurez le même message d’erreur qu’au début. Donc, il se passe quelque chose dans ce programme.

Analyse de l’algorithme

Il va falloir reverser les algorithmes du programme pour comprendre ce qu’il faut faire avec ce mot de passe. Revenons donc au main.

Nous allons analyser toute la partie après l’affichage =-=-=-= Very secure vault =-=-=-=.

bloc_check_char

Appel de printf donc affichage, appel de fflush pour vider le buffer puis nous avons un saut à l’adresse 0xb50.

user_input

Les développeurs ont utilisé la fonction getchar() pour gérer la saisie utilisateur. Je vous laisse regarder comment fonctionne la fonction getchar() ici.

Comme vous pouvez le voir, cette fonction est particulière car elle lit tous les caractère du flux d’entrée. Ce qui veut dire, qu’en langage C, il serait développé sous cette forme.

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

int main(int argc, char **argv) {
    unsigned char currentChar;
    
    while((currentChar = getchar) != EOF) {
        // Code si le caractère n'est pas égale à EOF (End Of File)
    }
}

Ensuite, nous avons une comparaison sûrement pour vérifier le caractère EOF (’\n’).

Explication : Nous avons la ligne cmp byte [var_a5h], 0xa En hexadécimal, 0xa vaut 10 et le caractère de EOF (’\n’) vaut 10. Si vous souhaitez tester, essayer le code suivant.

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

int main(int argc, char **argv) {
    printf("%d\n", '\n'); // Affiche 10
    return 0;
}

Et si c’est le cas, il retourne à l’adresse 0xaba. À partir de cet endroit, nous apercevons un appel à putchar avec comme argument edi mov edi, 0xa donc notre caractère EOF.

Ensuite, le reste du programme part pour mettre fin au programme. Donc selon notre évaluation du code. Il faut un break; pour sortir de la boucle while.

Donc le code du développeur diabolique devrait être le suivant.

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

int main(int argc, char **argv) {
    unsigned char caractere;
    
    while((caractere = getchar()) != EOF) {
        if (caractere == '\n') {
            putchar('\n');
            break;
        }
    }
}

Mais nous avons besoin du reste, le moment le plus intéressant du programme. Nous allons donc sauter à l’adresse 0xae9.

check_char

Si vous avez suivi le code avec assuidité, nous connaissons la valeur de [var_a4h] qui est égale à 0.

arg_1

Donc nous avons [type?] check_char(0, arg_2(var_a5h)). Essayons de savoir que vaut le deuxième argument.

Vous suivez bien ! En effet, [var_a5h] a été comparé à EOF précédemment. Donc c’est la variable c dans notre code évalué.

Pour la suite de notre investigation, regardons ce que fait check_char(a, b).

func_check_char

Bon comme vous le voyez, c’est pas si simple à lire donc nous allons voir comment le décompilateur nous présente la fonction.

decompiler

Avec cet aperçu, nous allons pouvoir reproduire le code source de la fonction.

int check_char(int arg1, char arg2) {
    uint32_t rax_10;
    rax_10 = *(((arg1 + 0xa) % 0x28) + password) == arg2;
    return rax_10;
}

Mouais, ce n’est pas si parlant que ça. Mais nous allons réadapter tout ça.

int check_char(int position, char car) {
    return password[(position + 10) % 40] == car;
}

Un peu plus clair. Voici le code source avec ce que nous avons pu reverse.

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

int check_char(int position, char car) {
    return password[(position + 10) % 40] == car;
}

int main(int argc, char **argv) {
    unsigned char caractere;
    
    while((caractere = getchar()) != EOF) {
        if (caractere == '\n') {
            putchar('\n');
            break;
        }
    }
}

Nous approchons du but. Il nous manque l’appel de cette fonction. Retournons à notre assembleur.

flag_part

Nous sommes à quelques lignes du flag.

add dword [var_a0h], eax
cmp dword [var_a0h], 1
jne 0xb49

Cette ligne effectue un ET logique sur le résultat de la fonction check_char().

resultat = resultat & check_char(postion, car);
if (resultat == 1) {
    // Suite
}
// Factorisation
resultat &= check_char(postion, caractere);
if (resultat == 1) {
    // Suite
}
jne 0xb49

Cet operateur JUMP NOT EQUAL permet de partir sur la prochaine operation.

add dword [var_a4h], 1

Il faut comprendre ici, une incrémentation par 1 dans la boucle while((c = getchar()) =! EOF) {}. Modifions notre code évalué !

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

int check_char(int position, char car) {
    return password[(position + 10) % 40] == car;
}

int main(int argc, char **argv) {
    unsigned char caractere;
    unsigned char int position = 0;
    int resultat = 1; // mov dword [var_a0h], 1
    
    
    while((caractere = getchar()) != EOF) {
        if (caractere == '\n') {
            putchar('\n');
            break;
        }
        resultat &= check_char(position, caractere);
        
        if (resultat == 1) {
            // Flag apparaît ici
        }
        position += 1;
    }
}

Scripting

À partir de ce moment, il ne sert plus à rien d’étudier l’assembler. En effet, nous connaissons l’algorithme qui décode la saisie utilisateur et le password qui doit être encoder pour afficher le flag.

Pour faire simple, j’ai conçu le programme en C qui me donne le bon mot de passe. Le voici.

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

static const char *password = "b87de397e1346bc605be4ed8361a68a3d9748fc9";

void test() {
    int i;
    for(i = 0; i < 40; i++) {
        putchar(password[(i + 10) % 40]);
    }
    putchar('\n');
}

int main() {
    test();
    return 0;
}

Ce qui nous retourne le résultat 346bc605be4ed8361a68a3d9748fc9b87de397e1 que vous n’avez plus qu’à mettre dans le programme.

./vault
=-=-=-= Very secure vault =-=-=-=
Please enter you very secure password: 
\o/ Access granted! \o/
Here is your flag: ECSC{346bc605be4ed8361a68a3d9748fc9b87de397e1}

Alors, c’est qui le lion maintenant ! :)