J'exécute mon fichier a.out. Après exécution, le programme s'exécute pendant un certain temps, puis se ferme avec le message suivant:
**** stack smashing detected ***: ./a.out terminated*
*======= Backtrace: =========*
*/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)Aborted*
Quelles pourraient en être les raisons possibles et comment puis-je le corriger?
Le smacking de pile ici est en fait dû à un mécanisme de protection utilisé par gcc pour détecter les erreurs de débordement de tampon. Par exemple, dans l'extrait suivant:
#include <stdio.h>
void func()
{
char array[10];
gets(array);
}
int main(int argc, char **argv)
{
func();
}
Le compilateur (dans ce cas gcc) ajoute des variables de protection (appelées canaries) ayant des valeurs connues. Une chaîne d'entrée de taille supérieure à 10 provoque la corruption de cette variable, ce qui a pour résultat que SIGABRT met fin au programme.
Pour obtenir un aperçu, vous pouvez essayer de désactiver cette protection de gcc en utilisant l'option -fno-stack-protector
pendant la compilation. Dans ce cas, vous obtiendrez une erreur différente, probablement une erreur de segmentation, lorsque vous tenterez d'accéder à un emplacement de mémoire illégal. Notez que -fstack-protector
devrait toujours être activé pour les versions validées car il s'agit d'une fonctionnalité de sécurité.
Vous pouvez obtenir des informations sur le point de débordement en exécutant le programme avec un débogueur. Valgrind ne fonctionne pas bien avec les erreurs liées à la pile, mais comme un débogueur, cela peut vous aider à localiser avec précision l'emplacement et la raison du crash.
Exemple de reproduction minimale avec analyse du 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;
}
_
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
Regardons maintenant 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 automatiquement ajoutés par objdump
's module d'intelligence artificielle .
Si vous exécutez ce programme plusieurs fois via GDB, vous constaterez que:
myfunc
est exactement ce qui modifie l'adresse du canariLe canari est randomisé en le configurant avec _%fs:0x28
_, qui contient une valeur aléatoire comme expliqué à:
Tentatives de débogage
A partir de maintenant, on modifie le code:
_ myfunc(arr, len + 1);
_
être à la place:
_ myfunc(arr, len);
myfunc(arr, len + 1); /* line 12 */
myfunc(arr, len);
_
être plus intéressant.
Nous tenterons ensuite de voir si nous pouvons identifier l'appel coupable _+ 1
_ avec une méthode plus automatisée que la simple lecture et compréhension du code source complet.
gcc -fsanitize=address
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 indique clairement la ligne problématique 12.
Valgrind SGCheck
Comme mentionné par d'autres , Valgrind n'est pas bon pour résoudre ce genre de problème.
Il possède un outil expérimental appelé SGCheck :
SGCheck est un outil permettant de détecter les dépassements de pile et de tableaux globaux. Cela fonctionne en utilisant une approche heuristique dérivée d'une observation sur les formes probables d'accès aux piles et aux tableaux globaux.
Donc je n’ai 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 apparemment: erreur manquante pour Valgrind
GDB
Une observation importante est que si vous exécutez le programme via GDB, ou examinez le fichier core
après coup:
_gdb -nh -q a.out core
_
ensuite, comme nous l'avons vu à l'Assemblée, GDB devrait vous indiquer la fin de la fonction qui a effectué la vérification canari:
_(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 probablement dans l'un des appels que cette fonction a fait.
Ensuite, nous essayons de localiser l’appel ayant échoué en commençant par un simple, juste après le réglage 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 en regardant 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
_, et dans ce cas particulier, nous a indiqué la ligne 12 du coupable.
Cependant, la trace est corrompue et contient des corbeilles. Un backtrace correct 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.
En outre, cette méthode nécessite de savoir quel est le dernier appel de la fonction de vérification de canary, sinon vous aurez des faux positifs, ce qui ne sera pas toujours possible, à moins que vous tilisez le débogage inversé .
Veuillez regarder la situation suivante:
ab@cd-x:$ cat test_overflow.c
#include <stdio.h>
#include <string.h>
int check_password(char *password){
int flag = 0;
char buffer[20];
strcpy(buffer, password);
if(strcmp(buffer, "mypass") == 0){
flag = 1;
}
if(strcmp(buffer, "yourpass") == 0){
flag = 1;
}
return flag;
}
int main(int argc, char *argv[]){
if(argc >= 2){
if(check_password(argv[1])){
printf("%s", "Access granted\n");
}else{
printf("%s", "Access denied\n");
}
}else{
printf("%s", "Please enter password!\n");
}
}
ab@cd-x:$ gcc -g -fno-stack-protector test_overflow.c
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out wepassssssssssssssssss
Access granted
ab@cd-x:$ gcc -g -fstack-protector test_overflow.c
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./a.out wepassssssssssssssssss
*** stack smashing detected ***: ./a.out terminated
======= Backtrace: =========
/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)[0xce0ed8]
/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x0)[0xce0e90]
./a.out[0x8048524]
./a.out[0x8048545]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6)[0xc16b56]
./a.out[0x8048411]
======= Memory map: ========
007d9000-007f5000 r-xp 00000000 08:06 5776 /lib/libgcc_s.so.1
007f5000-007f6000 r--p 0001b000 08:06 5776 /lib/libgcc_s.so.1
007f6000-007f7000 rw-p 0001c000 08:06 5776 /lib/libgcc_s.so.1
0090a000-0090b000 r-xp 00000000 00:00 0 [vdso]
00c00000-00d3e000 r-xp 00000000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so
00d3e000-00d3f000 ---p 0013e000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so
00d3f000-00d41000 r--p 0013e000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so
00d41000-00d42000 rw-p 00140000 08:06 1183 /lib/tls/i686/cmov/libc-2.10.1.so
00d42000-00d45000 rw-p 00000000 00:00 0
00e0c000-00e27000 r-xp 00000000 08:06 4213 /lib/ld-2.10.1.so
00e27000-00e28000 r--p 0001a000 08:06 4213 /lib/ld-2.10.1.so
00e28000-00e29000 rw-p 0001b000 08:06 4213 /lib/ld-2.10.1.so
08048000-08049000 r-xp 00000000 08:05 1056811 /dos/hacking/test/a.out
08049000-0804a000 r--p 00000000 08:05 1056811 /dos/hacking/test/a.out
0804a000-0804b000 rw-p 00001000 08:05 1056811 /dos/hacking/test/a.out
08675000-08696000 rw-p 00000000 00:00 0 [heap]
b76fe000-b76ff000 rw-p 00000000 00:00 0
b7717000-b7719000 rw-p 00000000 00:00 0
bfc1c000-bfc31000 rw-p 00000000 00:00 0 [stack]
Aborted
ab@cd-x:$
Lorsque j'ai désactivé le protecteur de destruction de pile, aucune erreur n'a été détectée, ce qui aurait dû se produire lorsque j'ai utilisé "./a.out wepassssssssssssssssss"
Donc, pour répondre à votre question ci-dessus, le message "** pile détruite détectée: xxx" était affiché car votre protecteur de destruction de pile était actif et avait détecté un débordement de pile dans votre programme.
Trouvez simplement où cela se produit et corrigez-le.
Vous pouvez essayer de déboguer le problème en utilisant valgrind :
La distribution Valgrind comprend actuellement six outils de qualité production: un détecteur d’erreur de mémoire, deux détecteurs d’erreur de thread, un profileur de cache et de prédiction de branche, un profileur de cache générant un graphe d’appel et un profileur de segment de mémoire. Il comprend également deux outils expérimentaux: un détecteur de dépassement de pile tas/pile/global , et un générateur de vecteurs de bloc de base SimPoint. Il fonctionne sur les plates-formes suivantes: X86/Linux, AMD64/Linux, PPC32/Linux, PPC64/Linux et X86/Darwin (Mac OS X).
Cela signifie que vous avez écrit de manière illégale sur certaines variables de la pile, probablement à la suite d'un dépassement de mémoire tampon .
Quelles pourraient en être les raisons et comment puis-je le corriger?
Un scénario serait dans l'exemple suivant:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void swap ( char *a , char *b );
void revSTR ( char *const src );
int main ( void ){
char arr[] = "A-B-C-D-E";
revSTR( arr );
printf("ARR = %s\n", arr );
}
void swap ( char *a , char *b ){
char tmp = *a;
*a = *b;
*b = tmp;
}
void revSTR ( char *const src ){
char *start = src;
char *end = start + ( strlen( src ) - 1 );
while ( start < end ){
swap( &( *start ) , &( *end ) );
start++;
end--;
}
}
Dans ce programme, vous pouvez inverser une chaîne ou une partie de la chaîne si vous appelez par exemple reverse()
avec quelque chose comme ceci:
reverse( arr + 2 );
Si vous décidez de passer la longueur du tableau comme ceci:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void swap ( char *a , char *b );
void revSTR ( char *const src, size_t len );
int main ( void ){
char arr[] = "A-B-C-D-E";
size_t len = strlen( arr );
revSTR( arr, len );
printf("ARR = %s\n", arr );
}
void swap ( char *a , char *b ){
char tmp = *a;
*a = *b;
*b = tmp;
}
void revSTR ( char *const src, size_t len ){
char *start = src;
char *end = start + ( len - 1 );
while ( start < end ){
swap( &( *start ) , &( *end ) );
start++;
end--;
}
}
Fonctionne bien aussi.
Mais quand tu fais ça:
revSTR( arr + 2, len );
Vous obtenez:
==7125== Command: ./program
==7125==
ARR = A-
*** stack smashing detected ***: ./program terminated
==7125==
==7125== Process terminating with default action of signal 6 (SIGABRT)
==7125== at 0x4E6F428: raise (raise.c:54)
==7125== by 0x4E71029: abort (abort.c:89)
==7125== by 0x4EB17E9: __libc_message (libc_fatal.c:175)
==7125== by 0x4F5311B: __fortify_fail (fortify_fail.c:37)
==7125== by 0x4F530BF: __stack_chk_fail (stack_chk_fail.c:28)
==7125== by 0x400637: main (program.c:14)
Et cela se produit car dans le premier code, la longueur de arr
est vérifiée à l'intérieur de revSTR()
, ce qui est correct, mais dans le second code où vous passez la longueur:
revSTR( arr + 2, len );
la longueur est maintenant plus longue que la longueur que vous passez quand vous dites arr + 2
.
Longueur de strlen ( arr + 2 )
! = strlen ( arr )
.
Les corruptions de pile généralement causées par des débordements de mémoire tampon. Vous pouvez vous défendre contre eux en programmant de manière défensive.
Chaque fois que vous accédez à un tableau, placez-y une assertion pour vous assurer que l'accès n'est pas interdit. Par exemple:
assert(i + 1 < N);
assert(i < N);
a[i + 1] = a[i];
Cela vous fait penser aux limites d'un tableau et à l'ajout de tests pour les déclencher si possible. Si certaines de ces assertions peuvent échouer lors d'une utilisation normale, convertissez-les en un if
normal.
J'ai eu cette erreur en utilisant malloc () pour allouer de la mémoire à une structure * après avoir passé quelque chose à déboguer le code, j'ai finalement utilisé la fonction free () pour libérer la mémoire allouée, puis le message d'erreur a disparu :)
Une autre source de destruction de pile est l'utilisation (incorrecte) de vfork()
au lieu de fork()
.
Je viens de déboguer un cas de ce type, où le processus enfant était incapable de execve()
l'exécutable cible et renvoyait un code d'erreur plutôt que d'appeler _exit()
.
Parce que vfork()
avait engendré cet enfant, celui-ci est retourné alors qu'il était toujours en cours d'exécution dans l'espace de processus du parent, non seulement en corrompant la pile du parent, mais en imprimant deux ensembles de diagnostics disparates avec du code "en aval".
Changer vfork()
en fork()
corrigeait les deux problèmes, tout comme changer l'instruction return
de l'enfant en _exit()
à la place.
Mais comme le code enfant précède l'appel execve()
avec des appels vers d'autres routines (pour définir l'uid/gid, dans ce cas particulier), il ne répond pas techniquement aux exigences de vfork()
, use fork()
est correct ici.
(Notez que l’instruction problématique return
n’a pas été codée en tant que telle. Une macro a été invoquée et cette macro a décidé si _exit()
ou return
était basée sur une variable globale. il n'était pas immédiatement évident que le code enfant ne soit pas conforme pour vfork()
usage.)
Pour plus d'informations, voir: