--- title: "Sécurité système : introduction" date: 2023-09-07 tags: ["bibliothèque", ".got"] categories: ["Sécurité système", "Cours"] --- ## Définition : un exécutable C'est un fichier contenant un programme et identifié par le système d'exploitation en tant que tel ([définition par Wikipédia][l_execwiki]). Il existe plusieurs format d'exécutable en fonction du système d'exploitation. Il ne faut pas confondre programme et scripts, le premier contenant le code **exécutable**, le second est **interprété** (par un programme) et passe éventuellement par du code intermédiaire ou [bytecode][l_bytecode] : *Portable Executable* Pour Microsoft Windows, *Match Object* pour Apple MacOS / iOS, *Executable and Linkable Format* pour beaucoup de système Unix. Pourquoi compiler du code? Plusieurs avantages : * un programme compilé est moins consommateur de ressources et par conséquent plus rapide à l'exécution; * pour protéger sa propriété intellectuelle (par la possibilité d'obfuscation du code machine généré); * pour se passer de l'**interpréteur** et ainsi éviter une étape (complexification de la pile d'exécution). Le code interprété a aussi ses avantages : * il est indépendant de la plate-forme d'exécution; * en général il existe une multitude de bibliothèques utilisable rapidement (coucou Python); * le développement est simplifié et plus interactif, le langage est alors plus accessible et le développement peut-être plus rapide (langage de plus haut niveau) [l_bytecode]: https://fr.wikipedia.org/wiki/Bytecode [l_execwiki]: https://fr.wikipedia.org/wiki/Fichier_ex%C3%A9cutable ## La compilation Un exécutable contient donc du code machine, le code source qui le défini est alors **compilé** en code machine. Mais la compilation est en fait plus compliquée que cette simple *"traduction"*. Elle se compose en fait de 4 grande phases : 1. le préprocessing; 2. la compilation; 3. l'assemblage; 4. l'édition de liens ### le préprocessing Chargé de modifier le code source avant la compilation. Ici le moteur de préprocessing se charge de remplacer les macro (`#define`), les constantes [^f_const], les inclusion d'entêtes (`#include`) par le code effectif; [^f_const]: Voici quelques exemples de constantes de bases : - `__FILE__` : affiche le nom du fichier source; - `__LINE__` : affiche le numéro de la ligne atteinte; - `__DATE__` : affiche la date de compilation du code source; - `__TIME__` : affiche l'heure de compilation de la source. ### la compilation C'est à cette étape que le code (`C`, `C++`, `Rust` etc.) est transforme en assembleur. Cette étape se compose elle-même de plusieurs parties. #### L'analyse lexicale Lors de cette étape, le compilateur analyse le texte du code source. Il est découpé en *mots* ou *tokens* définis par des **expressions rationnelles** et **des automates à états finis**. Il revient à **l'analyseur lexical** de réaliser cette opération. Lors de cette étape, les *"bruits"* sont ignorés : commentaires et espaces. Les *lexèmes* -- représentant une instance de la petite unité de signification -- sont ainsi définis. Si l'analyseur syntaxique rencontre un lexème inconnu, il lèvera une erreur. Il existe plusieurs analyseur lexicaux : flex, lex[^n_lex] etc. Il ne revient pas à l'analyseur syntaxique de vérifier que l'enchainement des lexème est correct, il transmets juste le résultat de son analyse à l'étape suivante. [^n_lex]: analyseur en *C* co-écrit par Eric Schmidt, cofondateur de Google. #### L'analyse syntaxique Réalisée par **l'analyseur syntaxique** qui traite les informations reçues par l'analyseur lexical. Contrairement à l'étape précédente, il est question ici de se focaliser sur la validités syntaxique *d'une phrase*. C'est un processus itératif dont le résultat est un *Arbre Syntaxique Abstrait* construit en fonction de la **grammaire du langage**.Il est construit au fur et à mesure que l'analyseur avance dans le code source. Deux outils d'analyse syntaxique sont principalement utilisés : #### L'analyse sémantique Lors de cette étape le compilateur vérifie la cohérence du code, certaines erreurs relevant de la grammaire seront détectées comme par par exemple : ```c // erreur sur les types int my_integer = "a string"; // Accès à une variable en dehors de son scope { int x = 3;} x = 4; ``` Lors de l'analyse syntaxique, l'*Arbre Syntaxique Abstrait* est enrichi d'information sur le sens des phrases du code. #### La production de code intermédiaire Cette étape, le code est transformé dans un langage intermédiaire entre le langage de haut niveau (*C*, *C++*) et le langage machine. C'est à partir de ce code que le compilateur appliquera des optimisations. C'est aussi lors de cette étape que le code peut être offusqué afin de rendre son analyse plus complexe (dans le logiciel privateur, ça va de soit). #### Les optimisations Ici, le compilateur réalise des optimisations sur le code intermédiaire. La plupart de ces optimisations effectué sur le code de plus haut niveau le rendraient moins compréhensible et pourrait se révéler plus fastidieux à écrire. #### La génération de code assembleur Ici le code intermédiaire optimisé est transformé en assembleur. Le compilateur créé les fichiers `*.S`. On appelle aussi cette étape **génération de code natif**. Lors de cette étape, le code généré est spécifique à une architecture donnée et ce afin de profiter des instructions disponible sur celle-ci (*MMX*, *SSE*, *NEON* etc.). #### L'assemblage Le compilateur créé un fichier objet `*.o` par fichier source. Ces fichiers objets sont bien des binaires, il contienne déjà les segments nécessaires (`.text`, `.data` ...) dans un format dépendant du système cible (PE pour Microsoft Windows©, Mach-O pour Apple MacOS©, ELF pour Linux). Lors de cette étape, le compilateur enlève tous les *labels*, mais afin de pouvoir résoudre les symboles (fonctions, variables, etc.) il va créer la **table des symboles**. Cette table contenu dans chacun des fichiers objets générés fait référence à la position des élément dans ce fichier. Le compilateur résout ces symboles **lors de l'édition de liens**. #### L'édition de liens C'est le moment ou le compilateur agrège les différents fichiers objets en un seul exécutable. Le *linker* responsable de cette étape doit résoudre les symboles et les liers aux différents fichiers objets et bibliothèques. C'est aussi lors de cette étape que les différents segments composants le programme sont créés et que la **table de relocation** est créée (très utile pour les mécanismes d'ALSR). Il existe deux type d'édition de liens : * **statique** : les éléments issus de bibliothèques sont inclus dans le binaire final. Dans ce cas la résolution des symboles **est totale**; * **dynamique** : les bibliothèques partagées ne sont pas incluses dans le binaire. La résolution des symboles ne peut-être que partielle. Dans le cas de la compilation **dynamique**, le *linker* doit identifier les symboles appartenant aux bibliothèques partagées. Leur résolution se fera alors au moment de l'exécution. ## Exécution L'exécution d'un programme se passe en plusieurs étapes : 1. le noyau est averti que qu'il doit charger le fichier binaire; 2. l'élément chargé du chargement de notre binaire se charge de l'analyser; 3. le noyau alloue de la mémoire pour notre programme; 4. il *map* la mémoire allouée avec les éléments du programme; 5. il est maintenant temps de passer le flot d'exécutions au programme. Bien évidement le binaire n'est pas chargé tel quel en mémoire, d'où l'étape *d'analyse*. Sous Linux et les système Unix en général, le format d'exécutable utilisé est ELF. ce format, largement documenté, sert de base pour le *loader* ### Segments et sections C'est deux éléments qui composent un fichier binaire au format ELF sont souvent source de confusion. *Les segments* contiennent des éléments nécessaires à l'exécution alors que *les sections* contiennent des informations utiles pour les liens, la relocation et la résolution de symboles. Les *sections* sont des composantes des *segments*. Prenons l'exemple du segment `LOAD` qui contient des sections `.data`, `.got`, `.bss` ... Elles contiennent soit des données brutes: * `.text` : du code; * `.data` : des données initialisées comme par exemple: ```c int x = 42; ``` * `.bss` : des données non initialisées * `.rodata` : des données en lecture seule comme les constantes statiques ## Le format ELF Comme nous le disions, c'est le format binaire d'enregistrement de code compilé utilisé par la plupart des systèmes de type *Unix*. Il commence par les nombres magiques[^n_magic] `0x7fELF`. Il se compose ensuite d'un entête fixe. Celui ci contient (entre autres) l'*endianess* (big ou little), l'ABI, le type (bibliothèque, exécutable), l'architecture cible. Après viens le *Program Header Table* qui contient des segments et informations nécessaire à la création du processus en mémoire. Ensuite la *Section Header Table* référençant et décrivant les sections Enfin le fichier ELF contient les données référencées par ces deux tables. [^n_magic]: dans notre cas, ensemble de caractères utilisés pour désigner un format de fichier. ## ELF : Pratique ### Propriété de `ls` Le plus simple pour obtenir des information est d'utiliser le programme `file`. ```shell $ file /bin/ls ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=4d53da289d128a5ccee3f944db35244bf91c7a99, for GNU/Linux 3.10.0, not stripped ``` Nous apprenons donc que cet exécutable est un ELF 64bits pour uns architecture *Intel x86_64*, lié dynamiquement, chargé par la bibliothèque `ld-linux-x86-64`. Ici le binaire est *not stripped* : les différents *labels* ne sont pas supprimés (ici pour NixOS). Il est aussi possible de récolter plus d'informations avec `readelf` notamment la version de la *libc* utilisée. Ce genre d'information peut permettre de mener une attaque. ### les sections `.plt` et `.got` Ces différentes sections permette la résolution de symboles. `.plt` signifie *Procedure Linkag Table* et `.got` *Global Offset Table*. On parle alors de relocation. Pour la section `.plt`, toutes ses entrées sont initialisées pour pointr vers l'editeur du lien et non l'adresse de la bonne fonction. C'est lors de premier appel de la fonction en question que sont adresse sera résolue - elle st donc effectuée au *runtime* - et mise à jour. On parle alors de *lazy symbols binging* La section `.got` fait plus office d'annuaire référençant juste les adresss de fonctions mais aussi de variables globales des bibliothèques). #### `.got` et `plt.got Ces deux sections sont utilisés pour les symboles ayant besoin d'être résolus **au moment du chargement du binaire en mémoire**. #### `.plt` et `.got.plt` Ces deux sections se chargent de la résolution fainéante. ### Segments et sections avec `realelf` #### Le segment `GNU_STACK` Il permet la configuration de la pile lorsque le binaire est chargé. Il sert par exemple à la mise en place de protection (pile non exécutable par exemple). #### Le segment `GNU_RELRO` Il indique quelles régions de la mémoire doivent être marquées en lecture seule une fois la résolution des symboles effectuée. Il existe deux type : * **full**: `.got` et `.got.plt` sont passés en lecture seule * **partiel**: seule la `.got` est en lecture seule. *[ELF]: Executable and Linkable Format ## Le format PE PE pour *Portable Execution* est le format de binaire utilisé par Microsoft Windows. Comme pour Linux, un *loader* réalise l'analyse et le chargement du binaire en mémoire. Dans ce format, la notion de segment n'existe pas, mais il se compose de 3 entêtes : * un reliquat de l'ère MS DOS avec comme *magic number* `MZ`. cette section affiche un message d'erreur du style *Ce programme ne n'exécute pas en mode MSDOS* lors de son exécusion en mode DOS; * une entête *PE* avec comme *magic number* `PE`; * une entête optionnelle. Il se compose aussi: * une partie *data directories*; * une table des sections (`text`, `data`, etc.); * les sections en questions (avec leurs entêtes). ### CFF explorer Ce programme disponible uniquement pour Windows permet d'analyser les fichiers binaires (exécutable, bibliothèques, etc.). ### Entête optionnelle Elle est quasiment tout le temps présente et permet notamment de définir le point d'entrée (*entrypoint*), d'activer les protections comme l'ASLR ou autre mécanismes de sécurité. ### Entête *data directories* Elle contient la localisation et la taille de chaque `directory`. Ces *directories* sont de plusieurs natures : * signatures cryptographiques; * icônes; * configurations; * *export-table* : référençant les fonctions exportées par le binaire; * *import-table* : référençant les DLL importées par le binaire, les fonctions issues de ces DLL sont référencées dans l'*Imports Address Table*; * *relocation table* : table référençant les relocations. Plusieurs *directories* peuvent appartenir à une section. ### Entêtes de sections Elles contiennent les information relatives à leurs sections respectives : nom, adresse virtuelle, taille, droit d'accès.