Welcome to Coding : Sécurité Programmation Réseaux

Search   in  

 Create an Account Home | Submit News Your Account Content | Topics | Top 10  


Accueil
· Home
· Listing des Articles
· Top 10
· Repository des Exploits

Les sujets / parties
· C / C ++
· Visual Basic
· Asm
· Reseaux
· Java
· Securite
· Divers

Utile
· Listing des Articles

· Telecharger
· Le Forum
· Liens
· Proposer un article

Top20 des Downloads
· 1: Etude des reseaux generalites et protocoles
· 2: Cheval de troie en VB avec sources
· 3: Netcat 1.1
· 4: Keylogger
· 5: Etudes des reseaux hauts debits architectures et protocoles
· 6: Ecoute de port
· 7: Etude du Smart Spoofing
· 8: Win Packet Capture Utils
· 9: Tutorial on Traffic Interception on Switched Lan using ARP spoofing
· 10: Cours de C

User Info
Welcome, Anonymous
Nickname
Password
(Register)
Membership:
Latest: trapcodien
New Today: 1
New Yesterday: 0
Overall: 2207

People Online:
Visitors: 43
Members: 1
Total: 44

Online Now:
01: trapcodien

  
Se proteger des buffers overflows
Posted on Tuesday, February 01 @ 13:26:17 CET
Topic: Securité
Securité

	Cet article traitera des protections contre l'exploitation des débordements de buffer. Chaque article présentera différentes méthodes de protections, et ce contre quoi elle protège précisément.

Introduction Cet article est le premier d'une série qui traitera des protections contre l'exploitation des débordements de buffer. Chaque article présentera différentes méthodes de protections, et ce contre quoi elle protège précisément. En effet, il ne faut pas installer ces protections et croire qu'elles constituent un rempart infranchissable. Avant de rentrer dans le vif du sujet dès le prochain article, nous rappelons ici quelques notions indispensables à la compréhension de la suite, comme le format ELF des binaires Linux, l'organisation de la mémoire des processus et la PLT/GOT (Procedure Linkage Table/Global Offset Table). L'organisation de la mémoire Rappels sur le format ELF Le format ELF (Executable and Linking Format -- format d'exécution et d'édition de liens) est le format actuel des binaires sous Linux. Il a remplacé le format a.out pour différentes raisons :
  • plus souple et plus pratique grâce à sa structure (cf. elf.h) ;
  • possibilité de créer des bibliothèques partagées ;
  • format supporté sur plusieurs autres systèmes ;
  • ...
La principale chose à connaître sur ce format est son organisation. En fait, un binaire au format ELF est découpé en plusieurs sections. Chacune possède sa propre finalité. Par exemple, la section .text contient les instructions machines du programme, c'est-à-dire son code exécutable. Ainsi, une fois chargée en mémoire, comme un processus ne peut modifier son propre code, toutes les autres instances de ce programme utiliseront cette même portion de mémoire. La section .text est chargée une seule et unique fois pour tous les processus issus de ce binaire. La commande file fournit les renseignements relatifs au format d'un fichier : $ file /usr/bin/vim /usr/bin/vim: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked (uses shared libs), stripped Le format ELF possède également une table des symboles, c'est-à-dire une liste de tous les symboles (labels, noms de fonctions, adresses de variables, etc.) qui sont définis ou référencés dans le fichier, ainsi que des informations sur ces symboles. Examinons les informations fournies par hello.c : /* hello.c */ #include char world[6] = "world"; char * empty; main(int argc, char ** argv ) { printf( "Hello %s ", argv[1] ); } Avec gcc, nous transformons ces instructions en fichier objet, puis la commande nm en affiche le contenu : $ gcc -c hello.c -o hello.o $ ls hello.c hello.o $ nm hello.o 00000004 C empty 00000000 t gcc2_compiled. 00000000 T main U printf 00000000 D world La commande nm affiche tous les symboles contenus dans un fichier objet. Pour chaque symbole, nm donne :
  • la valeur du symbole ;
  • son type (en minuscule, le symbole est local, en majuscule, il est global) :
    • B : le symbole se trouve dans la zone mémoire .bss ;
    • D : le symbole se situe dans la zone mémoire des données initialisées .data ;
    • C : ce flag sert pour les symboles qui ne sont pas initialisés après la compilation. Dans notre exemple, empty est défini mais pas encore initialisé. S'il ne l'est nulle part, son type changera alors en B ;
    • T : le symbole est dans la zone mémoire .text (code) ;
    • U : le symbole est indéfini (undefined).
    • Il existe de nombreux autres types décrits dans la page info nm ;
  • le nom du symbole.
Dans le fichier objet, la fonction printf() n'est pas encore définie. Dans le fichier exécutable, il faudra connaître l'emplacement de cette fonction (i.e. la bibliothèque et son adresse dans celle-ci). Comme cette fonction est externe, un mécanisme de réadressage est prévu. Tout d'abord, il contient un décalage (offset) dans la table des symboles qui référence le symbole lui-même. Ensuite, il recèle un décalage dans la section .text qui réfère l'adresse du code de la fonction. Enfin, un tag indique le type de réadressage utilisé. Lors de l'édition de liens, le linker recherche l'adresse réelle de la fonction printf(). Une fois découverte, elle est recopiée en mémoire afin que les appels à la fonction soient effectués sans repasser par cette étape de résolution. Ce mécanisme décrit de manière très générale le comportement de la PLT (Procedure Linkage Table) et de la GOT (Global Offset Table). De plus amples détails sont donnés ci-après. Les régions mémoire d'un processus Nous ne détaillons pas ici le fonctionnement de la mémoire d'un processus, mais simplement l'organisation de ses régions mémoire. Au cours de l'exécution d'un programme, il est tout à fait possible de retrouver les caractéristiques des régions (plage d'adresses, droits d'accès ...) grâce au fichier maps du processus, dans le système de fichiers /proc (/proc//maps). Même si ces informations ne sont pas toujours exactes, elles décrivent néanmoins l'organisation du processus dans la mémoire : $ /bin/cat /proc/11384/maps 08048000-080ca000 r-xp 00000000 03:01 419059 /usr/bin/vim [1] 080ca000-080d1000 rw-p 00081000 03:01 419059 /usr/bin/vim [2] 080d1000-080f8000 rwxp 00000000 00:00 0 [3] 40000000-40012000 r-xp 00000000 03:01 225598 /lib/ld-2.1.3.so 40012000-40014000 rw-p 00011000 03:01 225598 /lib/ld-2.1.3.so 40016000-40048000 r-xp 00000000 03:01 225579 /lib/libncurses.so.5.0 40048000-40050000 rw-p 00031000 03:01 225579 /lib/libncurses.so.5.0 40050000-40055000 rw-p 00000000 00:00 0 40055000-40059000 r-xp 00000000 03:01 563425 /usr/lib/libgpm.so.1.17.3 40059000-4005b000 rw-p 00003000 03:01 563425 /usr/lib/libgpm.so.1.17.3 4005b000-40130000 r-xp 00000000 03:01 225600 /lib/libc-2.1.3.so 40130000-40134000 rw-p 000d4000 03:01 225600 /lib/libc-2.1.3.so 40134000-40138000 rw-p 00000000 00:00 0 40138000-40142000 r-xp 00000000 03:01 225613 /lib/libnss_compat-2.1.3.so 40142000-40143000 rw-p 00009000 03:01 225613 /lib/libnss_compat-2.1.3.so 40143000-40155000 r-xp 00000000 03:01 225606 /lib/libnsl-2.1.3.so 40155000-40157000 rw-p 00011000 03:01 225606 /lib/libnsl-2.1.3.so 40157000-40159000 rw-p 00000000 00:00 0 bfffb000-c0000000 rwxp ffffc000 00:00 0 [4] La ligne [1] représente la région mémoire .text où le code exécutable du programme est chargé. La commande objdump -d affiche les instructions Assembleur présentes dans cette section. La ligne [2] indique la région des données globales initialisées (.data), et la [3] la région des données globales non initialisées (.bss). La commande objdump est une espèce de couteau suisse pour lire ces informations : $ /usr/bin/objdump -h /usr/bin/vim /usr/bin/vim: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn [...] 12 .text 00073eec 08049c90 08049c90 00001c90 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE [...] 15 .data 000058d0 080ca940 080ca940 00081940 2**5 CONTENTS, ALLOC, LOAD, DATA [...] 21 .bss 00002ecc 080d04a0 080d04a0 000874a0 2**5 ALLOC Signalons que la commande readelf est capable de performances identiques. Lorsqu'un programme au format ELF est lancé, le noyau organise la mémoire virtuelle allouée au processus : des plages mémoires sont réservées pour les besoins du programme (pile, tas, données, code, etc). S'il utilise des bibliothèques dynamiques, le binaire contient le nom de l'éditeur de liens à utiliser (/lib/ld-linux.so.2 en général) dans la section .interp : $ /usr/bin/objdump -s -j .interp /usr/bin/vim /usr/bin/vim: file format elf32-i386 Contents of section .interp: 80480f4 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so 8048104 2e3200 .2. Le noyau passe d'abord le contrôle des opérations à l'éditeur de liens afin qu'il charge les symboles (c'est-à-dire les références aux fonctions et variables des bibliothèques dynamiques ou d'autres fichiers objet, que nous avons vues précédemment) qui ne sont pas encore résolus, puis au programme qui commence alors le cours normal de son exécution. Variables et mémoire Comme il existe différents types de variables, il existe également différentes zones de mémoires dans lesquelles celles-ci sont stockées. Nous savons déjà qu'il existe les sections .data et .bss (cf. le paragraphe précédent). Ces zones sont réservées dès la compilation car leur taille est définie et connue de par la nature même des objets qu'elles contiennent. Se pose maintenant le problème des variables locales et des variables dynamiques. Elles sont regroupées dans une zone mémoire réservée à l'exécution du programme (user stack frame). Les fonctions pouvant s'invoquer de manière récurrente, le nombre d'instances d'une variable locale n'est pas connu à l'avance. Elles seront donc placées, au moment de leur définition dans la pile du processus (stack). Cette pile se situe dans les adresses hautes de l'espace d'adressage de l'utilisateur, et fonctionne sur un modèle LIFO (Last In, First Out), dernier entré, premier sorti. Le bas de la zone user frame sert à l'allocation des variables dynamiques. Cette région s'appelle le tas (heap) : elle contient les zones mémoires adressées par les pointeurs, les variables dynamiques. Lors de sa déclaration un pointeur occupe 32 bits soit dans BSS, soit dans la pile et ne pointe nulle part en particulier. Lors de son allocation, il reçoit une adresse qui correspond à celle du premier octet réservé pour lui dans le tas. L'exemple suivant illustre la disposition des variables en mémoire : /* mem.c */ int indice = 1; //dans data char * str; //dans bss int rien; //dans bss void f( char c ) { int i; //dans la pile /* Réservation de 5 caractères dans le tas */ str = ( char * ) malloc ( 5 * sizeof (char) ); strncpy( str, "abcde", 5 ); } int main( void ) { f( 0 ); } Des débordements de buffer peuvent se produire indistinctement dans ces régions. Nous illustrons ceci simplement avec quatre petits programmes qui simulent un débordement. Ils vont nous permettre de constater l'imprécision de certaines informations contenues dans le système de fichier /proc : Shellcode dans le .data $ cat sh_data.c /* sh_data.c */ char shellcode[] = "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh"; int main() { int * ret; *( (int *) & ret + 2 ) = ( int ) shellcode; sleep( 5 ); return( 0 ); } $ ./sh_data sh-2.04$ gdb nous permet (comme toujours ;) de mieux voir les choses : (gdb) info symbol shellcode shellcode in section .data (gdb) p &shellcode $2 = (char (*)[46]) 0x8049520
Maintenant, si nous regardons dans le système de fichiers /proc pour obtenir des informations sur la mémoire utilisée par le processus, nous obtenons les informations suivantes : $ ./sh_data ^Z [3]+ Stopped ./sh_data $ cat /proc/`ps | grep sh_ | awk '{print $1}'`/maps 00110000-00126000 r-xp 00000000 08:01 26579 /lib/ld-2.2.2.so 00126000-00127000 rw-p 00015000 08:01 26579 /lib/ld-2.2.2.so 00127000-00128000 rw-p 00000000 00:00 0 00133000-0025c000 r-xp 00000000 08:01 26588 /lib/libc-2.2.2.so 0025c000-00261000 rw-p 00128000 08:01 26588 /lib/libc-2.2.2.so 00261000-00265000 rw-p 00000000 00:00 0 08048000-08049000 r-xp 00000000 08:03 884812 /tmp/sh_data 08049000-0804a000 rw-p 00000000 08:03 884812 /tmp/sh_data bfffe000-c0000000 rwxp fffff000 00:00 0 Comme vous pouvez le constater, notre shellcode se situe à l'adresse 0x8049520. Or, cette zone n'est pas marquée comme exécutable dans /proc//maps ! Et pourtant, il tourne ;) Shellcode dans le .bss /* sh_bss.c */ char shellcode[64]; int main() { int * ret; memset( shellcode, 0, 64 ); sprintf( shellcode, "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh" ); * ( (int *) & ret + 2 ) = (int)shellcode; return( 0 ); } La variable globale shellcode est définie, mais n'est initialisée que dans la fonction main(). Elle se situe dans dans la zone .bss : (gdb) info symbol shellcode shellcode in section .bss (gdb) p &shellcode $1 = (char (*)[64]) 0x80496c0 Bien que l'adresse du shellcode le situe dans une zone marquée rw-, nous parvenons tout de même à l'exécuter : $ ./sh_bss sh-2.04$ Shellcode dans le tas (heap) $ cat sh_heap.c /* sh_heap.c */ int main() { int * ret; char * shellcode = ( char * ) malloc( 64 ); sprintf( shellcode, "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh" ); *( (int *) & ret + 2 ) = ( int ) shellcode; return( 0 ); } La variable shellcode se trouve dans la pile (stack), mais la mémoire qui lui est allouée lors du malloc() est réservée dans le tas (heap) : (gdb) p &shellcode $1 = (char **) 0xbffff6d0 //dans la pile (gdb) info symbol 0xbffff6d0 No symbol matches 0xbffff6d0. (gdb) p shellcode $2 = 0x80496b0 "ë37^211v1À210Fa211Ff°13211ó215N215VfÍ2001Û211Ø(at)Í200èÜÿÿÿ/bin/sh" Lorsque nous l'exécutons, tout se déroule sans surprise, bien que la mémoire allouée pour shellcode dans le tas (en 0x80496b0) soit toujours indiquée comme non exécutable : $ ./sh_heap sh-2.04$ Shellcode dans la pile (stack) $ cat sh_stack.c /* sh_stack.c */ int main() { int * ret; char shellcode[] = "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh"; *( (int *) & ret + 4 ) = ( int ) shellcode; return( 0 ); } Ici, le décalage vers l'adresse de retour est différent car des registres sont placés sur la pile à l'entrée de la fonction (un disass main sous gdb montre ceci). (gdb) p &shellcode $2 = (char (*)[46]) 0xbffff6d0 //dans la pile ... $ ./sh_stack sh-2.04$ Cette fois, tout se passe comme prévu puisque cette zone est bien indiquée comme exécutable dans le système de fichiers /proc ;) Maintenant que nous avons vu la disposition de la mémoire et des variables, revenons à l'édition de liens. La Procedure Linkage Table ou PLT Son fonctionnement Une section qui nous intéresse particulièrement est la Procedure Linkage Table (ou PLT). Elle joue en quelque sorte le rôle d'éditeur de liens (ou linker) pour les fonctions. Par défaut, toutes ses entrées sont initialisées non pas pour pointer vers la bonne fonction, mais sur l'éditeur de liens lui-même (celui dont nous avons parlé auparavant). Au premier appel d'une fonction donnée, le linker recherche la fonction dans la bibliothèque appropriée et met à jour son adresse. Le prochain appel de la fonction pointe ainsi directement où il faut. $ /bin/cat elf.c #include main() { printf( "Bonjour monde " ); } $ make elf cc elf.c -o elf $ gdb ./elf [...] (gdb) disass main Dump of assembler code for function main: 0x80483e0 : push %ebp 0x80483e1 : mov %esp,%ebp 0x80483e3 : sub $0x8,%esp 0x80483e6 : add $0xfffffff4,%esp 0x80483e9 : push $0x8048460 0x80483ee : call 0x804830c 0x80483f3 : add $0x10,%esp 0x80483f6 : jmp 0x8048400 0x80483f8 : jmp 0x8048402 0x80483fa : lea 0x0(%esi),%esi 0x8048400 : jmp 0x80483f6 0x8048402 : jmp 0x8048404 0x8048404 : leave 0x8048405 : ret [...] End of assembler dump. (gdb) disass printf Dump of assembler code for function printf: 0x804830c : jmp *0x80494a8 0x8048312 : push $0x18 0x8048317 : jmp 0x80482cc End of assembler dump. (gdb) x 0x80494a8 0x80494a8 : 0x08048312 La fonction main() contient un appel à la fonction printf(). En examinant le contenu de la mémoire à l'adresse indiquée (0x804830c, i.e. l'adresse de printf()), nous constatons que la première instruction exécutée est en fait un saut à une adresse contenue dans la section .got (Global Offset Table ou GOT). En simplifiant, cette GOT joue le rôle d'index de la PLT : elle signale qu'il faut revenir dans la PLT en 0x08048312, soit juste après le saut. Ensuite, un autre saut rend l'exécution du programme au linker pour qu'il recherche l'adresse de la fonction dans la bibliothèque adéquate. Précisons qu'il est tout à fait possible d'obtenir les mêmes résultats avec la commande objdump : $ /usr/bin/objdump -T ./elf | grep printf 0804830c DF *UND* 0000002f GLIBC_2.0 printf $ /usr/bin/objdump -R ./elf | grep printf 080494a8 R_386_JUMP_SLOT printf La première donne l'adresse de la PLT de la fonction printf(), et la seconde son entrée dans le GOT. Il faut bien comprendre ici le rôle distinct de la PLT et de la GOT. La première effectue une action : construire le lien entre une fonction requise dans le code du programme et le code machine associé dans une bibliothèque. En quelque sorte, la PLT est un mini-éditeur de liens. D'ailleurs, tout comme la section .text qui contient les instructions du programme, la PLT est en lecture seule. De son côté, la GOT, qui est en lecture/écriture, est un annuaire qui référence juste l'adresse d'une fonction (en toute rigueur, elle indexe également les variables globales définies dans les bibliothèques et utilisées dans le programme) Cette approche s'appelle lazy symbol binding (résolution tardive des symboles). L'idée est que si un programme utilise beaucoup de bibliothèques dynamiques, l'édition de liens est très (trop) longue. Ainsi, celle-ci ne se fait que lorsqu'il y en a réellement besoin. Il est possible de forcer la résolution des symboles par l'éditeur de liens avec la variable d'environnement LD_BIND_NOW dès l'appel du programme, et non plus lorsqu'un symbole est requis : $ export LD_BIND_NOW=1 $ gdb ./elf [...] (gdb) b main Breakpoint 1 at 0x80483e6 (gdb) r Starting program: /home/zorgon/dev/articles/intro/./elf Breakpoint 1, 0x80483e6 in main () (gdb) disass printf Dump of assembler code for function printf: 0x804830c : jmp *0x80494a8 0x8048312 : push $0x18 0x8048317 : jmp 0x80482cc End of assembler dump. (gdb) x 0x80494a8 0x80494a8 : 0x40059d44 (gdb) info symbol 0x40059d44 printf in section .text (gdb) Cette fois, la résolution est faite avant même l'exécution de la fonction printf(). Nous remarquons que l'adresse contenue dans la GOT pointe maintenant dans la section .text où se trouvent les instructions de la fonction. Alchimie avec les fonctions Pour illustrer ce mécanisme, nous montrons maintenant comment transformer l'appel d'une fonction en une autre à l'aide d'un petit programme très simple : $ /bin/cat foobar.c #include #include int main( int argc, char * argv[] ) { unsigned int got_addr = strtoul( argv[1], 0, 16 ); unsigned int value = strtoul( argv[2], 0, 16 ); * (int *) got_addr = value; printf( argv[3] ); return; } $ gcc foobar.c -o foobar Nous voulons que le programme foobar transforme l'appel de la fonction printf() en un appel à system() en allant modifier la GOT. Pour y parvenir, nous devons nous procurer deux informations :
  1. l'adresse de printf() dans la GOT : $ /usr/bin/objdump -R ./foobar | grep printf 08049518 R_386_JUMP_SLOT printf
  2. l'adresse de system() dans la libc : $ gdb ./foobar [...] (gdb) b main Breakpoint 1 at 0x8048426 (gdb) r Starting program: /home/zorgon/dev/articles/intro/./foobar Breakpoint 1, 0x8048426 in main () (gdb) p system $1 = {} 0x4004e2f0
Ainsi, la PLT va chercher l'adresse de la fonction printf() en 0x08049518. Il nous suffit alors de remplacer le contenu de cette adresse par 0x4004e2f0 qui correspond à l'adresse de la fonction system() en mémoire, ce qui est réalisé par l'instruction * (int *) got_addr = value; : $ ./foobar 0x08049518 0x4004e2f0 /bin/sh sh-2.03$ Conclusion Nous avons présenté ici différentes notions relatives à l'exécution d'un programme. Chacune nous servira dans le prochain article où nous étudierons de multiples solutions offertes sous Linux pour se prémunir de l'exécution de shellcode résultant d'un débordement de buffer : Openwall, Stackguard, PaX, LibSafe. Nous détaillerons les mécanismes mis en oeuvre par ces approches et les défenses qu'elles fournissent, mais nous en verrons également les limites. Frédéric Raynal - pappy(at)linuxmag-france.org Samuel Dralet - zorgon(at)mastersecurity.fr

 
Liens connexes
· Plus à propos de Securité
· Nouvelles transmises par Romain_Le_Guen


L'article le plus lu à propos de Securité:
Tutoriel Aircrack


Article Rating
Average Score: 0
Votes: 0

Please take a second and vote for this article:

Excellent
Very Good
Good
Regular
Bad


Options

 Format imprimable Format imprimable


Associated Topics

AsmCLinuxSecurité

PHP-Nuke Copyright © 2005 by Francisco Burzi. This is free software, and you may redistribute it under the GPL. PHP-Nuke comes with absolutely no warranty, for details, see the license.
Page Generation: 1.52 Seconds