lundi 16 juin 2008

Double-free attack V2.0

Suite aux nombreux messages d'encouragement qui ont suivis la première version de ce post (traduction : après deux remarques du genre "Putin mais ton dernier post c'est de la merde t'as rien décris du tout") j'ai décidé de reprendre à zéro l'analyse du double-free() et de pousser un peu plus loin. Et j'ai bien fait... puisqu'il y avait plus de choses à dire que ce que je pensais :-)


I- Il y a fort longtemps...

Donc on reprend l'histoire depuis le début : toute personne ayant déjà suivie un cours de programmation en C s'est entendu dire "il ne faut pas oublier de free() tout pointeur alloué avec malloc(), et ne pas re-free() une zone déjà libéré". En effet, si on en croit la page de manuel qui va bien : "if the space [qu'on essaye de free()] has been deallocated by a call to free() [...] the behavior is undefined." Donc si on "double-free()" un pointeur, le comportement serait aléatoire ?

Nous allons donc voir, à travers plusieurs versions de la libc ( puisque ce qui nous intéresse ici c'est les fonctions malloc() et free() ) quel est ce fameux comportement.

Testons ça dans un premier temps sur une version Windows XP SP2 ...

int main()
{
void * first=malloc(20);
void * second;
void * third;

free(first);
free(first); // Oh My God, un double-free !

second=malloc(20);
*((int *)second)=2;
third=malloc(20);
*((int *)third)=3;

printf("second : %d, third : %d\n",*((int*)second),*((int*)third));

return 0;
}

Et là que se passe t'il dude ?

> second : 3, third : 3

Là tu n'en crois pas tes yeux cher lecteur, mais pour finir de te convaincre de ce comportement intéressant, je rajoute un petit :

...
if(third==second)
printf("third = second !!!\n");
...

Et ça donne :

> second : 3, third : 3
third = second !!!

WTF ? Et bien oui les deux pointeurs désignent la même zone mémoire ! Donc quand on fait un double-free() avec cette version de la libc, les deux malloc() qui suivent vont désigner le même endroit, on comprend aisément que si dans un programme ce genre d'erreur se produit, ou du moins qu'on arrive à faire en sorte que ça soit le cas ( genre un cas à la con que le programmeur a pas prévu qui va "double-free()" une variable globale) et que l'utilisateur à la main sur les données rentrées à partir du deuxième malloc(), il va écraser celles du premier et c'est le 0v3RFl0W dude !

II- And now, what's up ?

Il est temps de tester le double-free() avec la dernière version de la libc (2.7), je reprend donc mon superbe code, je lance l'exécution et là :

*** glibc detected *** ./test: double free or corruption (fasttop): 0x0804a008 ***

Pinaiz, ils ont implémenté une protection anti-"double free()". Shame on me, mon overflow de la mort ne fonctionne plus ?

Intéressons nous donc réellement au fonctionnement de free()|malloc(), pour voir quelle est cette nouvelle protection qui ma pwned.
Donc, direction la libc et plus particulièrement malloc.c qui contient le code des deux fonctions sus-nommées ( et oué ).

Première étape, se renseigner sur la gestion des blocs (chunks) libres, puisque c'est celà qui nous intéresse ici.
On trouve un passage intéressant dans les commentaires ligne 2049 :
"Chunks of the same size are linked with the most recently freed at the front, and allocations are taken from the back."
Donc les blocs libres sont mis dans des "free-list" qui contiennent des blocs de même taille et l'allocation fonctionne sur un modèle FIFO : quand on libère un bloc il est ajouté en tête de la liste qui le concerne et quand on a besoin d'un bloc on prend le plus "vieux" ( donc celui qui se trouve à la fin ).

Regardons donc maintenant la protection "anti-double free()" : on cherche le code de free() et on voit à la ligne 4594 :

if (__builtin_expect (*fb == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}

où p est l'adresse du bloc qu'on veut libérer et fb un pointeur vers la free-list qui lui est associé (celle qui contient les blocs libres de même taille que lui).
Tiens donc ? Ce test vérifie donc que le bloc qu'on veut libérer n'est pas celui qui se trouve en tête de la free-list associée, donc si il n'est pas le dernier qui a été free().
Vérifions ce comportement :

int main()
{

void * first=malloc(12);
void * second=malloc(12); // Ces deux pointeurs seront stockés dans la même free-list lors de leurs libération

free(first);
free(second);
free(first); // Oh my God, double-free() on first !

return 0;
}

On lance le bouzin et... aucun message d'erreur ! On a pu double-free() notre pointeur sans aucun souci. Le check qui est effectué par la libc est donc facilement bypassable... Mais en même temps on "comprend" qu'un check complet de la free-list ( donc un parcours linéaire ) serait très couteux en temps.

Maintenant qu'on voit comment on peut pwned la protection, voyons si on pourrait exploiter celà pour faire un overflow. Réprésentons l'évolution de la free-list associée à mes deux pointeurs first et second :





C'est là que dans ton cerveau malade la possibilité de l'overflow apparait (rappelons que lors de l'appel à malloc(), le bloc choisi est celui qui se trouve "en bas" de la free-list), si je rajoute dans mon code :

...

void * third=malloc(12); // va prendre le premier bloc du bas (=first)
void * fourth=malloc(12); // va prendre le deuxième bloc en partant du bas (=second)
void * cinqth=malloc(12); // va prendre le troisième bloc en partant du bas (=first !)

if(third==cinqth)
printf("Double free inside the heap !\n");

}

Et l'exécution donne :
$ Double free inside the heap !

Cinqth (et oué) et third pointe sur le même bloc ! Donc on en reviens au principe originel : ce qui sera écris à partir de cinqth écrasera ce qui a été écris à partir de third -> game over.

On peut donc voir que les double-free() sont encore possibles, il faut néanmoins, pour qu'ils soient exploitables, qu'il y ait eu au moins un autre free() d'un bloc de même taille entre temps.

III- The ultimate protection...

La solution pour éviter de devoir checker dans des codes assez lourd à chaque free(ptr) ce qu'on a fait avant avec le pointeur, c'est de faire :

free(ptr); ptr=NULL;

puisque d'après le manuel "If ptr is NULL, no operation is performed.", donc un deuxième free() ne foutra pas la zone. Easy, no ? A noter que ce genre de "bug" est déjà arrivé sur des projets assez costaud (voir ici).


5 commentaires:

Anonyme a dit…

T'es complèTemenT Fou, Joe ! Où pluTôT, frôlant la foLie des Genies ;) !

Adn_1, a Pseudo_Geek° !

Anonyme a dit…

Hey, sympa de voir en pratique ce que fait le double-free. En théorie les zones allouées dynamiquement sont marquées par des sentinelles, supprimées lors de la libération.
Ce serait classe de voir plus en détail le fonctionnement de free sur ces sentinelles quand justement il ne les trouve pas.

Sinon il existe des outils comme valgrind par exemple, qui permettent de chercher les problèmes de gestion de mémoire. Tu as déjà dû le manipuler hein =P

Bonne continuation pour ton blog :)

JoE a dit…

Merci Sha pour les précisions, oué Valgrind me dis quelque chose, ptete bien que je l'ai vu en TD ;-D
En tout cas je vais m'intéresser au fonctionnement de free() dès que j'aurai un peu de temps :)

Anonyme a dit…

Intéressant tout ça :)

Bonne continuation à toi.

Anonyme a dit…

mouais...j'attend une v3 plus détaillée pour etre pleinement satisfait de ton travail sur le double free()
Perso valgrind j'aime pas, je te conseille plutot ce programme
. Je le conseille à mes étudiants qui en sont ravis, surtout pour repérer les erreurs sur le shell.

Allez bonne continuation joe kun