-ftrivial-auto-var-init : le prix du progrès

Magazine
Marque
MISC
Numéro
127
Mois de parution
mai 2023
Spécialité(s)


Résumé

En C et en C++, les variables déclarées sur la pile sans initialisation n’ont pas de valeur par défaut. Accéder à leur valeur est à la fois une source de comportement indéfini et une source de fuite de données reconnue. De telles situations sont complexes à détecter à la compilation et coûteuses à détecter à l’exécution. Cet article propose d’explorer une alternative disponible dans les compilateurs clang et gcc : -ftrivial-auto-var-init=<value>.


Body

Tout relecteur à l’œil un peu exercé saura trouver (au moins) une erreur dans le code suivant :

#include <stdio.h>
void scan_int(char const* data) {
    int value;
    sscanf(data, "%d", &value);
    printf("value=%d\n", value);
}

Et pourtant ni GCC ni Clang n’affichent d’avertissement à la compilation de cette fonction. Par contre si on l’appelle avec data positionné à "vive misc", on obtient un message d’erreur non équivoque lors d’une exécution dans Valgrind :

==658164== Use of uninitialised value of size 8
==658164==    at 0x48C398B: _itoa_word (_itoa.c:177)
==658164==    by 0x48DF574: __vfprintf_internal (vfprintf-internal.c:1516)
==658164==    by 0x48C9A2E: printf (printf.c:33)
==658164==    by 0x40116F: scan_int (a.c:7)

En effet, on se retrouve à afficher à l’écran une valeur lue depuis la pile, non initialisée, car c’est ce que permet le standard C et que c’est plus rapide de ne rien faire que de positionner une valeur par défaut.

On n’apprendra pas au lecteur de MISC l’impact potentiel d’un tel comportement : la valeur de value va dépendre de ce qui a été écrit sur la pile précédemment, fuitant ainsi de potentielles données importantes (stack cookie, clef ou mot de passe, adresse permettant de deviner la valeur de base utilisée par l’ASLR, etc.).

Ce problème est si fréquent dans les codes C que quelqu’un a eu l’idée assez triviale de forcer une valeur par défaut pour toute variable allouée sur la pile. C’est compatible avec le standard et cela réduit les fuites de données à travers la pile. Mais à quel prix ?

1. Bienvenue -ftrivial-auto-var-init=zero

L’idée parait simple : et si on forçait toutes les allocations sur la pile à zéro ? Après tout c’est déjà le comportement standard pour les variables globales et les variables statiques. Cette approche propose plusieurs avantages : elle est reproductible, elle est rapide et elle est facile à implémenter. En effet, cela revient à écrire dans le code mentionné en introduction :

#include <stdio.h>
void scan_int(char const* data) {
    int value = 0;
    sscanf(data, "%d", &value);
    printf("value=%d\n", value);
}

Et cela suffit à satisfaire Valgrind. Notons néanmoins que le code n’est pas devenu correct (il faudrait vérifier la valeur de retour de sscanf), mais au moins il ne fuite plus d’information.

Cette approche a la bonne idée de parfois avoir un coût nul. Prenons en exemple le code suivant :

#include <string.h>
int len(char const* data) {
    int value = 0;
    if (data) value = strlen(value);
    else value = 0;
    return value;
}

Un compilateur avec les optimisations adéquates (-O1 suffit pour Clang et GCC) saura supprimer l’assignation value = 0, puisque tous les chemins menant à la lecture de value passent par une autre écriture que cette initialisation. Malheureusement, ce n’est pas le cas en général, même quand l’initialisation est inutile (penser par exemple aux conséquences d’un quelconque appel à une fonction définie dans une autre bibliothèque).

Mais cette approche est-elle suffisante ? Il se trouve qu’elle a plusieurs défauts.

Premièrement pour des objets de grande taille alloués sur la pile, l’initialisation s’avère coûteuse. Et de telles initialisations sont plus fréquentes qu’on le penserait : un petit buffer char buffer[256] ou une structure standard de type struct statbuf suffisent.

Deuxièmement, forcer la valeur à zéro introduit un comportement non standard sur lequel on peut avoir envie de se reposer. On pourrait alors voir apparaître une « simplification » du code précédent :

#include <stdio.h>
int len(char const* data) {
    int value;
    if (data) value = strlen(value);
    return value;
}

Qui se comporterait comme la version originale, mais seulement si on utilise le bon drapeau de compilation, ce qui n’est pas franchement une bonne idée.

Troisièmement, la valeur zéro ayant souvent un sens, elle risque de faire en sorte que le programme tombe en marche. Un pointeur à zéro, un flottant à zéro, un entier à zéro, autant de cas qui peuvent être traités normalement par le programme sans déclencher d’alerte.

2. Bienvenue -ftrivial-auto-var-init=pattern

À défaut d’utiliser la valeur zéro, on peut utiliser un motif particulier, qui a de fortes chances de déclencher un comportement inattendu. L’auteur original du patch dans clang pour cette option propose astucieusement le « screaming pattern », à base de 0xAAAAAAAA. C’est une valeur entière incongrue. Si on la préfixe de 0xFFF on obtient un « screaming not a number » flottant (qui a la bonne idée de se propager à travers les opérations arithmétiques, et donc de faire surface plus facilement). Sur 64 bits, cela fait une adresse mémoire généralement non adressable (à moins d’avoir une mémoire de plus de 1e19 octets), etc.

Notons cependant que Linus Torvald préfère l’initialisation à zéro [1].

Niveau implémentation, le compilateur génère systématiquement un appel à memset après chaque allocation, et cet appel est ensuite soit supprimé (meilleur des cas), soit transformé en un ou plusieurs chargements mémoires explicites (cas des petites allocations de taille fixe), soit conservé (cas des grosses allocations ou des allocations à taille dynamique, i.e. appels à alloca ou allocation de variable length array).

3. Bienvenue __attribute__((uninitialized))

Si l’on accepte de passer à -ftrivial-auto-var-init=pattern, on peut vouloir contrôler finement la génération des initialisations, afin d’éviter le surcoût de l’initialisation quand on sait que l’initialisation n’est pas nécessaire, même si le compilateur n’arrive pas à le prouver. Ainsi, un peu à la manière du marqueur unsafe que l’on retrouve en Rust, on peut se contenter d’auditer le code en se concentrant sur les zones marquées explicitement.

Cela se traduit par un attribut porté par des variables initialisées sur la pile : __attribute__((uninitialized)) ou (en C++) [[clang::uninitialized]]. Cette approche est incrémentale, portable et permet d’avoir le meilleur des deux mondes.

Par exemple, dans le cas suivant le compilateur ne sait pas que lstat va correctement initialiser la variable sb si la valeur de retour est différente de -1, mais après une relecture minutieuse du code, le développeur peut se permettre de forcer la non-initialisation de cette variable :

#include <sys/stat.h>
int main(int argc, char ** argv) {
    __attribute__((uninitialized)) struct stat sb;
if (lstat(argv[1], &sb) == -1) {
    perror("lstat");
    exit(EXIT_FAILURE);
}
/* do stuff with sb */
return 0;
}

4. Vers une évolution du standard ?

L’auteur du patch dans clang/LLVM a récemment (novembre 2022) proposé une évolution du standard C++ [2] pour harmoniser l’initialisation des variables allouées sur la pile avec celles allouées de manière globale : tout à zéro. Cette proposition suggère également la normalisation d’une porte de sortie à travers l’attribut [[uninitialized]].

Prévoyant une levée de boucliers pour des raisons de performance, en vertu du principe qu’on ne paie que pour ce que l’on demande, l’auteur de la proposition avance que d’une, les performances des options actuelles sont déjà très bonnes en pratique, et que de deux, les compilateurs ont encore une belle marge de manœuvre dans leur capacité à supprimer les initialisations inutiles, avec nombre patch à l’appui.

On pourrait rétorquer que la situation actuelle est déjà satisfaisante, et que si l’on veut explicitement initialiser une variable à zéro, un appel à memset suffit. En termes de posture, l’auteur défend une approche sécurisée par défaut, dont on peut se désengager localement pour des raisons de performance, alors que l’état actuel est une approche rapide par défaut dont on peut se désengager localement pour des raisons de sécurité.

Conclusion

L’utilisation de -ftrivial-auto-var-init=pattern combinée à __attribute__((uninitialized)) permet au développeur soucieux d’éviter les fuites de données à travers la lecture de variables de pile non initialisées. Le surcoût est généralement négligeable et quand ce n’est pas le cas, on peut le contrôler finement. Cette option est utilisée par défaut dans le noyau Linux.

Références

[0] RFC sur la liste de discussion de clang/llvm :
https://lists.llvm.org/pipermail/cfe-dev/2018-November/060172.html

[1] Fil de discussion sur le sujet sur la liste du noyau : https://lkml.org/lkml/2019/7/28/194

[2] Proposition d’évolution du standard C++ : https://isocpp.org/files/papers/P2723R0.html



Article rédigé par

Par le(s) même(s) auteur(s)

Des soucis à la chaîne

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

L’histoire, ou plutôt l’Histoire, est une coquine. Et quand Dennis Ritchie et Ken Thompson inventent le langage C en 1972, ils prennent une décision anodine, une micro-optimisation qui fait gagner quelques octets, mais qui aura un impact important sur la sécurité de nombreux systèmes : en C, les chaînes de caractères sont terminées par un octet positionné à zéro.

Édito

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Résumé

En regardant la liste des 25 failles les plus dangereuses éditées par MITRE chaque année, on ne peut qu’être frappé par la présence (ou la persistance) de thèmes bien connus : écriture illégale dans une zone mémoire, utilisation d’une zone mémoire désallouée, lecture illégale d’une zone mémoire, déréférencement de pointeur NULL, dépassement de la capacité d’un entier… Autant de sujets qui sont pourtant abordés dans les premiers chapitres de tout bouquin traitant de la sécurité logicielle. Ce qui n’en fait pas pour autant des sujets faciles dès lors que les considérations de base de code existant et de performances rentrent en compte. C’est compliqué l’optimisation multicritère !

Smash Bros

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

En 1996, Aleph One publiait dans l’e-zine Phrack un article intitulé « Smashing the Stack for Fun and Profit ». C’était il y a plus de 25 ans et les principes énoncés dans cet article sont toujours valides, même si leur exploitation est devenue plus technique.De manière plus conventionnelle, l’écriture dans une zone mémoire non-autorisée est un vecteur d’attaque classique connu sous le doux nom de CWE-787.Dans cet article, on se concentrera sur les attaques ciblant la pile, en détaillant quelques bugs classiques, leur exploitation historique et quelques contre-mesures qui ont été mises en place au fil du temps.

Les derniers articles Premiums

Les derniers articles Premium

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Bash des temps modernes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les scripts Shell, et Bash spécifiquement, demeurent un standard, de facto, de notre industrie. Ils forment un composant primordial de toute distribution Linux, mais c’est aussi un outil de prédilection pour implémenter de nombreuses tâches d’automatisation, en particulier dans le « Cloud », par eux-mêmes ou conjointement à des solutions telles que Ansible. Pour toutes ces raisons et bien d’autres encore, savoir les concevoir de manière robuste et idempotente est crucial.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 68 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous