Détraction de pile détectée

246

J'exécute mon fichier a.out. Après l'exécution, le programme s'exécute pendant un certain temps puis se termine avec le message:

**** stack smashing detected ***: ./a.out terminated*
*======= Backtrace: =========*
*/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)Aborted*

Quelles pourraient être les raisons possibles de cela et comment puis-je y remédier?

Biswajyoti Das
la source
2
Pourriez-vous peut-être identifier les parties de votre code qui provoquent l'écrasement de la pile et la publier? Ensuite, nous pourrons probablement expliquer exactement pourquoi cela se produit et comment le corriger.
Bjarke Freund-Hansen
Je pense que c'est synonyme d'erreur de débordement. Par exemple, si vous initialisez et tableau de 5 éléments, cette erreur apparaîtra lors de la tentative d'écriture du 6e élément ou de tout élément en dehors des limites du tableau.
DorinPopescu

Réponses:

349

Le smashing de pile ici est en fait dû à un mécanisme de protection utilisé par gcc pour détecter les erreurs de dépassement de tampon. Par exemple dans l'extrait de code 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 canaris) qui ont des valeurs connues. Une chaîne d'entrée de taille supérieure à 10 provoque la corruption de cette variable, ce qui entraîne SIGABRT pour terminer le programme.

Pour obtenir des informations, vous pouvez essayer de désactiver cette protection de gcc à l'aide de l'option -fno-stack-protector lors de la compilation. Dans ce cas, vous obtiendrez une erreur différente, probablement une erreur de segmentation lorsque vous essayez d'accéder à un emplacement de mémoire illégal. Notez que cette option -fstack-protectordoit toujours être activée pour les versions, car il s'agit d'une fonction de sécurité.

Vous pouvez obtenir des informations sur le point de dépassement 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, il peut vous aider à identifier l'emplacement et la raison de l'accident.

sud03r
la source
3
merci pour cette réponse! J'ai trouvé que dans mon cas, je n'avais pas initialisé la variable dans laquelle j'essayais d'écrire
Ted Pennings
5
Valgrind ne fonctionne pas bien pour les erreurs liées à la pile, car il ne peut pas y ajouter de zones rouges
toasted_flakes
7
Cette réponse est incorrecte et fournit des conseils dangereux. Tout d'abord, la suppression du protecteur de pile n'est pas la bonne solution - si vous obtenez une erreur d'écrasement de pile, vous avez probablement une grave vulnérabilité de sécurité dans votre code. La bonne réponse est de corriger le code buggy . Deuxièmement, comme le souligne grasGendarme, la recommandation d'essayer Valgrind ne sera pas efficace. Valgrind ne fonctionne généralement pas pour détecter les accès mémoire illégaux aux données allouées à la pile.
DW
22
Le PO demande des raisons possibles pour ce comportement, ma réponse donne un exemple et comment il se rapporte à une erreur raisonnablement connue. En outre, la suppression du protecteur de pile n'est pas une solution, c'est une sorte d'expérience que l'on pourrait faire pour obtenir plus d'informations sur le problème. Le conseil est en fait de corriger l'erreur d'une manière ou d'une autre, merci d'avoir pointé sur valgrind, je vais modifier ma réponse pour refléter cela.
sud03r
4
@DW, la protection de la pile doit être désactivée dans une version finale, car au début, le message d' écrasement de pile détecté n'est utile qu'aux développeurs; au second - une application pourrait encore avoir des chances de survivre; et au troisième - c'est une minuscule optimisation.
Hi-Angel
33

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 objdumple 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 myfuncest 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' + 1appel 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 corefichier 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 = 5et 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 .

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
16

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 suppression 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 "** smashing de pile détecté: xxx" a été affiché car votre protecteur de smashing de pile était actif et a constaté un débordement de pile dans votre programme.

Découvrez où cela se produit et corrigez-le.

wearetherock
la source
7

Vous pouvez essayer de déboguer le problème à l'aide de valgrind :

La distribution Valgrind comprend actuellement six outils de qualité de production: un détecteur d'erreurs de mémoire, deux détecteurs d'erreurs de threads, un profileur de prédiction de cache et de branche, un profileur de cache générant un graphique d'appels et un profileur de tas. Il comprend également deux outils expérimentaux: un détecteur de dépassement de tas / pile / tableau global et un générateur de vecteur 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).

hlovdal
la source
2
Oui, mais Valgrind ne fonctionne pas bien pour les débordements de tampons alloués à la pile, ce qui est le cas indiqué par ce message d'erreur.
DW
4
Comment pourrions-nous utiliser ce détecteur de saturation de réseau de piles ? Peux-tu élaborer?
Craig McQueen
@CraigMcQueen J'ai essayé d'utiliser le détecteur heuristique expérimental de vérification de pile SGCheck de Valgrind sur un exemple minimal: stackoverflow.com/a/51897264/895245 mais il a échoué.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
4

Cela signifie que vous avez écrit sur certaines variables de la pile de manière illégale, probablement à la suite d'un débordement de tampon .

starblue
la source
9
Le débordement de pile est la pile écrasant quelque chose d'autre. Ici, c'est l'inverse: quelque chose s'est écrasé dans la pile.
Peter Mortensen
5
Pas vraiment. C'est une partie de la pile qui se brise dans une autre partie. Il s'agit donc vraiment d'un débordement de tampon, simplement pas au-dessus de la pile, mais "uniquement" dans une autre partie de la pile.
Bas Wijnen
2

Quelles pourraient être les raisons possibles de cela et comment puis-je y remédier?

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 reverse()par exemple avec quelque chose comme ceci:

reverse( arr + 2 );

Si vous décidez de transmettre 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 vous faites cela:

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 arrest vérifiée à l'intérieur de revSTR()ce qui est bien, mais dans le deuxième code où vous passez la longueur:

revSTR( arr + 2, len );

la longueur est maintenant plus longue que la longueur réelle que vous passez lorsque vous dites arr + 2.

Longueur de strlen ( arr + 2 )! = strlen ( arr ).

Michi
la source
1
J'aime cet exemple car il ne repose pas sur des fonctions de bibliothèque standard comme getset scrcpy. Je me demande si nous pourrions minimiser davantage. Je m'en débarrasserais au moins string.havec size_t len = sizeof( arr );. Testé sur gcc 6.4, Ubuntu 16.04. Je donnerais également l'exemple de l'échec avec arr + 2pour minimiser le collage de copie.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1

Les corruptions de pile sont généralement causées par des dépassements de tampon. Vous pouvez vous défendre contre eux en programmant défensivement.

Chaque fois que vous accédez à un tableau, placez une assertion avant pour vous assurer que l'accès n'est pas hors limites. Par exemple:

assert(i + 1 < N);
assert(i < N);
a[i + 1] = a[i];

Cela vous fait penser aux limites du tableau et vous fait également penser à ajouter des tests pour les déclencher si possible. Si certains de ces assertions peuvent échouer lors d'une utilisation normale, transformez-les en un régulier if.

Calmarius
la source
0

J'ai obtenu cette erreur en utilisant malloc () pour allouer de la mémoire à une structure * après avoir dépensé un peu de débogage du code, j'ai finalement utilisé la fonction free () pour libérer la mémoire allouée et par la suite le message d'erreur a disparu :)

djangodude
la source
0

Une autre source d'écrasement de pile est l'utilisation (incorrecte) de vfork()au lieu de fork().

Je viens de déboguer un cas de cela, où le processus enfant n'a pas pu execve()exécuter l'exécutable cible et a renvoyé un code d'erreur plutôt que d'appeler _exit().

Parce qu'il vfork()avait engendré cet enfant, il est revenu alors qu'il s'exécutait toujours dans l'espace de processus du parent, non seulement corrompant la pile du parent, mais provoquant l'impression de deux ensembles de diagnostics disparates par du code "en aval".

Changer vfork()pour fork()résoudre les deux problèmes, tout comme changer la returndéclaration de l'enfant à la _exit()place.

Mais puisque le code enfant précède l' execve()appel avec des appels à d'autres routines (pour définir l'uid / gid, dans ce cas particulier), il ne répond techniquement pas aux exigences de vfork(), donc le changer pour l'utiliser fork()est correct ici.

(Notez que l' returnénoncé problématique n'a pas été réellement codé en tant que tel - à la place, une macro a été invoquée, et cette macro a décidé si elle devait être basée sur une variable globale _exit()ou en returnfonction de celle-ci. Il n'était donc pas immédiatement évident que le code enfant n'était pas conforme pour l' vfork()utilisation. )

Pour plus d'informations, voir:

La différence entre fork (), vfork (), exec () et clone ()

James Craig Burley
la source