339 lines
13 KiB
Markdown
339 lines
13 KiB
Markdown
---
|
|
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.
|