Dans la partie précédente, nous avions vu comment se passait un appel de fonction sous x86 dans un cas simple, puis, une fois le décor planté, comment celui-ci était modifié pour permettre la protection de la pile face à certaines attaques. Dans cette deuxième partie, nous verrons dans un premier temps comment utiliser le SSP sous GCC, et dans un second temps comment le déployer dans un environnement from scratch, comme par exemple un bootloader écrit en C ou en C++.

Tout d'abord, il est nécessaire de savoir comment le SSP est ajouté à votre programme. Cette opération à en effet besoin de deux éléments.

Vous devez avoir un compilateur à disposition qui supporte l'ajout du canary dans la convention d'appel. Cela tombe bien, GCC le fait plus ou moins depuis les version 3.X et la fonction est considérée comme mature sur les versions du moment. Pour ceux qui ont d'autres compilateur sous la main, il est possible que celui-ci ne le supporte pas. Rassurez-vous, le SSP existe aussi sous Visual Studio mais la suite ne marchera pas.

Pour ceux qui ont bonne mémoire, nous faisions référence à une adresse mémoire et une fonction précise à la fin de l'article précédent. Ceci est fourni par notre second élément, une librairie, à savoir libssp. L'installation de celle-ci diffère selon les distributions, mais elle est généralement fournie de base par la glibc.

La suite est relativement simple. Lors de la compilation, GCC injecte le code du SSP, puis les références aux symboles supplémentaires sont résolues par l'ajout de la libssp à l'étape d'édition des liens.

Pour la pratique, testons avec un simple programme écrit en C que tout le monde devrait connaître.

#include "stdio.h"
#include "string.h"
 
int hello (const char* arg)
{
  char str[20];
 
  strcpy (str, arg);
  printf ("%s", str);
 
  return 0;
}
 
int main (int argc, char* argv[])
{
  hello ("Hello World!\n");
 
  return 0;
}

Pour la compilation, nous supprimons les optimisations à l'aide du flag -O0 pour y voir un peu plus clair par la suite.

Ajouter le SSP à la compilation est relativement simple à l'aide de différents flags.

  • -fstack-protector permet d'activer le SSP pour les fonctions à risque.
  • -fstack-protector-all permet d'activer le SSP pour toutes les fonctions.
  • -Wstack-protector affiche des avertissements pour les fonctions qui ne seront pas protégées dans le premier cas.
  • --param ssp-buffer-size=<value> permet dans le premier cas de spécifier la taille minimale d'un tampon pour que celui-ci soit protégé.

Nous compilons l'exemple précédent de la manière suivante:

#~ gcc -O0 -fstack-protector -Wstack-protector -o main main.c

Si jamais GCC vous informe que la fonction n'a pas été protégée, vous pouvez le forcer avec -fstack-protector-all.

On vérifie le bon résultat à l'aide de objdump

#~ objdump -x main | grep "__stack_chk_fail"
00000000       F *UND*  00000000              __stack_chk_fail@@GLIBC_2.4

Je laisse le soin de déclencher le SSP à ceux qui aimeraient connaître le résultat.

Reste la question de savoir comment utiliser le SSP lorsque l'on travaille from scratch. Si l'on tente de compiler avec, on obtient des erreurs correspondant à des symboles non-définis lors de l'édition des liens, en particulier __stack_chk_fail. En réalité, il ne faut pas non plus oublier un détail. En effet, il faut bien à un moment déclarer notre canary et l'initialiser.

La solution est beaucoup plus simple qu'elle n'en a l'air, puisqu'il suffit de rajouter le code suivant à votre projet.

#ifdef __cplusplus
extern "C"
{
#endif //__cplusplus
 
void* __stack_chk_guard = NULL;
 
void __attribute ((constructor)) __stack_chk_guard_setup ()
{
  unsigned p = (unsigned) &__stack_chk_guard; //notre "canary"
  //initialisation du "canary"
}
 
void __attribute__((noreturn)) __stack_chk_fail ()
{
  //panique!
}
 
#ifdef __cplusplus
}
#endif //__cplusplus

De préférence, mettez pour le canary une valeur facile à identifier comme un nombre magique. Le compilateur se charge lui-même de le définir sous la forme de la variable __stack_chk_guard, et l'attribut constructor s'assure que __stack_chk_guard_setup est appelée avant le démarrage du programme. Enfin, mettez votre code de gestion d'erreur dans la fonction __stack_chk_fail, en prenant bien soin de noter que la pile n'est peut-être plus valide à ce moment là.

Nous avons du vu lors de cette seconde partie comment utiliser le SSP sous x86 lorsque celui-ci est disponible. Puis comment fournir un support minimaliste dans le cas contraire. Cette protection, couplée aux autres disponibles à l'heure actuelle, reste très efficace contre les erreurs de programmation en C et C++ qui peuvent provoquer des failles de sécurité. Cependant il est aussi possible de l'exploiter afin de déboguer des programmes développés dans des environnements très réduit comme des bootloaders.

partie I