|
Allocation de mémoire sécurisée en C et C++
7 Avril 2003
Par Haypo
Sommaire :
Le but de cet article est d'écrire un allocateur de mémoire sécurisé.
Par "sécurisé", j'entend qu'il devra être capable
de :
- Générer une erreur en cas d'échec d'allocation
de mémoire (plus assez de mémoire disponnible)
- Lister la mémoire (les pointeurs) non libérée
(libérés)
- Ajoute des informations à chaque pointeur alloué : nom
du fichier et ligne du fichier où le pointeur a été
alloué, taille du pointeur, signature (pour vérifier que
le pointeur soit valide), etc.
- Vérifier les écritures en dehors de la zone mémoire
allouée (avant : "underflow", et après : "overflow")
- Donner la possibilité d'activer ou désactiver les différentes
fonctions (à la compilation)
- Tout cela doit pouvoir s'appliquer sans modifier le code source existant
... Facile ! Ne vous inquiétez pas, je vais vous guider pas à
pas.
Cet article est basé sur une implémentation en langage C, avec
une extension pour le langage C++. Je pense qu'on peut adapter le principe à
d'autres langages, mais il faudra modifier le code source.
ATTENTION: Le but de nos modifications
ne visent non pas à rendre un programme plus sûr et inviolable,
mais à corriger des erreurs dans le code source d'un programme.
Vous vous en douterez sûrement, ces modifications ont un coût sur
les performances. C'est pour cela que j'ai rajouté le point (6) dans
notre cahier des charges : il faut pouvoir désactiver une ou plusieurs
(toutes) options. Je pense que le point (1) devrait toujours être actif
: écrire dans un pointeur NULL provoque une erreur de segmentation, quelque
soit le système d'exploitation !!!
NOTE: Rien ne sert de s'amuser à copier/coller le code source,
un exemple complet est fourni à la
fin de cette page.
Le langage C nous offre plusieurs fonctions d'allocation de mémoire
:
- malloc (n) : alloue n octets de mémoire
- calloc (n, taille) : alloue n*taille octets de mémoire
- realloc (ptr, n) : redimensionne le pointeur ptr pour qu'il
atteingne la taille de n octets (qui peut être inférieur
ou supérieur à l'ancienne taille de ptr)
- free (ptr) : libère la mémoire qui était allouée
à l'adresse ptr
Et il existe encore des fonctions dérivées comme strdup qui est
une combinaison de strlen (lit la longueur d'une chaîne de caractère),
malloc (allocation de mémoire) et strcpy (copie les données d'une
chaîne de caractères dans une autre).
Le langage C++ nous offres quatres opérateurs (et non plus fonctions)
:
- new objet : crée un objet
- new objet[n] : crée un tableau de n objet(s)
- delete ptr : efface un objet
- delete []ptr : efface un tableau d'objets
En plus d'allouer la mémoire, il va également appeler le "constructeur"
de chaque objet après l'allocation. Ceci est gêré automatiquement
par l'opérateur new.
On va commencer par redefinir les fonctions d'allocation de mémoire
en C. Pour cela, il faut commencer par réécrire les fonctions
en se basant sur le prototype standard du C :
void *malloc(size_t size);
void *realloc(void *block, size_t size);
void *calloc(size_t nitems, size_t size);
void free(void *block); |
Pour éviter les conflits de noms, on va les appeler : MallocSecurise,
ReallocSecurise, CallocSecurise et FreeSecurise. Il faut maintenant rajouter
automatiquement le nom du fichier et la ligne du fichier où le pointeur
a été alloué. Pour cela, le langage C nous offre deux macros
: __FILE__ (nom : const char *) et __LINE__ (ligne : unsigned int). Le problème
est de les passer automatiquement en paramètre sans modifier le code
source existant ... Heureusement, le langage C nous laisse la possibilité
d'écrire des macros (#define). On pourra donc utililiser :
// Prototypes
void *malloc(size_t size, const char* nomfich, unsigned int numligne);
void *realloc(void *block, size_t size, const char* nomfich, unsigned int
numligne);
void *calloc(size_t nitems, size_t size, const char* nomfich, unsigned int
numligne);
void free(void *block, const char* nomfich, unsigned int numligne); |
|
// Macros
#define malloc(size) MallocSecurise(size, __FILE__, __LINE__)
#define realloc(ptr,new_size) ReallocSecurise(ptr, new_size, __FILE__,
__LINE__)
#define calloc(n,size) CallocSecurise(n, size, __FILE__, __LINE__)
#define free(ptr) FreeSecurise(ptr, __FILE__, __LINE__)
|
Pour le langage C++, on va redéfinir :
void* operator new (size_t size) throw (std::bad_alloc);
void* operator new[] (size_t size) throw (std::bad_alloc);
void operator delete (void *) throw ();
void operator delete[] (void *ptr) throw (); |
Petit problème : on ne modifie plus des fonctions, mais des opérateurs.
De plus, ces opérateurs sont spéciaux car new doit appeler le
créateur de l'objet après l'avoir alloué. Pas question
donc de renommer ces opérateurs ! Après avoir fait des tests,
j'ai compris qu'on pouvait changer le nombre de paramètres de l'opérateur
new, mais pas de l'opérateur delete ... Cela rend l'ajout automatique
des paramètres encore un peu plus difficile. Il faut alors passer par
une variable globale pour l'opérateur delete. Pour l'opérateur
new, on va reprendre Ce qui va nous donner :
// Variables globales
extern const char *delete_FILE;
extern unsigned long delete_LINE; |
// Opérateurs
void* operator new (size_t size, const char* nomfich, unsigned int
numligne) throw (std::bad_alloc);
void* operator new[] (size_t size, const char* nomfich, unsigned int
numligne) throw (std::bad_alloc);
void operator delete (void *) throw ();
void operator delete[] (void *ptr) throw (); |
|
// Macro
#define new new (__FILE__, __LINE__)
#define delete delete_FILE=__FILE__, delete_LINE=__LINE__, delete
|
La macro "#define new new (__FILE__, __LINE__)" est un peu bizzare,
"new nombre;" devient "new (__FILE__, __LINE__) nombre;".
C'est spécial, mais le principal n'est pas que ça marche ?
Utiliser des virgules pour "#define delete ..." n'est pas très
beau, mais ça marche ! Exemple de code qui poserait problème avec
des points virgules :
| for (unsigned int i=0; i<nbr_element; i++) delete liste[i]; |
Ce qui donne :
| a) for (unsigned int i=0; i<nbr_element; i++) delete_FILE=__FILE__,
delete_LINE=__LINE__, delete liste[i]; |
| b) for (unsigned int i=0; i<nbr_element; i++) delete_FILE=__FILE__;
delete_LINE=__LINE__; delete liste[i]; |
Vous ne voyez vraiment pas l'erreur dans b) ? Je vais vous aider : le compilateur
bloque avec "symbole 'i' non défini" ... Note : le code
a) est tout à fait valide !
L'algorithme le plus adapté pour lister des pointeurs alloués
tout au long de l'exécution du programme est la liste doublement chaînée.
J'ai testé avec une liste simplement chaînée, mais il fallait
parcourir toute la liste pour supprimer un élément (pour corriger
l'élément précédent) : dès qu'on dépassait
1000 éléments, les performances chutaient rapidement ...
Pour les informations ajoutées à chaque pointeur, je vous propose
:
// Informations écrites avant les données du pointeur
typedef struct
{
unsigned long nbr_magique; // Nombre magique (signature)
bool est_alloue; // Pointeur alloué ou libéré ?
size_t taille; // Taille allouée (sans informations de débogage)
const char* nomfich; // Nom du fichier source
ulong numligne; // Ligne du fichier
struct PtrChaineMalloc *ptr_liste; // Pointeur dans la liste
unsigned long somme_info; // Somme des informations (checksum)
} InfoMalloc;
|
Le membre "somme_info" est la somme simple (sur, au moins, 32 bits)
des autres membres. Le membre "ptr_liste" est un pointeur vers l'élément
de la liste des pointeurs alloués. Je ne pense pas que les autres membres
ont besoin d'explications supplémentaires. Pour les pointeurs, je vous
propose cette structure :
// Un pointeur chaîné
struct PtrChaineMalloc
{
struct PtrChaineMalloc *precedent; // Elément suivant
struct PtrChaineMalloc *suivant; // Elément suivant
ulong nbr_magique; // Nombre magique (signature)
InfoMalloc *ptr_info; // Pointeur à libérer
};
|
On retrouve les membres "precedent" et "suivant"
du même type que la structure, typiques des listes doublement chaînées.
Le membre "nbr_magique" permet de vérifier que le pointeur
est bien un pointeur "valide".
Pour la structure de la liste elle-même, j'utilise :
// Le conteneur Malloc
struct
{
struct PtrChaineMalloc *debut; // Pointeur vers le premier élément
struct PtrChaineMalloc *fin; // Pointeur vers le dernier élément
} ConteneurMalloc = {NULL,NULL};
|
La liste est initialisée à {debut=NULL, fin=NULL} automatiquement
!
Nous aurons besoin de deux fonctions :
- Ajout d'un élément dans la liste
- Suppression d'un élément de la liste
Vocabulaire :
- Une erreur d'underflow (mot anglais) est une écriture en dehors
d'un tampon, avant la mémoire allouée
- Une erreur d'overflow (mot anglais) est une écriture en dehors
d'un tampon, après la mémoire allouée
Ces deux erreurs sont très dangeureuse, car dans la plupart des
ordinateurs le code source et les données sont placées dans
le même espace mémoire. On peut donc modifier le programme
en écrivant des instructions invalides. Dans le meilleur des cas,
la mémoire n'est pas utilisée, et le programme continue
comme si de ne rien n'était. Mais dans la plupard des cas, le programme
réagit très bizzarement (plantages divers), ou alors une
erreur de segmentation (SIGSEGV) est générée.
Il existe une méthode très simple à mettre en oeuvre
: on alloue quelques octets en plus, on initialise la mémoire ajoutée
en plus, enfin on vérifie que cette zone n'a pas été
modifiée. Pour l'initialisation, le mieux est d'écrire une
chaîne aléatoire, mais je préfère écrire
une chaîne simple (on part d'une valeur, puis on incrémente
à chaque itération). La taille de cette mémoire ajoutée
ne doit pas être trop importante car chaque pointeur prendra cette
taille en plus (ajoute 1024 octets sur 1024 pointeurs consomme "inutilement"
1 Mo ...). J'ai choisi 16 octets, mais 4 octets pourrait suffire ! Je
rappelle que le but de nos modifications ne visent pas à rendre
un programme inviolable, mais corriger des erreurs ...
Pour les erreurs d'underflow, la signature de nos informations
et la "checksum" (somme de vérification) vont s'occuper
de les détecter.
Je ne vais pas détailler tout le code, mais je vais vous présenter
les deux fonctions principales : malloc et free.
Notre fonction malloc :
// Taille du tampon anti-overflow (en octets)
#define TAILLE_TAMPON_OVERFLOW 16
// Nombres magiques (32 bits) pour valider un pointeur
#define MALLOC_NBR_MAGIQUE 0x1A71A25C
// Allocation de mémoire sécurisée (malloc)
void* MallocSecurise (size_t size, const char *nomfich, unsigned long numligne)
{
// Tente d'allouer le tampon
void* ptr;
size_t taille_allouee;
InfoMalloc *info;
char *overflow;
unsigned long i;
unsigned char rand = 0xA0;
// Taille nulle : Erreur!
if (size == 0) ERREUR ("Taille du pointeur à allouer nulle !");
// Ajoute la taille des informations sur le pointeur
taille_allouee = size +sizeof(InfoMalloc) +TAILLE_TAMPON_OVERFLOW;
// Alloue la mémoire (appelle le malloc du langage C)
ptr = malloc(taille_allouee);
// Pointeur NULL : erreur!
if (ptr == NULL)
ERREUR ("Allocation de mémoire impossible : pas assez de mémoire libre !");
// Ajoute le pointeur au conteneur
info = (InfoMalloc *)ptr;
CONTENEUR_MALLOC_AJOUTE (info);
// Ecrit les informations sur le pointeur
info -> nbr_magique = MALLOC_NBR_MAGIQUE;
info -> est_alloue = true;
info -> taille = taille_allouee;
info -> nom_fich = nomfich;
info -> num_ligne = numligne;
info -> somme_info = MallocCalculSomme (info);
// Prépare le tampon anti-overflow
overflow = (char *)info +taille_allouee;
for (i=0; i<TAILLE_TAMPON_OVERFLOW; i++) *ptr++ = rand++;
// Renvoie le pointeur vers l'espace alloué vierge
return (info+1);
}
// Calcule la somme de contrôle des informations d'un pointeur
ulong MallocCalculeSomme (const InfoMalloc *info)
{
// Calcule la somme des données d'un pointeur
return
((ulong)(info -> nbr_magique)
+(ulong)(info -> est_alloue)
+(ulong)(info -> taille)
+(ulong)(info -> est_local)
+(ulong)(info -> nom_fich)
+(ulong)(info -> num_ligne));
}
|
Notre fonction free :
// Fonction 'free' sécurisé
void FreeSecurise (const void *ptr,
const char* nomfich, const ulong numligne)
{
infoMalloc *info;
ulong somme;
// Pointeur NULL : La norme demande de ne rien faire, on va pas se gêner!
if (ptr == NULL) return;
// Lit les informations du pointeur
info = (InfoMalloc *)ptr-1;
//--- Vérifie les informations ---
if (info -> nbr_magique != MALLOC_NBR_MAGIQUE)
ERREUR ("Pointeur invalide !");
// Calcule la somme des données
if (info -> somme_info != MallocCalculeSomme (info))
ERREUR ("Somme de vérification incorrect (%s:%u) !",
info -> nom_fich, info -> num_ligne);
// Vérifie qu'on est pas écrit en dehors du tampon
if (!Malloc_TamponOverflowIntact(info))
ERREUR ("Erreur d'overflow (%s:%u) !",
info -> nom_fich, info -> num_ligne);
// Sort le pointeur du conteneur en vérifiant les
// informations sur le pointeur
CONTENEUR_MALLOC_EFFACE (info -> ptr_liste);
// Le pointeur n'est plus alloué ! (evite de libérer deux fois le même
// pointeur) Même si la mémoire est 'libérée', elle n'est pas effacée
// avec un 'free'.
info -> est_alloue = false;
// Enfin, libère la mémoire
free (info);
}
|
Notes :
- Le standard demande à ce que malloc(0) renvoie NULL, mais moi
je n'aime pas NULL, alors je génère une erreur !
- ERREUR (format, ...) est une fonction affichant un message d'erreur
(ayant la forme de printf) puis quittant le programme.
- CONTENEUR_MALLOC_AJOUTE (info) est une fonction qui ajoute
un pointeur dans la liste des pointeurs.
- CONTENEUR_MALLOC_EFFACE (info) est une fonction qui efface
un pointeur de la liste des pointeurs.
Bien sûr, cet exemple est (volontairement) simplifié. Il
est possible de mieux écrire le code, en particulier placer certaines
parties dans des fonctions.
Les autres fonctions se résument à :
- realloc : on utilise la fonction realloc du C, puis on retrouve
le pointeur dans la liste des pointeurs, et on corrige son adresse.
- calloc : ça se résume à appler notre malloc,
puis faire un memset.
- strdup : la bonne combinaison de strlen, (notre) malloc
et strcpy.
- new et new[] : appelle simplement notre malloc.
- delete et delete[] : appelle simplement notre free.
AVERTISSEMENT: Le code source proposé
est sous licence GPL, je
vous invite à la lire avant de réutiliser ce code !
Télécharger le code source sous licence GPL :
Je vous conseille également de télécharger
le code source de ma calculatrice, car vous y trouverez la dernière
version de ma librairie HAlloc (dans le répertoire "include"
du code source). D'ailleurs le code donné plus haut est une version
(très) simplifiée, même si elle est utilisable.
Revenir sur la page de Haypo
|