Retrouvez cet article dans : Misc 23
1. Prévention générique d’exploitation de failles de sécurité
Introduction
Beaucoup savent qu’une simple erreur de programmation peut être exploitée par un attaquant pour exécuter du code arbitraire avec les privilèges du processus vulnérable. La possibilité d’exploiter certaines failles est souvent due à des détails d’implémentation de bas niveau, rarement connus du programmeur : par exemple le fait que des données soient mélangées à des structures de contrôle, que ce soit sur la pile (variables locales et adresse de retour sur architecture Intel), sur le tas (données et métadonnées de gestion du tas) ou dans une chaîne de format. De nombreux domaines, a priori orthogonaux, se trouvent mélangés. Qui aurait prévu que les choix a priori indépendants des encodages ASCII et des opcodes d’Intel ou l’adresse virtuelle choisie par le linker pour une fonction jouent un rôle dans la facilité d’exploitation d’une faille ? De ce fait, déterminer l’exploitabilité ou non d’une faille de sécurité est délicat. Il est cependant possible de rendre plus difficile, voire impossible, l’exploitation de certaines erreurs de programmation de manière générique, sans connaître la faille en question a priori.Classification
Dans cet article, nous nous limiterons aux failles de sécurité dont l’exploitation se fait en contrôlant le flot d’exécution du processus. Cela exclut par exemple les erreurs logiques et un cas particulier de dépassement de tampon où l’on pourrait écraser une variable vitale gérant nos privilèges. Les étapes de l’exploitation d’une faille (prenons ici pour simplifier un buffer overflow) sont généralement les suivantes :- 1 Recherche d’une faille dans un programme ;
- 2 Communication avec le processus vulnérable afin de déclencher le dépassement de tampon ;
- 3 Injection de code arbitraire dans l’espace d’adressage du processus (facultatif) ; Protection de l’espace d’adressage : état de l’art sous Linux et OpenBSD
- 4 Contrôle du flot d’exécution du programme (éventuellement pour le rediriger vers le code arbitraire préalablement injecté) ;
- 5 Détournement du processus pour réaliser diverses actions (installation de rootkit, récupération de certaines données, lancement d’un nouvel exploit) ;
- 1 Détection d’erreurs de programmation par analyse statique (sparse, Coverity, Microsoft PREfix/PREfast, etc.) ;
- 2 Empêcher que les conditions de la faille (par exemple l’overflow) se produisent à l’exécution (libsafe, BCC (bound checking GCC) et CASH) ;
- 3 Empêcher l’injection de code arbitraire dans l’espace d’adressage d’un processus [0] (PaX, OpenBSD’s W^X) ;
- 4 Empêcher le contrôle du flot d’exécution par l’attaquant (SSP/ Propolice), obscurcissement de l’espace d’adressage (PaX, Ozone, Whentrust) ;
- 5 Empêcher un processus contrôlé par l’attaquant de faire certaines actions en limitant ses droits (Système de contrôle d’accès, SELinux, TrustedBSD, RSBAC, GrSecurity’s RBAC, LIDS, McAfee Entercept, CISCO CSA).
2. Éléments de système
Nous allons décrire quelques éléments du fonctionnement d’un système d’exploitation sur architecture Intel. Une grande partie de la sécurité d’un système d’exploitation moderne n’est possible que grâce à l’existence d’une MMU (Memory Management Unit) dans le processeur utilisé. Grâce à celle-ci : 0 Dans cet article, " injecter du code " signifiera injecter ce code dans une zone exécutable ou injecter ce code dans une zone non exécutable, puis rendre cette zone exécutable.- Il existe un niveau de privilège matériel qui sépare le noyau du code exécuté en mode utilisateur.
- Le noyau contrôle quels périphériques sont accessibles pour Figure 1 un processus en mode utilisateur (à l’aide du champ IOPL du registre EFLAGS et du bitmap de permissions enregistré dans le TSS). En règle générale, seul le noyau peut accéder directement aux périphériques et, en mode utilisateur, il faut utiliser des services du noyau (appels système) pour que le processus puisse exécuter des instructions (contrôlées) en mode noyau et accéder au matériel.
- La mémoire d’un processus est virtualisée. Il se voit seul dans son espace d’adressage.
Segmentation et pagination
Dans cet article cependant, nous allons voir comment l’on peut réagir un niveau auparavant, afin d’empêcher le contrôle du processus par l’attaquant. Ce qui nous intéresse surtout est le procédé de virtualisation de l’espace d’adressage et nous allons l’étudier rapidement dans le cadre de l’architecture Intel IA32 en mode protégé. Un programme travaille avec des adresses dites " logiques " : dans l’instruction- 1 La première, appelée " la segmentation ", transforme l’adresse logique en adresse linéaire (aussi appelée adresse virtuelle) ;
- 2 La deuxième, appelée " pagination ", transforme l’adresse linéaire en adresse physique.

Unité de segmentation
Nous l’avons vu, les adresses mémoire utilisées par un programmeur sont des adresses logiques. Celles-ci sont de la forme " selector:offset " où " selector " est un sélecteur de segment de 16 bits et offset un décalage de 32 bits. Les sélecteurs de segment sont en pratique rarement spécifiés, car par défaut, (c’est-à -dire sans préfixe segment override), le segment de code (cs) est utilisé pour tous les instruction fetch, le segment de pile (ss) pour tous les push, pop et références utilisant ESP ou EBP, le segment de données (ds) pour toutes les références sauf celles liées à la pile ou aux chaînes, le sélecteur ES pour les instructions liées aux chaînes de caractères. Nous allons décrire, de manière peu détaillée (les lecteurs intéressés peuvent se reporter à la documentation [Intel]) le mécanisme de segmentation. C’est dans la segmentation (et non pas dans la pagination) que l’on retrouve les fameux rings, c’est-à -dire les niveaux de privilèges Intel. Ils vont de 0 à 3, 0 étant le privilège le plus fort. En pratique les systèmes d’exploitation courants n’utilisent que les privilèges 0 (mode noyau) et 3 (mode utilisateur). Considérons par exemple l’adresse logique précédente avec 7B pour valeur de SS (7B:0xBFFFF884). Le sélecteur de segment se décompose en trois champs (voir figure ci-dessous).
Le champ TI (table indicator) indique dans quelle table de descripteurs, GDT (global descriptor table) ou LDT (local descriptor table), le descripteur de segment correspondant doit être cherché.
- 2 Le champ Index indique l’index du descripteur de segment dans la GDT ou la LDT
- 3 Le champ RPL indique le requested privilege level, i. e. le plus haut privilège nécessaire pour avoir accès au segment. Dans le cas de CS, ce champ indique le CPL (current privilege level, c’est-à -dire le niveau de privilège courant (typiquement 0 en mode kernel et 3 en mode utilisateur).
Ainsi, notre sélecteur SS dont la valeur est 0x7B indique que le descripteur de segment à utiliser est le numéro 15 (0b1111) dans la GDT et que le RPL est 3.

Grâce au sélecteur de segment, le processeur repère un descripteur de segment. Selon l’indicateur de table (TI), le descripteur de segment est recherché dans la LDT ou dans la GDT (les adresses linéaires de ces tables sont situées dans les registres LDTR et GDTR). À l’aide du champ index, le processeur repère le descripteur de segment désiré dans la table. Ce descripteur de 8 octets contient de nombreux champs, entre autres un champ base et un champ limit : l’addition de ce champ base et de l’offset donne l’adresse linéaire, le champ limit permet de limiter la taille du segment. Parmi les autres champs importants, citons le champ DPL (descriptor privilege level) qui, combiné au CPL (derniers bits de CS) et au RPL du descripteur utilisé permet de vérifier les privilèges. Par défaut, la majorité des systèmes d’exploitation actuels utilisent le Flat memory model, c’est-à -dire qu’ils utilisent zéro comme base pour les principaux segments (ceux dont les sélecteurs sont chargés dans cs, ds, ss, es), au moins pour les segments utilisés en mode user, et 0xFFFFF comme limite (ce qui donne une limite de 4 Go au segment). L’offset d’une adresse logique peut ainsi dans la majorité des cas être identifiée à l’adresse linéaire. Ceci explique les nombreuses confusions entre adresse linéaire et adresse logique. La segmentation joue en général un rôle peu important dans les systèmes d’exploitation modernes, son utilisation est cependant obligatoire sur processeur Intel, pour des raisons historiques. Nous verrons cependant plusieurs méthodes pour tirer parti de la segmentation. Vous pouvez explorer vos descripteurs de segments à l’aide du programme [DTDUMPER].

Unité de pagination
La pagination constitue la deuxième étape de la translation d’adresse : elle transforme les adresses linéaires en adresses physiques que le processeur envoie sur le bus mémoire. L’espace d’adressage linéaire est découpé en pages (en général de 4 Ko), la pagination fait la correspondance entre les pages et les cadres de pages (pages en mémoire physique). En pratique, sur la plupart des systèmes d’exploitation, c’est elle qui joue le rôle le plus important, elle permet de vérifier les droits d’accès aux pages (droit en écriture, privilège nécessaire pour accéder à la page)... et grâce à elle des mécanismes avancés sont possibles :
- Un cadre de page n’est pas forcément alloué en mémoire physique pour une page effectivement utilisable par le processus : l’utilisation de cette page provoquera une page fault et le handler correspondant du noyau allouera le cadre de page correspondant en mémoire physique, en y plaçant éventuellement les données nécessaires :
- Swaping : des cadres de page peuvent être déchargés de la mémoire physique et placés sur le disque.
- On demand paging : des fonctions telles que
mmap()ouMapViewOfFilesont utilisées pour mapper un fichier en mémoire, les pages ne seront effectivement mappées qu’à l’utilisation.
- Le copy on write : deux processus sans mémoire partagée peuvent partager le même cadre de page ; lorsque l’un d’eux écrira sur sa page, le cadre de page sera dupliqué. Ce procédé est particulièrement avantageux pour mapper des bibliothèques.
Il existe plusieurs types de pagination sur les processeurs Intel.
Nous allons pour simplifier décrire ici rapidement le mode de pagination simple avec pages de 4 Ko (sans PSE, sans PAE, sans IA32e). Dans ce mode, la pagination s’effectue à deux niveaux : une adresse linéaire contient un index de Directory, un index de Table (de 10 bits chacun, qui permettent de repérer une entrée dans une table de 1024 entrées) et un offset de 12 bits qui permet de se repérer à un octet près dans une page de 4 Ko.

Le registre cr3 contient l’adresse physique du Page Directory, cette adresse, combinée au Directory Index nous fournit l’adresse d’un PDE (Page Directory Entry), qui lui-même permet de repérer une Page table qui, combinée au Table Index, fournit l’adresse du PTE (Page table entry) de la page. Celui-ci contient un champ Page base address de 20 bits qui permet de repérer le cadre de page (Page Frame) en mémoire physique ainsi que divers flags qui indiquent notamment les droits de la page (Read/Write), ses privilèges (User/Supervisor; notons qu’il n’y a ici que deux niveaux et non pas 4 Rings), si elle est présente en mémoire physique, si elle a été accédée... Nous ne détaillerons pas les PDE qui sont similaires aux PTE (voir Figure). Remarquons qu’un PTE permet de marquer une page comme étant disponible en écriture ou non (W), mais qu’il n’existe pas de flag pour marquer une page disponible en exécution. L’absence de ce flag pose de gros problèmes de sécurité (il fut ensuite réintroduit en fanfare sous la dénomination de flag NX, nous y reviendrons). Notons que comme nous ne perdons pas d’information, les 32 bits d’une adresse linéaire permettent bien d’adresser 4 Go de mémoire.
Espace d’adressage d’un processus
Voyons à titre d’exemple comment se crée l’espace d’adressage d’un processus sous Linux 2.4. On peut voir la partie utilisateur de l’espace d’adressage linéaire d’un processus en lisant le fichier /proc/pid/maps. Sous Linux 2.4, tous les descripteurs de segment qui nous intéressent ont une base nulle; on peut donc ici directement confondre l’espace d’adressage logique (tel qu’il est vu par le processus) et l’espace d’adressage linéaire (après segmentation).
$ cat /proc/self/maps 08048000-0804c000 r-xp 00000000 03:03 32672 /bin/cat [1] 0804c000-0804d000 rw-p 00003000 03:03 32672 /bin/cat [2] 0804d000-0806e000 rwxp 00000000 00:00 0 [3] 40000000-40016000 r-xp 00000000 03:03 179102 /lib/ld-2.3.2.so 40016000-40017000 rw-p 00015000 03:03 179102 /lib/ld-2.3.2.so 40017000-40018000 rw-p 00000000 00:00 0 4001d000-40145000 r-xp 00000000 03:03 179607 /lib/libc-2.3.2.so 40145000-4014d000 rw-p 00127000 03:03 179607 /lib/libc-2.3.2.so 4014d000-40150000 rw-p 00000000 00:00 0 40150000-4019f000 r--p 00000000 03:03 942989 /usr/lib/locale/locale-archive bfffe000-c0000000 rwxp fffff000 00:00 0 [pile]L’espace d’adressage d’un processus est changé lors de l’appel système
$ readelf -l cat Elf file type is EXEC (Executable file) Entry point 0x8048b70 There are 7 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x03a2d 0x03a2d R E 0x1000 LOAD 0x003a3 0x0804ca30 0x0804ca30 0x001d0 0x0033c RW 0x1000 DYNAMIC 0x003a7c 0x0804ca7c 0x0804ca7c 0x000c8 0x000c8 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata 03 .data .eh_frame .dynamic .ctors .dtors .jcr .got .bss 04 .dynamic 05 .note.ABI-tag 06Voici quelques étapes du chargement d’un programme :
- 1 La fonction
flush_old_exec()du noyau libère les ressources utilisées par le précédent programme, en particulier toutes les régions mémoire user. - 2 La pile est allouée, juste en dessous de la limite de l’espace utilisateur. Nous pouvons la voir ici de 0xbfffe000 à 0xc0000000.
- 3 Les segments de type
PT_LOADsont chargés en mémoire. On peut voir ici qu’il y a une zone mémoire avec les permissionsreadetexecute[1] et une zone mémoire avec des permissionsreadetwrite[2]. On retrouve bien ces zones dans notre fichier maps. Notons que le deuxième segment a uneMemSizsupérieure à saFileSiz. Cela est à l’origine de la création de la zone [3] qui contient les données non initialisées (section.bss) et qui sert de point de départ au tas (heap). - 4 Grâce au Program Header (repéré par son type
PT_INTERP), le noyau localise le chargeur dynamique (ld-linux.so), le charge en mémoire et lui transfère l’exécution. - 5 Le chargeur dynamique utilise l’appel système
sys_mmap()pour charger les bibliothèques dynamiques (entréesDT_NEEDEDdans la table dynamique) en mémoire. Ces bibliothèques dynamiques sont elles-mêmes au formatELF, de typeET_ DYN. - 6 Le chargeur dynamique réalise des relocations pour le programme principal et les bibliothèques chargées. C’est lui qui met éventuellement en place le mécanisme GOT/PLT.
- 7 Le chargeur dynamique transfère l’exécution au point d’entrée du programme
Le format ELF est très complexe (il est par exemple beaucoup plus complexe que le format PE de Microsoft). Les lecteurs intéressés peuvent se reporter à la spécification TIS [ELF]. Le fichier maps de la page précédente correspond à un noyau 2.4. Avec un noyau 2.6 nous verrions quelques différences :
- Presque tout en haut de l’espace d’adressage linéaire(0xffffe000), une page est réservée par le noyau. Elle contient une bibliothèque ELF appelée
vDSOqui est utilisée pour réaliser des appels systèmes. Celle-ci est chargée automatiquement en mémoire par le kernel dans tous les processus. Selon le type de processeur, cette bibliothèque réalise des appels système en utilisant int0x80/iretousysenter/sysexit. - Les adresses des bibliothèques dynamiques sont changées car l’allocation se fait maintenant du haut vers le bas.
- À partir du noyau 2.6.12 on peut constater une faible randomisation des adresses des bibliothèques dynamiques et de la pile (nous reviendrons dessus dans la partie consacrée à Exec-Shield).
3. Prévention d’exécution de code arbitraire
Nous décrivons ici plusieurs techniques visant à empêcher la prise de contrôle du processus par un attaquant. Comme nous l’avons expliqué, nous nous limiterons aux prises de contrôles fortes qui permettent de détourner le flot d’exécution du programme. Cela est réalisé en :
- 1 Empêchant l’attaquant de détourner le flot d’exécution du programme ;
- 2 Empêchant l’attaquant d’injecter du code arbitraire.
OpenWall
Le premier patch pour le noyau Linux destiné à rendre plus difficile l’exploitation d’erreurs de programmation fut OpenWall (sorti en 1998) [Openwall]. Bien avant l’ère des exploits de format strings (~2000) et avant la démocratisation des heap overflows, la plupart de ces erreurs de programmation avaient pour conséquence un dépassement de tampon sur la pile. La technique classique d’exploitation étant d’écraser une adresse retour afin d’exécuter du code injecté sur la pile, il semblait souhaitable de rendre la pile non exécutable. On peut certes marquer la pile comme étant non exécutable (c’est-à -dire changer les données internes au noyau, les virtual memory areas), mais à cause de l’absence de flag marquant le droit d’exécution dans les PTE, cela n’impliquera pas de changement dans les structures utilisées par le processeur et n’aura donc aucun effet utile. Pour pallier ce défaut de l’architecture Intel, Solar Designer utilisa une technique relativement simple afin de rendre la pile non exécutable : en mode utilisateur, le descripteur de segment référencé par le registre cs possède une limite inférieure à l’adresse linéaire de la pile. Les autres descripteurs de segment sont inchangés. Ainsi, lorsqu’un accès à la pile est réalisé relativement aux registres ds, ss et es, aucune différence n’apparaît. La base du descripteur de segment (0) est ajoutée à l’offset demandé, la somme ne dépassant pas la limite du segment (4 Go), aucune protection matérielle due à la segmentation ne vient perturber l’accès à la mémoire. En revanche, lorsque le processeur tente d’exécuter du code sur la pile, le descripteur de segment référencé par le sélecteur contenu dans le registre cs est utilisé. La somme de la base de ce descripteur (0) et de l’offset demandé dépasse la limite de ce segment (modifiée dans ce but), le processeur lève une exception de protection générale et le noyau Linux tue le processus.

Les limites de cette protection sont nombreuses. Il reste en effet possible d’injecter du code exécutable ailleurs que sur la pile et d’utiliser du code existant. Solar Designer fut également le premier à proposer d’utiliser la technique du return-to-libc afin de passer outre sa protection. Mais on peut également imaginer utiliser de nombreuses techniques plus avancées, par exemple des return-to-libc chaînés (voir l’article de Rafal Wojtczuk dans Phrack [Wojtczuk]), afin de recopier son code exécutable dans une autre zone mémoire puis de l’exécuter.
Afin de déjouer ces techniques visant à exploiter du code existant dans l’espace d’adressage, Solar Designer a proposé l’une des premières techniques d’obfuscation de l’espace d’adressage, l’armure ASCII. Le principe est que les bibliothèques sont chargées à une adresse inférieure à 0x01010000 afin que leurs adresses contiennent toujours un zéro. Cela peut en effet empêcher l’injection d’une adresse de retour cible (par exemple celle de system()) dans l’espace d’adressage d’un processus via des chaînes de caractères. Cette méthode n’est bien entendue utile que s’il est difficile pour un attaquant d’injecter des zéros et il est toujours possible d’utiliser du code existant dans les zones mémoires correspondants au fichier exécutable principal, qui lui ne se trouvera pas dans l’ASCII armor.
Malgré ses limites très importantes, ce patch extrêmement simple déjoue un grand nombre des exploits publics visant des stack overflows (ce qui n’a certes pas grand chose à voir avec la sécurité réelle, n’en déplaise aux éditeurs d’antivirus).
PaX
PaX [PaX] est né en 2000, il était alors plus une preuve de concept exploitant une des idées du projet PleX86 (utiliser les TLB séparés pour pouvoir instrumenter séparément la lecture/écriture et l’exécution d’une page sur architecture Intel) qu’un véritable patch de sécurité. Il a évolué en introduisant successivement l’ASLR (Address Space Layout Randomization) en juillet 2001 puis le VMA mirroring, (base de SEGMEXEC et RANDEXEC) en juillet 2002, ainsi qu’en démocratisant des méthodes de compilation permettant d’obtenir des exécutables ET_DYN. Il supporte plus de dix architectures (Alpha, i386, ia64, Mips, Mips64, Parisc, PPC, PPC64, Sparc, Sparc64, x86_64) et sert de base aux distributions sécurisées Hardened Gentoo et Adamantix ainsi qu’au patch GrSecurity. Nous l’avons vu, la pagination Intel fournit nativement un flag permettant de rendre ou non une page disponible en écriture dans les PTE. Idéalement, les protections d’une page devraient pouvoir être une combinaison arbitraire de PROT_READ, PROT_WRITE, PROT_EXEC (cette terminologie est celle utilisée notamment dans l’appel système
PAGEEXEC
Nous décrivons rapidement le fonctionnement de PAGEEXEC, pour plus d’informations, le lecteur pourra se reporter à la documentation de PaX [PaX]. Lorsque le processeur ne supporte pas physiquement la possibilité de marquer une page comme étant non exécutable, PaX utilise la séparation des TLB introduite avec les processeurs Pentium pour émuler cette fonctionnalité. Les TLB (Translation Lookaside Buffers) sont des caches situés dans le processeur qui contiennent les entrées récemment utilisées des Page Directories et Page Tables afin d’accélérer la traduction d’adresses linéaires en adresses physiques. Dans les processeurs récents, les TLB sont séparés selon que l’adresse translatée est utilisée pour récupérer du code (instruction- Les pages à rendre non exécutables sont marquées " superviseur " (flag U/S dans le PTE).
- Toute utilisation d’une telle page par le processus en mode utilisateur génère une faute. Le Page fault handler du noyau vérifie si cette faute est due à une tentative d’exécution de code (en comparant EIP du mode utilisateur et l’adresse de la page). Dans ce cas le processus est terminé (en réalité, les fonctionnalités EMUTRAMP et EMUSIGRT qui servent à émuler deux cas communs de génération de code à la volée, trampolines GCC et signal return du noyau sur la pile (vieilles libcs), compliquent ce processus).
- Dans le cas où l’accès demandé n’est pas en exécution, le PTE est modifié afin de permettre un accès à la page depuis le mode utilisateur, puis un accès est réalisé afin de charger le PTE correspondant dans le DTLB.
- Le drapeau
supervisorest alors repositionné dans le PTE afin que tout accès en exécution soit à nouveau détecté. - Lorsque l’exécution du processus en mode utilisateur reprend, tout accès en lecture/écriture utilisera le DTLB et ne provoquera pas de nouvelle faute alors qu’un accès en exécution passera par le PTE avec le drapeau superviseur.
Ce procédé est relativement coûteux en temps processeur. C’est pourquoi une autre méthode, SEGMEXEC, a été introduite en 2002. Cependant, fin 2004, PAGEEXEC a été amélioré dans le noyau 2.6. Il combine maintenant pagination et segmentation : une limite au segment de code est établie, au-dessus de la page exécutable la plus basse. De ce fait, toutes les pages situées au-dessus de cette limite ne sont pas exécutables grâce à la segmentation, ce qui a pour conséquence de ne pas imposer une surcharge lors de l’utilisation de ces pages. Les pages non exécutables situées en dessous de cette limite sont traitées avec les TLB séparés, comme expliqué ci-dessus. D’un point de vue pratique, cette nouvelle méthode est relativement efficace car les " grosses " zones de données (le tas par exemple), sont situées au-dessus de la limite de cs. La surcharge n’existe que pour les zones de données qui n’ont pas pu être logées au-dessus de cette limite (par exemple les sections .data des bibliothèques). Des mesures de performances sont disponibles sur [paxperf].
SEGMEXEC
L’introduction de SEGMEXEC dans la deuxième moitié de 2002 a été un pas majeur pour PaX. Grâce à cette nouvelle méthode, les surcharges induites par PaX sont devenues négligeables, au prix cependant d’une complexité d’implémentation accrue. SEGMEXEC est une méthode ingénieuse permettant d’obtenir des pages non exécutables en utilisant la segmentation, grâce à une technique appelée le VMA mirroring. Le prix à payer est que la taille de l’espace des adresses logiques réellement utilisables par un processus en mode utilisateur passe de 3 Go à 1,5 Go. L’idée est de séparer complètement le segment de code utilisateur du segment de données utilisateur. Nous l’avons vu, en temps normal les segments utilisateurs ont une base de 0 et une limite de 4 Go et se chevauchent donc parfaitement dans l’espace d’adressage linéaire : une adresse 0xAABBCCDD utilisée pour lire/écrire des données ou pour exécuter du code se traduira invariablement par une adresse linéaire 0xAABBCCDD. Imaginons maintenant que le segment de code et le segment de données soient disjoints dans l’espace d’adressage linéaire de la manière suivante :- Un descripteur de segment de données " User " (
USER_DS) a pour adresse de base 0 et pour limite 1,5 Go, celui-ci est référencé par les sélecteurs chargés dans ds, es et ss - Un descripteur de segment de code " User " (
USER_CS) a pour adresse de base 0x60000000 (1,5 Go) et pour limite 1,5 Go, celui-ci est référencé par le sélecteur chargé dans le registre cs
Grâce à cette technique, une page située dans le segment de code (c’est-à -dire entre 1,5 et 3 Go dans l’espace d’adressage linéaire) sera exécutable (accessible depuis cs) mais pas lisible/inscriptible (rappelez-vous que les accès aux données se font par rapport aux sélecteurs ds, ss ou es), alors qu’une page située dans le segment de données (c’est-à -dire entre 0 et 1,5 Go dans l’espace d’adressage linéaire) ne sera pas exécutable. Afin d’être certain que des pages situées dans le segment de données ne puissent jamais être exécutées (le but étant par la suite de pouvoir prouver que l’injection de nouveau code exécutable dans l’espace d’adressage est impossible, cf. la partie dédiée à MPROTECT), il faut s’assurer qu’un autre sélecteur de segment moins restrictif ne puisse être chargé dans cs. C’est pourquoi PaX s’assure que la GDT utilisée par les processus utilisant SEGMEXEC possède un seul descripteur de segment de code en DPL3. En effet, il ne faut pas qu’un processus contrôlé par l’attaquant (par exemple à l’aide de return-to-libc chaînés) puisse être amené à charger dans cs un descripteur de segment lui permettant d’exécuter des pages contenues dans le segment de données (on pourrait imaginer par exemple que l’attaquant exécute un far return dont l’opcode était déjà présent dans la partie exécutable de l’espace d’adressage du processus).
Avons-nous à ce stade réellement une sémantique de pages non exécutables ? Pas vraiment, car notre nouvelle protection PROT_ EXEC n’est pas ici combinable avec les protections PROT_WRITE et PROT_READ : il n’est pas possible de lire des données qui seraient situées dans une page exécutable. Cela est bloquant; si vous faites attention à la sortie de readelf -l /bin/cat ci dessus, vous verrez que la section .rodata (qui contient les données en lecture seule) par exemple se retrouve dans le segment (au sens ELF) de code. En pratique, il est, à de nombreuses occasions, nécessaire de pouvoir lire une page exécutable.
La solution à ce problème, le VMA mirroring, est ce qui rend SEGMEXEC complexe. Toute page disponible dans le segment de code doit être disponible également dans le segment de données. Pour cela, pour toute page présente dans le segment de code (adresses linéaires de 1,5 à 3 Go), un PTE " miroir " est ajouté dans le segment de données, référençant le même cadre de page que le PTE présent dans le segment de code (notez bien qu’il n’y a toujours qu’un cadre de page correspondant en mémoire physique !). Notre espace d’adressage linéaire présente l’aspect suivant où l’on peut remarquer trois zones exécutables mirorées (voir également figure 7) :
08048000-0804c000 r-xp 00000000 03:03 32672 /bin/cat [1] 0804c000-0804d000 rw-p 00003000 03:03 32672 /bin/cat 0804d000-0806e000 rw-p 00000000 00:00 0 20000000-20016000 r-xp 00000000 03:03 179102 /lib/ld-2.3.2.so [2] 20016000-20017000 rw-p 00015000 03:03 179102 /lib/ld-2.3.2.so 20017000-20018000 rw-p 00000000 00:00 0 2001d000-20145000 r-xp 00000000 03:03 179607 /lib/libc-2.3.2.so [3] 20145000-2014d000 rw-p 00127000 03:03 179607 /lib/libc-2.3.2.so 2014d000-20150000 rw-p 00000000 00:00 0 20150000-2019f000 r--p 00000000 03:03 942989 /usr/lib/locale/locale-archive 5fffe000-60000000 rw-p 00000000 00:00 0 [pile] 68048000-6804c000 r-xp 00000000 03:03 32672 /bin/cat [1] 80000000-80016000 r-xp 00000000 03:03 179102 /lib/ld-2.3.2.so [2] 8001d000-80145000 r-xp 00000000 03:03 179607 /lib/libc-2.3.2.so [3]De nombreuses modifications sont nécessaires afin que les deux pages " miroir " restent synchronisées en cas de changement d’état (lorsque le kernel gère un page fault, un

Tous ces changements influent sur la segmentation et changent l’espace d’adressage linéaire. Pour un processus, qui lui ne " voit " que l’espace d’adressage logique, rien n’a changé, si ce n’est que son espace d’adressage logique utilisable en mode User a maintenant une taille de 1,5 Go (en effet, les segments de code et de données ont tous les deux une taille de 1,5 Go et l’utilisation d’un offset supérieur à 1,5 Go produirait une exception de protection générale).
Le programme [DTDUMPER] af f iche précisément les descripteurs de segments
DT Dumper julien@cr0.org GDT size: 0x7F (16 entries), GDT LA: 0xC03028D0 LDT’s selector is 0x68 (entry #13 in GDT) TSS’s selector is 0x60 (entry #12 in GDT) IDT size: 0x7FF (256 entries), IDT LA: 0xC0302000 CS: 0x23 DS: 0x2B SS: 0x2B ES: 0x2B Dumping GDT (0xC03028D0) [...] (#004) 0x23 60C5FB000000FFFF Code D/B=32bts AVL=0 Present DPL=3 TYPE= _RA BASE=0x60000000 LIMIT=0x5FFFFFFF (#005) 0x2B 00C5F3000000FFFF Data D/B=32bts AVL=0 Present DPL=3 TYPE= _WA BASE=0x00000000 LIMIT=0x5FFFFFFF [...]
MPROTECT
Nous avons vu deux méthodes, PAGEEXEC et SEGMEXEC, permettant d’obtenir des pages non exécutables, même sur une architecture Intel ne les supportant pas (sur les architectures le supportant nativement, PAGEEXEC utilise bien sûr le support natif du processeur). Les restrictions MPROTECT de PaX permettent d’utiliser ces pages non exécutables afin de contribuer à rendre impossible l’injection de code arbitraire dans l’espace d’adressage d’un processus. Le noyau Linux maintient pour chaque zone de mémoire linéaire (VMA pour Virtual Memory Area) un ensemble de drapeaux qui déterminent si la zone est exécutable, inscriptible, potentiellement exécutable ou potentiellement inscriptible (c’est-à -dire après un appel à mprotect()): VM_EXEC, VM_WRITE, VM_MAYEXEC, VM_MAYWRITE.
Les restrictions MPROTECT font en sorte qu’un VMA ne possède jamais une combinaison d’un drapeau WRITE et d’un drapeau EXEC. Pour cela :
- Les mappings anonymes (pile et tas en particulier) ainsi que les mappings de mémoire partagée sont automatiquement marqués VM_WRITE|VM_MAYWRITE.
- Les mappings de fichiers sont marqués avec VM_WRITE|VM_MAYWRITE si la protection PROT_WRITE a été demandée lors de l’appel de
mmap()et avec VM_EXEC|VM_MAYEXEC dans le cas contraire.
Une exception existe afin de gérer le cas des fichiers ELF (le plus souvent des bibliothèques) ayant des relocations dans le segment de code (entrée DT_TEXTREL dans la table dynamique) : le linker dynamique doit pouvoir changer les droits d’une page, afin d’effectuer une relocation, puis rechanger les droits. Cette exception peut être supprimée si l’on est certain d’avoir un système ne contenant aucun fichier ELF avec des relocations dans le segment (au sens ELF) exécutable. Nous reviendrons sur ce sujet dans la partie suivante, consacrée à la mise aléatoire de l’espace d’adressage. Les restrictions MPROTECT ne laissent qu’une possibilité à un attaquant pour introduire du nouveau code exécutable dans un processus dont il aurait pris le contrôle (en utilisant du code existant, à l’aide de return-to-libc enchaînés par exemple) : charger en mémoire à l’aide de mmap() un fichier dans lequel il a injecté du code, en demandant la protection PROT_EXEC.
C’est avec cette même méthode que les bibliothèques utilisées par un programme sont chargées en mémoire. C’est le rôle d’un système de contrôle d’accès (RSBAC, SELinux, partie RBAC de GrSecurity) de prévenir cette attaque. Si un tel contrôle d’accès est correctement mis en place, et si l’exception liée aux relocations TEXTREL est supprimée, on peut avec PaX prouver qu’il est impossible d’injecter du code arbitraire dans l’espace d’adressage du processus et cela sans faire aucune hypothèse sur le degré de contrôle d’un attaquant sur le processus ! Même sans la restriction du mappage de fichier avec la protection PROT_EXEC, notons que l’attaquant doit pouvoir bénéficier d’un grand contrôle du processus vulnérable (en ne s’appuyant que sur du code déjà présent dans l’espace d’adressage !) pour pouvoir lui faire créer un fichier, lui faire mapper ce fichier avec la protection PROT_EXEC, puis lui faire exécuter son code (évidemment le fichier peut être créé indépendamment, si l’attaquant a déjà un compte local par exemple). Les restrictions MPROTECT sont évidemment incompatibles avec les quelques programmes générant du code à la volée (par exemple la machine virtuelle JAVA). Pour pallier ce problème, PaX permet de marquer tout fichier exécutable afin de désactiver certaines fonctions, dont MPROTECT.
Address space layout randomization (ASLR)
Comme nous l’avons mentionné, nous nous limitons dans cet article aux techniques permettant à l’attaquant d’avoir un contrôle fort du flot d’exécution du processus. Le contrôle le plus fort est l’exécution de code arbitraire, nous avons vu dans la partie précédente que PaX, s’il est bien utilisé, le prévient totalement.
Cependant il est également possible d’obtenir un contrôle relativement puissant du processus en réutilisant du code existant et en détournant le flot d’exécution. Le degré de contrôle est toutefois sans commune mesure avec celui obtenu avec l’exécution de code arbitraire.
C’est pourquoi des exploits utilisent souvent ces méthodes pour ensuite réussir à exécuter du code arbitraire, par exemple en utilisant mprotect() pour rendre un buffer exécutable puis en retournant sur celui-ci.
Dans le cas de PaX, ceci n’est pas possible, mais il reste quand même souhaitable d’empêcher le contrôle du processus à l’aide de code existant (sans compter que PaX n’est pas toujours utilisé de manière optimale et que les restrictions MPROTECT sont parfois retirées).
L’idée derrière la randomisation de l’espace d’adressage est d’empêcher toute adresse écrite en dur d’avoir un sens. Un exploit fiable doit au maximum éviter les adresses écrites en dur et utiliser des techniques plus avancées ou avoir recours à l’information leak, mais c’est loin d’être toujours possible. Pour une version compilée donnée d’un programme les adresses dans le fichier ELF (ET_EXEC) sont par exemple considérées comme stables. La randomisation de l’espace d’adressage utilisateur se décompose en trois parties :
- 1 Randomisation de la pile utilisateur (RANDUSTACK)
- 2 Randomisation des zones mmap()ées (sans adresse fixée) (RANDMMAP)
- 3 Randomisation de l’exécutable principal (RANDEXEC)
Les deux premières randomisations sont simples (voir [ASLR26] pour une implémentation d’ASLR pur). Pour RANDUSTACK, les bits 4 à 27 sont randomisés (alignement sur 16 octets). Cela est réalisé au niveau de deux fonctions de fs/exec.c responsables de la création de la pile lors de l’appel système execve(). Pour RANDMMAP, la fonction arch_get_unmapped_area() (mm/mmap.c) est modifiée.
Celle-ci est responsable de la recherche d’une zone de mémoire libre lorsque le drapeau MAP_FIXED n’a pas été utilisé (c’est-à dire lorsque l’appelant ne souhaite pas spécifier d’adresse précise où sera créé son mapping, qu’il soit anonyme où d’un fichier). Les bits 12 à 27 sont ici randomisés (alignement sur une page). Grâce à RANDMMAP, les adresses des bibliothèques, du tas lorsque la libc utilise mmap() pour le gérer (rare en pratique), et des requêtes manuelles de mappage de fichier ou anonyme qui n’utilisent pas le drapeau MAP_FIXED (c’est presque toujours le cas) sont automatiquement aléatoires. Cependant, les adresses de l’exécutable principal (ET_EXEC) ne sont pas randomisées par RANDMMAP. Comme le tas, lorsqu’il est géré avec brk() (ce qui est courant) débute dans la section .bss, mappée avec l’exécutable principal, celui-ci n’est pas non plus randomisé (en fait une faible randomisation est tout de même ajoutée par PaX en " gâchant " volontairement de l’espace). Cela est évidemment très gênant, d’autant plus qu’un attaquant peut ainsi retrouver les adresses de fonctions dans les bibliothèques (comme system()), en utilisant dl-resolve() et le mécanisme GOT/PLT [Wojtczuk] et passer ainsi outre la randomisation des bibliothèques.
Le problème avec l’exécutable principal est que celui-ci est placé en mémoire à partir d’un fichier ELF, au format ET_EXEC qui contient énormément d’adresses " en dur ". De plus ce fichier ne contient pas les informations de relocations nécessaires pour le reloger !
La solution consiste à générer des fichiers exécutables de type ET_DYN (type normalement réservé pour les bibliothèques). La PaX Team a introduit en 2003 une méthode pour générer de tels exécutables [ET_DYN]: en utilisant un crt1.o relogeable (utilisant la GOT), l’option -shared de GCC, et en spécifiant un lieur dynamique à utiliser, il est possible de générer un fichier exécutable ET_DYN. Notons que ceci n’était pas révolutionnaire, la libc est déjà un tel exécutable (vous pouvez essayer de l’exécuter !).
La génération de fichiers exécutables ET_DYN est également l’occasion d’éviter les relocations dans le code (cf. TEXTREL mentionnée dans la partie MPROTECT) en utilisant le switch -fPIC permettant d’obtenir du code indépendant de la position. Avec ces modifications, il est maintenant possible de randomiser l’exécutable principal : cela est fait dans la fonction load_elf_ binary() du noyau et cela fait partie de l’option RANDMMAP de PaX.
Redhat s’est appuyé sur cette idée pour introduire en 2003 un switch -pie (position independant executable) à ld (et -fPIE, similaire à -fPIC, dans GCC), réalisant exactement les opérations décrites plus haut. Redhat a ensuite introduit le switch -z relro [RELRO] permettant de rassembler toutes les sections qui ne sont disponibles en écriture que pour des raisons de relocation et de créer un program header spécifique (PT_GNU_RELRO) afin que le lieur dynamique puisse s’il le souhaite réaliser un mprotect() sur cette zone après que les relocations aient été faites pour la mettre en lecture seule. Grâce à ce changement, on peut rendre la GOT disponible en lecture seule après le chargement du programme.
Lorsque notre système n’utilise que des exécutables de type ET_DYN, nous pouvons être dans une situation de full ASLR, où tout l’espace d’adressage du processus est aléatoire. C’est le cas dans les distributions [Adamantix], Hardened [Gentoo], et en partie dans les dernières Redhat. Il faut cependant garder à l’esprit plusieurs choses :
- Certaines erreurs de programmation permettent de faire du leak d’information, et de donner à l’attaquant des informations précieuses sur l’espace d’adressage, parfois des adresses complètes.
- Il est possible d’affaiblir la randomisation à l’aide d’un bourrage avec des instructions de type
"nop". Cela est particulièrement réaliste sur la pile où des bits de poids faible (à partir du 5ième) sont aléatoires. - Tout n’est ici qu’une question de probabilités : si l’attaquant a de la chance ou peut utiliser la force brute aussi longtemps qu’il le souhaite, il finira par réussir. Il faut absolument utiliser des techniques anti-bruteforce, telles que SegvGuard ou celle utilisée dans [GRSecurity].
- L’espérance mathématique lors d’une tentative de brute force est très différente selon que l’on crashe tout le démon (one-shot) ou seulement un fils. En effet, si les processus que l’on attaque sont tous issus d’un fork() du même démon, l’espace d’adressage est toujours semblable et nous en apprenons un peu plus après chaque tentative
- Lors d’une attaque locale il est facile d’obtenir des informations sur la mémoire d’un processus, même sans privilèges (par exemple
/proc/pid/maps). Il faut utiliser un patch tel que [PAX+OBS] ou un système de contrôle d’accès pour l’éviter.
Ce miroir à l’adresse originale n’est pas exécutable (cette partie repose sur PAGEEXEC ou SEGMEXEC [notons que dans le cas de SEGMEXEC il n’y aura pas 3 miroirs comme on pourrait s’y attendre car on n’a pas besoin d’accéder aux données de manière relative, et on ne veut pas qu’il soit possible d’accéder au code de manière absolue]). Les adresses absolues peuvent donc être utilisées pour écrire ou lire des données. Si une adresse absolue est utilisée pour exécuter du code, une exception se produira et PaX va utiliser une heuristique pour déterminer s’il s’agit d’une exécution légitime ou d’une attaque. Cette méthode est simplement une preuve de concept et ne doit pas être réellement utilisée à cause de la faiblesse de l’heuristique et des coûts en performances. Elle a été retirée dans les dernières version de PaX pour noyaux 2.6. Le programme [PAXTEST] est capable de vérifier la randomisation par des tests statistiques. On retrouve des valeurs proches des valeurs théoriques (16 bits pour RANDMMAP et 24 pour RANDUSTACK), sauf pour le heap qui bénéficie d’un traitement spécial (randomisation par " perte de place ") :
Anonymous mapping randomisation test : 16 bits (guessed) Heap randomisation test (ET_EXEC) : 13 bits (guessed) Heap randomisation test (ET_DYN) : 25 bits (guessed) Main executable randomisation (ET_EXEC) : No randomisation Main executable randomisation (ET_DYN) : 17 bits (guessed) Shared library randomisation test : 16 bits (guessed) Stack randomisation test (SEGMEXEC) : 23 bits (guessed) Stack randomisation test (PAGEEXEC) : 24 bits (guessed)
Exec-Shield
Exec-Shield a été annoncé en 2003 par Ingo Molnar, un " kernel hacker " réputé de chez Redhat. À son annonce, exec-shield était une évolution d’OpenWall, dont l’idée était d’avoir une limite dynamique (au lieu de statique) pour le segment de code, correspondant pour tout processus à l’adresse mémoire exécutable la plus haute de son espace d’adressage, ce qui permet, contrairement à OpenWall, de rendre non exécutables d’autres zones que la pile. De plus, Exec-Shield utilise également l’armure ASCII et y place les mappings PROT_EXEC (donc les bibliothèques), afin de rendre potentiellement plus difficiles certains return-to-libc et de concentrer le maximum de code exécutable le plus bas possible afin d’avoir le segment de code le plus petit possible. Voici les descripteurs de segment d’un processus [DTDUMPER] et son fichier maps (noyau 2.4), on peut voir la limite d’exécutabilité à 0x804A000:DT Dumper julien@cr0.org (#004) 0x23 00C0FB0000008049 Code D/B=32bts AVL=0 Present DPL=3 TYPE= cRA BASE=0x00000000 LIMIT=0x08049FFF (#005) 0x2B 00CFF3000000FFFF Data D/B=32bts AVL=0 Present DPL=3 TYPE= eWA BASE=0x00000000 LIMIT=0xFFFFFFFF 00c15000-00d3d000 r-xp 00000000 03:02 765565 /lib/libc-2.3.2.so 00d3d000-00d45000 rw-p 00127000 03:02 765565 /lib/libc-2.3.2.so 00d45000-00d48000 rw-p 00000000 00:00 0 00d5a000-00d70000 r-xp 00000000 03:02 765562 /lib/ld-2.3.2.so 00d70000-00d71000 rw-p 00015000 03:02 765562 /lib/ld-2.3.2.so 08048000-0804a000 r-xp 00000000 03:02 162889 /root/es/dtdumper 0804a000-0804b000 rw-p 00001000 03:02 162889 /root/es/dtdumper 09da3000-09dc4000 rw-p 00000000 00:00 0 200b3000-200b5000 rw-p 00000000 00:00 0 bffad000-c0000000 rw-p fffb8000 00:00 0Depuis 2003, Exec-Shield a légèrement évolué. Outre quelques corrections de bugs exploitables, il a repris l’idée de randomisation utilisée dans PaX et tire en particulier profit des exécutables ET_ DYN générés à l’aide des switches
- On n’a pas de réelle sémantique des pages non exécutables, cela se traduit par un problème important : certaines zones de données seront situées sous la limite du segment de code et seront donc exécutables. En particulier, toutes les zones correspondant aux sections
.dataou.bssdes bibliothèques seront donc exécutables. Pour certains programmes, cela peut être bien pire si une zone exécutable est chargée à des adresses hautes pour une raison ou pour une autre. - Exec-Shield a longtemps été incompatible avec le vDSO
(linux-gate.so.1, le système introduit dans Linux 2.6 permettant de tirer parti des appels système rapides de Intel à l’aide de sysenter/sysexit) : en effet, il s’agit d’une zone exécutable située tout en haut de l’espace d’adressage, ce qui est incompatible avec une limite basse pour le segment de code. Cependant, en juin 2005, Roland McGrath a proposé un patch grâce auquel il est possible de reloger le vDSO, qui est donc maintenant relogé et même randomisé dans l’ASCII armor comme une bibliothèque normale. - Il n’y a pas d’équivalent aux restrictions MPROTECT de PaX : un attaquant ayant acquis un certain degré de contrôle du processus (par exemple à l’aide de returnto- libc enchaînés) peut faire un
mprotect()afin de rendre la pile (et donc tout l’espace d’adressage car la pile est située en haut !) exécutable ! Contrairement à PaX qui distingue complètement l’ASLR de l’anti-injection de code exécutable, Exec-Shield repose indirectement sur l’ASLR pour rendre potentiellement plus difficile (et non pas impossible) l’injection de code exécutable. - La randomisation dans Exec-Shield est plus faible que celle de PaX, notamment à cause du fait que les bibliothèques et l’exécutable principal doivent êtres " groupés " sous la limite du segment de code :
Anonymous mapping randomisation test : 8 bits (guessed) Heap randomisation test (ET_EXEC) : 13 bits (guessed) Heap randomisation test (ET_DYN) : 13 bits (guessed) Main executable randomisation (ET_EXEC) : No randomisation Main executable randomisation (ET_DYN) : 12 bits (guessed) Shared library randomisation test : 12 bits (guessed) Stack randomisation test : 19 bits (guessed)La " fonctionnalité " la plus controversée introduite par Exec-Shield est sans nul doute PT_GNU_STACK. Les autres fonctionnalités s’inscrivent bien dans le modèle " faire le plus possible dans un patch simple et léger sans trop se soucier de la sécurité ", mais PT_GNU_STACK introduit de réels problèmes et est un non-sens du point de vue de la sécurité. PT_GNU_STACK est un drapeau du program header permettant de marquer un fichier ELF (programme ou bibliothèque) comme " ayant besoin d’une pile exécutable ". Ce marquage est réalisé de manière automatique par la toolchain (GCC, ld,...) en détectant des constructions ayant besoin d’une pile exécutable (en pratique, PT_GNU_STACK se limite aux trampolines GCC). Le lieur dynamique tire parti de cette information lors du chargement du programme principal ou d’une bibliothèque pour, si l’un au moins de ces composants réclame une pile exécutable, appeler la fonction
Anonymous mapping randomisation test : 8 bits (guessed) Heap randomisation test (ET_EXEC) : No randomisation Heap randomisation test (ET_DYN) : No randomisation Main executable randomisation (ET_EXEC) : No randomisation Main executable randomisation (ET_DYN) : No randomisation Shared library randomisation test : 10 bits (guessed) Stack randomisation test : 19 bits (guessed)
OpenBSD’s W^X
W^X (W xor X) est apparu dans OpenBSD 3.3 en mai 2003. Il a pour but d’empêcher l’existence de pages à la fois disponibles en écriture et en exécution. Dans la version 3.3 d’OpenBSD, W^X ne fonctionnait que sur les processeurs supportant le marquage non exécutable d’une page. W^X pour i386 est apparu avec la version 3.4 d’OpenBSD en novembre 2003. L’approche d’OpenBSD est de ne réaliser que des modifications simples dans le noyau et de réaliser le gros du travail en userland. Là encore, sur i386, la segmentation est utilisée : le segment de code a une limite à 512 Mo, ce qui signifie que toute adresse (dans l’espace logique) supérieure à 0x20000000 ne sera pas exécutable. La première étape pour la réalisation de W^X (utile même sur les processeurs ayant un marquage NX) a été de s’arranger pour ne plus avoir besoin de zones à la fois exécutables et disponibles en écriture. Pour cela le trampolineAnonymous mapping randomisation test : 20 bits (guessed) Heap randomisation test (ET_EXEC) : No randomisation Main executable randomisation (ET_EXEC) : No randomisation Shared library randomisation test : 16 bits (guessed) Stack randomisation test : 15 bits (guessed)Comme Exec-Shield, OpenBSD n’offre pas d’équivalent aux restrictions MPROTECT de PaX. De ce fait, le processus (et donc potentiellement l’attaquant !) est autorisé à rendre une page exécutable. Nous avons également pu remarquer à l’aide de [DTDUMPER] que plusieurs entrées dans la LDT et la GDT sont des segments de code parfaitement valides pour exécuter du code n’importe où dans la partie utilisateur de l’espace d’adressage (les limites 0x1FFFFFFF correspondent à la limite de 512 Mo alors que les limites 0xCFBFDFFF couvrent tout l’espace utilisateur). Cela constitue un énorme trou de sécurité puisqu’il suffit de réaliser un branchement inter-segment (far call, far jump, far ret,...) afin d’exécuter du code dans une zone censée être non exécutable ! Voici les segments de code utilisables :
DT Dumper julien@cr0.org GDT size: 0xFFFF (8192 entries), GDT LA: 0xE7B25000 LDT’s selector is 0x18 (entry #3 in GDT) TSS’s selector is 0x168 (entry #45 in GDT) IDT size: 0x7FF (256 entries), IDT LA: 0xD05CEF60 CS: 0x1F DS: 0x27 SS: 0x27 ES: 0x27 Dumping GDT (0xE7B25000) [...] (#004) 0x23 00CCFB000000FBFD Code D/B=32bts AVL=0 Present DPL=3 TYPE= cRA BASE=0x00000000 LIMIT=0xCFBFDFFF (#005) 0x2B 00C1FB000000FFFF Code D/B=32bts AVL=0 Present DPL=3 TYPE= cRA BASE=0x00000000 LIMIT=0x1FFFFFFF [...] Dumping LDT (0xD05CEEC0) (#002) 0x17 00CCFB000000FBFD Code D/B=32bts AVL=0 Present DPL=3 TYPE= cRA BASE=0x00000000 LIMIT=0xCFBFDFFF (#003) 0x1F 00C1FB000000FFFF Code D/B=32bts AVL=0 Present DPL=3 TYPE= cRA BASE=0x00000000 LIMIT=0x1FFFFFFF [...]1 Comme dit mon ami Yoann Guillot, il manque give_me_a_root_shell(). Par exemple, dans le cas d’un débordement de tampon sur la pile, W^X ne sert absolument à rien car il suffit d’effectuer un far ret vers le segment de code illimité. Par chance (pour les pirates), l’opcode de far ret (0xCB ou 0xCA) ne prend qu’un octet, il est donc très simple de le trouver dans des zones exécutables à adresse fixe (afin de ne pas avoir à déjouer l’ASLR) : par exemple la section .text de l’exécutable principal (OpenBSD ne propose pas de système d’exécutables relogeables ET_DYN). Il suffit donc en cas de stack overflow de préparer la pile : le descripteur 0x23 (ou 0x17), l’adresse de retour classique (qui peut être l’adresse du shellcode si on la connaît ou quelque chose de plus compliqué..), l’adresse d’un octet 0xCB. Le code ci-dessous [RETF_DEMO] simule cette situation (en construisant la pile à l’aide de
; This would be in our target program fret db 0xCB runstack: jmp esp ; obviously we execute code on the stack ; Entry point global _start _start: ; This is our payload push 0xFEEB ; shellcode (this is jmp -2) push 0x17 ; our segment selector push runstack ; this is the classic return address ; don’t expect to have a jmp esp in real-life though ;) push fret ; finding a static offset with 0xCB in standard ELF’s .text ; is very easy ; This is the standard ret after we control the stack retQuand la fonction vulnérable retourne, l’exécution reprend sur un far ret, ce qui a pour conséquence de retourner sur l’adresse
Comparaison
Nous avons vu plusieurs techniques qui essaient d’empêcher l’injection de code arbitraire dans un processus. Nous avons vu que parmi celles-ci, l’approche de PaX est la seule qui permette de réellement prouver qu’il n’est pas possible d’injecter du code (en dehors du Autres considérations
Protection d’un noyau
Ce thème avait déjà été évoqué à une Rump Session de SSTIC [SECULINUX]. À l’heure actuelle, lorsqu’un pirate a déjà un accès local à une machine sous GNU/Linux et désire élever ses privilèges, les failles du noyau sont souvent la meilleure option. La plupart des binaires privilégiés sont largement audités et au fil du temps de mieux en mieux sécurisés. Au contraire, le noyau Linux est en développement constant, le code est complexe et rarement audité et de nombreuses vulnérabilités sont présentes dans ses millions de lignes de code. D’autant plus qu’en mode noyau l’erreur ne pardonne vraiment pas : plus que jamais le moindre bug peut se traduire en faille de sécurité. Pour ajouter encore à cela, le paradigme est complètement différent lorsque l’on essaie d’exploiter un bug du noyau : on possède déjà l’exécution de code arbitraire en mode utilisateur et l’on veut simplement augmenter ses privilèges. Cela signifie que l’on peut créer un processus dont on contrôle parfaitement l’espace adressage en mode utilisateur et que l’on peut exécuter le code de notre choix pour créer la situation où la faille pourra être exploitée. Par exemple, les déréférences de pointeurs NULL qui sont, lorsqu’on exploite un programme en mode user, difficilement exploitables pour le commun des mortels [DELALLEAU] deviennent exploitables très facilement puisqu’on contrôle la partie basse de l’espace d’adressage. Protéger le noyau avec des méthodes classiques telles que rendre la pile noyau non exécutable, rendre son adresse aléatoire (RANDKSTACK dans PaX) ou recompiler le noyau avec [SSP] (OpenBSD) ne sert en général à rien : presque toute erreur de programmation dans le noyau est une vulnérabilité et il est difficile de les classifier. On voit finalement très peu d’erreurs classiques telles que les débordements de tampon. De plus comme le code du noyau est le plus privilégié, il n’est pas possible d’appliquer une politique de " moindre privilège " comme le fait PaX pour les processus en mode utilisateur : on ne peut pas empêcher un noyau compromis d’effectuer certaines opérations. Il est donc actuellement très difficile d’empêcher un attaquant d’exploiter des failles dans son noyau. La seule méthode est sans doute de recourir à un système de contrôle d’accès ou un TPE (Trusted Path Execution) pour empêcher les démons et les utilisateurs locaux d’exécuter du code qui n’a pas été préalablement validé par l’administrateur. Là encore le changement de paradigme est lourd de conséquences : on doit sans doute veiller à interdire l’utilisation d’interpréteurs (Perl, Python, Ruby...) qui permettent plus ou moins à leur utilisateur d’exécuter du code arbitraire (en tout cas d’avoir un grand degré de contrôle du processus de l’interpréteur). Il faut également voir que dans cette situation, tout exécutable autorisé par l’administrateur devient privilégié (puisqu’il est autorisé à exécuter du code), et devient donc une cible pour l’attaquant : un bug exploitable dans /bin/ls donne à l’attaquant l’occasion d’exécuter du code arbitraire et d’exploiter une faille noyau. Cette situation est délicate, car tous ces programmes n’ont pas été conçus pour être considérés comme privilégiés et sont très rarement audités.Le cas de Windows
Il est difficile pour un éditeur différent de Microsoft de protéger l’espace d’adressage car cela nécessite des modifications au coeur du système. Microsoft a cependant, avec Windows XP SP2 et Windows server 2003, commencé à introduire quelques éléments de prévention générique. Outre les modifications au niveau du compilateur, proches de SSP/Propolice, on peut notamment citer l’utilisation du flag NX lorsqu’il est présent afin de rendre certaines zones de données non exécutables. Il n’y a pas cependant sous Windows de mécanisme qui correspondrait aux restrictions- On peut obtenir beaucoup d’informations sur l’espace d’adressage du processus exploité en utilisant le TEB et le PEB que l’on peut retrouver via le sélecteur de segment fs et un offset fixe (ce qui rend la randomisation du TEB/PEB (par rapport au segment ds) réalisée par Windows XP SP2 inutile une fois l’exécution de code arbitraire acquise par l’attaquant). Ces informations peuvent ensuite être exploitées :
- On peut recopier son shellcode dans une zone mémoire jugée " saine " (en général, cela nécessite un peu de travail car une zone mémoire disponible en écriture ne devrait pas être jugée saine).
- On peut mettre à profit du code existant dans l’espace d’adressage afin qu’il réalise les appels de fonctions pour nous, tout en réalisant un " lifting " approprié de la pile pour simuler une situation normale. On peut, si l’on sait d’avance quel HIPS on veut contourner " suivre les hooks " pour retrouver la fonction originale. Ces techniques sont toujours utilisables, mais sont plus ou moins complexes à mettre en oeuvre selon la qualité de l’heuristique du produit à contourner. Il faut également noter que les hooks de bibliothèques en mode user peuvent être contournés en appelant directement les NSS (pour rester générique, on peut utiliser NTDLL.DLL pour en retrouver les numéros).
Retrouvez cet article dans : Misc 23





Donnez votre avis
Vous devez avoir ouvert une session pour écrire un commentaire.