Add the end of system security part 3
This commit is contained in:
parent
a0d73353e6
commit
6a44c422e0
1 changed files with 123 additions and 20 deletions
|
@ -67,7 +67,7 @@ le voici donc:
|
|||
`-IntegerLiteral 0x56028bd93e58 <col:10> 'int' 0
|
||||
```
|
||||
|
||||
#### Langage intermédiaire (Middle-end)
|
||||
### Langage intermédiaire (Middle-end)
|
||||
|
||||
Maintenant, regardons le code LLVM produit par clang:
|
||||
|
||||
|
@ -99,14 +99,14 @@ declare i32 @puts(ptr noundef) #1
|
|||
#### Optimisation (Middle-end)
|
||||
|
||||
Voici la commande permettant de lancer les optimisations sur le code
|
||||
intermediaire LLVM:
|
||||
intermédiaire LLVM:
|
||||
|
||||
|
||||
```shell
|
||||
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` :
|
||||
|
||||
```text
|
||||
|
@ -133,15 +133,15 @@ code assembleur:
|
|||
llc main.ll -o main.s
|
||||
```
|
||||
|
||||
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
|
||||
Le code obtenu sera cette fois dépendant de l'architecture cible. Une fois
|
||||
transformé en code machine, il se sera pas directement exécutable, il manque
|
||||
l'édition de lien.
|
||||
|
||||
## LLVM
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
; 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);
|
||||
}
|
||||
```
|
||||
le code correspondant en représentation intermediaire:
|
||||
le code correspondant en représentation intermédiaire:
|
||||
|
||||
```llvm
|
||||
%4 = icmp eq i32 %0, 0
|
||||
|
@ -215,7 +215,7 @@ if (v < 10)
|
|||
b = a;
|
||||
```
|
||||
|
||||
Le code *LLVM-IR* coorespondant est le suivant:
|
||||
Le code *LLVM-IR* correspondant est le suivant:
|
||||
|
||||
```llvm
|
||||
a1 = 1;
|
||||
|
@ -225,30 +225,30 @@ b = PHI(a1, a2);
|
|||
```
|
||||
|
||||
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
|
||||
%10 = PHI i32 [valeur, label] [valeur, label]
|
||||
```
|
||||
`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`,
|
||||
`store`, `cmpxchg`,
|
||||
|
||||
### 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 structures** sous la forme `my_struct = type { i32, i32}`
|
||||
|
||||
### Les exceptions
|
||||
|
||||
*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
|
||||
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,
|
||||
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:
|
||||
|
||||
* exécution plus lente
|
||||
* consommation mémoire alourdie
|
||||
* 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
|
||||
|
||||
|
@ -282,7 +282,7 @@ A ^ B == A + B - (A & B)<<1
|
|||
|
||||
### 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
|
||||
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:
|
||||
|
||||
```
|
||||
|
@ -312,13 +312,13 @@ formule suivante:
|
|||
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
|
||||
|
||||
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
|
||||
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é.
|
||||
|
||||
Il faut savoir que certaines optimisations effectuées par les compilateurs
|
||||
|
@ -335,3 +335,106 @@ exemple:
|
|||
// est ce que I est une fonction
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue