Architecture et debugger
Architecture x86
La mémoire
Quand un programme est exécuté, il alloue de la mémoire en respectant les limites imposées par le système.
Allocation de la mémoire dans Windows entre l'adresse mémoire la plus basse 0x00000000
et l'adresse mémoire la plus haute 0x7FFFFFFF
:
La pile
Quand un thread est en cours d'exécution, il a besoin d'une zone de stockage de donnée à court terme pour :
- les fonctions
- les variables locales
- les informations de contrôle du programme
C'est dans la pile (stack) que ces éléments sont stockés.
Pour simplifier l'exécution multi-thread chaque thread dans une même application utilise une pile différente.
La pile est vue par le CPU comme une mémoire de structure Last-In, First-Out (LIFO)
:
- Les éléments déposés (
pushed
) en haut de la pile sont supprimés (popped
) en premier - L'architecture x86 implémente deux instructions Assembleur dédiées
PUSH
: Ajout dans le pilePOP
: Retrait de la pile
Conventions d'appels
Les conventions d'appels décrivent comment les fonctions reçoivent leurs paramètres depuis leur appelant et comment elles renvoient leur résultat.
L'architecture offre plusieurs conventions d'appels dont les différences sont liées à :
- Comment les paramètres et la valeur de retour sont envoyés :
- Registres CPU
- Déposé sur la pile
- Les deux
- Dans quel ordre sont passés les paramètres
- Comment la pile est préparée et nettoyée après l'appel
- Quels registres CPU la fonction appelée doit conserver pour l'appelant
Généralement, le compilateur détermine quelle convention d'appel est utilisée pour toutes les fonctions d'un programme. cdecl
est un exemple de conventions d'appel.
Retours de fonction
Quand du code au sein d'un thread
appelle une fonction il doit connaitre son adresse de retour pour revenir au code appelant une fois la fonction complétée.
L'adresse de retour est stockée dans la pile :
- Avec les paramètres de la fonction
- Ainsi que les variables locales
Ces données sont associées à un appel de fonction et sont stockées dans une section de la stack
nommée stack frame
.
Quand une fonction se termine, l'adresse de retour est récupérée depuis la pile et utilisée pour restaurer le flot d'exécution de la fonction appelante.
Les registres du CPU
Les registres sont de tout petits et très rapides espaces mémoire permettant de lire et manipuler rapidement les données.
Dans une architecture 32 bits, le CPU utilise 9 registres de 32 bits.
Les noms ont étaient définis pour l'architecture 16 bits et ont ensuite été étendus pour les plates-formes 32 bits avec la lettre E
.
Chaque registre peut contenir une valeur de 32 bits entre 0x00000000
et 0xFFFFFFFF
ou bien 16 bits ou 8 bits dans les sous-registres associés.
EAX
: Accumulator : Opérations logiques et arithmétiquesEBX
: Base : Pointeur de base adresse mémoireECX
: Counter : Boucle, déplacement, rotationEDX
: Data : Input/Output, multiplication, divisionESI
: Source Index : Pointer addressing of data and source in string copy operationsEDI
: Destination Index : Pointer addressing of data and destination in string copy operations
ESP : Pointeur de pile
La pile permet de stocker des données, des pointeurs et des arguments. La pile évolue en permanence durant l'exécution du programme.
Le pointeur de pile permet de suivre l'évolution de la localisation la plus récente de la pile. Il pointe sur le haut de la pile.
Pointeur
Un pointeur est une référence vers une adresse en mémoire. Quand on dit qu'un registre contient un pointeur ou pointe sur adresse, cela veut dire que le registre contient l'adresse ciblée.
EBP : Pointeur de base
La pile évolue constamment. Il peut devenir compliqué pour une fonction de suivre sa stack frame
qui contient les arguments requis, les variables locales et l'adresse de retour.
EBP
va pointer sur le haut de la pile quand la fonction est appelée. La fonction va ensuite pouvoir utiliser EBP
pour référencer les informations via offset.
Chaque bloc est sur 4 octets :
- Si on est a 2 blocs de EBP :
[ebp] + 8
- Si on est a 4 blocs de EBP :
[ebp] + 12
EIP : Pointeur d'instructions
Un des registres les plus importants. Ce registre pointe toujours vers la prochaine instruction de code à exécuter.
EIP
permet de gérer le flot du programme, c'est la cible principale pour les dépassements de tampon.
Windows Debugger
Il existe plusieurs outils de debug tel que :
- OllyDbg
- Immunity Debugger
- WinDBG
Un debugger est un programme informatique inséré entre une application et le CPU. Un debugger agit comme un proxy. Il permet de voir et d'interagir avec la mémoire et le flot d'exécution.
Le CPU travaille avec du code binaire difficilement lisible par un humain. Le langage Assembleur
fournis un mappage d’un pour un entre le binaire et le langage de programmation. Langage lisible par un humain.
Opcode
Opcode
: Séquence binaire interprétée par le CPU comme une instruction précise.Le debugger affichera la valeur en hexadécimal et en langage assembleur.
Le debugger étudié ici est WinDBG
.
Les symboles
Les fichiers de symboles permettent à WinDBG de référencer les fonctions et structures internes ainsi que les variables globales en utilisant des noms plutôt que des adresses.
On peut utiliser le fichier de symbole de Microsoft pour les exécutables Windows.
Les fichiers de symboles (.PDB
) sont créés à la compilation par Microsoft de ses fichiers natifs. Microsoft ne fournit pas les fichiers de symbole pour chaque fichier. Les applications tierces peuvent avoir leur fichier PDB
.
Accéder et manipuler la mémoire
On peut afficher la traduction en assembleur d'une partie du code du programme avec la commande u
. Elle permet de lire le code ASM des API Windows ou de n'importe quelle partie du programme en cours.
La commande u
accepte :
- Une adresse mémoire
- Une plage d'adresse (debut - fin)
- Le nom d'une fonction (e.g. API Windows) via un symbole
- S'il n'y a pas d'argument, le désassemblage commence dans l'EIP
0:006> u kernel32!GetCurrentThread
KERNEL32!GetCurrentThread:
77732620 b8feffffff mov eax,0FFFFFFFEh
77732625 c3 ret
77732626 cc int 3
[...]
Lire la mémoire
On peut utiliser la commande display
(d
) suivit un indicateur de taille pour afficher le contenu de la mémoire :
b
:byte
: un byte
0:006> db esp
07e2fa5c f9 9b 87 77 bf ba 31 c3-c0 9b 87 77 c0 9b 87 77 ...w..1....w...w
07e2fa6c 00 00 00 00 60 fa e2 07-00 00 00 00 d4 fa e2 07 ....`...........
[...]
w
:word
: deux bytes
0:006> dw esp
07e2fa5c 9bf9 7787 babf c331 9bc0 7787 9bc0 7787
07e2fa6c 0000 0000 fa60 07e2 0000 0000 fad4 07e2
[...]
d
:dword
: quatre bytes
0:006> dd esp
07e2fa5c 77879bf9 c331babf 77879bc0 77879bc0
07e2fa6c 00000000 07e2fa60 00000000 07e2fad4
[...]
q
:qword
: huit bytes
0:006> dq 07e2fa5c
07e2fa5c c331babf`77879bf9 77879bc0`77879bc0
07e2fa6c 07e2fa60`00000000 07e2fad4`00000000
[...]
On peut choisir d'afficher les caractères ASCII
:
dc
: 4 octets et les caractèresASCII
dW
: 2 octets et les caractèresASCII
0:006> dW 07e2fa5c
07e2fa5c 9bf9 7787 babf c331 9bc0 7787 9bc0 7787 ...w..1....w...w
07e2fa6c 0000 0000 fa60 07e2 0000 0000 fad4 07e2 ....`...........
[...]
0:006> dc 07e2fa5c
07e2fa5c 77879bf9 c331babf 77879bc0 77879bc0 ...w..1....w...w
07e2fa6c 00000000 07e2fa60 00000000 07e2fad4 ....`...........
[...]
La taille par défaut peut être modifiée avec le paramètre L*
. La valeur qui va suivre L
va influencer la quantité de données affichées.
0:006> dW esp L10
07e2fa5c 9bf9 7787 babf c331 9bc0 7787 9bc0 7787 ...w..1....w...w
07e2fa6c 0000 0000 fa60 07e2 0000 0000 fad4 07e2 ....`...........
#
0:006> dW esp L2
07e2fa5c 9bf9 7787 ...w
#
0:006> dW esp L1
07e2fa5c 9bf9 ..
La commande poi()
affiche le contenu de l'adresse contenu dans le registre :
0:006> dd esp L1
07e2fa5c 77879bf9
#
0:006> dd 77879bf9
77879bf9 c03307eb 658bc340 fc45c7e8 fffffffe
77879c09 e0e8006a ccfff98d cccccccc cccccccc
Est équivalent à :
0:006> dd poi(esp)
77879bf9 c03307eb 658bc340 fc45c7e8 fffffffe
77879c09 e0e8006a ccfff98d cccccccc cccccccc
Afficher les structures en mémoire
Les structures sont un concept de programmation qui combine différents types de données. Elles sont faciles à lire avant la compilation, après la compilation le code est traduit en valeur binaire et devient compliqué à lire.
- La commande
display
peut permettre d'afficher des structures - La commande
Display Table
(dt
) prends le nom de la structure à afficher
Affichage de la structure Thread Environment Block
(TEB
) :
0:006> dt ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
+0x034 LastErrorValue : Uint4B
[...]
La commande affiche les champs, leur type et leur offset. Il est possible qu'un champ de la structure pointe sur une deuxième structure, exemple NtTib : _NT_TIB
. On peut utiliser -r
pour afficher les structures de manières récursives.
0:006> dt -r ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION
+0x004 StackBase : Ptr32 Void
[...]
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
[...]
+0x01c EnvironmentPointer : Ptr32 Void
Les pseudo-registres
Les pseudo-registres sont des variables spécifiques au sein de WinDbg qui peuvent être utilisées notamment durant les opérations de calculs.
$teb
est un pseudo-registre qui contient l'adresse duTEB
du thread courant, on peut l'utiliser de cette manière :
0:006> dt -r ntdll!_TEB @$teb
+0x000 NtTib : _NT_TIB
+0x000 ExceptionList : 0x0720fa50 _EXCEPTION_REGISTRATION_RECORD
+0x000 Next : 0x0720faac _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x77847390 _EXCEPTION_DISPOSITION ntdll!_except_handler4+0
+0x004 StackBase : 0x07210000 Void
[...]
+0x014 ArbitraryUserPointer : (null)
+0x018 Self : 0x00ead000 _NT_TIB
+0x000 ExceptionList : 0x0720fa50 _EXCEPTION_REGISTRATION_RECORD
[...]
+0x014 ArbitraryUserPointer : (null)
+0x018 Self : 0x00ead000 _NT_TIB
+0x01c EnvironmentPointer : (null)
+0x020 ClientId : _CLIENT_ID
[...]
Il existe 20 pseudo-registres définis par défaut nomme de $t0
à $t19
qui peuvent être utilisé comme variable durant les opérations mathématiques.
# Stockage de l'addition dans $t0
0:000> r @$t0 = (41414141 + 41414141)
# Affichage de $t0
0:000> r @$t0
$t0=82828282
# Stockage de la soustraction dans $t1
0:000> r @$t1 = @$t0 / 5
# Affichage de $t1
0:000> r @$t1
$t1=1a1a1a1a
Écrire en mémoire
Il est aussi possible d'écrire en mémoire. La commande pour ce faire est e*
avec *
la taille à écrire :
d
: Dword
# Lecture du registre ESP
0:006> dd esp L1
0720fa34 77879bf9
# Ecriture du DWORD 41414141 dans le registre ESP
0:006> ed esp 41414141
0:006> dd esp L1
0720fa34 41414141
w
: Worda
: ASCII string
Chercher en mémoire
Le développement d'exploit peut nécessiter la recherche dans l'espace mémoire pour un motif spécifique. WinDbg permet de chercher avec la commande s
.
La commande prend 4 paramètres :
- Le type de mémoire dans lequel chercher
- Le point de départ
- La longueur sur laquelle chercher
- Le motif à chercher
# Écriture
0:000> ed esp 41414141
# Lecture
0:000> da esp
0096f69c "AAAA.6|w"
# Recherche
## On identifie bien `0096f69c` (adresse de ESP) dans la recherche - parmi d'autres
0:000> s -d 0 L?80000000 41414141
0096f69c 41414141 777c36cc 00bca000 00000000 AAAA.6|w........
67d23820 41414141 db5e5e5e b6b7b7db b4b6b6b6 AAAA^^^.........
6e804600 41414141 00d5b7af 00000000 00000000 AAAA............
Inspecter et éditer les registres
On peut accéder aux registres CPU via la commande r
. Permets d'afficher et modifier les valeurs des registres.
# Tout afficher
0:000> r
eax=00000000 ebx=00000000 ecx=0096f680 edx=77841670 esi=00bca000 edi=777c36cc
eip=7787bb62 esp=0096f69c ebp=0096f6c8 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
7787bb62 cc int 3
# Afficher un seul registre
0:000> r ecx
ecx=0096f680
# Modifier un seul registre
0:000> r edx
edx=77841670
0:000> r edx=42424242
0:000> r edx
edx=42424242
Les points d'arrêt
WinDBG permet de définir des breakpoints
pour stopper l'exécution aux endroits voulus dans le code.
Il existe 2 types de points d'arrêts :
- Logiciel
- Matériel
Points d'arrêts logiciels
Quand on positionne un point d'arrêt logiciel avec WinDBG il remplace temporairement le 1er opcode de l'instruction ou l'on veut s'arrêter avec une instruction assembleur INT 3
. On peut en définir autant qu'on veut.
Définir un point d'arrêt sur l'API Win32 WriteFile
:
Puis on peut lister les BP avec bl
:
- On peut désactiver un point d'arrêt avec
bd
- On utilise
bc
pour supprimer un point d'arrêt
Fonctions non résolues
On utilise la commande bu
pour définir des points d'arrêts sur des fonctions unresolved. Fonction présente au sein d'un module qui n'est pas encore chargé dans l'espace mémoire d'un processus. Le point d'arrêt va être activé quand le module sera chargé et que la fonction cible sera résolue.
# Chercher le module OLE32.dll
0:000> lm m ole32
Browse full module list
start end module name
# Créer un point d'arrêt unresolved sur la fonction `WriteStringStream` du module `OLE32`
0:000> bu ole32!WriteStringStream
0:000> bl
0 e Disable Clear u 0001 (0001) (ole32!WriteStringStream)
# On relance l'exécution - On voit que le module `ole32` est chargé
0:000> g
ModLoad: 776e0000 77706000 C:\Windows\System32\IMM32.DLL
[...]
ModLoad: 75fc0000 760b7000 C:\Windows\System32\ole32.dll
# Notre point d'arrêt est résolu
0:008> bl
0 e Disable Clear 75fe1cd0 0001 (0001) 0:**** ole32!WriteStringStream
Points d'arrêts automatiques
Il est possible d'automatiser l'exécution de commande avec le debugger quand un point d'arrêt est déclenché.
Exemple, afficher le nombre de octets écrit dans un fichier à chaque fois qu'un point d'arrêt sur kernel32!WriteFile
est déclenché :
- Utiliser la commande
.printf
avec le format%p
pour afficher un pointeur - La commande
.echo
affiche l'output de.printf
dans une fenêtre de WinDbg - On utilise
;
pour séparer les commandes - On affiche le contenu de
esp + 0x0C
Prototype de WriteFile
:
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
- Chaque paramètre occupe 4 octets en mémoire dans la pile.
0x0C
correspond donc a 12 octets. Ce qui correspond au 3em argument deWriteFile
qui estnNumberOfBytesToWrite
, a savoir le nombre d'octets écrit - Windows x86 API utilise la convention d'appel
__stdcall
qui positionne les arguments dans la pile de droite à gauche
Il est aussi possible de définir des points d'arrêt conditionnels. C'est-à-dire que l'exécution s'arrête uniquement si la condition est respectée.
Exemple, un point d'arrêt conditionnel sur kernel32!WriteFile
. Le point d'arrêt sera déclenché et l'exécution stoppée seulement si l'on écrit exactement 4 octets :
bp kernel32!WriteFile ".if (poi(esp + 0x0C) != 4) {gc} .else {.printf \"The number of bytes written is 4\";.echo;}"
gc
: Go from Conditional Breakpoint
Points d'arrêts matériels
Les points d'arrêts processeur ou matériel sont gérés par le CPU et stockés dans les registres de debug du CPU.
Il existe 6 registres de debug dans l'architecture x86
, le nombre est donc limité par rapport aux points d'arrêts logiciels :
DR0
àDR03
DR06
DR07
Les points d'arrêts matériels peuvent être définis avec la commande ba
qui prend 3 arguments :
- 1er : Type d'accès
e
:execute
r
:read
w
:write
- 2em : Taille en octets pour l'accès mémoire indiqué
- 3em : Adresse mémoire où on veut mettre le point d'arrêt
Point d'arrêt matériel sur l'exécution de l'API WriteFile
La finalité est la même qu'un point d'arrêt logiciel. Mais la on utilise les registres de debug du CPU plutôt que d'altérer le code avec des instructions INT 3
.
On définit un point d'arrêt matériel en write
sur les 2
premier octet du string Unicode
(0x00
et 0x77
) sur l'adresse mémoire 00a99b68
:
Les points d'arrêt matériel sont utiles pour trouver ou les données sont prises en charge durant l'exécution.
Savoir les différencier et les utiliser :
- Avec un point d'arrêt logiciel, il aurait fallu trouver l'endroit ou il allait y avoir la modification
- Avec un point d'arrêt matériel, on a pas besoin de connaitre l'endroit ou la modification se fait. On peut juste déclencher quand elle arrive et donc trouver l'endroit ou la modification est faite.
Lister les modules
On peut afficher les modules chargés dans l'espace mémoire du processus avec la commande lm
:
0:000> lm
start end module name
010a0000 010df000 NOTEPAD (deferred)
584f0000 5856a000 DUser (deferred)
58570000 586df000 DUI70 (deferred)
[...]
On peut lister les modules en filtrant avec *
. On affiche ici les modules qui commencent par kernel
:
0:000> lm m kernel*
Browse full module list
start end module name
74220000 7422e000 kernel_appcore
74540000 74718000 KERNELBASE
77710000 777a5000 KERNEL32
Opérations mathématiques
On utilise l'opérateur ?
pour gérer les expressions mathématiques :
Formats de données
L'affichage par défaut est hexadécimal. Mais il est possible de demander un autre format :
# Hexa
## h`41414141` = d`1094795585`
0:000> ? 41414141
Evaluate expression: 1094795585 = 41414141
# Hexa -> Decimal
# d`41414141` = h`0277edfd`
0:000> ? 0n41414141
Evaluate expression: 41414141 = 0277edfd
# Binary to Decimal to Hexa
## b`1110100110111` = d`7479` = h`00001d37`
0:000> ? 0y1110100110111
Evaluate expression: 7479 = 00001d37
On peut utiliser .formats
pour convertir en plusieurs types
0:000> .formats 41414141
Evaluate expression:
Hex: 41414141
Decimal: 1094795585
Octal: 10120240501
Binary: 01000001 01000001 01000001 01000001
Chars: AAAA
Time: Thu Sep 9 22:53:05 2004
Float: low 12.0784 high 0
Double: 5.40901e-315
Les protections mémoire
Plusieurs mécanismes mis en oeuvre dans les systèmes Windows peuvent rendre la prise de contrôle de EIP
plus complexe.
Ces contrôles doivent être actifs à la compilation pour être pris en compte durant l'exploitation
Data Execution Prevention
Data Execution Prevention
(DEP
) : Préviens l'exécution de code depuis les page data
en levant une exception quand une tentative est faite
ASLR
Address Space Layout Randomization
(ASLR
) : Randomise les adresses de base des applications chargées et des DLL a chaque redémarrage de l'OS.
Dans les OS comme XP sans ASLR toutes les DLL sont chargées systématiquement à la même adresse mémoire ce qui facilite l'exploitation.
CFG
Control Flow Guard
(CFG
) : Implémentation de Microsoft du Control Flow Integrity. Ce mécanisme contrôle les indirect code branching. Utilisation de call <instruction>
utilisant un registre tel que call EAX
comme opérande plutôt qu'une adresse mémoire.
Préviens la réécriture de pointeur de fonction dans des exploits.