Les bases de l'ingénierie inverse avec GDB

Effectivement, je commence à m'intéresser à l'ingénierie inverse, plus communément appelée reverse engineering, ou RE. C'est pourquoi cette fois j'ai choisis d'utiliser un outil vraiment utile : GDB (GNU DeBugger).

GDB ?

Avant de partir explorer le code assembleur je vais parler un peu de l'outil que j'ai utilisé. GNU DeBugger est un déboggeur standard, portable sur la majorité des systèmes UNIX, il est compatible avec un grand nombre de langages tels que le C, C++ ou encore GO ; également compatible avec un bon nombre d'architectures de processeur.
GDB est un outil en ligne de commandes (CLI), pour commencer à l'utiliser avec un exécutable :

$ gdb fichier_executable
(gdb)

Une fois en utilisation, on peut exécuter des commandes pour GDB, il est possible d'afficher la valeur d'une adresse mémoire, afficher les différentes fonctions détectées dans le programme et un tat d'autres choses.

Reverse time !

Pour mieux comprendre les différentes notions liées au reverse j'ai créé un tout petit programme en C :

#include <stdio.h>

void main(){
    char name[] = "Ninja";
    printf("Hello, nice to meet you %s !", name);
    //output : Hello, nice to meet you Ninja !
}

Mon but était de pouvoir modifier l'affichage final en changeant la valeur de la variable name en une autre valeur. En théorie ce programme pourrait sembler "sans faille" car à aucun moment l'utilisateur n'a le choix sur le contenu de la variable name, mais grâce aux déboggeurs il est possible de comprendre ce qu'un programme fait, mais aussi de modifier à notre guise la valeur des registres et des adresses mémoire.

Compiler le programme

Afin de le rendre exécutable, nous devons compiler le programme C :

gcc re1.c -o re1
./re1
Hello, nice to meet you Ninja !

Le programme fonctionne comme prévu, passons au déboggage

GDB

On va alors utiliser GDB :

$ gdb re1
(gdb) info functions
All defined functions:

Non-debugging symbols:
0x0000000000001000  _init
0x0000000000001030  __stack_chk_fail@plt
0x0000000000001040  printf@plt
0x0000000000001050  _start
0x0000000000001080  deregister_tm_clones
0x00000000000010b0  register_tm_clones
0x00000000000010f0  __do_global_dtors_aux
0x0000000000001140  frame_dummy
0x0000000000001149  main
0x00000000000011a0  __libc_csu_init
0x0000000000001210  __libc_csu_fini
0x0000000000001218  _fini
(gdb)

J'ai ici affiché les fonctions trouvées par GDB, on remarque la fonction main, mais aussi des fonctions bien connues comme printf ou des références à la librairie standard libc.
À présent, passons dans le côté obscur de la force, le code assembleur. En effet, GDB nous permet de débogger un programme, mais est aussi capable d'afficher le code assembleur d'une fonction par exemple :

$ gdb re1
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000001149 <+0>:     push   %rbp
   0x000000000000114a <+1>:     mov    %rsp,%rbp
   0x000000000000114d <+4>:     sub    $0x10,%rsp
   0x0000000000001151 <+8>:     mov    %fs:0x28,%rax
   0x000000000000115a <+17>:    mov    %rax,-0x8(%rbp)
   0x000000000000115e <+21>:    xor    %eax,%eax
   0x0000000000001160 <+23>:    movl   $0x6a6e694e,-0xe(%rbp)
   0x0000000000001167 <+30>:    movw   $0x61,-0xa(%rbp)
   0x000000000000116d <+36>:    lea    -0xe(%rbp),%rax
   0x0000000000001171 <+40>:    mov    %rax,%rsi
   0x0000000000001174 <+43>:    lea    0xe89(%rip),%rdi        # 0x2004
   0x000000000000117b <+50>:    mov    $0x0,%eax
   0x0000000000001180 <+55>:    callq  0x1040 <printf@plt>
   0x0000000000001185 <+60>:    nop
   0x0000000000001186 <+61>:    mov    -0x8(%rbp),%rax
   0x000000000000118a <+65>:    xor    %fs:0x28,%rax
   0x0000000000001193 <+74>:    je     0x119a 
   0x0000000000001195 <+76>:    callq  0x1030 <__stack_chk_fail@plt>
   0x000000000000119a <+81>:    leaveq 
   0x000000000000119b <+82>:    retq   
End of assembler dump.
(gdb)

Ça fait un peu mal aux yeux, mais ne prenez pas peur, utilison plutôt le format intel :

$ gdb re1
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000001149 <+0>:     push   rbp
   0x000000000000114a <+1>:     mov    rbp,rsp
   0x000000000000114d <+4>:     sub    rsp,0x10
   0x0000000000001151 <+8>:     mov    rax,QWORD PTR fs:0x28
   0x000000000000115a <+17>:    mov    QWORD PTR [rbp-0x8],rax
   0x000000000000115e <+21>:    xor    eax,eax
   0x0000000000001160 <+23>:    mov    DWORD PTR [rbp-0xe],0x6a6e694e   ; "Ninj"
   0x0000000000001167 <+30>:    mov    WORD PTR [rbp-0xa],0x61          ; "a"
   0x000000000000116d <+36>:    lea    rax,[rbp-0xe]
   0x0000000000001171 <+40>:    mov    rsi,rax
   0x0000000000001174 <+43>:    lea    rdi,[rip+0xe89]        # 0x2004
   0x000000000000117b <+50>:    mov    eax,0x0
   0x0000000000001180 <+55>:    call   0x1040 <printf@plt>              ; apel de la fonction printf
   0x0000000000001185 <+60>:    nop
   0x0000000000001186 <+61>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000000000000118a <+65>:    xor    rax,QWORD PTR fs:0x28
   0x0000000000001193 <+74>:    je     0x119a 
   0x0000000000001195 <+76>:    call   0x1030 <__stack_chk_fail@plt>
   0x000000000000119a <+81>:    leave  
   0x000000000000119b <+82>:    ret    
End of assembler dump.
(gdb)

Voilà qui est mieux. Par défaut, GDB utilise le format AT&T, qui est mon lisible qu'intel.
Mon but ici n'est pas d'expliquer l'utilité de chaque instruction, mais de présenter comment modifier un registre afin de changer le comportement d'un programme compilé. Je vais donc mettre en avant les parties les plus intéressantes afin de se concentrer sur l'essentiel du programme.
Ici, les instructions intéressentes pour nous sont main <+23> : mov DWORD PTR [rbp-0xe],0x6a6e694e et main <+55> : call 0x1040 .

Effectivement, c'est là que la valeur "Ninja" est affectée à l'adresse mémoire, qui est ensuite affichée à l'écran. Pour s'en persuader utilisons le déboggeur :

(gdb) break main #créer un point d'arrêt du programme à l'adresse de la fonction main
Breakpoint 1 at 0x114d
(gdb) run #lance le programme
Breakpoint 1, 0x000055555555514d in main () #nous avons atteint le point d'arrêt, le prgramme est en pause mais toujours en exécution
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000555555555149 <+0>:     push   rbp
   0x000055555555514a <+1>:     mov    rbp,rsp
=> 0x000055555555514d <+4>:     sub    rsp,0x10                                ; le programme est arrêté juste avant cette instruction
   0x0000555555555151 <+8>:     mov    rax,QWORD PTR fs:0x28
   0x000055555555515a <+17>:    mov    QWORD PTR [rbp-0x8],rax 
   0x000055555555515e <+21>:    xor    eax,eax
   0x0000555555555160 <+23>:    mov    DWORD PTR [rbp-0xe],0x6a6e694e
   0x0000555555555167 <+30>:    mov    WORD PTR [rbp-0xa],0x61
   0x000055555555516d <+36>:    lea    rax,[rbp-0xe]
   0x0000555555555171 <+40>:    mov    rsi,rax
   0x0000555555555174 <+43>:    lea    rdi,[rip+0xe89]        # 0x555555556004
   0x000055555555517b <+50>:    mov    eax,0x0
   0x0000555555555180 <+55>:    call   0x555555555040 <printf@plt>
   0x0000555555555185 <+60>:    nop
   0x0000555555555186 <+61>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000055555555518a <+65>:    xor    rax,QWORD PTR fs:0x28
   0x0000555555555193 <+74>:    je     0x55555555519a 
   0x0000555555555195 <+76>:    call   0x555555555030 <__stack_chk_fail@plt>
   0x000055555555519a <+81>:    leave  
   0x000055555555519b <+82>:    ret    
End of assembler dump.
(gdb) ni #avant d'une instruction dans le programme
0x0000555555555151 in main ()
(gdb) ni #on va avancer jusqu'à <+23>
0x000055555555515a in main ()
(gdb) ni
0x000055555555515e in main ()
(gdb) ni
0x0000555555555160 in main ()
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000555555555149 <+0>:     push   rbp
   0x000055555555514a <+1>:     mov    rbp,rsp
   0x000055555555514d <+4>:     sub    rsp,0x10
   0x0000555555555151 <+8>:     mov    rax,QWORD PTR fs:0x28
   0x000055555555515a <+17>:    mov    QWORD PTR [rbp-0x8],rax
   0x000055555555515e <+21>:    xor    eax,eax
=> 0x0000555555555160 <+23>:    mov    DWORD PTR [rbp-0xe],0x6a6e694e         ; juste avant l'attribution de la valeur "Ninja" à l'adresse @rbp-0xe
   0x0000555555555167 <+30>:    mov    WORD PTR [rbp-0xa],0x61
   0x000055555555516d <+36>:    lea    rax,[rbp-0xe]
   0x0000555555555171 <+40>:    mov    rsi,rax
   0x0000555555555174 <+43>:    lea    rdi,[rip+0xe89]        # 0x555555556004
   0x000055555555517b <+50>:    mov    eax,0x0
   0x0000555555555180 <+55>:    call   0x555555555040 <printf@plt>
   0x0000555555555185 <+60>:    nop
   0x0000555555555186 <+61>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000055555555518a <+65>:    xor    rax,QWORD PTR fs:0x28
   0x0000555555555193 <+74>:    je     0x55555555519a 
   0x0000555555555195 <+76>:    call   0x555555555030 <__stack_chk_fail@plt>
   0x000055555555519a <+81>:    leave  
   0x000055555555519b <+82>:    ret    
End of assembler dump.
(gdb) p $rbp-0xe # on affiche l'adresse @rbp-0xe
$1 = (void *) 0x7fffffffde22
(gdb) x/s 0x7fffffffde22 # on affiche la valeur de l'adresse
0x7fffffffde22: "\377\377\377\177" # rien
(gdb) ni # on passe à l'instruciotn suivante
0x0000555555555167 in main ()
(gdb) x/s 0x7fffffffde22
0x7fffffffde22: "Ninj" # l'adresse contient maintenant cette valeur
(gdb) p $rbp-0xa # on affiche l'adresse @rbp-0xa
$2 = (void *) 0x7fffffffde26
(gdb) x/s 0x7fffffffde26
0x7fffffffde26: "" # rien
(gdb) ni
0x000055555555516d in main ()
(gdb) x/s 0x7fffffffde26
0x7fffffffde26: "a" # l'adresse contient maintenant cette valeur
(gdb) set *0x7fffffffde22 = 0x65746962 # on modifie la valeur à l'adresse
(gdb) set *0x7fffffffde26 = 0x0a # on modifie la valeur à l'adresse
(gdb) c # on continue le programme
Continuing.
Hello, nice to meet you bite

Super, on vient d'afficher autre chose que "Ninja" !

En résumé

GDB est un outil super cool, il permet de faire beaucoup de choses, voici une liste des commandes utilisées et d'autres :

  • set disassembly : modifie le format d'affichage du code assembleur
  • info functions : affiche les fonctions du prgoramme
  • info registers : affiche les adresses et les valeurs des registres
  • run : lance le programme
  • break main : créer un point d'arrêt du programme à la fonction main
  • p $rbp-0xe : réalise des opérations sur des nombres et affiche le résultat
  • x/s 0x7fffffffde22 : affiche le contenu de l'adresse, apres une conversion hexa -> string
  • set *0x7fffffffde22 = 0x65746962 : modifie la valeur d'une adresse
  • c : continue l'exécution normale du programme