Contexte
Nous devons atteindre un score supérieur à 1000 pour obtenir le flag.
Nous avons deux fichiers :
- docker-compose.yml : Exécute un docker avec le binaire.
- bonuspoints : Qui est le binaire à exploiter.
Comportement du binaire
$ chmod +x bonuspoints
$ file bonuspoints
bonuspoints: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=785ccc1769db4cc76c8c188395c2c81c6ce52a50, for GNU/Linux 3.2.0, not stripped
$ ./bonuspoints
Hello, here you can get some bonus points for the competition.
You cannot get more than 100 bonus points.
If you go above 1000 you win.
Your score is currently 12
How many bonus points do you want?
>>> 100
Your new score is 112
You should try to get more points
$ ./bonuspoints
Hello, here you can get some bonus points for the competition.
You cannot get more than 100 bonus points.
If you go above 1000 you win.
Your score is currently 45
How many bonus points do you want?
>>> 1000
Stop cheating!
Les premières étapes sont :
- On télécharge le fichier et on le rend exécutable avec chmod +x.
- On regarde quel type d’exécutable on a affaire avec file, ici on voit que c’est un exécutable ELF en 64 bit.
- On joue avec le binaire, ici on remarque :
- Qu’il faut avoir plus de 1000 points pour avoir le flag.
- On peut pas avoir plus de 100 bonus points
- Au départ on nous donne des points de manière aléatoire, il nous semble que c’est pas suffisant pour atteindre 1000 points.
Analyse du binaire
On décompile le binaire avec Ghidra :
undefined8 main(void)
{
uint __seed;
int iVar1;
char local_1a [10];
int local_10;
uint local_c;
local_c = 0;
__seed = getpid();
srand(__seed);
iVar1 = rand();
local_c = iVar1 % 100;
puts("Hello, here you can get some bonus points for the competition.");
puts("You cannot get more than 100 bonus points.");
puts("If you go above 1000 you win.");
printf("Your score is currently %u\n",(ulong)local_c);
printf("How many bonus points do you want?\n>>> ");
fflush(stdout);
fgets(local_1a,8,stdin);
local_10 = atoi(local_1a);
if (local_10 < 0x65) {
local_c = local_c + local_10;
printf("Your new score is %u\n",(ulong)local_c);
if (local_c < 0x3e9) {
puts("You should try to get more points");
}
else {
puts("Congratulations! Here is your flag:");
fflush(stdout);
system("cat flag.txt");
}
}
else {
puts("Stop cheating!");
}
return 0;
}
La création des variables
uint __seed; // entier non signé
int iVar1; // entier signé
char local_1a [10]; // un tableau de 10 caractères
int local_10; // entier signé
uint local_c; // entier non signé
On a une création de plusieurs variables de différent type qui dans certains cas peuvent poser problèmes.
Comme les tableaux, les entiers non signé.
local_c = 0;
__seed = getpid();
srand(__seed);
iVar1 = rand();
local_c = iVar1 % 100;
Ici on a une initialisation de la variable local_c à 0.
On initialise la graine aléatoire avec le PID du binaire et on génère un nombre entre 0 et 99.
puts("Hello, here you can get some bonus points for the competition.");
puts("You cannot get more than 100 bonus points.");
puts("If you go above 1000 you win.");
printf("Your score is currently %u\n",(ulong)local_c);
printf("How many bonus points do you want?\n>>> ");
fflush(stdout);
fgets(local_1a,8,stdin);
local_10 = atoi(local_1a);
Du texte est affiché pour expliquer le se l’on peu faire et doit faire.
On remarque que les points donnés au début fait parti de la variable local_c, donc entre 0 et 99, il est impossible d’atteindre 1000, car 100+99 < 1000.
Vidage du tampon stdout pour l’affichage.
La saisie fgets est une saisie contrôlée de 8 caractères donc on ne peut pas déborder sur le tableau local_1a.
Conversion du tableau local_1a en entier avec atoi et on met cette valeur dans local_10.
if (local_10 < 0x65) {
local_c = local_c + local_10;
printf("Your new score is %u\n",(ulong)local_c);
if (local_c < 0x3e9) {
puts("You should try to get more points");
}
else {
puts("Congratulations! Here is your flag:");
fflush(stdout);
system("cat flag.txt");
}
}
else {
puts("Stop cheating!");
}
return 0;
}
Le programme teste si la saisie est inférieure 100 (0x65), alors on peut voir déjà un problème, la variable local_10 est signée donc elle peut être négatif et ici il n’y a pas de condition qui teste si local_10 est négatif.
Si on est inférieur à 100 alors on ajoute le nombre saisie sur la variable local_c, ici il y a un grave problème car local_c est un entier non signé, si on met un nombre négatif on provoque un underflow.
Regardons ce problème plus en détails
Comment un ordinateur stock le nombres signés négatif
La plupart des ordinateurs moderne utilise la technique du complément à deux.
Voyons comment on fabrique un nombre négatif pour un entier signé.
Par exemple on prend le nombre 100 et on le convertit en binaire sur une valeur d’une longueur de 32 bits qui donne :
00000000 00000000 00000000 01100100
On inverse les 1 :
11111111 11111111 11111111 10011011
On ajoute 1 :
11111111 11111111 11111111 10011100
En décimal ce qui donne signé donne -100 et non signé 4294967196.
Ici on a local_c qui est non signé qui sera un très grand nombre si on ne dépasse pas 4294967296, donc il faut mettre au minimum un nombre négatif de -100 car si local_c est supérieur ou égal local_10 alors on aura un petit nombre positif.
Und fois le nombre obtenu est supérieur à 1000, le binaire affiche le résultat en appelant cat flag.txt à partie de system().
Solution
nc 127.0.0.1 4000
Hello, here you can get some bonus points for the competition.
You cannot get more than 100 bonus points.
If you go above 1000 you win.
Your score is currently 96
How many bonus points do you want?
>>> -100
Your new score is 4294967292
Congratulations! Here is your flag:
FCSC{XXX}
En mettant un nombre négatif assez grand on obtient le flag.