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)

Introduction au dossier : Sécurisez vos codes

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

La sécurité des codes… En voilà un sujet fourre-tout où n’importe quel article de MISC trouverait sa place ! Pour apporter une petite touche personnelle, et faire un petit clin d’œil à mon héritage familial, j’ai choisi de prendre un axe historique pour la constitution de ce dossier.

Garder ses parties privées

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

Il y a 19 ans, Ulrich Drepper, alors développeur chez Red Hat et un des contributeurs principaux de la glibc, ajoutait au changelog de la glibc la ligne suivante : sysdeps/i386/bsd-_setjmp.S: Use PTR_MANGLE for PC if defined, inaugurant ainsi l’arrivée de la protection des pointeurs de fonction stockés dans des structures internes à la glibc.

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.

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

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous