Rédacteur : Heurs
Date de création : 14/09/2007
Section : Sécurité > Failles applicatives
Imprimer cet article : en noir et blanc ou en couleurs
Je profite d'avoir Visual C++ 2005 Express sous la main pour étudier plus en détail les attaques par débordement de tampon quand le flag GS est activé (ce qui est le cas par défaut sous cet IDE). Bon, il ne faut pas se voiler la face, le security check (flag GS) apporte une bonne sécurité au programme. Bien qu'en théorie les buffer overflow devaient finir par connaitre le meme sort que les dinosaures, ils sont toujours présents et nous allons voir comment ils ont survécu...
Nous avons déja parlé à plusieurs reprise des débordements de tampon, vous devriez donc commencer à bien maitriser le sujet. Mais une petite remémoration ne fait jamais de mal.
Un petit schéma de la pile :
Quand notre buffer va déborder, il va écraser des variables, puis la sauvegarde d'EBP, puis celle d'EIP, et enfin celle des arguments passés à la fonction.
Ce que nous faisions avant c'était de placer un shellcode dans le buffer, et écraser la sauvegarde d'EIP pour la faire pointer sur notre buffer afin de rediriger le flux d'execution vers notre shellcode. Rien qu'avec ça, on en bavait parfois (car généralement, il faut éviter certains opcodes notamment les \x00).
Le flag GS est une option de Visual C++ qui a pour effet d'apporter un "cannary". Ce mécanisme place deux entiers, un placé juste aprés les variables locales, et l'autre directement stocké dans la section .data de l'executable. L'un des deux entiers est généré aléatoirement, et l'autre reçoit une copie de la valeur du premier. On appelle généralement "cannary" le 1er entier, celui placé sur la pile.
Reprenons le cas d'un buffer overflow classique. Les données placées dans le buffer vont aller écraser toutes les données jusqu'à EIP, et écraseront donc le cannary.
Juste avant d'effectuer le "ret" final, le Security Check va vérifier si les deux valeurs des entiers sont les mêmes. Si c'est le cas, cela veut dire qu'il n'y a pas eut de débordement. Dans le cas contraire il terminera le processus. Voici le code C++ du programme :
Le code assembleur de la fonction main maintenant : (c'est un petit programme de présentation)
Nous voyons bien qu'après le strcpy(), CALL
La comparaison a lieu avec une adresse statique ; si les deux valeurs sont égales on retourne dans le main. Dans le cas contraire on saute à l'adresse 00401070 :
Bon il n'y a pas grand chose à commenter... suivons le jump en mémoire.
Je vous fait grace du reversing, on va juste constater que la fonction TerminateProcess sera appelée pour killer le processus courent. Donc ironiquement, l'épilogue ne sert à rien (LEAVE et RETN).
Voilà : c'est ca que nous a collé Bilibilou pour nous punir de trouver de l'overflow partout. Et comme nous pouvons le voir le système a l'air parfait.
Le SEH est comme son nom l'indique une structure pour gérer les handle d'exceptions. Concrètement, cela veut dire que quand une exception sera déclanchée, le kernel l'enverra à la fonction KiUserExceptionDispatcher() contenue dans ntdll.dll. Cette fonction va récupérer un pointeur vers le dernier handle afin de sauter dessus.
Un SEH se compose de deux entiers stockés sur la pile. la structure est la suivant :
Ce sont deux pointeurs, le premier vers la structure SEH suivente, et le deuxième vers le code à exécuter en cas d'exeption. De cette façon, les structures SEH forment une liste chainée. On pourrait les représenter de la façon suivante :
Le next_seh de la dernière structure contient un 0xffffffff (-1) et le dernier SEH à etre empilé est pointé par fs[0]. Lors de notre execution nous pouvons apercevoir cette structure sur la pile :
Et les registres sont les suivants :
Nous voyons que FS pointe sur 0x7FFDD000. Et à cette adresse nous trouvons la valeur suivante :
Et l'adresse 0x0013FFB0 est bien l'adresse de notre dernier SEH empilé. Bon, je pense qu'on a fait un bon petit tour de ce qu'est le SEH, on va maintenant pouvoir en tirer partie !
Comme nous l'avons vu, si la fonction de check des cannarys est appelée, le processus sera killé et notre exploitation sera foirée. Si on regarde bien, on peut voir que le SEH se trouve en dessous de notre buffer de la fonction main. Ca veut dire que l'on peut tout à fait ecraser le SEH. On va donc essayer de modifier la valeur du dernier seh_handle empilé avec une adresse bidon afin de voir qi on peut (quand une exception est générée) rediriger le flux vers une adresse arbitraire. J'ai donc réécrie un bout de ma pile comme ceci :
Puis, à l'instruction :
Nous allons placer 0 à la place de EDX. Ainsi une exception sera déclanchée. Si on passe la gestion de l'exception à l'utilisateur et qu'on débugge on peut s'appercevoir qu'à un momment nous arrivons sur l'instruction suivante :
Regardons les registres maintenant :
On va donc sauter sur un beau 0xAAAAAAAA !
Le plus dur sera à présent de provoquer une exception avant que la fonction ne se termine... Dans mon exemple, comme la pile occupée est petite on peut mettre un argument plus grand que la taille de stockage de la pile. De cette facon le programme voudra écrire à une adresse non mappée et dans ce cas une exception sera déclenchée. Généralement, lors d'exploitations de BoF nous avons plusieurs endroits où nous pouvons faire planter le programme, et donc on peut toujours réussir à rediriger le flux :-)
Je sais certains vont se plaindre qu'aucun exploit n'a été codé, mais les exploitations génériques sous Windows sont loin d'etre simple, autant niveau shellcode que des adresses de retour. Ici nous avons vu que malgrès une Security Check, l'adresse de retour a simplement été déplacée. En ce qui concerne l'exploitation j'écrirais un article sur comment exploiter de facon générique les BoF sur Windows. A suivre...