Compare commits
3 commits
2f023a33a5
...
359c629860
Author | SHA1 | Date | |
---|---|---|---|
359c629860 | |||
324179092e | |||
a17a401f00 |
2 changed files with 668 additions and 0 deletions
339
content/secu_systeme/1_introduction/index.md
Normal file
339
content/secu_systeme/1_introduction/index.md
Normal file
|
@ -0,0 +1,339 @@
|
|||
---
|
||||
title: "Sécurité système : introduction"
|
||||
date: 2023-09-07
|
||||
tags: ["bibliothèque", ".got"]
|
||||
categories: ["Sécurité système", "Cours"]
|
||||
---
|
||||
|
||||
## Définition : un exécutable
|
||||
|
||||
C'est un fichier contenant un programme et identifié par le système
|
||||
d'exploitation en tant que tel ([définition par Wikipédia][l_execwiki]). Il
|
||||
existe plusieurs format d'exécutable en fonction du système d'exploitation.
|
||||
|
||||
Il ne faut pas confondre programme et scripts, le premier contenant le code
|
||||
**exécutable**, le second est **interprété** (par un programme) et passe
|
||||
éventuellement par du code intermédiaire ou [bytecode][l_bytecode] : *Portable
|
||||
Executable* Pour Microsoft Windows, *Match Object* pour Apple MacOS / iOS,
|
||||
*Executable and Linkable Format* pour beaucoup de système Unix.
|
||||
|
||||
Pourquoi compiler du code? Plusieurs avantages :
|
||||
|
||||
* un programme compilé est moins consommateur de ressources et par conséquent
|
||||
plus rapide à l'exécution;
|
||||
* pour protéger sa propriété intellectuelle (par la possibilité d'obfuscation
|
||||
du code machine généré);
|
||||
* pour se passer de l'**interpréteur** et ainsi éviter une étape
|
||||
(complexification de la pile d'exécution).
|
||||
|
||||
Le code interprété a aussi ses avantages :
|
||||
* il est indépendant de la plate-forme d'exécution;
|
||||
* en général il existe une multitude de bibliothèques utilisable rapidement
|
||||
(coucou Python);
|
||||
* le développement est simplifié et plus interactif, le langage est alors plus
|
||||
accessible et le développement peut-être plus rapide (langage de plus haut
|
||||
niveau)
|
||||
|
||||
|
||||
[l_bytecode]: https://fr.wikipedia.org/wiki/Bytecode
|
||||
[l_execwiki]: https://fr.wikipedia.org/wiki/Fichier_ex%C3%A9cutable
|
||||
|
||||
## La compilation
|
||||
|
||||
Un exécutable contient donc du code machine, le code source qui le défini est
|
||||
alors **compilé** en code machine. Mais la compilation est en fait plus
|
||||
compliquée que cette simple *"traduction"*. Elle se compose en fait de 4 grande
|
||||
phases :
|
||||
|
||||
1. le préprocessing;
|
||||
2. la compilation;
|
||||
3. l'assemblage;
|
||||
4. l'édition de liens
|
||||
|
||||
### le préprocessing
|
||||
|
||||
Chargé de modifier le code source avant la compilation. Ici le moteur de
|
||||
préprocessing se charge de remplacer les macro (`#define`), les constantes
|
||||
[^f_const], les inclusion d'entêtes (`#include`) par le code effectif;
|
||||
|
||||
[^f_const]: Voici quelques exemples de constantes de bases :
|
||||
- `__FILE__` : affiche le nom du fichier source;
|
||||
- `__LINE__` : affiche le numéro de la ligne atteinte;
|
||||
- `__DATE__` : affiche la date de compilation du code source;
|
||||
- `__TIME__` : affiche l'heure de compilation de la source.
|
||||
|
||||
### la compilation
|
||||
|
||||
C'est à cette étape que le code (`C`, `C++`, `Rust` etc.) est transforme en
|
||||
assembleur. Cette étape se compose elle-même de plusieurs parties.
|
||||
|
||||
#### L'analyse lexicale
|
||||
|
||||
Lors de cette étape, le compilateur analyse le texte du code source. Il est
|
||||
découpé en *mots* ou *tokens* définis par des **expressions rationnelles** et
|
||||
**des automates à états finis**.
|
||||
|
||||
Il revient à **l'analyseur lexical** de réaliser cette opération. Lors de
|
||||
cette étape, les *"bruits"* sont ignorés : commentaires et espaces. Les
|
||||
*lexèmes* -- représentant une instance de la petite unité de signification --
|
||||
sont ainsi définis. Si l'analyseur syntaxique rencontre un lexème inconnu, il
|
||||
lèvera une erreur.
|
||||
|
||||
Il existe plusieurs analyseur lexicaux : flex, lex[^n_lex] etc.
|
||||
|
||||
Il ne revient pas à l'analyseur syntaxique de vérifier que l'enchainement des
|
||||
lexème est correct, il transmets juste le résultat de son analyse à l'étape
|
||||
suivante.
|
||||
|
||||
[^n_lex]: analyseur en *C* co-écrit par Eric Schmidt, cofondateur de Google.
|
||||
|
||||
#### L'analyse syntaxique
|
||||
|
||||
Réalisée par **l'analyseur syntaxique** qui traite les informations reçues par
|
||||
l'analyseur lexical. Contrairement à l'étape précédente, il est question ici de
|
||||
se focaliser sur la validités syntaxique *d'une phrase*.
|
||||
|
||||
C'est un processus itératif dont le résultat est un *Arbre Syntaxique Abstrait*
|
||||
construit en fonction de la **grammaire du langage**.Il est construit au fur et
|
||||
à mesure que l'analyseur avance dans le code source. Deux outils d'analyse
|
||||
syntaxique sont principalement utilisés :
|
||||
|
||||
#### L'analyse sémantique
|
||||
|
||||
Lors de cette étape le compilateur vérifie la cohérence du code, certaines
|
||||
erreurs relevant de la grammaire seront détectées comme par par exemple :
|
||||
|
||||
```c
|
||||
// erreur sur les types
|
||||
int my_integer = "a string";
|
||||
|
||||
// Accès à une variable en dehors de son scope
|
||||
{ int x = 3;} x = 4;
|
||||
```
|
||||
Lors de l'analyse syntaxique, l'*Arbre Syntaxique Abstrait* est enrichi
|
||||
d'information sur le sens des phrases du code.
|
||||
|
||||
#### La production de code intermédiaire
|
||||
|
||||
Cette étape, le code est transformé dans un langage intermédiaire entre le
|
||||
langage de haut niveau (*C*, *C++*) et le langage machine. C'est à partir de ce
|
||||
code que le compilateur appliquera des optimisations. C'est aussi lors de cette
|
||||
étape que le code peut être offusqué afin de rendre son analyse plus complexe
|
||||
(dans le logiciel privateur, ça va de soit).
|
||||
|
||||
#### Les optimisations
|
||||
|
||||
Ici, le compilateur réalise des optimisations sur le code intermédiaire. La
|
||||
plupart de ces optimisations effectué sur le code de plus haut niveau le
|
||||
rendraient moins compréhensible et pourrait se révéler plus fastidieux à écrire.
|
||||
|
||||
#### La génération de code assembleur
|
||||
|
||||
Ici le code intermédiaire optimisé est transformé en assembleur. Le compilateur
|
||||
créé les fichiers `*.S`. On appelle aussi cette étape **génération de code
|
||||
natif**. Lors de cette étape, le code généré est spécifique à une architecture
|
||||
donnée et ce afin de profiter des instructions disponible sur celle-ci (*MMX*,
|
||||
*SSE*, *NEON* etc.).
|
||||
|
||||
#### L'assemblage
|
||||
|
||||
Le compilateur créé un fichier objet `*.o` par fichier source. Ces fichiers
|
||||
objets sont bien des binaires, il contienne déjà les segments nécessaires
|
||||
(`.text`, `.data` ...) dans un format dépendant du système cible (PE pour
|
||||
Microsoft Windows©, Mach-O pour Apple MacOS©, ELF pour Linux).
|
||||
|
||||
Lors de cette étape, le compilateur enlève tous les *labels*, mais afin de
|
||||
pouvoir résoudre les symboles (fonctions, variables, etc.) il va créer la
|
||||
**table des symboles**.
|
||||
|
||||
Cette table contenu dans chacun des fichiers objets générés fait référence à la
|
||||
position des élément dans ce fichier. Le compilateur résout ces symboles **lors
|
||||
de l'édition de liens**.
|
||||
|
||||
#### L'édition de liens
|
||||
|
||||
C'est le moment ou le compilateur agrège les différents fichiers objets en un
|
||||
seul exécutable. Le *linker* responsable de cette étape doit résoudre les
|
||||
symboles et les liers aux différents fichiers objets et bibliothèques.
|
||||
|
||||
C'est aussi lors de cette étape que les différents segments composants le
|
||||
programme sont créés et que la **table de relocation** est créée (très utile
|
||||
pour les mécanismes d'ALSR).
|
||||
|
||||
Il existe deux type d'édition de liens :
|
||||
|
||||
* **statique** : les éléments issus de bibliothèques sont inclus dans le
|
||||
binaire final. Dans ce cas la résolution des symboles **est totale**;
|
||||
* **dynamique** : les bibliothèques partagées ne sont pas incluses dans le
|
||||
binaire. La résolution des symboles ne peut-être que partielle.
|
||||
|
||||
Dans le cas de la compilation **dynamique**, le *linker* doit identifier les
|
||||
symboles appartenant aux bibliothèques partagées. Leur résolution se fera alors
|
||||
au moment de l'exécution.
|
||||
|
||||
## Exécution
|
||||
|
||||
L'exécution d'un programme se passe en plusieurs étapes :
|
||||
|
||||
1. le noyau est averti que qu'il doit charger le fichier binaire;
|
||||
2. l'élément chargé du chargement de notre binaire se charge de l'analyser;
|
||||
3. le noyau alloue de la mémoire pour notre programme;
|
||||
4. il *map* la mémoire allouée avec les éléments du programme;
|
||||
5. il est maintenant temps de passer le flot d'exécutions au programme.
|
||||
|
||||
Bien évidement le binaire n'est pas chargé tel quel en mémoire, d'où l'étape
|
||||
*d'analyse*. Sous Linux et les système Unix en général, le format d'exécutable
|
||||
utilisé est ELF. ce format, largement documenté, sert de base pour le *loader*
|
||||
|
||||
### Segments et sections
|
||||
|
||||
C'est deux éléments qui composent un fichier binaire au format ELF sont souvent
|
||||
source de confusion.
|
||||
|
||||
*Les segments* contiennent des éléments nécessaires à l'exécution alors que *les
|
||||
sections* contiennent des informations utiles pour les liens, la relocation et la
|
||||
résolution de symboles.
|
||||
|
||||
Les *sections* sont des composantes des *segments*. Prenons l'exemple du segment
|
||||
`LOAD` qui contient des sections `.data`, `.got`, `.bss` ... Elles contiennent
|
||||
soit des données brutes:
|
||||
|
||||
* `.text` : du code;
|
||||
* `.data` : des données initialisées comme par exemple:
|
||||
```c
|
||||
int x = 42;
|
||||
```
|
||||
* `.bss` : des données non initialisées
|
||||
* `.rodata` : des données en lecture seule comme les constantes statiques
|
||||
|
||||
## Le format ELF
|
||||
|
||||
Comme nous le disions, c'est le format binaire d'enregistrement de code compilé
|
||||
utilisé par la plupart des systèmes de type *Unix*. Il commence par les nombres
|
||||
magiques[^n_magic] `0x7fELF`. Il se compose ensuite d'un entête fixe. Celui ci
|
||||
contient (entre autres) l'*endianess* (big ou little), l'ABI, le type
|
||||
(bibliothèque, exécutable), l'architecture cible.
|
||||
|
||||
Après viens le *Program Header Table* qui contient des segments et informations
|
||||
nécessaire à la création du processus en mémoire.
|
||||
|
||||
Ensuite la *Section Header Table* référençant et décrivant les sections
|
||||
|
||||
Enfin le fichier ELF contient les données référencées par ces deux tables.
|
||||
|
||||
[^n_magic]: dans notre cas, ensemble de caractères utilisés pour désigner un
|
||||
format de fichier.
|
||||
|
||||
## ELF : Pratique
|
||||
|
||||
### Propriété de `ls`
|
||||
|
||||
Le plus simple pour obtenir des information est d'utiliser le programme `file`.
|
||||
|
||||
```shell
|
||||
$ file /bin/ls
|
||||
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
|
||||
interpreter /usr/lib/ld-linux-x86-64.so.2,
|
||||
BuildID[sha1]=4d53da289d128a5ccee3f944db35244bf91c7a99,
|
||||
for GNU/Linux 3.10.0, not stripped
|
||||
```
|
||||
|
||||
Nous apprenons donc que cet exécutable est un ELF 64bits pour uns architecture
|
||||
*Intel x86_64*, lié dynamiquement, chargé par la bibliothèque `ld-linux-x86-64`.
|
||||
Ici le binaire est *not stripped* : les différents *labels* ne sont pas
|
||||
supprimés (ici pour NixOS).
|
||||
|
||||
Il est aussi possible de récolter plus d'informations avec `readelf` notamment
|
||||
la version de la *libc* utilisée. Ce genre d'information peut permettre de mener
|
||||
une attaque.
|
||||
|
||||
### les sections `.plt` et `.got`
|
||||
|
||||
Ces différentes sections permette la résolution de symboles. `.plt` signifie
|
||||
*Procedure Linkag Table* et `.got` *Global Offset Table*. On parle alors de
|
||||
relocation.
|
||||
|
||||
Pour la section `.plt`, toutes ses entrées sont initialisées pour pointr vers
|
||||
l'editeur du lien et non l'adresse de la bonne fonction. C'est lors de premier
|
||||
appel de la fonction en question que sont adresse sera résolue - elle st donc
|
||||
effectuée au *runtime* - et mise à jour. On parle alors de *lazy symbols
|
||||
binging*
|
||||
|
||||
La section `.got` fait plus office d'annuaire référençant juste les adresss de
|
||||
fonctions mais aussi de variables globales des bibliothèques).
|
||||
|
||||
#### `.got` et `plt.got
|
||||
|
||||
Ces deux sections sont utilisés pour les symboles ayant besoin d'être résolus
|
||||
**au moment du chargement du binaire en mémoire**.
|
||||
|
||||
#### `.plt` et `.got.plt`
|
||||
|
||||
Ces deux sections se chargent de la résolution fainéante.
|
||||
|
||||
### Segments et sections avec `realelf`
|
||||
|
||||
#### Le segment `GNU_STACK`
|
||||
|
||||
Il permet la configuration de la pile lorsque le binaire est chargé. Il sert par
|
||||
exemple à la mise en place de protection (pile non exécutable par exemple).
|
||||
|
||||
#### Le segment `GNU_RELRO`
|
||||
|
||||
Il indique quelles régions de la mémoire doivent être marquées en lecture seule
|
||||
une fois la résolution des symboles effectuée. Il existe deux type :
|
||||
|
||||
* **full**: `.got` et `.got.plt` sont passés en lecture seule
|
||||
* **partiel**: seule la `.got` est en lecture seule.
|
||||
|
||||
*[ELF]: Executable and Linkable Format
|
||||
|
||||
## Le format PE
|
||||
|
||||
PE pour *Portable Execution* est le format de binaire utilisé par Microsoft
|
||||
Windows. Comme pour Linux, un *loader* réalise l'analyse et le chargement du
|
||||
binaire en mémoire. Dans ce format, la notion de segment n'existe pas, mais il
|
||||
se compose de 3 entêtes :
|
||||
|
||||
* un reliquat de l'ère MS DOS avec comme *magic number* `MZ`. cette section
|
||||
affiche un message d'erreur du style *Ce programme ne n'exécute pas en mode
|
||||
MSDOS* lors de son exécusion en mode DOS;
|
||||
* une entête *PE* avec comme *magic number* `PE`;
|
||||
* une entête optionnelle.
|
||||
|
||||
Il se compose aussi:
|
||||
|
||||
* une partie *data directories*;
|
||||
* une table des sections (`text`, `data`, etc.);
|
||||
* les sections en questions (avec leurs entêtes).
|
||||
|
||||
### CFF explorer
|
||||
|
||||
Ce programme disponible uniquement pour Windows permet d'analyser les fichiers
|
||||
binaires (exécutable, bibliothèques, etc.).
|
||||
|
||||
### Entête optionnelle
|
||||
|
||||
Elle est quasiment tout le temps présente et permet notamment de définir le
|
||||
point d'entrée (*entrypoint*), d'activer les protections comme l'ASLR ou autre
|
||||
mécanismes de sécurité.
|
||||
|
||||
### Entête *data directories*
|
||||
|
||||
Elle contient la localisation et la taille de chaque `directory`. Ces
|
||||
*directories* sont de plusieurs natures :
|
||||
|
||||
* signatures cryptographiques;
|
||||
* icônes;
|
||||
* configurations;
|
||||
* *export-table* : référençant les fonctions exportées par le binaire;
|
||||
* *import-table* : référençant les DLL importées par le binaire, les fonctions
|
||||
issues de ces DLL sont référencées dans l'*Imports Address Table*;
|
||||
* *relocation table* : table référençant les relocations.
|
||||
|
||||
Plusieurs *directories* peuvent appartenir à une section.
|
||||
|
||||
### Entêtes de sections
|
||||
|
||||
Elles contiennent les information relatives à leurs sections respectives : nom,
|
||||
adresse virtuelle, taille, droit d'accès.
|
329
content/secu_systeme/2_initiation_re/index.md
Normal file
329
content/secu_systeme/2_initiation_re/index.md
Normal file
|
@ -0,0 +1,329 @@
|
|||
---
|
||||
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]({{<ref "secu_logicielle/4_acces_memoire/index.md#accès-indirects-à-la-mémoire">}})
|
||||
|
||||
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]({{<ref "secu_logicielle/3_assembleur_approfondissement/index.md#flags-de-résultats">}})
|
||||
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.
|
Loading…
Add table
Add a link
Reference in a new issue