Rodger

Aller au contenu | Aller au menu | Aller à la recherche

vendredi, avril 23 2010

Un Buffer overflow de A à Z (sous Linux) - Première partie

Après une introduction sur l'utilité et le fonctionnement de la pile, introduction dans laquelle nous avons joué à modifier le déroulement normal d'un programme C, nous allons voir d'un peu plus près le problème du buffer overflow et ce que l'on entend par un exploit de buffer overflow.

Au programme dans ce post : le principe général et l'étude du programme "vulnérable" qui sera la cible de notre attaque dans les prochains posts.

Principe

Le principe d'un buffer overflow est le suivant :
  • un programme copie des données dans un buffer local sans s'assurer que la place allouée est suffisante pour les accueillir (notre programme d'exemple fera un simple strcpy au lieu d'un strncpy, mais il aurait pu faire un fscanf(in,"%s",buf); ou autre)
  • dans la plupart des cas, déborder de la zone mémoire va provoquer une erreur de segmentation, dans d'autres cas cela va corrompre des données
  • nous verrons par la suite qu'en fournissant les données adéquates au programme, on peut réussir à exécuter le code machine que l'on veut, et ainsi lancer un programme quelconque (un shell par exemple) par l'intermédiaire du programme défaillant (au lieu de bêtement le faire planter)

Le gros problème de sécurité se pose lorsque le programme vulnérable possède le droit "setuid root" et qu'un utilisateur de base exploite le buffer overflow pour obtenir un shell root. Une autre exploitation de cette vulnérabilité consiste à faire exécuter non pas un shell mais du code qui va ouvrir une connexion sur un port de la machine et ensuite exécuter des commandes envoyées depuis une machine distante ("remote shell").

Notre but dans cette série de billets est de réussir à ouvrir un shell root en passant des données corrompues à un programme vulnérable qui s'exécute en mode setuid root.

Le programme vulnérable

Pour fixer les idées et se donner un exemple le plus simple possible, nous allons utiliser le programme suivant qui fait l'erreur de ne pas vérifier que la chaîne passée en 1er argument du programme ne fait pas plus que 7 caractères (+ l'octet nul de fin de chaîne).

#include <stdio.h>
#include <string.h>

int main(int argc, char** argv) {
    char name[8];
    if (argc == 2) {
        strcpy(name,argv[1]);
        printf("hello %s !\n",name);
    }
    puts("bye bye");
    return 0;
}

Faire planter le programme

Compilons ce programme avec les informations de débug (qui serviront plus bas) :
        $ gcc -g vulnerable.c -o vulnerable

Lorsque l'on lance ce programme avec en premier argument une chaîne de caractères trop longue, on obtient une erreur de segmentation:

        $ ./vulnerable Rodger
        hello Rodger !
        bye bye
        $ ./vulnerable ABCDEFGHIJKLMNOPQRSTUVWX
        hello ABCDEFGHIJKLMNOPQRSTUVWX !
        Segmentation fault
Le programme fait cette erreur car le strcpy a copié plus de caractères que le tableau name ne pouvait en accueillir et les caractères en trop sont allés écraser l'adresse de retour de la fonction main(). Le main s'exécute donc parfaitement, en faisant correctement le printf, mais lorsque l'on doit revenir à l'appelant (la libc) l'adresse à laquelle on aurait dû retourner a été corrompue et l'on saute à une adresse invalide. Un exploit de buffer overflow consiste à passer au programme vulnérable une chaîne de caractères spécialement écrite pour que l'on ne saute pas n'importe où dans la mémoire mais au contraire à l'adresse où se trouve un peu de code machine qui, par exemple, lancera un shell.

Comprendre ce plantage

Lancer "ddd" (d'où le flag "-g" de la compilation précédente).
        $ ddd ./vulnerable
Dans le menu "Source", cocher "Display Machine Code". On obtient ainsi la correspondance entre le code C et le code assembleur. Dans cette zone contenant le code assembleur, poser un point d'arrêt sur l'instruction "ret". Exécuter le programme avec en lui passant la chaîne de caractères "ABCDE...WX" en argument : dans le menu "Program" cliquer sur "Run..." et saisir la chaîne complète.
Le programme s'exécute jusqu'au point d'arrêt. Dans la sous-fenêtre gdb, taper la commande suivante :
        (gdb) x $esp
On obtient l'affichage suivant :
        0xbffff44c:        0x58575655
qui nous indique que la pile (le registre esp) pointe à l'adresse 0xbffff44c, et que l'on trouve à cette adresse les 4 octets 0x58, 0x57, 0x56 et 0x55 qui sont respectivement les codes ASCII des caractères 'X', 'W', 'V' et 'U' (man ascii pour le vérifier).
Exécuter l'instruction "ret" en cliquant sur "NEXTI" (ou en tapant "nexti" dans la sous-fenêtre gdb), l'erreur de segmentation se produit car on a essayé de sauter à l'adresse 0x58575655 qui n'est pas accessible. En tapant la commande suivante (dans la sous-fenêtre gdb) :
        (gdb) x $eip
on en a la confirmation : eip est le registre qui contient l'adresse de l'instruction à exécuter (EIP = Extended Instruction Pointer).

En passant la bonne chaîne de caractères en 1er argument du programme vulnérable, il est donc possible de faire sauter l'exécution à (presque) n'importe quelle adresse, si notre code "malicieux" se trouve à cette adresse c'est gagné... C'est ce que nous verrons dans les prochains posts...

mercredi, avril 7 2010

Jouer avec la pile (en C/ASM sous linux)

En guise d'introduction aux prochains billets sur le buffer-overflow, et pour bien comprendre comment fonctionne la pile, voici ce premier billet qui devrait clarifier les choses.

La pile

Il s'agit d'une zone de mémoire utilisée pour mémoriser des données temporairement:

  • lorsque l'on appelle un sous-programme, l'adresse à laquelle il faudra revenir pour continuer le déroulement du programme
  • les variables locales à ce sous-programme
Ces stockages sont temporaires car lorsque l'on sort du sous-programme ces données sont dépilées et donc perdues définitivement. C'est la raison pour laquelle il ne faut jamais retourner l'adresse d'une variable locale: elle est inutilisable hors du sous-programme où elle est définie.

Sur les architectures Intel c'est le registre ESP qui sert à accéder au sommet de la pile. Plus on a de variables locales et/ou d'appels imbriqués à des fonctions (par exemple pour de la récursivité), plus la pile grossit. Son sommet progresse vers les adresses décroissantes (l'adresse pointée par le registre ESP est de plus en plus basse).

Initialement la pile contient dans l'ordre (en commençant par les adresses hautes):

  • 4 octets nulls
  • le nom du programme
  • les variables d'environnement
  • les arguments du main
  • d'autres données
  • l'adresse de retour lorsque l'on sort du main (et qu'on retourne à la libc)
  • les variables locales au main
  • ...
En assembleur, les instructions pushl et popl permettent respectivement d'empiler et dépiler une donnée de 4 octets. Lorsqu'on empile, le registre esp est décrémenté de 4 et la donnée est écrite à cette nouvelle adresse. Lorsqu'on dépile c'est l'inverse qui se produit. L'instruction call sert à appeler un sous-programme : l'adresse de l'instruction qui succède au call est empilée (pour qu'on puisse y revenir plus tard) et le programme saute à l'adresse passée en paramètre au call. Inversement, l'instruction ret saute à l'adresse qu'elle dépile.

Comment connaître l'adresse à laquelle se trouve la pile ?

Première chose à savoir : lorsqu'une fonction retourne une valeur qui tient dans 32 bits (un int par exemple) gcc utilise le registre eax pour transmettre cette valeur à l'appelant. Par exemple, dans la fonction suivante (et sans optimisation particulière du compilateur, la valeur 1234 va être placée dans le registre eax, qui sera utilisée (ou pas) dans le code appelant:
int f() {
  return 1234;
}
Donc une solution pour connaître l'adresse du sommet de la pile peut s'écrire:
unsigned long* getesp() {
  __asm__("movl %esp, %eax");
}
Dans ce code, on appelle de l'assembleur en ligne (pour beaucoup plus d'infos voir http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html). Le code met juste la valeur actuelle du registre esp (le sommet de la pile) dans le registre eax (la valeur de retour de la fonction getesp). Donc on peut utiliser la fonction ainsi:
  printf("Ma pile est à l'adresse %x\n", getesp());

Jouons avec la pile

Que faut-il écrire comme code dans la fonction "function()" pour qu'à l'exécution le programme affiche que x vaut 12 (au lieu de 1) ?
#include <stdio.h>
unsigned long* getesp() {
  __asm__("movl %esp, %eax");
}

void function() {
  unsigned long* stack = getesp();
  // QUE MANQUE-T-IL ICI ???
}

void main() {
  int x;
  x = 12;
  function();
  x = 1;
  printf("maintenant x == %d\n",x);
}

Créons l'exécutable:
$ gcc stack_trick.c -o stack_trick

Si on l'exécute, l'affichage indiquera que x vaut 1...

Regardons le code machine produit par GCC

Pour cela utilisons la commande objdump ( -D pour désassembler, et le grep pour ne lister que les 7 lignes qui suivent celle contenant "<function>:" ) :
$ objdump -d stack_trick | grep \<function\>\: -A 7
08048237 <function>:
 8048237:    55                       push   %ebp
 8048238:    89 e5                    mov    %esp,%ebp
 804823a:    83 ec 10                 sub    $0x10,%esp
 804823d:    e8 ee ff ff ff           call   8048230 <getesp>
 8048242:    89 45 fc                 mov    %eax,-0x4(%ebp)
 8048245:    c9                       leave 
 8048246:    c3                       ret

Sur chaque ligne, les informations présentées par objdump sont ici en 3 colonnes :
  • l'adresse de chaque instruction. Cette adresse sera ensuite modifiée (on dit "relogée") lors du chargement du programme (et ne sera donc jamais celle affichée ici)
  • une ou plusieurs valeurs hexadécimales, ce sont les opcodes de l'instruction
  • l'instruction assembleur avec éventuellement une sorte de commentaire (pour le call, on a l'information que l'adresse à laquelle on saute est en fait la fonction getesp)
Le code que l'on va ajouter, va venir s'intercaler entre le mov et le leave.

Concernant les instructions qui modifient la pile, on repère:
  • 1 push (4 octets)
  • 1 sub de 16 octets (0x10)
  • 1 call (qui aura pour effet de descendre la pile de 4 octets supplémentaires)
  • à cela il faut ajouter encore 4 octets à cause du call vers function() depuis le main
Ce qui nous fait un total de 28 octets (ou 7 longs) entre le sommet de la pile renvoyé par getesp() et le sommet de la pile dans le main juste avant d'appeler function().

On sait donc maintenant que l'adresse à laquelle le programme ira quand on sortira de function() est stockée en "*(stack+7)". Nous devons donc modifier cette adresse pour qu'elle indique l'instruction C après le "x=1;".

Pour savoir de combien d'octets il faut augmenter l'adresse de retour de function(), regardons le code machine du main:
$ objdump -d stack_trick | grep \<main\>\: -A 16
080483db <main>:
 80483db:    55                       push   %ebp
 80483dc:    89 e5                    mov    %esp,%ebp
 80483de:    83 e4 f0                 and    $0xfffffff0,%esp
 80483e1:    83 ec 20                 sub    $0x20,%esp
 80483e4:    c7 44 24 1c 0c 00 00     movl   $0xc,0x1c(%esp)
 80483eb:    00
 80483ec:    e8 da ff ff ff           call   80483cb <function>
 80483f1:    c7 44 24 1c 01 00 00     movl   $0x1,0x1c(%esp)
 80483f8:    00
 80483f9:    b8 d0 84 04 08           mov    $0x80484d0,%eax
 80483fe:    8b 54 24 1c              mov    0x1c(%esp),%edx
 8048402:    89 54 24 04              mov    %edx,0x4(%esp)
 8048406:    89 04 24                 mov    %eax,(%esp)
 8048409:    e8 ee fe ff ff           call   80482fc <printf@plt>
 804840e:    c9                       leave 
 804840f:    c3                       ret

On y voit le call function() suivi immédiatement par un movl qui met la valeur 1 dans une variable locale qui se situe en 0x1c(%esp), il s'agit de la variable x. Cette instruction est traduite par les opcodes suivants:
c7 44 24 1c 01 00 00 00
qui occupent un total de 8 octets (attention objdump les affiche sur deux lignes, il y en a donc bien 8 et pas seulement 7).

Pour éviter d'exécuter cette instruction lorsque l'on reviendra du call, il faut donc que l'adresse de retour ne pointe pas sur la ligne du mov mais 8 octets plus loin. On peut donc maintenant compléter function():
void function() {
  unsigned long* stack = getesp();
  *(stack+7) += 8;
}

Et voilà !