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)

Un défi laid : retour sur la conception d'un challenge de reverse

Magazine
Marque
MISC
Numéro
122
Mois de parution
juillet 2022
Spécialité(s)
Résumé

Aux dernières vacances de février, j'ai eu l'opportunité de contribuer au CTF d'Insomnihack sous la forme d'un challenge de reverse. Si j'ai eu beaucoup de joie lors de son écriture, et malgré des retours positifs de la part des participants, celui-ci souffrait d'un défaut majeur. Cet article est donc une sorte de rapport de chall' par l'auteur, commentaire de texte inclus !

Cheval de Troie : retour aux sources

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

Le premier novembre 2021, les CVE-2021-42574 et CVE-2021-42694 étaient rendues publiques. Ces deux CVE rendent comptent des limitations de l’utilisation de caractères Unicode dans les identifiants, commentaires et/ou chaînes de caractères littérales de codes sources. Elles sont intéressantes par deux aspects : elles sont relativement agnostiques au langage de programmation sous-jacent, et elles utilisent comme cheval de Troie le rendu fait par certains outils, notamment les forges logicielles en ligne, de certains caractères Unicode.

Les derniers articles Premiums

Les derniers articles Premium

Stubby : protection de votre vie privée via le chiffrement des requêtes DNS

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

Depuis les révélations d’Edward Snowden sur l’espionnage de masse des communications sur Internet par la NSA, un effort massif a été fait pour protéger la vie en ligne des internautes. Cet effort s’est principalement concentré sur les outils de communication avec la généralisation de l’usage du chiffrement sur le web (désormais, plus de 90 % des échanges se font en HTTPS) et l’adoption en masse des messageries utilisant des protocoles de chiffrement de bout en bout. Cependant, toutes ces communications, bien que chiffrées, utilisent un protocole qui, lui, n’est pas chiffré par défaut, loin de là : le DNS. Voyons ensemble quels sont les risques que cela induit pour les internautes et comment nous pouvons améliorer la situation.

Surveillez la consommation énergétique de votre code

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

Être en mesure de surveiller la consommation énergétique de nos applications est une idée attrayante, qui n'est que trop souvent mise à la marge aujourd'hui. C'est d'ailleurs paradoxal, quand on pense que de plus en plus de voitures permettent de connaître la consommation instantanée et la consommation moyenne du véhicule, mais que nos chers ordinateurs, fleurons de la technologie, ne le permettent pas pour nos applications... Mais c'est aussi une tendance qui s'affirme petit à petit et à laquelle à terme, il devrait être difficile d'échapper. Car même si ce n'est qu'un effet de bord, elle nous amène à créer des programmes plus efficaces, qui sont également moins chers à exécuter.

Donnez une autre dimension à vos logs avec Vector

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

Avoir des informations précises et détaillées sur ce qu’il se passe dans une infrastructure, et sur les applications qu'elle héberge est un enjeu critique pour votre business. Cependant, ça demande du temps, temps qu'on préfère parfois se réserver pour d'autres tâches jugées plus prioritaires. Mais qu'un système plante, qu'une application perde les pédales ou qu'une faille de sécurité soit découverte et c'est la panique à bord ! Alors je vous le demande, qui voudrait rester aveugle quand l'observabilité a tout à vous offrir ?

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

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous