Les options de compilation : Fun with Flags

Magazine
Marque
MISC
Numéro
121
Mois de parution
mai 2022
Spécialité(s)


Résumé

Machines et développeurs ont parfois du mal à communiquer. D’un côté l’esprit humain, avec sa volonté d’abstraire et de penser fonctionnel. De l’autre côté, la machine et sa froide rigueur électronique. Entre les deux, il y a un pont : le compilateur. Mais comment prend-il en compte les aspects sécurité ?


Body

Soyons honnêtes : quand il compile un code en langage C, quel que soit le niveau d’optimisation demandé, un compilateur comme Clang ne prend pas en compte les aspects liés à la sécurité informatique. Son seul objectif est de traduire le code source en code machine, en essayant de générer un code qui exploitera au mieux les capacités de calcul de la machine (-O2), parfois sous contrainte de taille de binaire (-Os) ou de facilité de debug (-g). Voilà qui laisse peu de place à la sécurité, voire qui offre plein de vecteurs d’attaque potentiels. Supprimer un memset qui remet à zéro une partie de la pile avant de quitter une fonction ? Aucun problème ! Utiliser une valeur de la pile non initialisée ? Aucun problème ! Écrire au-delà des bornes d’un tableau ? Aucun problème ! À coup d’undefined behavior, le compilateur s’autorise toutes les astuces lui permettant une génération de code efficace.

Fort heureusement, parmi la foultitude d’options supportées par Clang, plusieurs concernent l’amélioration de la sécurité, mais encore faut-il les connaître ! C’est donc un petit tour d’horizon que nous allons faire à travers cet article, en nous intéressant à quatre catégories d’options : les warnings, les sanitizers, le support matériel et... les autres :-).

1. Warnings : -Wsomething

Les compilateurs proposent un ensemble de drapeaux qui permettent de détecter de potentiels problèmes (accès hors des bornes, utilisation dangereuse d’un printf…) sans empêcher la génération de code : ce sont les avertissements que l’on trouve sous la forme -Wwarning-name. Plusieurs avertissements ont trait à la sécurité. On retiendra :

  • -Wformat-security : affiche un message d’avertissement en cas d’usage de fonction de formatage avec une chaîne de formatage non littérale. Par exemple :
a.c:3:10: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
  printf(fmt);
         ^~~
a.c:3:10: note: treat the string as an argument to avoid this
  printf(fmt);
         ^
         "%s",
1 warning generated.
  • Ce genre d’appel permettrait, à travers l’usage du format %n, d’écrire une valeur arbitraire sur la pile. On note la suggestion faite par Clang pour corriger l’avertissement.
  • -Warray-bounds : détecte certains accès de tableau hors des bornes. Tous les accès invalides ne sont pas détectés, sans surprise la présence d’informations statiques aide grandement le compilateur.
a.c:5:3: warning: array index 8 is past the end of the array (which contains 8 elements) [-Warray-bounds]
  buffer[8] = 0;
  ^      ~
a.c:4:3: note: array 'buffer' declared here
  char buffer[8];
  • Cet accès écrit au-delà des bornes d’un tableau déclaré ici sur la pile, ce qui conduit à une modification d’autres valeurs de la pile.
  • -Warray-bounds-pointer-arithmetic : cette option est similaire à -Warray-bounds et détecte certaines références à des zones mémoire non adressables.
<source>:4:9: warning: the pointer incremented by 10 refers past the end of the array (that contains 8 elements) [-Warray-bounds-pointer-arithmetic]
    foo(buffer + 10);
        ^        ~~
<source>:3:5: note: array 'buffer' declared here
    char buffer[8];
  • Si la fonction foo attend en entrée un pointeur sur une zone valide, elle risque là encore de modifier d’autres valeurs de la pile.
  • -Walloca et -Wvla : ces deux options sont similaires, car elles détectent l’usage de mécanismes permettant d’allouer dynamiquement de la mémoire sur la pile, soit à travers un appel à la fonction alloca définie dans le fichier d’entête standard <alloca.h>, soit à travers les tableaux de taille dynamique (Variable Lenght Array) disponibles depuis C99. Ce genre d’allocation peut conduire à une explosion de la taille de la pile, avec un stack overflow ou un stack clash à la clef.
<source>:4:17: warning: variable length array used [-Wvla]
    char buffer[n];
  • Si n est contrôlé par l’attaquant, ce dernier peut demander à allouer une valeur plus grande que celle prévue pour la pile au démarrage (valeur que l’on peut obtenir depuis le programme à travers l’appel système getrlimit et la ressource RLIMIT_STACK). Cela conduit alors au crash du programme (déni de service).
  • -Wcmse-union-leak : cet avertissement est assez particulier, puisqu’il n’a de sens que sur les architectures ARM disposant de Cortex-M Security Extension (CMSE).

Pour comprendre son fonctionnement, observons le code source suivant :

union U { double n; char b[4]; } u;
void (*fn2)(int, union U) __attribute__((cmse_nonsecure_call));
union U xyzzy(void) __attribute__((cmse_nonsecure_entry)) {
  fn2(0, u);
  return u;
}

La fonction fn2 déclenche un changement de mode du processeur de secure à unsecure, ce qui est nécessaire dans le modèle proposé par CMSE pour l’accès, par exemple, aux périphériques non sécurisés. Or, la fonction prend une union en paramètre. Cette union peut contenir des bits de padding (pour des raisons d’alignements des différents champs, par exemple), qui pourraient exfiltrer des bits d’information. Et ne sachant pas quel champ de l’union est actif, le compilateur ne peut pas forcer la valeur des champs de padding. L’utilisation de -Wcmse-union-leak renvoie alors :

<source>:6:10: warning: passing union across security boundary via parameter 1 may leak information [-Wcmse-union-leak]
  fn2(0, u);

Le correctif ici serait alors de ne pas passer l’union, mais le champ actif de celle-ci lors de l’appel à la fonction non sécurisée.

2. Sanitizers : -fsanitize=something

Les sanitizers sont une classe d’outils qui combinent instrumentation de code et runtime pour détecter certaines erreurs de programmation. On peut par exemple citer UBSan qui détecte à l’exécution la présence d’undefined behavior, par exemple un overflow d’entier signé. On les active à travers les options -fsanitize=nom-du-sanitizer.

Plusieurs sanitizers ayant un impact sur la sécurité du programme existent :

  • -fsanitize=address : permet de détecter des accès hors des bornes, après libération de la mémoire, une double libération de la mémoire, etc. L’impact de cette instrumentation sur le temps d’exécution est très important (la documentation mentionne un doublement du temps d’exécution), il n’est donc pas conseillé d’utiliser ce sanitizer en production, mais il permet de détecter de potentiels problèmes quand il est couplé à une batterie de tests ou un fuzzer.
  • On peut observer l’impact de ce sanitizer sur la sortie de clang -S -o- -fsanitize=address -Os sur un simple int foo(int* x) { return *x;} :
foo:
    pushq    %rax
    movq     %rdi, %rax
    shrq     $3, %rax
    movb     2147450880(%rax), %al
    testb    %al, %al
    jne      .LBB0_1
.LBB0_3:
    movl     (%rdi), %eax
    popq     %rcx
    retq
.LBB0_1:
    movl     %edi, %ecx
    andl     $7, %ecx
    addl     $3, %ecx
    cmpb     %al, %cl
    jl       .LBB0_3
    callq    __asan_report_load4
  • Le code commence par charger dans %al une valeur située dans une zone mémoire dédiée à l’instrumentation, puis si cette valeur est non nulle, elle est utilisée pour vérifier que la zone mémoire déréférencée est valide et éventuellement déclencher un appel à __asan_report_load4.
  • -fsanitize=memory : permet de détecter la lecture de valeurs non initialisées, pouvant conduire à de la fuite d’information de valeurs résidant précédemment sur la pile. Là encore, l’instrumentation a un impact important sur le temps d’exécution (fois 2 à fois 3 suivant les options utilisées), ce qui limite son usage à des configurations de test et non en production.
  • Le poids de l’instrumentation est particulièrement visible sur la sortie de clang -S -o- -fsanitize=memory -O2 sur int foo() { int x ; return x ; } :
foo:
   movq   __msan_retval_tls@GOTTPOFF(%rip), %rax    # lecture instrumentée par msan
   movl   $0, %fs:(%rax)
   xorl   %eax, %eax
   retq
  • Ce qui devrait n’être qu’une simple lecture de registre se transforme en appel de fonction !
  • -fsanitize=safe-stack : introduit une deuxième pile qui contient les valeurs pour lesquelles certains accès ne sont pas considérés comme sûrs. Les autres valeurs (dont l’adresse de retour...) restent dans la pile standard, considérée comme sûre. Cette séparation rend plus difficile l’écrasement des valeurs de la pile sûre depuis la pile non sûre. Cette instrumentation a peu d’impact sur les performances (peu de fonctions allouent plus d’une page sur la pile, et même dans ce cas, on se retrouve avec juste un prologue de fonction un peu plus long) ce qui permet de la déployer en production. Le code qui suit illustre la présence de cette pile supplémentaire :
foo:
   pushq   %r14
   pushq   %rbx
   pushq   %rax
   movq   __safestack_unsafe_stack_ptr@GOTTPOFF(%rip), %r14 # accès à la pile supplémentaire
   movq   %fs:(%r14), %rbx
  • -fsanitize=cfi : introduit un ensemble de vérification lors de sauts indirects (par exemple, suite à des appels par pointeur de fonction, ou par fonction virtuelle), ce qui limite l’exploitation de ces sauts par des attaques de type JOP (Jump Oriented Programming). On notera que cette instrumentation requiert que l’ensemble du code soit compilé avec les optimisations lors de l’édition de liens (Link Time Optimization). L’impact sur les performances est documenté comme de l’ordre de +1 % sur le temps d’exécution et +15 % sur la taille du binaire.
  • -fsanitize=shadow-call-stack : il implémente une forme restreinte, mais moins coûteuse de protection de la pile que -fsanitize=safe-stack, en stockant uniquement l’adresse de retour d’un appel de fonction dans une pile séparée, ce qui la rend plus difficile à atteindre par un débordement de tampon. Cette instrumentation n’est disponible que sur AArch64. La comparaison des deux sorties ci-dessous pour un appel à gcc -ffixed-x18 -O2 avec et sans -fsanitize=shadow-call-stack fait apparaître ce stockage supplémentaire :
foo_with_shadow_call_stack:                 foo_without_shadow_call_stack :
    sbfiz x0, x0, 2, 32                         sbfiz x0, x0, 2, 32                     
    str x30, [x18], 8
    stp x29, x30, [sp, -16]!                    stp x29, x30, [sp, -16]!
    add x0, x0, 15                              add x0, x0, 15
    and x0, x0, -16                             and x0, x0, -16
    mov x29, sp                                 mov x29, sp
    sub sp, sp, x0                              sub sp, sp, x0
    mov x0, sp                                  mov x0, sp
    bl bar                                      bl bar
    mov sp, x29                                 mov sp, x29
    ldr x30, [x18, -8]!                    
    ldr x29, [sp], 16                           ldr x29, [sp], 16
    ret                                         ret

3. Machine : -msomething

Les compilateurs permettent de spécifier des comportements spécifiques à un certain type de matériel. Les drapeaux que nous allons voir dans cette section permettent de tirer parti de certaines fonctionnalités matérielles qui protègent le binaire en cours d’exécution contre certains types d’attaques :

  • -mbranch-protection est spécifique aux processeurs ARM. Elle permet d’activer toute une gamme de protections contre les attaques de type ROP (Return Oriented Programing) et JOP en utilisant une combinaison d’authentification de pointeurs (où certains bits inutilisés des pointeurs sont utilisés pour stocker une information d’authentification calculée à l’aide d’instructions dédiées) et d’identification des cibles de branchement à l’aide d’une instruction spécifique bti qui permet de vérifier la validité d’un saut indirect. Cette instruction accepte un argument, qui vaut soit c (pour call), et vérifie alors que le saut qui a conduit à cette instruction est bien un appel de fonction, j (pour jump) et vérifie alors que le saut qui a conduit à cette instruction est bien un branchement. Tout branchement indirect vers une instruction différente de bti dans une page protégée est considéré comme une erreur.
  • -mshstk : permet d’activer une protection semblable à -fsanitize=shadow-call-stack, mais reposant sur le support matériel fournit par la technologie Intel CET qui maintient un shadow stack pointer sur une zone où sont stockées les adresses de retour de fonction. Ainsi, un débordement de tampon peut plus difficilement écraser l’adresse de retour pour un surcoût négligeable en termes de performance d’après la documentation officielle.

4. Autres drapeaux : -fsomething

Les drapeaux de la famille -fsomething affectent le processus de compilation de manière indépendante du matériel. Ils ont donc un avantage de portabilité par rapport aux drapeaux de la famille -msomething. Faisons donc connaissance avec...

-fstack-protector et ses camarades -fstack-protector-all, -fstack-protector-strong : un grand classique, puisqu’il installe un stack canary. Ses différentes versions contrôlent le type de fonctions qui sont concernées par ce drapeau : -fstack-protector pour les fonctions les plus propices à un dépassement de tampon : celles qui accroissent dynamiquement la taille de la pile (p. ex. via alloca) ou qui contiennent des tampons de plus de 8 octets, -fstack-protector-strong pour également protéger toutes les fonctions qui allouent un tableau sur la pile ou qui référencent une adresse de la pile, et -fstack-protector-all pour l’ensemble des fonctions. Offrons-nous un exemple de stack canary :

foo:
    pushq    %rbp
    movslq   %edi, %rdi
    movq     %rsp, %rbp
    subq     $16, %rsp
    movq     %fs:40, %rax            # lecture du stack canary
    movq     %rax, -8(%rbp)          # dépôt d’icelui sur la pile
    xorl     %eax, %eax
    leaq     15(,%rdi,4), %rax
    andq     $-16, %rax
    subq     %rax, %rsp
    movq     %rsp, %rdi
    call     bar
    movq     -8(%rbp), %rax
    xorq     %fs:40, %rax            # vérification du stack canary
    je       .L2
    call     __stack_chk_fail
.L2:
    leave
    ret

-fcf-protection=[full|branch|return|none|check] : apporte une protection contre les attaques qui modifient le flot de contrôle original du programme (type ROP / COP / JOP), en se reposant sur un contrôle de la validité de différents branchements indirects, appels indirects de fonction, etc. Si possible, Intel CET est utilisé pour améliorer les performances de l’implémentation.

foo:
    endbr64                      # Terminate Indirect Branch, il est valide de sauter ici
    xorl    %eax, %eax                 
    jmp     bar

-fstack-clash-protection : apporte une protection contre les attaques de type Stack Clash (collision entre la pile et le tas) en imposant un accès mémoire par page de pile nouvellement allouée.

foo:
    leaq    -32768(%rsp), %r11
.LPSRL0:                          # boucle d’allocation de pile par paquet de 4096
    subq    $4096, %rsp
    orq     $0, (%rsp)            # opération mémoire pour forcer un accès à la zone allouée
    cmpq    %r11, %rsp
    jne     .LPSRL0
    
    subq    $3240, %rsp
    movq    %rsp, %rdi
    call    bar
    addq    $36008, %rsp
    ret

-fsplit-stack : instrumente l’allocation sur la pile pour utiliser des zones mémoire disjointes quand l’espace initial est épuisé. Ce drapeau a généralement pour objectif de permettre de limiter la taille de la pile (p. ex. si l’on utilise un grand nombre de processus légers), mais un effet de bord amusant est d’offrir (à un coût supérieur à -fstack-clash-protection) une protection contre les attaques de type Stack Clash, et d’obtenir une organisation non standard de la pile, ce qui rend l’écrasement de cette dernière potentiellement plus délicat.

-fstack-usage : ce drapeau ne modifie pas le code en sortie, mais produit également un rapport (extension .su) où l’on trouve, pour chaque fonction générée, la quantité de mémoire requise sur la pile pour cette fonction, et le cas échéant si cette dernière à une taille variable (à cause d’un alloca, etc.). Ces informations, notamment la nature dynamique de l’allocation, peuvent donner des indices sur les fonctions à protéger de manière prioritaire.

Conclusion

GCC et Clang partagent de nombreux drapeaux, même si nous n’avons pas détaillé dans cet article les spécificités de chacun (architectures supportées, choix d’implémentation, etc.). Il n’en reste pas moins que le choix d’une bonne combinaison de drapeaux est complexe, puisqu’elle induit un compromis performance / sécurité propre à l’application.

Je vous laisse donc sur les drapeaux utilisés par défaut par Fedora Rawhide, en guise de référence. Tous n’ont pas été évoqués dans cet article, il faut bien garder un peu de mystère :-) :

-Werror=format-security          \
-Wp,-D_FORTIFY_SOURCE=2          \
-Wp,-D_GLIBCXX_ASSERTIONS        \
-fstack-protector-strong         \
-fasynchronous-unwind-tables     \
-fstack-clash-protection         \
-fcf-protection                  \
-fPIC

Remerciement

Un grand merci à FloFlo pour sa relecture et cette chouette suggestion de titre d’article !



Article rédigé par

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

Crévindiou, c’est pas du bon C d’chez nous ça, cé du C deu’l ville !

Magazine
Marque
GNU/Linux Magazine
Numéro
267
Mois de parution
janvier 2024
Spécialité(s)
Résumé

IANAL (I Am Not A Linguist), mais quand j’entends du québécois, je ne comprends pas tout, mais je comprends. Mais qu’en est-il des dialectes du langage C ? Car oui, le langage C a des dialectes, et nous allons voyager un peu à travers l’un d’entre eux, le dialecte GNU, supporté principalement par GCC, mais aussi, en partie, par Clang.

Les derniers articles Premiums

Les derniers articles Premium

Bénéficiez de statistiques de fréquentations web légères et respectueuses avec Plausible Analytics

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

Pour être visible sur le Web, un site est indispensable, cela va de soi. Mais il est impossible d’en évaluer le succès, ni celui de ses améliorations, sans établir de statistiques de fréquentation : combien de visiteurs ? Combien de pages consultées ? Quel temps passé ? Comment savoir si le nouveau design plaît réellement ? Autant de questions auxquelles Plausible se propose de répondre.

Quarkus : applications Java pour conteneurs

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

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

De la scytale au bit quantique : l’avenir de la cryptographie

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

Imaginez un monde où nos données seraient aussi insaisissables que le célèbre chat de Schrödinger : à la fois sécurisées et non sécurisées jusqu'à ce qu'un cryptographe quantique décide d’y jeter un œil. Cet article nous emmène dans les méandres de la cryptographie quantique, où la physique quantique n'est pas seulement une affaire de laboratoires, mais la clé d'un futur numérique très sécurisé. Entre principes quantiques mystérieux, défis techniques, et applications pratiques, nous allons découvrir comment cette technologie s'apprête à encoder nos données dans une dimension où même les meilleurs cryptographes n’y pourraient rien faire.

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 66 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous