Add obfuscation lesson
This commit is contained in:
parent
aee8afcdf1
commit
d129c59be2
1 changed files with 337 additions and 0 deletions
337
content/secu_systeme/3_compilation_obfiscation_llvm/index.md
Normal file
337
content/secu_systeme/3_compilation_obfiscation_llvm/index.md
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
---
|
||||||
|
title: "Sécurité système : Introduction à la compilation et obfuscation avec llvm"
|
||||||
|
date: 2023-10-12
|
||||||
|
tags: ["LLVM", "assembleur"]
|
||||||
|
categories: ["Sécurité système", "Cours", "TD"]
|
||||||
|
mathjax: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Le but de ce cours est de comprendre ce qui se passe lors de la création de
|
||||||
|
binaire. Nous nous concentrerons sur clang / LLVM dans le cadre de la sécurité,
|
||||||
|
que se soit en défense ou en attaque.
|
||||||
|
|
||||||
|
## compilation en trois étapes
|
||||||
|
|
||||||
|
Il est possible de découper la compilation en 3 étapes avec LLVM:
|
||||||
|
|
||||||
|
1. Le code source passe par un *frontend* avec l'analyse syntaxique et
|
||||||
|
sémantique;
|
||||||
|
2. Le résultat passe par un *middle-end* qui va procéder aux optimisations;
|
||||||
|
3. Et enfin par un *backend* qui va transformer le code optimisé en code
|
||||||
|
assembleur et réaliser des optimisations spécifique à l'architecture cible.
|
||||||
|
|
||||||
|
L'obfuscation du code se fait au niveau du *middle-end*.
|
||||||
|
|
||||||
|
Dans le cadre de la suite d'outils autour de LLVM nous avons plusieurs
|
||||||
|
*backends* (clang, flang, RetDec) qui transforme le code en entrée en code
|
||||||
|
LLVM (*middle-end*) sur lequel seront réalisées les optimisations. Et enfin
|
||||||
|
le résultat passe dans un compilateur qui le transforme en code binaire (*ARM.
|
||||||
|
x86_64, Webassembly,...*).
|
||||||
|
|
||||||
|
Nous avons déjà vu cette partie lors
|
||||||
|
[de l'introcution]({{<ref "secu_systeme/1_introduction/index.md#la-compilation">}})
|
||||||
|
|
||||||
|
### Vie d'un hello world
|
||||||
|
|
||||||
|
Voyons maintenant ce qu'il se passe dans le cadre du code suivant:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
puts("Hello, world!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
#### l'AST (Frontend)
|
||||||
|
|
||||||
|
Voyons l'AST produit avec `clang` grâce à la commande suivante avec la commande:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
clang -Xclang -ast-dump -fsyntax-only main.c
|
||||||
|
```
|
||||||
|
|
||||||
|
le voici donc:
|
||||||
|
|
||||||
|
```text
|
||||||
|
`-FunctionDecl 0x56028bd93cc0 <main.c:3:1, line:6:1> line:3:5 main 'int (int, char **)'
|
||||||
|
|-ParmVarDecl 0x56028bd93b68 <col:10, col:14> col:14 argc 'int'
|
||||||
|
|-ParmVarDecl 0x56028bd93be8 <col:20, col:27> col:27 argv 'char **'
|
||||||
|
`-CompoundStmt 0x56028bd93e88 <col:33, line:6:1>
|
||||||
|
|-CallExpr 0x56028bd93e00 <line:4:3, col:25> 'int'
|
||||||
|
| |-ImplicitCastExpr 0x56028bd93de8 <col:3> 'int (*)(const char *)' <FunctionToPointerDecay>
|
||||||
|
| | `-DeclRefExpr 0x56028bd93d70 <col:3> 'int (const char *)' Function 0x56028bd8f730 'puts' 'int (const char *)'
|
||||||
|
| `-ImplicitCastExpr 0x56028bd93e40 <col:8> 'const char *' <NoOp>
|
||||||
|
| `-ImplicitCastExpr 0x56028bd93e28 <col:8> 'char *' <ArrayToPointerDecay>
|
||||||
|
| `-StringLiteral 0x56028bd93d90 <col:8> 'char[15]' lvalue "Hello, world!\n"
|
||||||
|
`-ReturnStmt 0x56028bd93e78 <line:5:3, col:10>
|
||||||
|
`-IntegerLiteral 0x56028bd93e58 <col:10> 'int' 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Langage intermédiaire (Middle-end)
|
||||||
|
|
||||||
|
Maintenant, regardons le code LLVM produit par clang:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
clang -S -emit-llvm -Xclang -disable-O0-optnone main.c -o
|
||||||
|
```
|
||||||
|
|
||||||
|
À ce niveau, le code obtenu n'est pas optimisé:
|
||||||
|
|
||||||
|
```llvm
|
||||||
|
; ModuleID = '/home/user/code/main.c'
|
||||||
|
source_filename = "/home/user/code/main.c"
|
||||||
|
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
|
||||||
|
target triple = "x86_64-pc-linux-gnu"
|
||||||
|
@.str = private unnamed_addr constant [15 x i8] c"Hello, world!\0A\00", align 1
|
||||||
|
; Function Attrs: noinline nounwind uwtable
|
||||||
|
define dso_local i32 @main(i32 noundef %0, ptr noundef %1) #0 {
|
||||||
|
%3 = alloca i32, align 4
|
||||||
|
%4 = alloca i32, align 4
|
||||||
|
%5 = alloca ptr, align 8
|
||||||
|
store i32 0, ptr %3, align 4
|
||||||
|
store i32 %0, ptr %4, align 4
|
||||||
|
store ptr %1, ptr %5, align 8
|
||||||
|
%6 = call i32 @puts(ptr noundef @.str)
|
||||||
|
ret i32 0
|
||||||
|
}
|
||||||
|
declare i32 @puts(ptr noundef) #1
|
||||||
|
```
|
||||||
|
#### Optimisation (Middle-end)
|
||||||
|
|
||||||
|
Voici la commande permettant de lancer les optimisations sur le code
|
||||||
|
intermediaire LLVM:
|
||||||
|
|
||||||
|
|
||||||
|
```shell
|
||||||
|
opt -S -O2 main.ll
|
||||||
|
```
|
||||||
|
|
||||||
|
Le code produit contient plus les élements jugés inutiles comme ici `argv` et
|
||||||
|
`argc` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
; ModuleID = '<stdin>'
|
||||||
|
source_filename = "/home/user/code/main.c"
|
||||||
|
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
|
||||||
|
target triple = "x86_64-pc-linux-gnu"
|
||||||
|
@.str = private unnamed_addr constant [15 x i8] c"Hello, world!\0A\00", align 1
|
||||||
|
; Function Attrs: nofree noinline nounwind uwtable
|
||||||
|
define dso_local i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr #0 {
|
||||||
|
%3 = tail call i32 @puts(ptr noundef nonnull @.str)
|
||||||
|
ret i32 0
|
||||||
|
}
|
||||||
|
; Function Attrs: nofree nounwind
|
||||||
|
declare noundef i32 @puts(ptr nocapture noundef readonly) local_unnamed_addr #1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Transformation en assembleur (Backend)
|
||||||
|
|
||||||
|
Voici la commande qui permet de transformer le code intermédiaire optimisé en
|
||||||
|
code assembleur:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Prenons comme exemple le code *C* suivant :
|
||||||
|
|
||||||
|
|
||||||
|
```c
|
||||||
|
double polynome(float x) {
|
||||||
|
return 2*x*x*x + 7*x*x + 9*x + 1234;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Voici la représenation LLVM :
|
||||||
|
|
||||||
|
```llvm
|
||||||
|
; Function Attrs: mustprogress nofree nosync nounwind readnone willreturn uwtable
|
||||||
|
define dso_local double @polynome(float noundef %0) local_unnamed_addr #0 {
|
||||||
|
%2 = fmul float %0, 2.000000e+00
|
||||||
|
%3 = fmul float %2, %0
|
||||||
|
%4 = fmul float %0, 7.000000e+00
|
||||||
|
%5 = fmul float %4, %0
|
||||||
|
%6 = tail call float @llvm.fmuladd.f32(float %3, float %0, float %5)
|
||||||
|
%7 = tail call float @llvm.fmuladd.f32(float %0, float 9.000000e+00, float %6)
|
||||||
|
%8 = fadd float %7, 1.234000e+03
|
||||||
|
%9 = fpext float %8 to double
|
||||||
|
ret double %9
|
||||||
|
```
|
||||||
|
### Branchements
|
||||||
|
|
||||||
|
Il n'y a pas de structure de contrôle comme dans les langages de plus haut
|
||||||
|
niveau, mais certaines instructions permettent des branchements (conditionnels
|
||||||
|
ou non) comme `br`, `ret`, `switch`, `invoke`, `resume` ...
|
||||||
|
|
||||||
|
Prenons comme exemple :
|
||||||
|
|
||||||
|
```c
|
||||||
|
void then_(int);
|
||||||
|
void else_(int);
|
||||||
|
void if_then_else(int a, int b, int c) {
|
||||||
|
if(a) then_(b);
|
||||||
|
else else_(c);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
le code correspondant en représentation intermediaire:
|
||||||
|
|
||||||
|
```llvm
|
||||||
|
%4 = icmp eq i32 %0, 0
|
||||||
|
br i1 %4, label %6, label %5
|
||||||
|
|
||||||
|
5: ; preds = %3
|
||||||
|
tail call void @then_(i32 noundef %1) #2
|
||||||
|
br label %7
|
||||||
|
|
||||||
|
6: ; preds = %3
|
||||||
|
tail call void @else_(i32 noundef %2) #2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static Single Assignement et PHI-Node
|
||||||
|
|
||||||
|
Les instructions LLVM prennent la forme de *SSA* ce qui signifie principalement
|
||||||
|
qu'une variable peut être **assignée une seule fois**. C'est ici que les
|
||||||
|
*PHI-Nodes* entrent en jeu.
|
||||||
|
|
||||||
|
Prenons le code *C* suivant:
|
||||||
|
|
||||||
|
```c
|
||||||
|
a = 1;
|
||||||
|
if (v < 10)
|
||||||
|
a = 2;
|
||||||
|
b = a;
|
||||||
|
```
|
||||||
|
|
||||||
|
Le code *LLVM-IR* coorespondant est le suivant:
|
||||||
|
|
||||||
|
```llvm
|
||||||
|
a1 = 1;
|
||||||
|
if (v < 10)
|
||||||
|
a2 = 2;
|
||||||
|
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:
|
||||||
|
|
||||||
|
```llvm
|
||||||
|
%10 = PHI i32 [valeur, label] [valeur, label]
|
||||||
|
```
|
||||||
|
`PHI` peut faire référence à des variables non déclarées.
|
||||||
|
|
||||||
|
### Memoire
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
* **les vecteurs** sour 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
|
||||||
|
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]
|
||||||
|
|
||||||
|
[l_eh_exception]: https://llvm.org/docs/ExceptionHandling.html#exception-handling-intrinsics
|
||||||
|
|
||||||
|
## L'obfuscation
|
||||||
|
|
||||||
|
L'obfuscation a pour but principal de ralentir au maximum l'opération de reverse
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Obfusquer les instructions
|
||||||
|
|
||||||
|
Il est question de remplacer les instructions élémentaires par des équivalent
|
||||||
|
par exemple:
|
||||||
|
|
||||||
|
```text
|
||||||
|
A + B == (A & B)<<1 + (A ^ B)
|
||||||
|
A - B == (A & -B)<<1 + (A ^ -B)
|
||||||
|
A ^ B == A + B - (A & B)<<1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prédicat opaques
|
||||||
|
|
||||||
|
Il existe plusiers 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" :
|
||||||
|
|
||||||
|
```c
|
||||||
|
int predicat = F(x); // F(x) est toujours vrai
|
||||||
|
if ( predicat ) {
|
||||||
|
// donc on exécutera toujours le bon code
|
||||||
|
good_code();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// et ça c'est pour perdre le 'reverser'
|
||||||
|
useless_insanely_complex_code();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Il est aussi possible de remplacer certaines fonctions mathematiques par
|
||||||
|
certaines autres par exemple:
|
||||||
|
|
||||||
|
```
|
||||||
|
log2(x) == log10(x) - log10(2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Il est aussi possible des constantes opaque, trouver par exemple pi avec la
|
||||||
|
formule suivante:
|
||||||
|
|
||||||
|
```c
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
reproductibilité.
|
||||||
|
|
||||||
|
Il faut savoir que certaines optimisations effectuées par les compilateurs
|
||||||
|
peuvent faire penser à des obfuscations. La division etant très couteuses, elle
|
||||||
|
est remplacée par une série d'opérations plus rapide.
|
||||||
|
|
||||||
|
## Premiers TP
|
||||||
|
|
||||||
|
Il est question ici de modifier le code intermédiaire LLVM en utilisant son API.
|
||||||
|
Nous allons manipuler l'IR. LLVM est capable de faire de l'instrospection par
|
||||||
|
exemple:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// est ce que I est une fonction
|
||||||
|
isa<CAllInst>(I);
|
||||||
|
```
|
Loading…
Add table
Add a link
Reference in a new issue