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

9 commentaires:

Anonyme a dit…

Sympa gcc, mais ton titi "aléatoire mais pas trop" me titille, t'a test sur plusieurs configs pour voir si ça change ? J'y connais rien mais faut bien inaugurer un peu, d'ailleurs tu fais une pendaison de crémaillère pour l'ouverture de ton blog ? Ca se fête mec (avec ptit jaune au rdv bien sûr) ! En tout cas c'est interessant, continue.

JoE a dit…

Nop, je n'ai pu tester que sur mon Ubuntu avec kernel 2.6.24 et GCC 4.2.3,.. que je soit en root ou user ou que je mette un doigt dans le cul, il ne prend pas de random canarys, mais je vais continuer à chercher de ce côté là pour voir si je trouve quelque chose, la réponse doit pas être bien compliqué ;)
Oué on pendra la crémaillère ( le we prochain ? :-D )

Anonyme a dit…

'mov $gs:0x14, $eax' sauve le canary stocke a l'offset 0x14 du segment gs. Le segment gs ne demarre pas a l'adresse 0 evidemment, mais pointe sur TCB du thread courant je crois (l'equivalent de fs sous Windows). La question est donc de savoir a quoi est initialise $gs:0x14 initialement... est ce random? est-ce un dword inutilise et sette par gcc a 000aff0d? no idea...

Anonyme a dit…

Oui mais un canary c'est quand même petit : CTB et non pas TCB...faut pas tout confondre les jeunes !
Ahlala

Anonyme a dit…

Ils ont voulu nous faire renoncer aux Bof avec leur soit-disant random inaccesible, bluff 0xff0a0000 is it random ?

Anonyme a dit…

Salut, je sais bien que cet article date un peu, mais pour info 0xFF0A0000 est la valeur d'un canary statique utilisé par SSP lorsque la glibc n'est pas compilé avec l'option --enable-stackguard-randomization. En effet, le canary est stocké dans le TLS du thread, géré par la glibc, et c'est donc cette dernière qui génère l'aléa. Pour info, les glibcs de beaucoup de distribs sont compilées sans cette option, Cf. http://osdir.com/ml/debian-glibc/2009-01/msg00027.html, donc OxFF0A0000 a de grandes chances d'être un pass-partout sur beaucoup de machines !

JoE a dit…

Merci Anonyme d'éclairer nos lanternes ;-)

DM a dit…

0xFF0a0000 n'est pas choisi au hasard: 0x00 est un terminateur de chaîne, 0x0a aussi (pour gets, par exemple), et 0xFF éventuellement aussi (peut être considéré comme EOF).

Autrement dit, il est impossible de passer cette valeur via un buffer-overflow sur la plupart des fonctions de manipulations de chaîne standard. Évidemment, cela ne concerne pas les manipulations de tableaux à la main, ou les memcpy.

JoE a dit…

Exact, mais ça c'est déjà marqué dans le post :)