Hackflash #4 : Déboguer avec GDB : un bref didacticiel

Qui veut en apprendre plus sur le shell coding ne peut faire l'économie de se mettre à l'assembleur et d'apprendre à déboguer des programmes écrits en C ou C++ avec le très fameux GNU Project Debuger, "GDB" pour les intimes.
Bien évidemment, les didacticiels pour apprendre à utiliser GDB sont légion. Toutefois, pourquoi ne pas apporter une pierre de plus à l'édifice en présentant aussi succinctement que possible les principales commandes afin de faciliter une première prise en main, qui permet d'approcher le monstre avant de rentrer dans les entrailles de sa documentation ?
Dans ce petit didacticiel, il s'agira donc de déboguer un programme C des plus élémentaires compilé avec GNU Compiler Collection, "GCC" pour les intimes, dans Kali Linux.
Soit le programme test.c suivant :
#include <stdio.h>
int main (int argc, char *argv[]) {
	for (int i = 0; i != argc; i ++)
		printf ("Argument %d: %s", i, argv[i]);
	return (0);
}
Compiler avec l'option -g pour intégrer des informations supplémentaires pour le débogueur (-Wall pour demander l'affichage de tous les warnings) :
$ gcc -Wall -g test.c -o test
Exécuter dans le contexte de gdb. Soit en démarrant... :
$ gdb --args test one two three
...ou après avoir démarré gdb sans paramètres :
(gbb) file test
Reading symbols from test...

(gdb) set args one two three
NB : il est même possible d'attendre de spécifier via run.
A ce stade, l'exécution du programme n'a pas commencé, donc pas de débogage possible au sens propre. Il est tout de même possible de passer en revue certaines choses.
Le saviez-vous ?
Pour saisir plus vite :
  • Après avoir saisi le début d'une commande, presser Tab complète la commande si d'autres ne débutent pas pareillement.
  • Après avoir saisi le début d'une commande, presser deux fois Tab affiche celles qui débutent pareillement.
  • Il suffit de saisir le début d'une commmande si aucune autre ne débute pareillement (ex : br pour breakpoint).
  • Il suffit de saisir la première lettre des commandes les plus communes, même si d'autres débutent pareillement (ex : b pour breakpoint).
  • Après avoir utilisé une commande, il suffit de presser Entrée pour la saisir de nouveau.
Afficher la structure du programme en mémoire :
(gdb) i file
Symbols from "/root/Downloads/test".
Local exec file:
	`/root/Downloads/test', file type elf64-x86-64.
	Entry point: 0x1050
	0x00000000000002a8 - 0x00000000000002c4 is .interp
	0x00000000000002c4 - 0x00000000000002e8 is .note.gnu.build-id
	0x00000000000002e8 - 0x0000000000000308 is .note.ABI-tag
	0x0000000000000308 - 0x000000000000032c is .gnu.hash
	0x0000000000000330 - 0x00000000000003d8 is .dynsym
	0x00000000000003d8 - 0x000000000000045c is .dynstr
	0x000000000000045c - 0x000000000000046a is .gnu.version
	0x0000000000000470 - 0x0000000000000490 is .gnu.version_r
	0x0000000000000490 - 0x0000000000000550 is .rela.dyn
	0x0000000000000550 - 0x0000000000000568 is .rela.plt
	0x0000000000001000 - 0x0000000000001017 is .init
	0x0000000000001020 - 0x0000000000001040 is .plt
	0x0000000000001040 - 0x0000000000001048 is .plt.got
	0x0000000000001050 - 0x00000000000011f1 is .text
	0x00000000000011f4 - 0x00000000000011fd is .fini
	0x0000000000002000 - 0x0000000000002014 is .rodata
	0x0000000000002014 - 0x0000000000002050 is .eh_frame_hdr
	0x0000000000002050 - 0x0000000000002158 is .eh_frame
	0x0000000000003de8 - 0x0000000000003df0 is .init_array
	0x0000000000003df0 - 0x0000000000003df8 is .fini_array
	0x0000000000003df8 - 0x0000000000003fd8 is .dynamic
	0x0000000000003fd8 - 0x0000000000004000 is .got
	0x0000000000004000 - 0x0000000000004020 is .got.plt
	0x0000000000004020 - 0x0000000000004030 is .data
	0x0000000000004030 - 0x0000000000004038 is .bss
Afficher la liste des fonctions, avec comme on le voit la fonction main () :
(gdb) i functions
All defined functions:

File test.c:
2:      int main(int, char **);

Non-debugging symbols:
0x0000000000001000  _init
0x0000000000001030  printf@plt
0x0000000000001040  __cxa_finalize@plt
0x0000000000001050  _start
0x0000000000001080  deregister_tm_clones
0x00000000000010b0  register_tm_clones
0x00000000000010f0  __do_global_dtors_aux
0x0000000000001130  frame_dummy
0x0000000000001190  __libc_csu_init
0x00000000000011f0  __libc_csu_fini
0x00000000000011f4  _fini
Avant de démarrer l'exécution, prévoir de l'arrêter avant la fin du programme par un breakpoint, par exemple à l'entrée de main () :
NB : Utiliser starti pour arrêter l'exécution dès la première instruction d'un programme sans passer par un breakpoint.
(gdb) b main
Breakpoint 1 at 0x1144: file test.c, line 3.
NB : Utiliser tbreak pour un breakpoint qui ne fonctionne qu'une fois, dit temporaire.
Consulter la liste des breakpoints :
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000001144 in main at test.c:3
Désactiver le breakpoint à partir de son numéro :
(gdb) disable 1

(gdb) i breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x0000000000001144 in main at test.c:3
Activer le breakpoint à partir de son numéro :
(gdb) enable 1

(gdb) i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000001144 in main at test.c:3
Lancer l'exécution en fournissant "one", "two" et "three" en arguments :
(gdb) r one two three
Starting program: /root/Downloads/test one two three

Breakpoint 1, main (argc=4, argv=0x7fffffffe268) at test.c:3
3               for (int i = 0; i != argc; i ++)
Noter que GDB s'appuie sur le fichier source pour afficher les instructions en C. Il faut donc que test.c soit disponible pour conduire cette session.
Visualiser les arguments :
(gdb) i args
argc = 4
argv = 0x7fffffffe278
Explorer le contenu de argv[] :
(gdb) p argv
$1 = (char **) 0x7fffffffe278

(gdb) p *argv@4
$2 = {0x7fffffffe552 "/root/Downloads/test", 0x7fffffffe567 "one", 0x7fffffffe56b "two", 0x7fffffffe56f "three"}
Afficher argv[1] en mémoire à l'adresse de argv, sous la forme d'une séquence d'octets en hexadécimal d'abord, puis de caractères dont ces octets sont les codes ASCII, puis d'une chaîne :
(gdb) x /4xb 0x7fffffffe567
0x7fffffffe567: 0x6f    0x6e    0x65    0x00

(gdb) x /4cb 0x7fffffffe567
0x7fffffffe567: 111 'o' 110 'n' 101 'e' 0 '\000'

(gdb) x /s 0x7fffffffe567
0x7fffffffe567: "one"
Afficher les registres les plus communs :
(gdb) i registers
rax            0x555555555135      93824992235829
rbx            0x0                 0
rcx            0x7ffff7fb3718      140737353824024
rdx            0x7fffffffe290      140737488347792
rsi            0x7fffffffe268      140737488347752
rdi            0x4                 4
rbp            0x7fffffffe180      0x7fffffffe180
rsp            0x7fffffffe160      0x7fffffffe160
r8             0x0                 0
r9             0x7ffff7fe3530      140737354020144
r10            0x0                 0
r11            0x27                39
r12            0x555555555050      93824992235600
r13            0x7fffffffe260      140737488347744
r14            0x0                 0
r15            0x0                 0
rip            0x555555555144      0x555555555144 <main+15>
eflags         0x206               [ PF IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
Afficher des informations détaillées sur la pile, dite "frame" :
(gdb) i frame
Stack level 0, frame at 0x7fffffffe190:
 rip = 0x555555555144 in main (test.c:3); saved rip = 0x7ffff7e1de0b
 source language c.
 Arglist at 0x7fffffffe180, args: argc=4, argv=0x7fffffffe268
 Locals at 0x7fffffffe180, Previous frame's sp is 0x7fffffffe190
 Saved registers:
  rbp at 0x7fffffffe180, rip at 0x7fffffffe188
Afficher la valeur du registre ESP qui contient le pointeur sur l'entrée courante dans la pile :
(gdb) i r rsp
rsp            0x7fffffffe160      0x7fffffffe160
Afficher les 20 derniers mots de 32 bits stockés dans la pile (ou au-delà) sous forme hexadécimale :
(gdb) x /20xw $rsp
0x7fffffffe160: 0xffffe268      0x00007fff      0x55555050      0x00000004
0x7fffffffe170: 0xffffe260      0x00007fff      0x00000000      0x00000000
0x7fffffffe180: 0x55555190      0x00005555      0xf7e1de0b      0x00007fff
0x7fffffffe190: 0x00000000      0x00000000      0xffffe268      0x00007fff
0x7fffffffe1a0: 0x00040000      0x00000004      0x55555135      0x00005555
Désassembler la fonction courante main () :
(gdb) disassemble
Dump of assembler code for function main:
   0x0000555555555135 <+0>:     push   %rbp
   0x0000555555555136 <+1>:     mov    %rsp,%rbp
   0x0000555555555139 <+4>:     sub    $0x20,%rsp
   0x000055555555513d <+8>:     mov    %edi,-0x14(%rbp)
   0x0000555555555140 <+11>:    mov    %rsi,-0x20(%rbp)
=> 0x0000555555555144 <+15>:    movl   $0x0,-0x4(%rbp)
   0x000055555555514b <+22>:    jmp    0x55555555517e <main+73>
   0x000055555555514d <+24>:    mov    -0x4(%rbp),%eax
   0x0000555555555150 <+27>:    cltq   
   0x0000555555555152 <+29>:    lea    0x0(,%rax,8),%rdx
   0x000055555555515a <+37>:    mov    -0x20(%rbp),%rax
   0x000055555555515e <+41>:    add    %rdx,%rax
   0x0000555555555161 <+44>:    mov    (%rax),%rdx
   0x0000555555555164 <+47>:    mov    -0x4(%rbp),%eax
   0x0000555555555167 <+50>:    mov    %eax,%esi
   0x0000555555555169 <+52>:    lea    0xe94(%rip),%rdi        # 0x555555556004
   0x0000555555555170 <+59>:    mov    $0x0,%eax
   0x0000555555555175 <+64>:    callq  0x555555555030 <printf@plt>
   0x000055555555517a <+69>:    addl   $0x1,-0x4(%rbp)
   0x000055555555517e <+73>:    mov    -0x4(%rbp),%eax
   0x0000555555555181 <+76>:    cmp    -0x14(%rbp),%eax
   0x0000555555555184 <+79>:    jne    0x55555555514d <main+24>
   0x0000555555555186 <+81>:    mov    $0x0,%eax
   0x000055555555518b <+86>:    leaveq 
   0x000055555555518c <+87>:    retq     
End of assembler dump.
NB : L'option /r permet de visualiser les opcodes, et l'option /m permet d'afficher les lignes du source correspondantes. Par exemple : disassemble /rs.
NB : La commande x peut aussi être utilisée pour afficher la mémoire sous forme d'instructions, donc désassembler. Voir plus loin.
Il est possible d'utiliser des commandes pour modifier l'affichage et visualiser ainsi le source, l'assembleur et la ligne de commandes sous une forme bien plus agréable :
(gdb) layout split
Ce qui donne :
C'est le mode TUI (Text User Interface). Pour en sortir, comme pour le rappeler, utiliser presser <Ctrl-x> puis <Ctrl-a> ou tout simplement <a>.
Afficher les lignes du source autour de la ligne à laquelle l'exécution a été interompue :
(gdb) list
1       #include 
2       int main (int argc, char *argv[]) {
3               for (int i = 0; i != argc; i ++)
4                       printf ("Argument %d: %s", i, argv[i]);
5               return (0);
6       }
Avancer d'une étape dans l'exécution en rentrant dans la fonction printf () :
(gdb) step
4                       printf ("Argument %d: %s", i, argv[i]);

(gdb) step
__printf (format=0x555555556004 "Argument %d: %s") at printf.c:28
28      printf.c: No such file or directory.
NB : Pour ne pas rentrer dans le détail de printf (), il fallait utiliser la commande next.
Afficher la pile des appels :
(gdb) backtrace
#0  __printf (format=0x555555556004 "Argument %d: %s") at printf.c:28
#1  0x000055555555517a in main (argc=4, argv=0x7fffffffe268) at test.c:4
Exécuter printf () jusqu'à son terme et interrompre l'exécution au retour dans main () :
(gdb) finish
Run till exit from #0  __printf (format=0x555555556004 "%s") at printf.c:28
main (argc=2, argv=0x7fffffffe278) at test.c:4
4               return (0);
Value returned is $3 = 20
Ajouter un watch sur i pour arrêter l'exécution quand dès que i est accédée en écriture :
(gdb) watch i
Hardware watchpoint 2: i
NB : Il est possible de spécifier un watch en lecture (rwatch), en lecture / écriture (awatch)
Afficher la liste des watches :
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000555555555144 in main at test.c:3
	breakpoint already hit 1 time
2       hw watchpoint  keep y                      i
NB : Un watch est un breakpoint, donc on le supprime / active / désactive comme un breakpoint.
Reprendre l'exécution jusqu'à un accès en écriture sur i :
(gdb) c
Continuing.

Hardware watchpoint 2: i

Old value = 0
New value = 1
0x000055555555517e in main (argc=4, argv=0x7fffffffe268) at test.c:3
3               for (int i = 0; i != argc; i ++)
Désassembler à partir de l'instruction courante ($rip et $p désignent le même registre) :
(gdb) x /5i $pc
=> 0x555555555181 <main+76>:    cmp    -0x14(%rbp),%eax
   0x555555555184 <main+79>:    jne    0x55555555514d <main+24>
   0x555555555186 <main+81>:    mov    $0x0,%eax
   0x55555555518b <main+86>:    leaveq 
   0x55555555518c <main+87>:    retq   
Afficher le contenu registre EAX, qui contient visiblement la valeur de i :
(gdb) p $rax
$4 = 1
Supprimer le watch et le remplacer par un watch sur la lecture de i valant 2 :
(gdb) delete 2

(gdb) rwatch i==2
Hardware read watchpoint 3: i==2

(gdb) info breakpoints
Num     Type            Disp Enb Address            What
1       breakpoint      keep y   0x0000555555555144 in main at test.c:3
	breakpoint already hit 1 time
3       read watchpoint keep y                      i==2
Reprendre l'exécution jusqu'à ce qu'à un accès en lecture sur i valant 2 :
(gdb) c
Continuing.

Hardware read watchpoint 3: i==2

Value = 0
0x0000555555555181 in main (argc=4, argv=0x7fffffffe268) at test.c:3
3               for (int i = 0; i != argc; i ++)
Activer l'affichage des instructions suivantes en assembleur :
(gdb) set disassemble-next-line on
Avancer d'une étape dans l'exécution, mais d'une instruction en assembleur et non plus d'une instruction en C :
(gdb) si
0x0000555555555184      3               for (int i = 0; i != argc; i ++)
   0x000055555555517a <main+69>:        83 45 fc 01     addl   $0x1,-0x4(%rbp)
   0x000055555555517e <main+73>:        8b 45 fc        mov    -0x4(%rbp),%eax
   0x0000555555555181 <main+76>:        3b 45 ec        cmp    -0x14(%rbp),%eax
=> 0x0000555555555184 <main+79>:        75 c7   jne    0x55555555514d <main+24>
Supprimer le watch sur i valant 2 :
(gdb) delete 3
Reprendre l'exécution jusqu'au terme du programme :
(gdb) c
Continuing.
Argument 0: /root/Downloads/testArgument 1: oneArgument 2: twoArgument 3: three[Inferior 1 (process 32908) exited normally]
Hackflash #4 : Déboguer avec GDB : un bref didacticiel