Add the end of system security part 3

This commit is contained in:
Yorick Barbanneau 2023-11-19 23:52:05 +01:00
parent a0d73353e6
commit 6a44c422e0

View file

@ -67,7 +67,7 @@ le voici donc:
`-IntegerLiteral 0x56028bd93e58 <col:10> 'int' 0 `-IntegerLiteral 0x56028bd93e58 <col:10> 'int' 0
``` ```
#### Langage intermédiaire (Middle-end) ### Langage intermédiaire (Middle-end)
Maintenant, regardons le code LLVM produit par clang: Maintenant, regardons le code LLVM produit par clang:
@ -99,14 +99,14 @@ declare i32 @puts(ptr noundef) #1
#### Optimisation (Middle-end) #### Optimisation (Middle-end)
Voici la commande permettant de lancer les optimisations sur le code Voici la commande permettant de lancer les optimisations sur le code
intermediaire LLVM: intermédiaire LLVM:
```shell ```shell
opt -S -O2 main.ll opt -S -O2 main.ll
``` ```
Le code produit contient plus les élements jugés inutiles comme ici `argv` et Le code produit contient plus les éléments jugés inutiles comme ici `argv` et
`argc` : `argc` :
```text ```text
@ -133,15 +133,15 @@ code assembleur:
llc main.ll -o main.s llc main.ll -o main.s
``` ```
Le code obtenu sera cette-fois dépendant de l'architecture cible. Une fois Le code obtenu sera cette fois dépendant de l'architecture cible. Une fois
trasformé en code machine, il se sera pas directement exécutable, il manque transformé en code machine, il se sera pas directement exécutable, il manque
l'édition de lien. l'édition de lien.
## LLVM ## LLVM
LLVM signifie *Low Level Virtual Machine* est une spécification d'une LLVM signifie *Low Level Virtual Machine* est une spécification d'une
**représentation intermédiaire** (LLVM-IR) accompagnée d'un ensemble d'outils **représentation intermédiaire** (LLVM-IR) accompagnée d'un ensemble d'outils
qui communiquent autour de rette réprésentation. qui communiquent autour de cette représentation.
LLVM se compose de **modules**, son langage est de type RISC fortement typé et LLVM se compose de **modules**, son langage est de type RISC fortement typé et
non signé - certaine opération le sont par contre comme `div` et `sdiv`. non signé - certaine opération le sont par contre comme `div` et `sdiv`.
@ -155,7 +155,7 @@ double polynome(float x) {
} }
``` ```
Voici la représenation LLVM : Voici la représentation LLVM :
```llvm ```llvm
; Function Attrs: mustprogress nofree nosync nounwind readnone willreturn uwtable ; Function Attrs: mustprogress nofree nosync nounwind readnone willreturn uwtable
@ -186,7 +186,7 @@ void if_then_else(int a, int b, int c) {
else else_(c); else else_(c);
} }
``` ```
le code correspondant en représentation intermediaire: le code correspondant en représentation intermédiaire:
```llvm ```llvm
%4 = icmp eq i32 %0, 0 %4 = icmp eq i32 %0, 0
@ -215,7 +215,7 @@ if (v < 10)
b = a; b = a;
``` ```
Le code *LLVM-IR* coorespondant est le suivant: Le code *LLVM-IR* correspondant est le suivant:
```llvm ```llvm
a1 = 1; a1 = 1;
@ -225,30 +225,30 @@ b = PHI(a1, a2);
``` ```
L'instruction `b = PHI(a1, a2)` permet de faire une *affectation conditionnelle* L'instruction `b = PHI(a1, a2)` permet de faire une *affectation conditionnelle*
de `b`. Le fonctionement de phi est le suivant: de `b`. Le fonctionnement de phi est le suivant:
```llvm ```llvm
%10 = PHI i32 [valeur, label] [valeur, label] %10 = PHI i32 [valeur, label] [valeur, label]
``` ```
`PHI` peut faire référence à des variables non déclarées. `PHI` peut faire référence à des variables non déclarées.
### Memoire ### Mémoire
LLVM-IR dispose de quelques instructions pour l'accès à la mémoire comme `load`, LLVM-IR dispose de quelques instructions pour l'accès à la mémoire comme `load`,
`store`, `cmpxchg`, `store`, `cmpxchg`,
### Types complexe ### Types complexe
*LLVM-IR* dispose de plusieurs rypes complexe comme: *LLVM-IR* dispose de plusieurs types complexe comme:
* **les vecteurs** sour la forme `<4 x i32>` représentant 4 entiers de 32 bits; * **les vecteurs** sous la forme `<4 x i32>` représentant 4 entiers de 32 bits;
* **les tableaux** sous la forme `i32[10]` * **les tableaux** sous la forme `i32[10]`
* **les structures** sous la forme `my_struct = type { i32, i32}` * **les structures** sous la forme `my_struct = type { i32, i32}`
### Les exceptions ### Les exceptions
*LLVM-IR* permet la gestion des exceptions, mais nous n;utiliserons pas ces *LLVM-IR* permet la gestion des exceptions, mais nous n;utiliserons pas ces
mécanismes das le cadre de ce cours. LLVM dispose de fonction intrinsèques pour mécanismes dans le cadre de ce cours. LLVM dispose de fonction intrinsèques pour
la gestion des exceptions préfixée par `llvm.eh`. Toutes les fonctions la gestion des exceptions préfixée par `llvm.eh`. Toutes les fonctions
disponibles sont référencées [sur cette page][l_eh_exception] disponibles sont référencées [sur cette page][l_eh_exception]
@ -260,14 +260,14 @@ L'obfuscation a pour but principal de ralentir au maximum l'opération de revers
engineering. Il est souvent question de protéger les parties les plus sensibles, engineering. Il est souvent question de protéger les parties les plus sensibles,
celle contenant des clés de chiffrement, des algorithmes etc. celle contenant des clés de chiffrement, des algorithmes etc.
Cette protection sera de toutes manières éphemère et elle a un prix, voire même Cette protection sera de toutes manières éphémère et elle a un prix, voire même
plusieurs: plusieurs:
* exécution plus lente * exécution plus lente
* consommation mémoire alourdie * consommation mémoire alourdie
* binaire plus volumineux * binaire plus volumineux
Il faut alors trouver un compromis. nous allons voir les techniques utilisées. Il faut alors trouver un compromis. Nous allons voir les techniques utilisées.
### Obfusquer les instructions ### Obfusquer les instructions
@ -282,7 +282,7 @@ A ^ B == A + B - (A & B)<<1
### Prédicat opaques ### Prédicat opaques
Il existe plusiers façon d'opacifier certaines partie du code. Il est pas Il existe plusieurs façon d'opacifier certaines partie du code. Il est pas
exemple possible d'ajouter du **code mort** : une condition toujours vérifiée exemple possible d'ajouter du **code mort** : une condition toujours vérifiée
mène au code "correct" : mène au code "correct" :
@ -298,7 +298,7 @@ else {
} }
``` ```
Il est aussi possible de remplacer certaines fonctions mathematiques par Il est aussi possible de remplacer certaines fonctions mathématiques par
certaines autres par exemple: certaines autres par exemple:
``` ```
@ -312,13 +312,13 @@ formule suivante:
pi = 4 * (1 - 1/3 + 1/5 - 1/7 + ... + 1/N); pi = 4 * (1 - 1/3 + 1/5 - 1/7 + ... + 1/N);
``` ```
Après suffisement d'itération, la marge d'erreur est en dessous de 0,2. Après suffisament d'itération, la marge d'erreur est en dessous de 0,2.
### Tester ses obfuscations ### Tester ses obfuscations
Il est très important de **tester les obfuscations** déjà pour ne pas introduire de Il est très important de **tester les obfuscations** déjà pour ne pas introduire de
bugs, mais aussi pour vérifier qu'elles survivent aux optimisations. Il est bugs, mais aussi pour vérifier qu'elles survivent aux optimisations. Il est
possible de faires des tests unitaires, des tests par *fuzzing*, test de possible de faire des tests unitaires, des tests par *fuzzing*, test de
reproductibilité. reproductibilité.
Il faut savoir que certaines optimisations effectuées par les compilateurs Il faut savoir que certaines optimisations effectuées par les compilateurs
@ -335,3 +335,106 @@ exemple:
// est ce que I est une fonction // est ce que I est une fonction
isa<CAllInst>(I); isa<CAllInst>(I);
``` ```
## Analyse et obfuscation dynamique
Un *"reverver"* va à un moment donné analyser un binaire lors de son exécution,
le cas le plus simple est l'utilisation d'un debogeur logiciel (*gdb* ou
*x64dbg* par exemple). Mais il est aussi possible de lancer le binaire dans un
émulateur (l'analyste a alors un contrôle total de l'environnement d'exécution).
**En tant que défenseur** nous voulons essayer d'éviter de détecter les
éléments suivants:
* Les débogueurs;
* L'instrumentations -- exécutions dans des environnements spécifiques comme
les machines virtuelles;
* Les modifications de codes.
*Gdb* par exemple utilise `ptrace`, mais ce dernier ne peut être lancé **qu'une
seule fois**, il est alors possible de détecter s'il est déjà lancé. La
détection de points d'arrêts (*breakpoint*) est plus complexe, surtout sur
l'architecture *x86* car **les instructions sont de taille variable**.
Dans un autre registre, il est possible de vérifier l'intégrité du code via des
fonction de hashages : **on créée un condensat de la fonction** que l'on stocke dans
le binaire. Au moment de l'exécution on compare le condensa stocké avec celui de
la fonction calculé lors de l'exécution. Cette méthode comporte **beaucoup de
contraintes**:
* Du travail à effectuer au niveau du *linker*;
* Il faut gérer la relocation;
* Le coût au nouveau des ressources est important.
### Quand?
Il est important de déterminer le moment opportun pour effectuer les différentes
vérification.
Au démarrage par exemple? Il y a alors **peu d'impact sur les performances**
mais les protections sont faciles à détecter (`ptrace` par exemple).
Périodiquement? Mais il existe un risque d'injecter des vérifications dans du
**code chaud**, ou encore de ne jamais voir le code de vérification s'exécuter.
### Réponse à une attaque
Une fois une attaque sur le code détectée que faire?
* *Ralentissement*;
* *comportements aléatoires*;
* *Crash* de l'application, que se soit dès l'attaque ou plus tard histoire de
rendre la protection plus difficile à identifier;
Il peut être utile de **vider la pile** avant de planter complètement
l'application, histoire de compliquer le travail du *reverser*.
### Les builtins
Ce sont des vérifications intégrées au binaire. Ces vérifications **n'existent
pas dans le code source original**. En tant que défenseur, il est possible
d'écrire du codes dans l'IR, autant dire que c'est **long et fastidieux**!. Il
est aussi possible de faire de la **compilation à la volée**, cette solution
semble **idéale** mais elle est compliquée : dans le cas de *LLVM* il faut faire
appel à *clang* dans une passe (LLVM permet dispose d'option de JIT)
Il est aussi possible de passer par **des objets pré-générés** qui permet une
frontière claire entre la bibliothèque de protections et le code à protéger. Il
faut aussi faire en fonction du runtime.
*[JIT]: Just In Time
### L'exécution symbolique
Il est ici question de se placer du côté du *reverser*. Ici il faut travailler
avec un solveur [z3][z3] par exemple, nous lui fournissons une représentation du
code, les résultats que nous voudrions obtenir et il se charge de trouver les
entrées.
```
[binaire] --> [représenation] --> [solveur]
```
Cette approche gagne en complexité avec les structures de contrôles. Il est
alors nécessaire de déplier les boucles par exemple.
### Protection par machine virtuelle
Nous avons déjà évoqué ce type de protection, le binaire contient une machine
virtuelle qui interprètera un bytecode spécifique. Il est même possible
d'ajouter un niveau supplémentaire de protection en chiffrant le bytecode par
exemple.
En pratique, ce type de protection est **très peu utilisé**.
### Triton
[Triton][triton] est un bibliothèque d'analyse de binaire utilisés dans
l'ingénierie inverse. Cependant il contient certaines limites : approximations,
boucles etc.
Il est utile pour "casser" les *machines virtuelles* : Triton *construit* une
représentation intermédiaire du binaire (ou de certaines parties) et explore les
différents chemins. Il peut même construire du code *LLVM-IR*.
[triton]:https://github.com/JonathanSalwan/Triton