dimanche 22 juin 2008

First real exploitation on Windows

Le titre en anglais ça pête...

Les stacks overflows de base sous Windows sont, comme sous Linux, bien documentés, mais les cas pratiques sont pas forcément toujours très réalistes. L'idée est donc ici de se frotter à un ptit tool conseillé par mon maitre jedi Heurs : IntelliTamper 2.07.

Ce qui suit a été réalisé sous Windows XP SP2 FR.

I- Première approche de la cible

IntelliTamper est un "aspirateur web" : vous lui donnez un site, il vous en recrache l'arborescence. On a aussi la possibilité de sauvegarder l'architecture d'un site dans un fichier .map et bien sûr de charger un .map précédemment sauvé :-)

On dl le bouzin, on le lance et on observe...




Nous sommes à la recherche d'un overflow, donc on regarde les différents moyens que l'utilisateur a à sa disposition pour passer des données au programme. A première vue, il y en a deux : l'url que le logiciel doit scanner (qui se rentre dans la barre du haut) et les fichiers .map qu'il peut charger (accessible par un classique "File/Load").

It's time to fuzzzzzzz this bullshit

L'idée est donc ici de balancer des masses de données dans les différentes entrées possibles et de croiser les doigts pour que ça plante, ce qui indiquerait un overflow que nous pourrions peut être exploiter :-) Pour cela j'utilise un tool fournit avec le framework Metasploit : je lui donne la longueur de chaine que je veux et il me la génère en faisant en sorte que je n'ai jamais les 4 mêmes caractères côte à côte, ça permet de voir ( d'après le rapport d'erreur microsoft ) qu'elle est la longueur de données qu'il faut pour faire merder le bouzin.

First test, on balance une grosse chaine de caractères (500) dans la barre URL et il se passe... rien du tout ! On se décourage pas, on rajoute une url correcte devant notre chaine et on re-teste et ...


Yeah, un premier overflow spooted !

Deuxième test : on se fabrique un petit fichier .map en y mettant là encore une chaine très longue.
Et... bingo ! Après avoir balancer des données de longueur diverse ( et avoir rajouter un saut de ligne derrière ), un autre message d'erreur qui va bien apparaît aux alentours des 7500 caractères.
Nous avons donc deux overflows ( à première vue ) dans ce programme, et comme dans la vie il faut faire des choix, nous allons nous intéresser ici à celui qui est provoqué par le chargement de fichiers .map foireux.

II- This is not so easy

Regardons le rapport d'erreur issue de notre BOF :

Notre offset ne correspond à rien dans la chaine de caractère que nous avons mis dans notre fichier .map de la mort. Ce n'est donc pas directement un écrasement d'adresse de retour qui provoque l'overflow.
Pour vérifier ça on lance OllyDbg sur notre tool et on charge notre fichier crafté pour voir qu'est ce qui provoque le plantage :

La valeur chargée dans EBX est issue de la stack, où a surement été copié le contenu de notre fichier crafté, elle doit donc être foireuse et au moment d'accéder à EBX+73B0 on tombe sur une zone mémoire non allouée et ça déclenche un "Access violation". Simple, non ? Regardons exactement la gueule de la stack au moment du plantage pour s'en convaincre : la copie de notre fichier commence en 00123A64, et EBX a effectivement été réécris puisqu'il contient 6964652D qui est bien contenu dans notre .map. Voyons si nous ne pouvons pas nous servir de ce déclenchement d'erreur pour rediriger le flux d'exécution... :-)

Un autre "détail" intéressant est le fait que, à partir d'un certains nombres de caractères, notre programme est "killé" et sans aucun message d'erreur, essayons d'identifier la source de ce comportement étrange ( jsuis trop en forme là ) :

Je bourrine un bon gros fichier .map fabriqué avec amour ( plus de 12000 caractères ) et on observe avec Olly... Notre "Access violation" a bien lieu à la même ligne à cause de la valeur contenue dans EBX, le programme va alors chercher un gestionnaire d'exception pour traiter ce cas d'erreur...

Interlude - Gestion d'exception sous Windows

Pour bien comprendre ce qui suit, il est bon d'avoir quelques notions de la gestion d'exception sous windows : ce qui suit est lâchement pompé du très bon article de Carib publié dans theHackademyManuel #12

Lorsqu'un évènement imprévu survient et qu'une exception est déclenchée le contrôle est passé à la procédure KiUserExceptionDispatcher() qui va dérouler un algorithme pour décider où doit se poursuivre l'exécution du programme. Il existe différents types de gestionnaire, regroupés sous l'acronyme SEH (Structured Exception Handling), mais dans notre cas il est bon de retenir que ceux qui nous intéressent (car mis dans la pile :)) sont les gestionnaires de threads qui sont mis en place à l'aide d'une structure SEH simple :

DWORD Next SEH
DWORD SEH Handler

Le premier champ pointe sur la structure SEH suivante ( ou -1 si il y en a pas ) et le second désigne la procédure qui traitera l'exception.
Les gestionnaires d'un thread sont donc chainés les uns aux autres : si un handler n'est pas capable de gérer une exception, il a la possibilité de renvoyer une valeur signifiant à KiUserExceptionDispatcher() de passer le contrôle au gestionnaire suivant, qui pourra éventuellement la traiter. Rajoutons à cela le fait que ces structures SEH sont placés dans la pile et que le dernier gestionnaire installé ( donc celui qui sera appelé en premier ) est pointé par fs:[0].
Ce qu'il faut retenir, c'est que lorsqu'une exception se déclenche le dernier gestionnaire d'exception installé est appelé et qu'il y a donc redirection du flux d'exécution sur le SEH Handler qu'il contient.

Un dernier détail, qui n'en est pas un, c'est que au moment du lancement du SEH Handler, le système lui passe 3 arguments dont le premier est un pointeur vers un code d'erreur associé à l'exception ( et qui se trouve placé dans la pile ) et le deuxième est un pointeur vers la structure SEH dont on lance justement le SEH Handler ! ( donc ce deuxième argument contient exactement l'adresse du champ Next SEH ).

Fin de l'interlude

Donc on revient à notre programme qui cherche un gestionnaire d'exception après avoir eu un accès mémoire invalide : il regarde donc dans fs:[0] pour trouver le plus récemment enregistré et il y lit... 001265F4 qui est bien une adresse dans la pile et qui a été écrasé lors de la copie de notre .map fr0m the h3ll. Alors le programme JUMPe sur l'adresse du SEH Handler, qui est invalide car j'ai mis de la merde dans mon fichier, et donc ça redéclenche une exception qui ne peut être traitée car il n'y a plus de gestionnaires SEH et donc finalement le programme se kill ( c'est très schématique, toute information complémentaire est encouragée dans les commentaires ;))

Donc l'idée est assez simple : on va réécrire le champ SEH Handler avec une valeur "qui va bien" pour rediriger le flux d'exécution à partir du déclenchement d'une exception.
Le problème c'est que n'est plus aussi simple depuis l'introduction du flag de compilation /SafeSEH avec Visual Studio 2003 qui fait en sorte qu'on ne puisse donner comme adresse de SEH Handler que des valeurs qui ont été précédemment enregistrées, et donc on est un ptit peu niked. Vérifions donc si ce tag est présent ici, pour cela j'utilise le plugin pour Olly SafeSEH disponible ici.
Il va nous indiquer ce qui a été compilé avec le flag :

Alleluia ! L'exécutable n'a pas été compilé avec /SafeSEH, ce qui veut dire qu'on peut JUMPer où l'on veut dans son code... Le but étant de revenir dans la stack où l'on a copié notre .map ( et qui pourrait contenir un shellcode :)), c'est là qu'on se rapelle du fait que le gestionnaire d'exception prend en deuxième argument l'adresse de la structure SEH, qui se trouve donc être dans la stack à un endroit qu'on peut tout à fait écraser...

Une technique classique lors de ce genre d'exploitation consiste à mettre l'adresse d'une suite d'instructions : POP | POP | RET comme SEH Handler, on va donc ainsi dépiler l'adresse de retour, puis le premier argument et retourner sur le deuxième argument du handler ( le pointeur vers notre structure SEH dans la pile, c'est à dire le champ Next SEH ).
Une fois qu'on est revenu sur le Next SEH, on a à notre disposition les 4 octects de ce champ pour JUMPer sur un shellcode qu'on aura placé dans la pile...

III- Now it's time to do the payload

Essayons de tout mettre en place pour n'avoir plus que le shellcode à insérer dans notre fichier .map. Si on récapitule par un petit schéma de notre pile montrant les différentes étapes :



On met tout ça en place dans un fichier .map qui va bien en prenant comme adresse du POPPOPRET 00416CEB où l'on trouve :

00416CEB |. 59 POP ECX
00416CEC |. 59 POP ECX
00416CED \. C3 RETN

Le fait que ça soit POPer dans ECX a son importance comme nous le verrons tout à l'heure.

On lance le programme, l'exception se déroule et on regarde la tête de notre stack avant l'appel au gestionnaire d'exception (j'ai rempli tous les caractères inutiles par des 0x90 et mis des 0xCC dans le Next SEH) :

001265E4 90909090
001265E8 90909090
001265EC 90909090
001265F0 90909090
001265F4 CCCCCCCC Pointer to next SEH record
001265F8 00416CEB SE handler
001265FC 00000000
00126600 00000000
00126604 00000000

On laisse l'exception continuer... La main est donné au "gestionnaire" placé en 00416CEB qui double-POP et retourne à l'adresse 001265F4 pour s'interrompre ( instruction 0xCC). It's fun !

Mais on remarque quelque chose : après avoir copié mon adresse 00416CEB dans la pile (donc en partant du 0xEB pour finir par copier le 0x00), toute la suite a été mis à 0x00... Donc dès qu'on 0x00 est copié, tout ce qui suit est aussi mis à zéro !

Ca nous pose un problème : on ne peut pas utiliser l'espace mémoire situé après l'adresse du SE Handler pour mettre un shellcode ( on aurait pu atteindre ce shellcode en mettant un JMP 4 à la place du champ Next SEH et ainsi retomber après le champ SEH Handler ).
Donc on doit obligatoirement placer le shellcode avant la structure SEH et trouver un moyen, avec les 4 octets du champ Next SEH de "remonter" dans la pile.

Placer le shellcode plus haut ne pose aucun problème si ce n'est qu'il ne faut pas qu'il y ait de 0x00 dedans. Il faut maintenant JUMPer pour "remonter" au dessus. Pour celà, nous avons donc 4 octets à notre disposition. Ma première idée était de faire un JMP SHORT en négatif : on utilise des entiers signés donc un JMP 0xYY avec le premier bit de YY à 1 remontera dans la pile ( en fait descendra puisqu'elle croit vers les adresses décroissantes, mais bref vous avez compris l'idée ). Le problème est bien sûr évident : on ne peut "remonter" que à 127 octets à partir du JMP, donc ça limite grandement la taille de notre shellcode. Je n'y connais pas grand chose dans ce domaine mais en voyant la geule des générateurs automatiques, les shellcodes Windows semblent dépasser alègrement les 160 octets.
Donc il nous faut trouver un autre moyen pour remonter toujours plus haut (alllllerrrr plus hauuuut) in th3 st4ck.
C'est là qu'on se souvient du premier argument du SEH Handler qui est une adresse d'un code d'erreur mis dans la stack que l'on POP dans ECX au moment de notre POP|POP|RET. Essayons de manipuler cette adresse pour qu'elle tombe dans notre buff3r de la mort.
Pour cela, un petit rappel sur les registres :
si ECX=00123456
alors CX = 3456
CH = 34
CL = 56

La valeur qu'on a mis dans ECX ( une adresse sur la stack ) est de la forme 0012XXYY et rappelons nous que le fichier copié dans la stack est décris par une plage d'adresse de la forme 0012ZZYY et est d'une très grosse taille ( plus de 11k octets ). Donc il serait intéressant de manipuler CH pour transformer XX en ZZ et caler un JMP ECX derrière qui nous amènerait en plein milieu de notre buffer. Ceci est tout à fait faisable sur 4 octets puisque MOV CH,ZZ est sur 2 octets et JMP ECX de même.
Dans mon cas, le paramètre POPer dans ECX est 00123314 ( l'adresse où se trouve le code d'erreur de l'exception ) et mon fichier copié dans la stack commence à l'adresse 00123A64, il me suffit par exemple de faire un MOV CH,3B ( B5 3B en ASM x86 ) et je me retrouve avec ECX qui contient l'adresse 00123B14 qui pointe inside my buff3r. Je rajoute derrière un JMP ECX ( FF E1 ) et en 4 octets me voilà en position de force pour exécuter un shellcode :-)
Pour récapituler, ma stack ressemble finalement à :

00123A64 90909090 <- Début de la copie de mon file
00123A68 90909090
. . .
00123B14
90909090 <- Pointé par ECX après MOV CH,3B
. . .
001265E8 shellcode
001265EC from
001265F0 the h3ll
001265F4 E1FF3BB5 Pointer to next SEH record
001265F8 00416CEB SE handler
001265FC 00000000
00126600 00000000

J'ai transformé tous mes caractères caca par des NOPs (0x90) histoire de pas me prendre la tête avec les adresses, là je peux remonter très haut à partir de ECX et caler un shellcode de plus de 10k octets sans problème.

Pour conclure, on voit que les SEH c'est de la boulette pour se faire un BOF qui pwne, mais il faut bien avoué que si le tag /SafeSEH avait été mis à la compilation du tool, cette technique ne marcherait pas ( impossible de mettre l'adresse d'un POP|POP|RET en SEH Handler ).
Il doit aussi sûrement exister d'autres manières que le MOV CH,XX JMP ECX pour 'remonter' dans notre buffer.

Et pour pas vous vendre du vent, voici un fichier crafté avec un shellcode bien foireux made in Metasploit : il vous lance un processus cmd.exe mais vous ne le verrez pas puisqu'il appelle WinExec avec un paramètre mis à 0 au lieu de 1, et comme le shellcode est polymorphique et que j'y connais (pour le moment ! :=D) pas grand chose dans ce domaine, je vois pas bien ce qu'il faut modifier. Donc vous pouvez checker que ça marche dans le sens où le processus est bien créé :-) ( ou remplacer le shellcode par un qui marche mieux ...)

Je peux pas vous mettre directement le fichier .map ( la copie du 0x00 ne passe pas ;-X) mais je vous donne le dump hexadécimal du file, vous permettant de le générer vous même avec les outils qui vont bien pour ça ( python? :p ) :

map.txt

Special thanks to 0vercl0k and Ivan x-D

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).


jeudi 12 juin 2008

GCC is protecting you...

Pour un newbie qui veut devenir une pop-star du hack, les "buffers overflows" (BOF), techniques ancestrales, constituent une cible de choix car très documenté et assez facile à réaliser. Malheureusement pour lui, le jeune wanabee va très vite s'apercevoir que les buffers overflows ça marche plus comme dans les tutoriaux de grand-papa.

Dans ce post je vais m'efforcer de décrire les protections de GCC 4 ( donc la version actuelle ) concernant les "stack-overflows", la variante la plus abordable des BOFs, histoire de mieux comprendre was passiert.

Il est supposé que celui qui lit ça connait le principe d'un stack overflow, si ce n'est pas le cas, il peut se mettre un doigt dans le cul car je ne vais pas expliquer quelque chose que d'autres ont déjà décrit bien mieux que moi.

I- GCC has a protector...

Et son nom est le Stack Smashing Protector (SSP) dont le but est simple : tout d'abord protèger la cible principale d'un stack overflow c'est à dire l'adresse de retour empilé au moment d'un appel de fonction. Mais aussi une deuxième cible potentielle : la sauvegarde du Base Pointer qui est effectué par le prologue de la fonction ( pour éviter les attaques du type "Off by one"), et la troisième cible possible : les variables locales de la fonction qui pourraient également être écrasées par un buffer. Finalement, il existe une quatrième et dernière cible : les arguments de la fonction ( qui pourrait être écrasés pour faire de multiple "Ret-onto-ret" par exemple ).

Pour réaliser ces 4 objectifs, le SSP va réaliser trois actions particulières en intervenant directement au niveau du code assembleur produit par GCC : l'ajout de canary, la réorganisation des variables locales et la sauvegarde des arguments des fonctions.

Avant d'entrer plus dans les détails, remarquons que le SSP est activé par défaut dans GCC 4 et que vous pouvez le désactiver en compilant votre programme avec l'option -fno-stack-protector, ce qui permet de très vite comprendre les différences.

II- Cui-cui

SSP, notre Superman, va, pour protéger le Saved EBP et le Saved EIP, rajouter un "canary". Le principe est simple : pour éviter que l'on puisse venir à partir d'un buffer ( situé plus bas dans la pile ) écraser ses deux sauvegardes, on va placer une constante aléatoire avant de placer les variables locales à la fonction :



Le principe théorique du canary est alors facile à comprendre : on suppose que le programme a la possibilité de connaitre de façon certaine la valeur du canary (en en plaçant une copie dans un endroit inaccessible en écriture) et que l'utilisateur lui ne peut pas la deviner (car elle est aléatoire). Il suffit alors au programme de vérifier à la fin de la fonction que c'est bien la même valeur.

En pratique, le fonctionnement des canary est différent avec SSP, puisque d'après la documentation, il y a des cas où le programme n'est pas capable de générer une valeur aléatoire pour mettre dans son canary. Dans ce cas-là il se rabat sur une valeur fixe : 0xff0a0000 ( en big-endian). Et c'est là que tout un tas de questions up dans ton mind !

Pourquoi le programme ne pourrait pas générer une valeur aléatoire ? Alors là c'est mystère et boule de gommes, cette valeur est normalement générer avec /dev/urandom et je vois pas bien pourquoi il y aurait un problème d'accès. En tout cas ce qui est sûr c'est qu'après pas mal de test, cette valeur n'est jamais aléatoire chez moi, et avec le manque de clarté des diverses docs là dessus je suis toujours à la recherche de la réponse. Donc notre canary contient toujours la valeur 0xff0a0000.

Mais si le canary à toujours la même valeur, pourquoi qu'on le remplace pas par cette valeur au moment de l'overflow et ni vu ni connu jt'embrouille ?!
Tu aura remarqué que la valeur du canary parait avoir été choisie : elle contient les caractères 0x00 ( terminaison d'une string ) et 0x0a c'est à dire "\n" en ASCII (terminaison pour la fonction gets() ) qui vont donc rendre impossible tout buffer overflow par les fonctions de manipulation de string : quand tu va vouloir copier ta chaine de caractère pour tout écraser tu ne pourra pas remplacer le canary par sa valeur car ces caractères particuliers constituent des fins de string et tu n'ira donc pas plus loin.
Mais là, une boulette apparait : si le buffer overflow n'est pas déclenché par une fonction de manipulation de string "classique" mais par une fonction encore plus foireuse "faite maison" ou directement en manipulant la mémoire avec memcpy() ? Ben là tu pourra tout à fait réécrire le canary...

Et les performances dans tout ça ?
Les canary ne sont rajoutés que dans les fonctions "où ça en vaut la peine", c'est à dire dès qu'un tableau de char de taille au moins égale à 8 est déclaré dans une fonction, ce qui limite l'atteinte aux performances.

En pratique ça ressemble à quoi ?
Pour celà, codons nous un exemple bateau de fonction "vulnérable" à un stack overflow :


void function(char * src)
{
char buffer[10];
strcpy(buff,src);
}

int main(int argc, char * argv[])
{
function(argv[1]);
return 0;
}

Il est important de remarquer que nous ne occupons pas du cas où la faille apparait directement dans le main() car dans GCC 4 le prologue de cette fonction est différent de celui des autres ( j'y reviendrai dans un autre post, mais pour le moment j'ai pas tout compris ) et ça ne fait que compliquer les choses et ça n'a aucun intéret pour comprendre le fonctionnement des canarys.

Donc on prend notre courage à deux mains, on lance gdb sur le programme et on désassemble notre function() pour voir si on voit quelque chose qui concerne ces fameux oiseaux. Je vous présente ces lignes de codes ASM suivies de commentaires quand il y a besoin :

function+0 : push %ebp
function+1 : mov %esp,%ebp
function+3 : sub $0x18,%esp
Ceci est le prologue classique d'une fonction avec réservation de la place nécessaire aux variables locales...

function+6 : mov 0x8(%ebp),%eax
function+9 : mov %eax,-0x14(%ebp)
Ces deux lignes seront expliqués plus bas, elles ne concernent pas les canarys

function+12 : mov %gs:0x14,%eax
function+18 : mov %eax,-0x4(%ebp)
Tiens donc, sauvegarde d'une "valeur" contenue dans le registre GS juste après EBP xD

... <- Ici du code sans importance : appel a strpcy()

function+23 : mov -0x4(%ebp),%eax
function+26 : xor %gs:0x14,%eax
function+33 : je 0x80483bc
function+35 : call 0x80482fc <__stack_chk_fail@plt>
On récupère la valeur sauvegardée précédemment et on la compare à la valeur initiale contenue dans le registre GS. Si elle est égale, on se dirige vers l'épilogue de la fonction sinon on saute sur la routine __stack_chk_fail dont le nom est assez explicite :P

function+40 : leave
function+41 : ret
Epilogue classique d'une fonction.

Le fonctionnement parait assez clair : la valeur du canary est stocké dans le registre GS et on le met comme prévue juste après le Saved EBP ( pointé par EBP ). Dans le cas où il est écrasé on saute sur une routine qui doit sûrement déclencher un message d'erreur pas piqué des ours ( oué ça se dit ).

Confirmons ce fonctionnement en sautant à pied joint dans la mémoire lorsqu'on est dans function() :

(gdb) break function
(gdb) run aaaaaaaa

Et là, que vois t'on dans la pile ?
...
0xbffdff90: 0x61616161 0xff0a0000 0xbffdffb8 0x0804842f
...
Notre buffer a donc été rempli par les 'a' (0x61) que nous avons donné en argument et tout de suite après vient... notre canary ! Tout de suite suivie par le Saved EBP et le Saved EIP.
Tout semble au mieux dans le meilleur des mondes, notre canary est bien en place et va donc empêcher les écrasement classiques par des fonctions type strcpy(). Ce qu'on voit par un test :

$ ./prog aaaaaaaaaaaaaaaaaaaaa
*** stack smashing detected ***: ./prog terminated

Donc, nos deux premiers objectifs sont atteints !

III- Réorganisation des variables locales d'une fonction

Cette réorganisation pars d'un principe simple : un buffer ne peut écraser que les valeurs qui ont été empilées avant lui, SSP se charge donc de mettre les buffers le plus près possible de EBP ( c'est à dire en haut de la pile ) et de placer les autres variables locales en dessous. Si le buffer déborde ça sera vers le haut et il ne pourra donc pas les écraser.
Pour s'en convaincre compilons le petit code ci-dessous :

void function()
{
int c=3;
int d=4;
char buff[8];
}

int main(int argc, char * argv[])
{
function();
return 0;
}

On regarde la tête de la stack dans function() :

(gdb)x/16x $esp
...
0xbfaf4010: 0xb7ef4b19 0x00000004 0x00000003 0x080482c8
...
(gdb) x/x buff
0xbfaf401c: 0x080482c8

Notre buffer se trouve bien plus haut dans la pile que nos deux variables locales, malgré le fait qu'il soit déclaré avant les deux constantes, il a été empilé en premier.

IV- Recopie des arguments d'une fonction

Dernière protection apportée par SSP : la recopie de certains arguments des fonctions, en fait tous les pointeurs passés en argument sont recopiés dans le contexte de la fonction pour éviter de faire référence aux arguments. Let's test :

void function(char * src, char * src2, int * c)
{
char buff[8];
strcpy(buff,src);
}

int main(int argc, char * argv[])
{
int * d;
function(argv[1],"bbbbb",d);
return 0;
}

On saute avec gdb dans la stack de function() pour voir ce qui se passe :

(gdb) b function
(gdb) run
aaaaaaaa
function (src=0xbfcbd750 "aaaaaaaa", src2=0x8048510 "bbbbb", c=0xb7fe6dc0)
...

(gdb) x/16x $esp
0xbfcbb9a0: 0xbfcbb9bc 0xbfcbd750 0x00000000 0xb7fe6dc0
0xbfcbb9b0: 0x08048510 0xbfcbd750 0x00000000 0x61616161
0xbfcbb9c0: 0x61616161 0xff0a0000 0xbfcbb9f8 0x0804843c
0xbfcbb9d0: 0xbfcbd750 0x08048510 0xb7fe6dc0 0x080482ec

Voici donc que nos 3 adresses passées en arguments ont été recopiés dans le contexte de la fonction, avec bien sûr la réorganisation des variables locales elles se retrouvent plus haut que le buffer.
Pour finir de s'en convaincre, on regarde le code :

(gdb) disassemble function
...
prologue classique
...
function+6 : mov 0x8(%ebp),%eax
function+9 : mov %eax,-0x14(%ebp)
function+12: mov 0xc(%ebp),%eax
function+15: mov %eax,-0x18(%ebp)
function+18: mov 0x10(%ebp),%eax
function+21: mov %eax,-0x1c(%ebp)
...
mise en place du canary et suite de la fonction
...

Bref, rien de bien compliqué de ce côté là.

Pour conclure, j'espère que ce post t'aura permis de comprendre le fonctionnement de SSP. Maintenant on peut réfléchir à des attaques possibles contre les diverses protections misent en place :-)