La compilation : du code au binaire… et retour !

Magazine
Marque
MISC
HS n°
Numéro
24
Mois de parution
septembre 2021
Spécialité(s)


Résumé

On trouve des compilateurs partout : quand on construit des programmes, bien sûr, mais aussi quand on visite la page web des Éditions Diamond — elle embarque du JavaScript, qui est probablement compilé à la volée par un composant de votre navigateur. Quand on utilise un Notebook Jupyter pour du calcul scientifique — ne serait-ce que pour la compilation en bytecode du source Python. Quand on installe un APK pour ART, celui-ci est transformé en code natif depuis son bytecode Dalvik… encore de la compilation. Et nous allons voir que le domaine de la compilation peut aussi très largement intéresser celui de la sécurité informatique et du reverse engineering.


Body

1. Qu'est-ce qu'un compilateur ?

C'est un programme qui prend en entrée un code source et le transforme en une représentation d'un niveau d'abstraction généralement inférieur (ou égal). Ainsi, un programme qui transforme un fichier texte écrit en langage C en un fichier texte écrit en langage assembleur est un compilateur (voir figure 1). Le programme LaTeX est également un compilateur, puisqu'il transforme une description de haut niveau d'un document en une version bas niveau, au format DVI par exemple. Le vénérable cat est un cas limite de compilateur :-)

d1 compilation1 compilation c binaire-s

Fig. 1 : Du code C à l’assembleur.

2. Une famille éclectique

La famille des compilateurs est grande : il existe des compilateurs source à source dont la sortie est un format textuel et non binaire, comme les minifier JavaScript ; des compilateurs à la volée qui génèrent du code pendant l'exécution d'un programme hôte qui utilise directement le code généré pour continuer son exécution. Le moteur V8 de Chrome, dédié à l'exécution et la compilation à la volée de… JavaScript en est un bon exemple. Il existe aussi des compilateurs faisant de la compilation croisée, c'est-à-dire qui génèrent du code machine pour une architecture matérielle différente de celle sur laquelle ils s'exécutent, ce qui est très pratique quand on compile pour des architectures ayant de faibles capacités de calcul.

Il n'est pas rare d'avoir plusieurs compilateurs différents — et donc plusieurs formats — dans le même écosystème. Beaucoup de langages dits de haut niveau exposent une API C qui leur permet de fournir une surcouche autour de bibliothèques natives existantes. C'est notamment le cas du langage Python, dont plusieurs modules standards sont le résultat de la compilation d'un code C en code natif présent dans une bibliothèque partagée, bibliothèque dont l'importation est supportée par le mécanisme d'import de Python. Une exécution de code Python navigue constamment entre interprétation de bytecode et exécution de code natif.

Une branche de la famille des compilateurs chère au cœur des lecteurs de MISC est celle des décompilateurs, ces compilateurs qui partent d'une représentation bas niveau pour générer une représentation de plus haut niveau. Par exemple, le décompilateur RetDec est capable de décompiler un code assembleur en bitcode LLVM, puis de décompiler un bitcode LLVM vers un code C. Par nature, le processus de décompilation est imparfait, puisque certaines informations sont perdues lors de la compilation (un exemple trivial : le nom des variables locales et de certains types), mais on y reviendra.

3. La compilation, un domaine pour le reverser ?

Comprendre les différents mécanismes de compilation offre au reverser une meilleure compréhension de son objet d'étude : savoir qu'on est arrêté sur un code compilé à la volée, c'est avoir la possibilité de remonter à son entrée, probablement plus simple à comprendre. C'est aussi pouvoir séparer un binaire en sections différentes suivant leur langage d'origine, et pour chaque langage remonter aux idiomes spécifiques à ce langage, voire reconnaître les marqueurs de certains algorithmes.

Mais avoir des bases en compilation, c'est aussi s'offrir de nouvelles cordes à son arc pour mieux reverser ; on ne compte plus les outils mettant en œuvre des techniques de compilation et diffusés dans le monde de la sécurité informatique : RetDec [RetDec] (décompilation), Triton [Triton] (exécution concolique), Miasm [Miasm] (désobfuscation).

Les dernières sections de cet article illustrent plus en détails quelques-uns de ces liens entre compilation d'une part et sécurité informatique ou reverse engineering d'autre part, en se concentrant sur les plus classiques des compilateurs : les compilateurs statiques (on dit aussi ahead of time par opposition aux compilateurs à la volée just in time) de source vers binaire, et les outils de reverse correspondants.

4. Compiler pour mieux sécuriser

Les compilateurs peuvent optimiser un code pour la sécurité, en incluant des protections contre des erreurs de programmation communes ou des contre-mesures pour des failles matérielles connues, compliquant ainsi le travail du reverser malveillant.

Un des exemples les plus répandus est la protection contre les conséquences de certains types de dépassement de tampon. Prenons l'exemple du fameux stack canary. Pour chaque fonction compilée avec cette protection, le compilateur évalue si cette fonction est exposée à des attaques de type dépassement de tampon. Les critères varient suivant les options passées par l'utilisateur, mais elles permettent de prendre en compte la taille de la pile, ou la présence d'allocation dynamique de la pile, ces deux informations étant connues par le compilateur lors de l'émission du code assembleur. Le cas échéant, le compilateur insèrera une écriture dans la pile d'une valeur aléatoire fournie par le système lors du prologue de la fonction, et une vérification de cette valeur avant chaque saut hors de la fonction. Cette protection est activée par -fstack-protector sous GCC et Clang.

Dans certains cas, le compilateur pallie également les limitations du système : afin de détecter une collision entre la pile et le tas, le noyau Linux alloue systématiquement une page mémoire protégée en lecture et écriture entre ces deux espaces. Ainsi, un programme qui allouerait trop de pile (p.e. à travers des appels récursifs) finirait par allouer de la mémoire utilisant cette page, et l'accès à cette mémoire déclencherait une erreur. Cependant, si une allocation de pile venait à dépasser la taille d'une page, et que les accès à la pile sautent aussi cette page, un attaquant contrôlant la pile gagne un accès au tas. Pour contrer cette attaque, le compilateur découpe chaque allocation de la pile par pages, et force un accès mémoire entre chaque allocation de page, déclenchant ainsi la page de garde. Cette protection est activée par -fstack-clash-protection sous GCC et Clang.

Il y a quelques années, une attaque reposant sur la prédiction de branchement des processeurs modernes a défrayé la chronique. L’attaque « Spectre » [Spectre2019] permet de charger une adresse mémoire gardée par un prédicat faux si la prédiction de branchement considérait ce prédicat comme vrai, amenant ainsi la donnée en cache. La contre-mesure implémentée dans Clang/LLVM masque chaque adresse mémoire accédée à travers un prédicat avec une valeur dépendante du prédicat, ce qui rend l'adresse invalide si le prédicat est faux. Cette transformation se fait au niveau de la représentation interne (RI) de LLVM et est activée par -mspeculative-load-hardening.

De nombreuses autres protections ont vu le jour dernièrement : vérification de l'intégrité du flot de contrôle (CFI), parfois avec support matériel (XFG), authentification de pointeurs… C'est un sujet foisonnant !

5. Un obfuscateur dans un compilateur ou comment ramer à contre-courant

Obfusquer un programme, c'est le transformer en un programme fonctionnellement équivalent, mais dans lequel les éléments à protéger sont plus complexes à retrouver, quitte à en payer le prix en termes de performances.

Cette complexité peut prendre diverses formes, comme le remplacement d'une opération ou d'une constante par une série d'opérations plus complexes, le chiffrement des chaînes de caractères, l'ajout de branchements conditionnels, l'aplatissement du flot de contrôle des fonctions, la découpe en petits fragments ou la fusion de code, etc. L'éventail des possibilités est large [Hosseinzadeh2018] !

Et le tout doit se faire de la façon la plus diversifiée possible pour désorienter l'attaquant.

Transformer un programme, c'est le rôle des compilateurs, et l'idée est donc tentante de les réutiliser pour bénéficier de leurs infrastructures de modification de code. Cependant, il ne faut pas oublier que ces outils sont d'abord conçus pour être au service de la performance, en temps d'exécution, ou en empreinte mémoire, statique ou dynamique. Et ces objectifs vont à l'encontre de la complexification et de la variabilité recherchées pour l'obfuscation : le développeur d'obfuscations doit donc s'assurer qu'elles ne sont pas cassées par les optimisations du compilateur.

De ce point de vue, le choix de la partie du compilateur (voir figure 2) dans laquelle insérer les obfuscations n'est pas anodin [Guelton2018].

d1 compilation1 llvm architecture-s

Fig. 2 : Architecture du compilateur LLVM.

S'insérer dans le front-end, qui traduit le code source en une représentation intermédiaire (RI), permet de tenir compte des caractéristiques de haut niveau du langage, mais nécessite d'écrire un obfuscateur pour chaque langage source. De plus, le code obfusqué risque de ne pas bénéficier des optimisations du compilateur, car il devra être en mesure de résister à celles-ci pour rester obfusqué.

Intervenir au niveau du back-end, qui traduit la RI en code assembleur, permet d'introduire de la diversité au niveau des instructions ou séquences d'instructions assembleurs générées (par exemple, en remplaçant certains lea par des sub), d'introduire des instructions assembleur inutiles, etc. En revanche, cela nécessite de développer un obfuscateur par architecture cible.

La dernière option est d'implémenter les obfuscations dans l'optimiseur, qui travaille exclusivement sur la représentation intermédiaire : les transformations sont alors (quasiment) indépendantes du langage source et des architectures cibles. C'est le cas d'OLLVM [OLLVM] ou YANSOllvm [YANSOllvm] parmi les outils open source, ou de Quarks App Protect [QAP] (anciennement Epona). Dans tous ces outils, les transformations sont réalisées sur la RI LLVM, et bénéficient donc de ces caractéristiques et des outils et analyses mis à disposition (forme SSA, graphe de flot de contrôle, graphe d'appels, analyses de dépendances…).

Pour profiter des gains de performance apportés par les optimisations du processus de compilation normal, il est possible d'implémenter les obfuscations après les passes d'optimisation. Refaire tourner les optimisations après les obfuscations (voir figure 3) permet aussi de nettoyer le code généré et de s'assurer que ce dernier est en mesure de leur résister, que ce soit par des contrôles visuels, ou automatisés dans un processus d'intégration continue (CI).

d1 compilation1 obfuscator-s

Fig. 3 : Insertion des obfuscations dans l’optimiseur LLVM.

Nous allons d'ailleurs voir dans les sections suivantes que cette capacité à résister aux optimisations est loin d'être un détail !

6. Compiler pour reverser ?

Paradoxal, non ?

En fait pas tant que ça. Des désassembleurs/décompilateurs comme Binary ninja, Ghidra, IDA Pro, angr, radare2, Hoppe ou Miasm par exemple [Wagner2019] utilisent des techniques similaires à celles rencontrées dans les compilateurs : analyses de flot de données ou d'ensembles de valeurs, forme SSA, élimination de code mort… Et ce, aussi bien pour construire que pour raffiner leurs différents niveaux de représentations intermédiaires, qui sont généralement des représentations internes « maison ». Chacun de ces désassembleurs/décompilateurs réimplémente donc ses propres analyses et transformations…

Utiliser une représentation interne commune comme la RI LLVM leur permettrait de bénéficier directement des analyses et optimisations de LLVM, qui ont l'avantage d'être nombreuses et éprouvées, mais aussi des nombreux outils qui reposent sur cette représentation. Plusieurs initiatives ont vu le jour à cet effet, les plus actives et matures semblant être McSema/ReMill et RetDec :

  • RetDec [RetDec] s'appuie sur Capstone [Capstone] lors de son processus de traduction du binaire en RI LLVM. Il utilise certaines optimisations de LLVM et ajoute des transformations qui lui sont propres. Le code généré est donc déjà bien nettoyé.
  • McSema [McSema] utilise un désassembleur tiers (IDA ou Binary Ninja) pour récupérer le flot de contrôle de l'application étudiée. Il traduit alors les portions de binaire correspondant à du code en RI LLVM en utilisant ReMill [Remill] [McSemaLifting]. Le code généré est assez compliqué, car il fournit pour chaque instruction une émulation de son comportement.

Une fois le code assembleur transformé en RI LLVM, il s'agit d'en extraire des informations pertinentes, notamment en le débarrassant des différentes couches d'obfuscation qui peuvent le protéger. Parmi les nombreux outils reposant sur la RI LLVM, certains aident à l'analyse et/ou l'optimisation/simplification de programme. Citons par exemple le moteur d'exécution symbolique dynamique KLEE [Klee] qui permet d'explorer des chemins d'exécution et de raisonner au sujet de leur faisabilité à l'aide d'un solveur SMT. Ou encore le super-optimiseur Souper [Souper], qui permet de simplifier une portion de programme en validant la transformation à l'aide d'un solveur SMT.

Une étude [Saturn2019] s'est ainsi récemment penchée sur le couplage de la sortie de ReMill avec Souper afin de simplifier les prédicats opaques. Le code résultant est ensuite simplifié par l'optimiseur de LLVM, et ceci dans un processus itératif jusqu'à atteindre un état stable. Même si le processus reste coûteux et ne cible pas tous les types d'obfuscation, il démontre toutefois la faisabilité d'utiliser une représentation interne de compilateur comme support pour une chaîne d'outils automatiques d'aide au reverse engineering.

On notera cependant que les outils de décompilations ne font que retrouver une structure présente, implicitement ou explicitement, dans le binaire final. Certaines informations comme les noms de variables locales, absentes du binaire, sont à jamais perdues. Certaines abstractions, comme les classes ou les structures, peuvent être partiellement reconstruites en s’appuyant sur le mangling de certains symboles et sur les conventions d'appel pour les méthodes. Parfois, un idiome est reconnaissable et facilite la reconstruction de l'abstraction, p.e. pour la gestion des exceptions. Le processus reste donc intrinsèquement imparfait.

Alors, Tom ou Jerry ?

En résumé, les outils de la compilation sont devenus des éléments incontournables dans le domaine de la sécurité des applications, que ce soit pour les protéger, les obfusquer ou même les reverser. Ce qui nécessite de connaître un minimum leur fonctionnement interne, mais ce sera l'objet d'un article à venir !

Dans l'immédiat, une question se pose : si les reversers jouent à armes égales avec les compilateurs, est-ce vraiment une bonne idée d'utiliser ces derniers à des fins de protection ? Dans la pratique, les outils de reverse disponibles présentent souvent des inconvénients : ils sont efficaces contre un nombre limité de protections, et sont parfois coûteux à appliquer à grande échelle, ce qui nécessite de déterminer préalablement les zones d'intérêt dans le binaire à analyser. Empiler des couches de protections variées et protéger l'ensemble de l'application (et pas seulement les éléments sensibles) demeure donc un moyen efficace de complexifier la tâche des attaquants. Mais cela reste toujours un jeu du chat et de la souris !

Références

[RetDec] RetDec, https://github.com/avast/retdec

[Triton] Triton, https://triton.quarkslab.com/

[Miasm] Miasm, https://miasm.re

[Spectre2019] P. Kocher, J. Horn, A. Fogh, D. Genkin, D. Gruss, W. Haas, M. Hamburg, M. Lipp, S. Mangard, T. Prescher, M. Schwarz, et Y. Yarom, « Spectre Attacks: Exploiting Speculative Execution », 40th IEEE Symposium on Security and Privacy (S&P'19), IEEE, 2019

[Hosseinzadeh2018] S. Hosseinzadeh et al., « Diversification and Obfuscation Techniques for Software Security: A Systematic Literature Review », Information and Software Technology, 2018

[Guelton2018] S. Guelton, A. Guinet, P. Brunet, J. M. Martinez Caamaño, F. Dagnat et N. Szlifierski, « Combining Obfuscation and Optimizations in the Real World », 18th IEEE International Working Conference on Source Code Analysis and Manipulation (SCAM), 2018

[OLLVM] OLLVM, https://github.com/obfuscator-llvm/obfuscator

[YANSOllvm] YANSOllvm, https://github.com/emc2314/YANSOllvm

[QAP] Quarks App Protect, https://quarkslab.com/quarks-appshield-app-protect/

[Wagner2019] R. Wagner, « Modern Static Analysis of Obfuscated Code », 3rd ACM Workshop on Software Protection (SPRO'19), ACM, 2019

[Capstone] Capstone, https://www.capstone-engine.org/

[McSema] McSema, https://github.com/lifting-bits/mcsema

[ReMill] ReMill, https://github.com/lifting-bits/remill

[McSemaLifting] Heavy lifting with McSema 2.0, https://blog.trailofbits.com/2018/01/23/heavy-lifting-with-mcsema-2-0/, 2018

[Klee] KLEE, https://klee.github.io/

[Souper] Souper, https://github.com/google/souper

[Saturn2019] P. Garba et M. Favaro, « SATURN Software Deobfuscation Framework Based on LLVM », 3rd ACM Workshop on Software Protection (SPRO'19), ACM, 2019



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 !

Attention : pointeur libéré !

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

Ou plus spécifiquement, ne déréférencez en aucun cas un pointeur après un free ! Facile ? L'utilisation de pointeurs après leur libération (Use After Free) fait toujours aujourd'hui partie du top 5 des vulnérabilités listées par MITRE (https://cwe.mitre.org/top25/archive/2023/2023_top25_list.html). Avec des conséquences qui peuvent aller du crash de l'application à l'exploitation de la vulnérabilité induite par un attaquant, et, au final, avoir un impact sur votre réputation ou vous coûter beaucoup d'argent ! Dans cet article, nous verrons quelques cas dont il faut se méfier, comment les Use After Free constituent des vulnérabilités qui peuvent être exploitées par des attaquants, et surtout les mesures à mettre en place pour les éviter autant que possible.

Les derniers articles Premiums

Les derniers articles Premium

PostgreSQL au centre de votre SI avec PostgREST

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

Dans un système d’information, il devient de plus en plus important d’avoir la possibilité d’échanger des données entre applications. Ce passage au stade de l’interopérabilité est généralement confié à des services web autorisant la mise en œuvre d’un couplage faible entre composants. C’est justement ce que permet de faire PostgREST pour les bases de données PostgreSQL.

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.

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