From d129c59be294361cc6057b4c550ba4d7d21cc489 Mon Sep 17 00:00:00 2001 From: Yorick Barbanneau Date: Tue, 24 Oct 2023 01:19:10 +0200 Subject: [PATCH] Add obfuscation lesson --- .../3_compilation_obfiscation_llvm/index.md | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 content/secu_systeme/3_compilation_obfiscation_llvm/index.md diff --git a/content/secu_systeme/3_compilation_obfiscation_llvm/index.md b/content/secu_systeme/3_compilation_obfiscation_llvm/index.md new file mode 100644 index 0000000..324ff76 --- /dev/null +++ b/content/secu_systeme/3_compilation_obfiscation_llvm/index.md @@ -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]({{}}) + +### Vie d'un hello world + +Voyons maintenant ce qu'il se passe dans le cadre du code suivant: + +```c +#include +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 line:3:5 main 'int (int, char **)' + |-ParmVarDecl 0x56028bd93b68 col:14 argc 'int' + |-ParmVarDecl 0x56028bd93be8 col:27 argv 'char **' + `-CompoundStmt 0x56028bd93e88 + |-CallExpr 0x56028bd93e00 'int' + | |-ImplicitCastExpr 0x56028bd93de8 'int (*)(const char *)' + | | `-DeclRefExpr 0x56028bd93d70 'int (const char *)' Function 0x56028bd8f730 'puts' 'int (const char *)' + | `-ImplicitCastExpr 0x56028bd93e40 'const char *' + | `-ImplicitCastExpr 0x56028bd93e28 'char *' + | `-StringLiteral 0x56028bd93d90 'char[15]' lvalue "Hello, world!\n" + `-ReturnStmt 0x56028bd93e78 + `-IntegerLiteral 0x56028bd93e58 '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 = '' +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(I); +```