Quebra de pilha detectada

246

Estou executando meu arquivo a.out. Após a execução, o programa é executado por algum tempo e sai com a mensagem:

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

Quais poderiam ser as possíveis razões para isso e como retificá-lo?

Biswajyoti Das
fonte
2
Você poderia identificar quais partes do seu código causam o esmagamento da pilha e publicá-lo? Então, provavelmente seremos capazes de apontar exatamente por que isso acontece e como corrigi-lo.
Bjarke Freund-Hansen
Eu acho que é sinônimo de erro de estouro. Por exemplo, se você inicializar e matriz de 5 elementos, esse erro aparecerá ao tentar gravar o sexto elemento ou qualquer elemento fora dos limites da matriz.
DorinPopescu

Respostas:

349

O Stack Smashing aqui é realmente causado devido a um mecanismo de proteção usado pelo gcc para detectar erros de estouro de buffer. Por exemplo, no seguinte snippet:

#include <stdio.h>

void func()
{
    char array[10];
    gets(array);
}

int main(int argc, char **argv)
{
    func();
}

O compilador (neste caso o gcc) adiciona variáveis ​​de proteção (chamadas canaries) que possuem valores conhecidos. Uma sequência de entrada de tamanho maior que 10 causa corrupção nessa variável, resultando em SIGABRT para finalizar o programa.

Para obter algumas dicas, tente desativar essa proteção do gcc usando a opção -fno-stack-protector durante a compilação. Nesse caso, você receberá um erro diferente, provavelmente uma falha de segmentação, ao tentar acessar um local de memória ilegal. Observe que -fstack-protectorsempre deve estar ativado para compilações de versão, pois é um recurso de segurança.

Você pode obter algumas informações sobre o ponto de estouro executando o programa com um depurador. O Valgrind não funciona bem com erros relacionados à pilha, mas, como um depurador, pode ajudá-lo a identificar o local e o motivo da falha.

sud03r
fonte
3
obrigado por esta resposta! Descobri que no meu caso eu não tivesse inicializada a variável eu estava a tentar escrever
Ted Pennings
5
O Valgrind não funcionar bem para erros pilha-relacionada, uma vez que não é possível adicionar zonas vermelhas lá
toasted_flakes
7
Esta resposta está incorreta e fornece conselhos perigosos. Antes de tudo, remover o protetor de pilha não é a solução certa - se você estiver recebendo um erro de esmagamento de pilha, provavelmente terá uma séria vulnerabilidade de segurança em seu código. A resposta correta é corrigir o código do buggy . Segundo, como o grasGendarme ressalta, a recomendação para experimentar o Valgrind não será eficaz. O Valgrind normalmente não funciona para detectar acessos ilegais de memória a dados alocados por pilha.
DW
22
O OP pede possíveis razões para esse comportamento. Minha resposta fornece um exemplo e como ele se relaciona com um erro razoavelmente conhecido. Além disso, remover o protetor de pilha não é uma solução, é uma espécie de experimento que se poderia fazer para obter mais insights sobre o problema. O conselho é realmente corrigir o erro de alguma forma, obrigado por apontar sobre valgrind, editarei minha resposta para refletir isso.
sud03r
4
@DW, a proteção da pilha deve ser desativada em uma versão de lançamento, porque a princípio - a mensagem detectada de quebra de pilha é uma ajuda apenas para desenvolvedores; no segundo - um aplicativo ainda poderia ter chances de sobreviver; e na terceira - essa é uma pequena otimização.
Hi-Angel
33

Exemplo de reprodução mínima com análise de desmontagem

main.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 upstream .

Compile e execute:

gcc -fstack-protector -g -O0 -std=c99 main.c
ulimit -c unlimited && rm -f core
./a.out

falha conforme desejado:

*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)

Testado no Ubuntu 16.04, GCC 6.4.0.

Desmontagem

Agora olhamos para a desmontagem:

objdump -D a.out

que contém:

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 

Observe os comentários úteis adicionados automaticamente pelo objdump's módulo de inteligência artificial .

Se você executar este programa várias vezes através do GDB, verá que:

  • o canário recebe um valor aleatório diferente toda vez
  • o último loop de myfuncé exatamente o que modifica o endereço do canário

O canário foi randomizado definindo-o com %fs:0x28, que contém um valor aleatório, conforme explicado em:

Tentativas de depuração

A partir de agora, modificamos o código:

    myfunc(arr, len + 1);

em vez disso:

    myfunc(arr, len);
    myfunc(arr, len + 1); /* line 12 */
    myfunc(arr, len);

para ser mais interessante.

Em seguida, tentaremos verificar se podemos identificar a + 1chamada culpada com um método mais automatizado do que apenas ler e entender todo o código-fonte.

gcc -fsanitize=address para ativar o Sananizador de endereços do Google (ASan)

Se você recompilar com esse sinalizador e executar o programa, ele produzirá:

#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

seguido por uma saída mais colorida.

Isso indica claramente a linha problemática 12.

O código-fonte para isso está em: https://github.com/google/sanitizers, mas, como vimos no exemplo, ele já está incorporado no GCC.

O ASan também pode detectar outros problemas de memória, como vazamentos de memória: Como encontrar vazamento de memória em um código / projeto C ++?

Valgrind SGCheck

Como mencionado por outros , Valgrind não é bom em resolver esse tipo de problema.

Possui uma ferramenta experimental chamada SGCheck :

O SGCheck é uma ferramenta para encontrar excedentes de pilha e matrizes globais. Ele funciona usando uma abordagem heurística derivada de uma observação sobre as formas prováveis ​​de acesso à pilha e a matriz global.

Portanto, não fiquei muito surpreso quando não encontrou o erro:

valgrind --tool=exp-sgcheck ./a.out

Aparentemente, a mensagem de erro deve ser semelhante a esta: Erro ausente do Valgrind

GDB

Uma observação importante é que, se você executar o programa através do GDB, ou examinar o corearquivo após o fato:

gdb -nh -q a.out core

então, como vimos na montagem, o GDB deve apontar o final da função que fez a verificação do canário:

(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)

E, portanto, o problema provavelmente ocorre em uma das chamadas que essa função fez.

Em seguida, tentamos identificar a chamada com falha exata, primeiro aumentando rapidamente após o canário ser definido:

  400581:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400588:       00 00 
  40058a:       48 89 45 f8             mov    %rax,-0x8(%rbp)

e assistindo o endereço:

(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

Agora, isso nos deixa com as instruções ofensivas corretas: len = 5e i = 4, nesse caso em particular, nos apontou a linha culpada 12.

No entanto, o backtrace está corrompido e contém algum lixo. Um backtrace correto seria semelhante a:

#0  myfunc (src=0x7fffffffcf14 "abcd", len=4) at main.c:3
#1  0x00000000004005b8 in main () at main.c:11

talvez isso possa corromper a pilha e impedir que você veja o rastreio.

Além disso, esse método requer saber qual é a última chamada da função de verificação de canário, caso contrário, você terá falsos positivos, que nem sempre serão viáveis, a menos que você use a depuração reversa .

Ciro Santilli adicionou uma nova foto
fonte
16

Por favor, observe a seguinte situação:

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:$ 

Quando desativei o protetor de quebra de pilha, nenhum erro foi detectado, o que deveria ter acontecido quando usei "./a.out wepassssssssssssssssss"

Portanto, para responder à sua pergunta acima, a mensagem "** esmagamento de pilha detectado: xxx" foi exibida porque seu protetor de esmagamento de pilha estava ativo e descobriu que há excesso de pilha no seu programa.

Apenas descubra onde isso ocorre e corrija-o.

wearetherock
fonte
7

Você pode tentar depurar o problema usando o valgrind :

Atualmente, a distribuição Valgrind inclui seis ferramentas de qualidade de produção: um detector de erro de memória, dois detectores de erro de encadeamento, um criador de perfil de previsão de cache e ramificação, um gerador de perfil de cache que gera gráfico de chamadas e um criador de perfil de heap. Ele também inclui duas ferramentas experimentais: um detector de saturação de pilha / pilha / matriz global e um gerador básico de vetor de bloco SimPoint. É executado nas seguintes plataformas: X86 / Linux, AMD64 / Linux, PPC32 / Linux, PPC64 / Linux e X86 / Darwin (Mac OS X).

hlovdal
fonte
2
Sim, mas o Valgrind não funciona bem para estouros de buffers alocados à pilha, que é a situação que essa mensagem de erro indica.
DW
4
Como poderíamos usar esse detector de saturação de array de pilha ? Você pode elaborar?
Craig McQueen
@CraigMcQueen Tentei usar o detector de quebra de pilha heurística experimental SGCheck da Valgrind em um exemplo mínimo: stackoverflow.com/a/51897264/895245, mas ele falhou.
Ciro Santilli #
4

Isso significa que você escreveu para algumas variáveis ​​na pilha de maneira ilegal, provavelmente como resultado de um estouro de buffer .

starblue
fonte
9
Estouro de pilha é a pilha se esmagando em outra coisa. Aqui está o contrário: algo se chocou contra a pilha.
22640 Peter Mortensen
5
Na verdade não. É uma parte da pilha quebrando em outra parte. Portanto, é realmente um estouro de buffer, não apenas no topo da pilha, mas "apenas" em outra parte da pilha.
Bas Wijnen 30/11/2012
2

Quais poderiam ser as possíveis razões para isso e como retificá-lo?

Um cenário seria o seguinte exemplo:

#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--;
    }
}

Neste programa, você pode reverter uma String ou uma parte da string se, por exemplo, chamar reverse()com algo como isto:

reverse( arr + 2 );

Se você decidir passar o comprimento da matriz assim:

#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--;
    }
}

Funciona bem também.

Mas quando você faz isso:

revSTR( arr + 2, len );

Você obtém:

==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)

E isso acontece porque no primeiro código, o comprimento de arré verificado dentro do revSTR()qual está correto, mas no segundo código em que você passa o comprimento:

revSTR( arr + 2, len );

agora o comprimento é maior que o comprimento que você passa quando diz arr + 2.

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

Michi
fonte
1
Eu gosto deste exemplo porque ele não depende de funções de biblioteca padrão como getse scrcpy. Gostaria de saber se poderíamos minimizar se ainda mais. Eu, pelo menos, se livrar de string.hcom size_t len = sizeof( arr );. Testado no gcc 6.4, Ubuntu 16.04. Eu também daria o exemplo com falha arr + 2para minimizar a colagem de cópias.
Ciro Santilli #
1

Corrupções de pilha geralmente causadas por estouros de buffer. Você pode se defender contra eles programando defensivamente.

Sempre que você acessar uma matriz, coloque uma declaração antes dela para garantir que o acesso não esteja fora dos limites. Por exemplo:

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

Isso faz você pensar nos limites da matriz e também na adição de testes para acioná-los, se possível. Se algumas dessas afirmações falharem durante o uso normal, transforme-as em regulares if.

Calmarius
fonte
0

Eu recebi esse erro ao usar malloc () para alocar alguma memória para uma estrutura * depois de gastar um pouco dessa depuração do código, finalmente usei a função free () para liberar a memória alocada e, posteriormente, a mensagem de erro desapareceu :)

djangodude
fonte
0

Outra fonte de esmagamento de pilha é o uso (incorreto) de em vfork()vez de fork().

Acabei de depurar um caso disso, em que o processo filho não pôde execve()executar o executável de destino e retornou um código de erro em vez de chamar _exit().

Como vfork()gerou esse filho, ele retornou enquanto ainda estava em execução no espaço de processo do pai, não apenas corrompendo a pilha do pai, mas fazendo com que dois conjuntos diferentes de diagnósticos fossem impressos pelo código "downstream".

Mudar vfork()para fork()corrigir os dois problemas, assim como mudar a returndeclaração da criança para _exit().

Mas como o código filho precede a execve()chamada com chamadas para outras rotinas (para definir o uid / gid, nesse caso em particular), tecnicamente não atende aos requisitos vfork(), portanto, a alteração para uso fork()está correta aqui.

(Observe que a returndeclaração problemática não era realmente codificada como tal - em vez disso, uma macro foi chamada e essa macro decidiu se baseava _exit()ou returnbaseava-se em uma variável global. Portanto, não ficou imediatamente óbvio que o código filho não era conforme ao vfork()uso. )

Para mais informações, veja:

A diferença entre fork (), vfork (), exec () e clone ()

James Craig Burley
fonte