vendredi 28 février 2014

ASLR (Accélère? Vasistas?)

Le travail d'un compilateur est de générer un code exécutable pour une machine hôte donnée. Tout système moderne proposant un adressage virtuel, le compilateur est (presque) libre de choisir pour toute variable ou symbole devant avoir une adresse mémoire une adresse virtuelle. Saviez-vous que les loaders aussi se permettent de jouer avec l'espace ? Et oui, comme au bon vieux temps, votre code peut-être dans l'espace virtuel lui-même! Let's see...

Prenons, le cas du programme élémentaire suivant :

#include <stdio.h>
#include <stdlib.h>
int i;
int main() {
int j;
int *p = malloc(sizeof(int));
printf("%18p static\n",&i);
printf("%18p code\n",&main);
printf("%18p stack\n",&j);
printf("%18p heap\n",p);
 printf("%18p libs\n",&printf);
free(p);
return 0;
}

L'idée est d'essayer d'observer quelles parties de l'espace d'adressage sont réservées aux variables statiques, au code, à la pile et au tas.

Ok let's go.

<nivose-2-[~/tmp]> uname -a
SunOS nivose 5.10 Generic_144488-01 sun4u sparc SUNW,SPARC-Enterprise
<nivose-3-[~/tmp]> gcc --version
gcc (GCC) 3.4.3 (csl-sol210-3_4-branch+sol_rpath)
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

<nivose-4-[~/tmp]> gcc -Wall -o vir vir.c
<nivose-9-[~/tmp]> nm -tx vir
vir:
[Index]   Value      Size      Type  Bind  Other Shndx   Name
...
[76]    |0x00020a60|0x00000004|OBJT |GLOB |0    |22     |i
[68]    |0x00010700|0x000000a8|FUNC |GLOB |0    |9      |main
[58]    |0x00020938|0x00000000|FUNC |GLOB |0    |UNDEF  |printf
...
<nivose-12-[~/tmp]> ./vir
             20a60 static
             10700 code
          ffbffa4c stack
             20a78 heap
             20938 libs
<nivose-13-[~/tmp]> 

Parfait! Sur le SUN que j'ai sous la main tout semble aller pour le mieux. Dans l'ordre des adresses croissantes, mon espace d'adresse contient : le code, immédiatement suivi de la zone static, des fonctions de bibliothèques puis du tas, enfin la pile démarre d'une adresse en fin d'espace (on rappelle ici que les piles descendent en grandissant...). C'est une situation tout à fait conforme à ce qui est généralement décrit. Au passage, on remarque que très vraisemblablement l'espace d'adressage est 32 bits (bon c'est sûrement une combinaison subtile de vieilleries - machine, os, compilo, etc).

La curiosité nous tient, alors on regarde sur une autre machine :

<lucien-5-[~/tmp]> uname -a
Linux lucien 3.2.0-4-amd64 #1 SMP Debian 3.2.51-1 x86_64 GNU/Linux
<lucien-6-[~/tmp]> gcc --version
gcc (Debian 4.7.2-5) 4.7.2
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

<lucien-7-[~/tmp]> gcc -Wall -o vir vir.c
<lucien-8-[~/tmp]> nm vir
0000000000600a4c B i
00000000004005ac T main
                 U printf@@GLIBC_2.2.5
<lucien-9-[~/tmp]> ./vir
          0x600a4c static
          0x4005ac code
    0x7fff207bb8e4 stack
         0x10b8010 heap
          0x400470 libs
<lucien-11-[~/tmp]>

La vie est délicieuse, c'est légèrement différent mais conforme à l'esprit de la chose. Ici dans l'ordre on a les libs, le code, la zone statique, le tas et en fin d'espace la pile. Donc les trucs statiques d'abord, suivi par les machins dynamiques pour qu'il puissent s'étendre l'un d'un côté, l'autre de l'autre. Ok ici, l'adresse de la pile est vraiment grande, on doit être dans un espace d'adressage à 64 bits.

Allons voir encore ailleurs...

[Ciboulette:~/examples] yunes% uname -a
Darwin Ciboulette.local 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64
[Ciboulette:~/examples] yunes% gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.1.0
Thread model: posix
[Ciboulette:~/examples] yunes% gcc -Wall -o vir vir.c
[Ciboulette:~/examples] yunes% nm vir
0000000100000000 T __mh_execute_header
                 U _free
0000000100001030 S _i
0000000100000e70 T _main
                 U _malloc
                 U _printf
                 U dyld_stub_binder
[Ciboulette:~/examples] yunes% ./vir
       0x10a102030 static
       0x10a101e70 code
    0x7fff55afe958 stack
    0x7fd7fac03a40 heap
    0x7fff8c0218a8 libs
[Ciboulette:~/examples] yunes% 

Oh my god! J'ai bien quelque chose de cohérent puisque l'espace de largeur 64 bits contient visiblement et dans l'ordre : le code, la zone statique, puis loin ailleurs le tas, bien plus loin encore la pile et derrière la pile les libs. Ce qui perturbe est que les adresses virtuelles déterminées à la compilation, par exemple 100000103 pour la variable globale i a à l'exécution la valeur 10a102030!!! Bizarre ce truc (la même chose peut-être observée pour les autres adresses). Les informaticiens ont parfois des pensées étranges (le psychanalyste dirait magiques), me voilà saisit d'une idée (magique donc : « bon la machine n'a bien compris, je vais lui relancer le programme et maintenant elle va faire les choses bien, hein cocotte ? »). Alors soit :

[Ciboulette:~/examples] yunes% ./vir
       0x10d7e8030 static
       0x10d7e7e70 code
    0x7fff52418958 stack
    0x7fb8c0c03a40 heap
    0x7fff8c0218a8 libs
[Ciboulette:~/examples] yunes% 

Oh boy! Les valeurs ne sont pas les mêmes d'une exécution à l'autre! (Une pensée sombre traverse l'esprit perturbé de l'expérimentateur : il doit retourner refaire ses études depuis le départ). Troisième tentative (avant d'aller s'inscrire en première année) :

[Ciboulette:~/examples] yunes% ./vir
       0x106ee3030 static
       0x106ee2e70 code
    0x7fff58d1d958 stack
    0x7fc64b403a40 heap
    0x7fff8c0218a8 libs
[Ciboulette:~/examples] yunes% 

Bon, il y a tout de même un motif, les bits de poids faibles semblent bien correspondre à ce qu'indique la table des symboles de l'exécutable... Alors, c'est quoi ce truc ?

Essayons de déboguer :

[Ciboulette:~/examples] yunes% gdb vir
GNU gdb 6.3.50-20050815 (Apple version gdb-1824) (Wed Feb  6 22:51:23 UTC 2013)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done

(gdb) run
Starting program: /Users/yunes/examples/vir 
...
............ done
       0x100001030 static
       0x100000e70 code
    0x7fff5fbff8b8 stack
       0x100103aa0 heap
    0x7fff8c0218a8 libs

Program exited normally.
(gdb) 

Oh dear! Avec un débogueur j'ai les bonnes adresses! Qu'est ce que j'ai fait, qui m'en veut autant ?

La réponse est en fait simple. Le mécanisme est l'ASLR! Soit Address Space Layout Randomization. Koikidilui ? Le mécanisme est une protection (faible) destinée à perturber l'écriture de virus ou d'attaques. Les adresses étant virtuelle, la machine est donc libre, au chargement du code (i.e. lors de l'exec()) de reloger les variables comme bon lui semble! En pratique, le déplacement consiste simplement à perturber aléatoirement certains bits de l'adresse virtuelle de la table des symboles.

Cette fonctionnalité n'existe pas sur tous les systèmes, mais semble être désormais assez répandue. D'autre part, elle peut-être désactivée à divers niveaux. Nous renvoyons le lecteur à la documentation afférente à son système pour savoir : (1) si l'ASLR est présent, (2) quels sont les espaces qui peuvent être relogés ? (3) déterminer si la fonctionnalité est active, etc.

Pour mon MacOSX, je peux :

  • soit utiliser l'option --no-pie à la compilation qui a pour effet (entre autres) de désactiver l'ASLR
  • soit modifier le champ MH_PIE dans l'entête Mach-O de l'exécutable. Il existe un script Python sur Internet pour le faire : ici
  • peut-être une autre technique exotique... Genre un appel à une fonction pour désactiver la fonctionnalité avec un flag exotique lui aussi avant un exec() (_POSIX_SPAWN_DISABLE_ASLR par exemple)

DjiBee
PS: C'est à Jean-Marie Rifflet que je dois cette expérimentation. Le symptôme qu'il avait observé nous avait d'abord plongé tous les deux dans un drôle d'état (computer science?) ou abîme de perplexité (psychanalyse?)...

6 commentaires:

  1. Ah, cours de M2 Systèmes Avancés d'il y a une semaine! :-)
    Merci pour le complément!

    C'est effectivement fait pour ajouter un peu d'entropie de manière à ce que les attaques par buffer overflow, aient un peu plus de difficultés à trouver l'adresse du buffer écrasé dans la pile, et que les attaques de type return-to-libc aient un peu plus de mal à trouver les adresses dans la libc.

    Sur des architectures 32 bits, les analyses semblent montrer que la protection est quasi inefficace, les variations étant limitées. Sur 64 bits, l'effet de retardement des attaques est un peu meilleur, les possibilités de variations étant plus grandes.

    Sur Linux seuls la pile et les bibliothèques sont atteintes de bougeotte en cas d'ASLR. Sur Windows, et si je comprends bien sur Darwin, le code et les données sont aussi susceptibles d'avoir des adresses variables.

    Dans Linux, ça serait intéressant de regarder les effets de l'ASLR, avec les variations sur les espaces d'adressages "classiques" et "flexibles".

    RépondreSupprimer
  2. Il y a deux petites surprises cachées dans le résultat sur "nivose" (SunOS):
    - l'adresse imprimée par le programme pour le tas peut surprendre.
    - l'adresse imprimée pour printf, n'est manifestement pas dans une zone de code!

    Si on ajoute un pause() à la fin du programme et qu'on en profite pour faire pmap pid, et voir l'espace d'adressage du processus,
    on va voir que ces 2 adresses sont dans la même page (8KB sur SPARC) que les variables globales

    En fait, le tas démarre directement à la fin des données dans la dernière page des données globales. Si on ajoute un tableau de 7800 caractères pour occuper plus d'espace, l'adresse rendue par malloc va bien se retrouver dans la région suivante affichée par pmap.

    Le résultat de pmap montre que les bibliothèques dynamiques ne sont pas du tout à l'adresse imprimée. Je suppose qu'on doit probablement imprimer l'adresse d'une indirection.

    RépondreSupprimer
  3. Oui j'en avais déduit la même chose pour l'indirection de printf() sans vouloir creuser pour ne pas noyer la discussion, mais la remarque est pertinente! Pour le tas, j'étais moyennement surpris, disons c'est moins "surprenant" que pour printf(), l'essentiel était respecté : l'adresse est supérieure à celle des globales. Merci François pour ces précisions...

    RépondreSupprimer
  4. Merci pour le billet. La première chose que j'ai pensé concerne les "protections" qu'on trouve au niveau CPU au sujet des dépassement de tampon (NX Bit je crois). Une protection plus efficace ?

    RépondreSupprimer
  5. En fait, il y a plusieurs types de protections contre les débordement de tampon dans la pile:
    - les canaris (gcc -fstack-protector), mais certaines attaques peuvent inclure les canaris, donc efficacité pas garantie
    - l'interdiction d'exécution de code dans la pile (NX et autres variantes). Encore faut-il que l'application n'ait pas besoin d'exécuter du code généré dynamiquement dans la pile.
    - ASLR (j'y reviens après)

    L'exploitation d'un débordement de tampon, peut se faire de différentes manières:
    - injection d'un "shellcode" : la chaine va contenir le code de l'attaque. Et on va dérouter le programme vers ce code
    (en écrasant l'adresse de retour). Il faut alors avoir une "idée" de l'adresse du tampon pour trouver le code. On interdit ça
    effectivement avec les interdictions d'exécuter du code en pile (NX bit)
    - attaque de type "return-to-libc" (http://fr.wikipedia.org/wiki/Return-to-libc_attack) auquel cas la protection NX est inopérante.
    Dans ce cas, on déroute le programme vers une fonction de la libc ("Facile", elles sont toutes à disposition grâce aux librairies
    partagées!) Comme souvent les librairies partagées sont mappées à une adresse qui ne varie pas, les adresses sont faciles à
    déterminer. D'où l'introduction du mécanisme ASLR pour rendre les adresses de librairies (mais aussi des tampons dans la pile)
    plus difficile à déterminer.

    HTH

    RépondreSupprimer
    Réponses
    1. TH. Cela donne du coup une bonne base pour s'intéresser aux architectures et programmes 64 bits lorsqu'il s'agit d'aborder ce sujet de sécurité (En outre, essayer des combinaisons de compilation en 32 bits/64 bits, porter des programmes 32 bits sous système 64 bits, etc.).

      Supprimer