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 ()
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 ()
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[]
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]
argv[1] en mémoire à l'adresse de argv
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 ()
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 ()
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 ()
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 ()
printf () jusqu'à son terme et interrompre l'exécution au retour dans main ()
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
i pour arrêter l'exécution quand dès que i
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
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
i :
(gdb) p $rax
$4 = 1
Supprimer le watch et le remplacer par un watch sur la lecture de i
i valant 2
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
i valant 2
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
i valant 2
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]