Exemple de reproduction minimale avec analyse de démontage
principal c
void myfunc(char *const src, int len) {
int i;
for (i = 0; i < len; ++i) {
src[i] = 42;
}
}
int main(void) {
char arr[] = {'a', 'b', 'c', 'd'};
int len = sizeof(arr);
myfunc(arr, len + 1);
return 0;
}
GitHub en amont .
Compiler et exécuter:
gcc -fstack-protector -g -O0 -std=c99 main.c
ulimit -c unlimited && rm -f core
./a.out
échoue comme souhaité:
*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)
Testé sur Ubuntu 16.04, GCC 6.4.0.
Démontage
Maintenant, nous regardons le démontage:
objdump -D a.out
qui contient:
int main (void){
400579: 55 push %rbp
40057a: 48 89 e5 mov %rsp,%rbp
# Allocate 0x10 of stack space.
40057d: 48 83 ec 10 sub $0x10,%rsp
# Put the 8 byte canary from %fs:0x28 to -0x8(%rbp),
# which is right at the bottom of the stack.
400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400588: 00 00
40058a: 48 89 45 f8 mov %rax,-0x8(%rbp)
40058e: 31 c0 xor %eax,%eax
char arr[] = {'a', 'b', 'c', 'd'};
400590: c6 45 f4 61 movb $0x61,-0xc(%rbp)
400594: c6 45 f5 62 movb $0x62,-0xb(%rbp)
400598: c6 45 f6 63 movb $0x63,-0xa(%rbp)
40059c: c6 45 f7 64 movb $0x64,-0x9(%rbp)
int len = sizeof(arr);
4005a0: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
myfunc(arr, len + 1);
4005a7: 8b 45 f0 mov -0x10(%rbp),%eax
4005aa: 8d 50 01 lea 0x1(%rax),%edx
4005ad: 48 8d 45 f4 lea -0xc(%rbp),%rax
4005b1: 89 d6 mov %edx,%esi
4005b3: 48 89 c7 mov %rax,%rdi
4005b6: e8 8b ff ff ff callq 400546 <myfunc>
return 0;
4005bb: b8 00 00 00 00 mov $0x0,%eax
}
# Check that the canary at -0x8(%rbp) hasn't changed after calling myfunc.
# If it has, jump to the failure point __stack_chk_fail.
4005c0: 48 8b 4d f8 mov -0x8(%rbp),%rcx
4005c4: 64 48 33 0c 25 28 00 xor %fs:0x28,%rcx
4005cb: 00 00
4005cd: 74 05 je 4005d4 <main+0x5b>
4005cf: e8 4c fe ff ff callq 400420 <__stack_chk_fail@plt>
# Otherwise, exit normally.
4005d4: c9 leaveq
4005d5: c3 retq
4005d6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4005dd: 00 00 00
Remarquez les commentaires pratiques ajoutés automatiquement par objdump
le module d'intelligence artificielle de .
Si vous exécutez ce programme plusieurs fois via GDB, vous verrez que:
- le canari obtient une valeur aléatoire différente à chaque fois
- la dernière boucle
myfunc
est exactement ce qui modifie l'adresse du canari
Le canari randomisé en le définissant avec %fs:0x28
, qui contient une valeur aléatoire comme expliqué à:
Tentatives de débogage
Désormais, nous modifions le code:
myfunc(arr, len + 1);
être à la place:
myfunc(arr, len);
myfunc(arr, len + 1); /* line 12 */
myfunc(arr, len);
pour être plus intéressant.
Nous essaierons ensuite de voir si nous pouvons localiser l' + 1
appel coupable avec une méthode plus automatisée que la simple lecture et compréhension de tout le code source.
gcc -fsanitize=address
pour activer l'assainissement d'adresse de Google (ASan)
Si vous recompilez avec cet indicateur et exécutez le programme, il génère:
#0 0x4008bf in myfunc /home/ciro/test/main.c:4
#1 0x40099b in main /home/ciro/test/main.c:12
#2 0x7fcd2e13d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400798 in _start (/home/ciro/test/a.out+0x40079
suivi d'une sortie plus colorée.
Cela identifie clairement la ligne problématique 12.
Le code source pour cela est à: https://github.com/google/sanitizers mais comme nous l'avons vu dans l'exemple, il est déjà en amont dans GCC.
ASan peut également détecter d'autres problèmes de mémoire tels que des fuites de mémoire: Comment trouver une fuite de mémoire dans un code / projet C ++?
Valgrind SGCheck
Comme mentionné par d'autres , Valgrind n'est pas bon pour résoudre ce genre de problème.
Il dispose d'un outil expérimental appelé SGCheck :
SGCheck est un outil pour trouver des dépassements de pile et de tableaux globaux. Il fonctionne en utilisant une approche heuristique dérivée d'une observation sur les formes probables d'accès à la pile et au tableau global.
Je n'ai donc pas été très surpris quand il n'a pas trouvé l'erreur:
valgrind --tool=exp-sgcheck ./a.out
Le message d'erreur devrait ressembler à ceci: Erreur manquante de Valgrind
GDB
Une observation importante est que si vous exécutez le programme via GDB ou examinez le core
fichier après coup:
gdb -nh -q a.out core
puis, comme nous l'avons vu sur l'assemblage, GDB devrait vous indiquer la fin de la fonction qui a effectué la vérification des canaris:
(gdb) bt
#0 0x00007f0f66e20428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1 0x00007f0f66e2202a in __GI_abort () at abort.c:89
#2 0x00007f0f66e627ea in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7f0f66f7a49f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:175
#3 0x00007f0f66f0415c in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7f0f66f7a481 "stack smashing detected") at fortify_fail.c:37
#4 0x00007f0f66f04100 in __stack_chk_fail () at stack_chk_fail.c:28
#5 0x00000000004005f6 in main () at main.c:15
(gdb) f 5
#5 0x00000000004005f6 in main () at main.c:15
15 }
(gdb)
Et donc le problème est probable dans l'un des appels que cette fonction a fait.
Ensuite, nous essayons de localiser exactement l'appel défaillant en intensifiant le premier single juste après la mise en place du canari:
400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400588: 00 00
40058a: 48 89 45 f8 mov %rax,-0x8(%rbp)
et regarder l'adresse:
(gdb) p $rbp - 0x8
$1 = (void *) 0x7fffffffcf18
(gdb) watch 0x7fffffffcf18
Hardware watchpoint 2: *0x7fffffffcf18
(gdb) c
Continuing.
Hardware watchpoint 2: *0x7fffffffcf18
Old value = 1800814336
New value = 1800814378
myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
3 for (i = 0; i < len; ++i) {
(gdb) p len
$2 = 5
(gdb) p i
$3 = 4
(gdb) bt
#0 myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
#1 0x00000000004005cc in main () at main.c:12
Maintenant, cela nous laisse à la bonne instruction fautive: len = 5
et i = 4
, dans ce cas particulier, nous a pointés vers la ligne coupable 12.
Cependant, la trace est corrompue et contient des déchets. Une trace correcte ressemblerait à ceci:
#0 myfunc (src=0x7fffffffcf14 "abcd", len=4) at main.c:3
#1 0x00000000004005b8 in main () at main.c:11
alors peut-être que cela pourrait corrompre la pile et vous empêcher de voir la trace.
De plus, cette méthode nécessite de savoir quel est le dernier appel de la fonction de vérification des canaris, sinon vous aurez des faux positifs, ce qui ne sera pas toujours possible, sauf si vous utilisez le débogage inverse .