Skip to content

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 pile
    • POP : 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étiques
  • EBX : Base : Pointeur de base adresse mémoire
  • ECX : Counter : Boucle, déplacement, rotation
  • EDX : Data : Input/Output, multiplication, division
  • ESI : Source Index : Pointer addressing of data and source in string copy operations
  • EDI : 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ères ASCII
  • dW : 2 octets et les caractères ASCII
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 du TEB 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 : Word
  • a : ASCII string
0:006> da esp L5
0720fa34  "AAAA"
0:006> ea esp "Hello"
0:006> da esp L5
0720fa34  "Hello"

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 :

bp kernel32!WriteFile

Puis on peut lister les BP avec bl :

0:006> bl
     0 e Disable Clear  7773c6d0     0001 (0001)  0:**** KERNEL32!WriteFile
  • 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 de WriteFile qui est nNumberOfBytesToWrite, 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
bp kernel32!WriteFile ".printf \"The number of bytes written is: %p\", poi(esp + 0x0C);.echo;g"

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

0:006> ba e 1 kernel32!WriteFile
0:006> g

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 :

0:006> ba w 2 00a99b68
0:006> bl
     0 e Disable Clear  00a99b68 w 2 0001 (0001)  0:**** 

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 :

0:000> ? 74663bcf - 746bd888
Evaluate expression: -367801 = fffa6347

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.