Niveau 2, ou les joies de l’arithmétique sur machine

Le niveau 2 est très différent du premier :

level2@io:~$ ls /levels/level02*
/levels/level02  /levels/level02.c  /levels/level02_alt  /levels/level02_alt.c

Non seulement il existe en deux versions, mais en plus on a accès au code source. Ça devrait être simple, alors… Commençons par le premier, le code source est d’une simplicité déconcertante :

level2@io:~$ cat /levels/level02.c 
//a little fun brought to you by bla

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

void catcher(int a)
{
        setresuid(geteuid(),geteuid(),geteuid());
	printf("WIN!\n");
        system("/bin/sh");
        exit(0);
}

int main(int argc, char **argv)
{
	puts("source code is available in level02.c\n");

        if (argc != 3 || !atoi(argv[2]))
                return 1;
        signal(SIGFPE, catcher);
        return abs(atoi(argv[1])) / atoi(argv[2]);
}

On dirait qu’il suffit de se débrouiller pour appeler la fonction catcher()

Justement, la fonction signal() indique que le signal SIGFPE est géré par la fonction catcher(), donc il faut envoyer un SIGFPE. On ne peut pas l’envoyer par un simple appel à la commande kill, car le programme est lancé setuid (c’est comme cela qu’il peut nous donner accès au niveau suivant…), donc il faut faire en sorte que le programme l’envoie tout seul.

D’abord, c’est quoi, SIGFPE ? La page de manuel signal(7) nous dit en particulier :

       SIGFPE        8       Core    Floating point exception

Allons bon, “floating point exception” ? Mais on ne travaille qu’avec des entiers ! Le nom SIGFPE est juste une bizarrerie historique, et POSIX le définit comme “Erroneous arithmetic operation”, sans préciser qu’il s’agit de nombres à virgule flottante. Plus bas sur la page précédente il est indiqué dans quels cas ce signal est lancé, et on voit qu’ils incluent certaines opérations sur les entiers : “Integer divide by zero” et “Integer overflow”. On peut le vérifier très simplement pour le premier cas :

level2@io:/tmp/bach$ cat sigfpe.c 
int main(void)
{
    return 1/0;
}
level2@io:/tmp/bach$ gcc -o sigfpe sigfpe.c 
sigfpe.c: In function 'main':
sigfpe.c:3: warning: division by zero
level2@io:/tmp/bach$ ./sigfpe 
Floating point exception

Le problème, c’est qu’on ne peut pas faire ça pour passer le niveau 2, car il teste que le second argument n’est pas nul avant d’effectuer la division. Il faut donc faire un overflow, et là c’est un peu plus compliqué. On ne peut pas se contenter de passer des gros nombres en argument, car ils seront tronqués lors de la conversion (appel à atoi()), et la division sera effectuée sur les nombres tronqués, et donc sans causer de débordement. On doit donc se débrouiller pour que la division soit effectuée avec des nombres qui sont dans l’intervalle de valeurs du type int, mais produise un résultat qui soit en dehors.

Nous sommes sur une machine x86 tout ce qu’il y a de plus classique, et donc l’intervalle de valeurs du type int est -2^31 à 2^31-1. On tient alors une solution possible : effectuer la division de -2^31 par -1, qui produira un résultat de 2^31, en dehors de la plage de valeurs. Essayons donc :

level2@io:/tmp/bach$ /levels/level02 $((-2**31)) -1
source code is available in level02.c

WIN!
sh-4.1$ 

Bingo. :)

Une petite bizarrerie tout de même… Avant d’effectuer la division, le numérateur est passé à la moulinette de la fonction abs(). On sait que la valeur absolue du premier argument n’est pas dans la plage de valeurs du type int, mais pourquoi sa valeur est-elle inchangée ? En fait, rien n’oblige à ce qu’elle le soit, et d’ailleurs la page de manuel de la fonction abs() nous en avertit :

NOTES
       Trying to take the absolute value of the most negative integer is not defined.

Dans le cas présent, elle l’est, c’est une conséquence de l’implémentation de la fonction abs() sur la plupart des plateformes, et de la représentation en complément à 2. Petits rappels : pour prendre l’opposé d’un nombre en complément à 2, on inverse tous ses bits, et on ajoute 1 au résultat, de sorte que par exemple -5 est représenté par :

0000 0000 0000 0101     (5)
1111 1111 1111 1010     (inversion)
1111 1111 1111 1011     (+1)

La fonction abs() est généralement implémentée trés simplement ainsi :

int abs(int n)
{
    if (n < 0) {
        return -n;
    }
    return n;
}

Dans le cas présent, l’entier -2^31 est représenté par

1000 0000 0000 0000

et il est aisé de vérifier que son opposé en complément à 2 est lui-même. Ainsi il est bien inchangé par la fonction abs(), et on a bien le résulat voulu.

Leave a Reply

Your email address will not be published. Required fields are marked *