Add system security introduction
This commit is contained in:
parent
2f023a33a5
commit
a17a401f00
1 changed files with 265 additions and 0 deletions
265
content/secu_systeme/1_introduction/index.md
Normal file
265
content/secu_systeme/1_introduction/index.md
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue