--- title: "Sécurité système : initiation au reverse engeneering" date: 2023-09-21 tags: ["ingénieurie inverse", "assembleur"] categories: ["Sécurité système", "Cours", "TD"] --- Nous avons revu dans le cours précédents les bases du fonctionnement d'un binaire de la compilation à son exécution. Dans cette partie nous allons appréhender les bases de l'ingénierie inverse. À l'issue de celle-ci nous devrions être en mesure de réaliser de petits *crackme*. L'étape de compilation est en quelque sorte destructrice : Il n'est plus possible à partir du binaire compilé de retrouver le code source correspondant. Mais il est tout de même possible de **désassembler** : on reconstitue un code assembleur. Ainsi afin de faire du reverse engineering, il est nécessaire de **comprendre la compilation**, de savoir de quoi se **compose un binaire** et comment **il est exécuté**. Une compréhension de l'assembleur, des type de structures qu'il manipule est primordiale. Il est aussi nécessaire de maîtriser les outils à notre disposition, de savoir coder un minimum (en *C*, *Python*). ## La gestion de la mémoire Les sections `.rodata`, `.bss` et `.data` contiennent des données mémoires. Deux structures existent pour les manipuler : la pile et le tas. ### La pile Elle manipule des données statiques. C'est une structure de type LIFO (*Last In First Out) qui croit vers le bas : plus on descend plus les adresse augmentent. Sa taille est alignées sur un *int32* ou *int64* en fonction de l'architecture (32 ou 64 bits). *Deux "fonctions"* permettent de la manipuler : `push()` afin de placer un élément -- la pile descend alors d'une case -- et `pop()` pour reprendre un élément -- la pile remonte d'un élément. Elle se situe **dans l'espace mémoire d'un processus**, ainsi chaque processus a sa pile. Elle a une structure linéaire; son accès est **rapide** (pas d'allocation de mémoire à effectuer) mais la **taille des éléments que l'on peut y stockée est limitée**. ### Le tas Il manipule des données dynamiques représentées par une structure de données hiérarchisée. Le tas est lui aussi positionné dans l'espace mémoire d'un processus. Il n'est pas limité en taille (enfin presque pas...) mais nécessite de coûteuses allocations mémoire qui le rend plus lent. Le développeur peut lui même allouer de la mémoire sur le tas avec la présence de fonctions comme `malloc()` (et ne pas oublier de les libérer avec `free()`). L'implémentation de ces mécanismes mémoire dépendent de **l'allocateur**. ## Initiation au reverse Engineering ### L'assembleur C'est un langage de bas niveau mais encore compréhensible par un humain (initié...). Il est dépendant de l'architecture, prenons par exemple le code *C* suivant: ```c int sum (int a, int b ){ return a +b; } ``` Voici ce code en assembleur *x86_64* : ```asm sum(int, int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov DWORD PTR [rbp-8], esi mov edx, DWORD PTR [rbp-4] mov eax, DWORD PTR [rbp-8] add eax, edx pop rbp ret ``` Et en assembleur *ARM64*: ```asm sum(int, int): sub sp, sp, #16 str w0, [sp, 12] str w1, [sp, 8] ldr w1, [sp, 12] ldr w0, [sp, 8] add w0, w1, w0 add sp, sp, 16 ret ``` On parle alors d'ISA pour *Instruction Set Architecture*, cela représente le jeu d'instruction disponible. Nous y trouvons les opérations élémentaires : addition, soustraction, multiplication, division, et / ou (exclusifs ou non) etc. Ici mon manipule les registres et la mémoire directement. Une *ISA* ne contient pas d'opérateur avancés comme les structures de contrôles que nous pouvons trouver dans les langages de haut niveau (`while ...`, `if ... else ...`, `for ...`). ### L'assembleur x86 (32 et 64 bits) Tout comme pour le cours de sécurité logicielle, nous utiliserons principalement l'assembleur *x86_32* pour ce cours (et quelques fois sa version 64). Mais par contre nous utiliserons **la syntaxe *Intel*** majoritairement utilisée dans le monde de l'ingénierie inverse. Cette syntaxe est plus simple : * les suffixes de mnémoniques pour n'existes pas en syntaxe Intel, ainsi `movl`, `movw` ou encore `movb` deviennent **`mov`**; * les préfixes sur les registres et immédiats disparaissent : `%eax`, `$1`, `$0x0ff` deviennent **`eax`, `1` et `0x0ff`**; * l'ordre des opérandes est inversé : ce n'est plus `source, destination` mais **`destination, source`**. `movl $1, %eax` devient **`mov eax, 1`** * les accès indirect à la mémoire sont aussi plus simple : `(%eax)` devient `[eax]` et `3(%eax)` devient `[eax + 3]` voir le cours [accès à la mémoire de sécurité logicielle]({{}}) Le vocablulaire reste le même que celui vu lors des cours [d'introduction de sécurité logicielle]({{ref "secu_logicielle/1_introduction/index.md"}}) ### L'assembleur x86 : les registres LEs registres sont de petits espace memoire directement intégrés au processeur. Ce sont les espace mémoires les plus rapides disponible sur un ordinateur, mais aussi les plus petits. Il sont dédiés entre autres au stockage de données. Certains de ces registres ont des **rôles bien déterminés** par exemples : * `eip` pour *extended instruction pointer* pointe vers la prochaine instruction à exécuter[^eip], il porte aussi le nom de *Compteur Ordinal*. **il ne peut être modifié** contrairement à tous les autres registres; * `ebp` pour `extended base pointeur` pointe vers le bas de la pile; * `esp` pour `extented stack pointer` pointe lui vers le haut de la pile. Tous ces registres sont noté *extended* dans leurs versions **32 bits** Certains autres ont des spécificité, mais il est tout à fait possible pour le programmeur de les utiliser à sa guise : * `ecx` par exemple est utilisé comme compteur dans certaines instructions de repétition(typiquement les boucles); * `esi` et `edi` utilisés comme source (*extended source index*) et destination (*extended destination index*) pour certaines instructions de copie. Les registres peuvent être découpés en sous registres : > `a` 8 bits -> `ax` 16 bits -> `eax` 32 bits -> `rax` 64 bits Il est d'ailleurs possible de découper `ax` en deux registes de 8bits : `al` (bits 0 à 7) et `ah` (bits 8 à 15). Ces découpes permettent certaines optimisations comme par exemple le **stockage de deux entiers 16 bits** dans un registre 32bits ou encore pour les instruction se basant sur **l'interprétation du contenu** des registres. Il existe aussi des registes d'états, mais nous les avons vu [en sécurité logicielle]({{}}) DAs le cadre de ce cours, le *Zero Flag*, *Sign Flag* et *Carry flag* sont importants. Le carry flag correspond à la présence d'une retenue [voir sur Wikipedia][carry_flag]. Pour rappel, voici un exemple d'utilisation du *Zero flag* : ```asm start: mov eax, 1 dec eax ; décrémente eax ; comme le résultat de la précédente instruction est 0 ; alors notre programme va forcément brancher... jz hell ; [...] hell: ``` [^eip]: en x86, dans certaine ISA, il pointe l'instruction en cours). [carry_flag]:https://fr.wikipedia.org/wiki/Indicateur_de_retenue ## Pratique ### IDA Free C'est un outils gratuit et multi plate-forme d'analyse de binaire. Il existe aussi plusieurs versions payantes (pro, home, corporate ...). La version free comporte un desassembleur, un décompilateur "cloud" pour x86_64 seulement. Il permet la manipulation de binaire au formats différents (ELF, PE Mach-O). Il s'interface aussi avec des débogueras notamment GDB. C'est un outil puissant mais difficile à prendre en main. ### Étudions `ls` (binaire ELF) Nous ouvrons notre binaire ls modifié avec `lift` lors du précédent TD pour l'étudier avec *IDA Free*. #### question b `strcpy` se trouve dans la section `.dynsym`, section des symboles résolus dynamiquement lors de son premier appel. #### question c Dans IDA, le code couleur utilisé entre autres dans la section `.dynsym` correspond aux éléments fournis à l'extérieur de notre binaire, comme dans les bibliothèques partagées. #### question d Les chiffres hexadécimaux dans ne nom de la fonction `sub_xxxxxx` correspondent aus décalage (*offset*) de celle-ci par rapport à l'adresse de chargement de notre binaire. ### Les raccourcis clavier Ils sont une part importante de l'utilisation d'IDA, en voici quelques-uns: | raccourcis | fonction | commentaires | |------------|----------|--------------| | crtl + w | sauvegarder la base de données | très utile cat IDA est instable | ctrl + shitf + w | effectuer un snapshot de la base de données | CF au dessus | ctrl + z | annuler | | | n | renommer un objet (variable, fonction, etc.) | hexrays | | y | modifier le type d'un objet (variable, fonction, etc.) | asm / hexrays | | alt + a | interpréter l'objet comme une chaine de caractères | | | h | interpréter la selection comme un décimal / hexadécimal | asm / hexrays | | r | interpréter la selection comme un caractère | asm / hexrays | | c | interpréter la portion comme du code asm | asm | | d | interpréter la portion comme data | asm | | u | retirer l'interprétation | asm | | : ou ;|ajouter un commentaire au niveau de l'instruction | visualiseur | | insert | ajouter un commentaire avant l'instruction | asm / hexrays | | espace | cycle entre la vue graph et linéaire | hexrays | | F5 | décompiler (si possible) | | | entrée ou double-clic | su un symbole -> aller où pointe le symbole | asm / hexrays | | echap | revenir à la position précédente - en arrière | asm / hexrays | | ctrl + entrée | revenir à la position précédente - en avant | asm / hexrays | | x | afficher les cross références *xref* d'un objet | asm / hexrays | | alt + t | recherche de texte | | | alt + b | recherche de motif binaire | | | alt + i | rechetche un *immédiat* | | | tab | passer de la vue *asm* à la vue hexrays | | | ctrl + e | afficher les points d'entrées du binaire | | | maj + F1 | afficher la fenêtre des types locaux | | | maj + F12 | afficher la fenêtre des chaîne de caractères | | Il faut voir IDA comme un bloc note accompagnant le travail d'ingénierie inverse Il faut **absolument** documenter au fur et à mesure des investigations et faire autant d'instantanés que possible (stabilité...). ### Analyse du binaire `mysecrets` #### Question 1 Dans IDA, lorsque nous faisons `ctrl+e` nous atterrissons dans la liste des points d'entrées du binaire chargé. Dans le cas de `mysecret`, nous atterrissons dans la fonction `main:`. Ce qui est normal pour un binaire ELF. #### Question 2 Pou trouver la section `.rodata`, il suffit de trouver la liste des segments (mais pas au sens ELF...) avec le raccourci clavier `ctrl+s`. Cette section contient les données en lecture seule, ici toutes les *strings* de notre binaire. #### Question 3-a Dasn le code assembleur, il semble y avoir 3 arguments. IDA nous les donne en commentaire : ```asm ebp + arg0 ebp + arg4 ebp + arg8 ``` #### Question 3-b La convention d'appel utilisées ici est `__cdecl` (pour *C declaration*), c'est la convention utilisés par le langage C et C++ -- la convention d'appel dépend aussi de l'ABI système, du compilateur ). Ici les arguments sont placés sur la pile par la fonction appelante (*caller*). Il sont placé dans l'ordre inverse, prenons comme exemple le programme *C* suivant : ```c int add_3 ( int a, int b, int c){ // [...] } main() { add_3 (1, 2, 3); } ``` Ce qui donnera en assembleur : ```asm main: push ebp ; backup current base pointer mov ebp, esp ; get the new base pointer push 3 push 2 push 1 call add_3 ; [...] ``` #### Question 3-c Les instructions `push epb` et `mov epb, esp` permettent de mettre en place le *base pointer* après l'avoir sauvegardé pour assurer sa remise en place lors du retour de notre fonction `secrets`. #### Question 3-d La décente de `0x18` dans la pile permet à la fonction apellée de mettre en place un espace pour les variables locales.