diff --git a/content/secu_systeme/1_introduction/index.md b/content/secu_systeme/1_introduction/index.md new file mode 100644 index 0000000..dcb23ad --- /dev/null +++ b/content/secu_systeme/1_introduction/index.md @@ -0,0 +1,265 @@ +--- +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. + +## 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. + +### Segments et sections avec `realelf` + +#### Le segment `GNU_STACK` + +Il permet la configuration de la pile lorsque le binaire est charge. 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