Обнаружено разрушение стека


246

Я выполняю свой файл .out. После выполнения программа работает некоторое время, а затем завершается с сообщением:

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

Каковы могут быть возможные причины этого и как мне это исправить?


2
Не могли бы вы определить, какие части кода вызывают разрушение стека, и опубликовать его? Тогда мы, вероятно, сможем указать, почему именно это происходит и как это исправить.
Бьярке Фрейнд-Хансен

Я думаю, что это синоним ошибки переполнения. Например, если вы инициализируете массив из 5 элементов, эта ошибка появится при попытке записать 6-й элемент или любой элемент за пределами массива.
ДоринПопеску

Ответы:


349

Разрушение стека здесь на самом деле вызвано механизмом защиты, используемым gcc для обнаружения ошибок переполнения буфера. Например, в следующем фрагменте:

#include <stdio.h>

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

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

Компилятор (в данном случае gcc) добавляет переменные защиты (называемые канареями), которые имеют известные значения. Входная строка размером больше 10 приводит к повреждению этой переменной, в результате чего SIGABRT завершает программу.

Чтобы получить представление, вы можете попробовать отключить эту защиту gcc используя опцию -fno-stack-protector во время компиляции. В этом случае вы получите другую ошибку, скорее всего, ошибку сегментации, когда вы пытаетесь получить доступ к неправильной ячейке памяти. Обратите внимание, что -fstack-protectorвсегда следует включать для сборок релиза, так как это функция безопасности.

Вы можете получить некоторую информацию о точке переполнения, запустив программу с отладчиком. Valgrind плохо работает с ошибками, связанными со стеком, но, как и отладчик, он может помочь вам точно определить местоположение и причину сбоя.


3
спасибо за этот ответ! Я обнаружил, что в моем случае я не инициализировал переменную, в которую пытался записать
Тед Пеннингс

5
Valgrind не работает хорошо для ошибок, связанных со стеком, так как он не может добавлять красные зоны там
toasted_flakes

7
Этот ответ неверен и дает опасные советы. Прежде всего, удаление защитника стека не является правильным решением - если вы получаете ошибку разрушения стека, вы, вероятно, имеете серьезную уязвимость в своем коде. Правильный ответ - исправить ошибочный код . Во-вторых, как отмечает grasGendarme, рекомендация попробовать Valgrind не будет эффективной. Valgrind обычно не работает для обнаружения недопустимого доступа к памяти для данных, выделенных стеком.
DW

22
ОП спрашивает о возможных причинах такого поведения, мой ответ приводит пример и то, как это связано с достаточно известной ошибкой. Кроме того, удаление защитника стека не является решением, это своего рода эксперимент, который можно сделать, чтобы получить более полное представление о проблеме. На самом деле совет заключается в том, чтобы как-то исправить ошибку, спасибо за указание на valgrind, я отредактирую свой ответ, чтобы отразить это.
3

4
@DW защита стека должна быть отключена в версии выпуска, потому что сначала - сообщение об обнаружении разрушения стека помогает только разработчикам; во-вторых, у приложения еще могут быть шансы на выживание; и на третьем - это крошечная оптимизация.
Привет, Ангел,

34

Пример минимального воспроизведения с анализом разборки

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 вверх по течению .

Скомпилируйте и запустите:

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

терпит неудачу по желанию:

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

Протестировано на Ubuntu 16.04, GCC 6.4.0.

разборка

Теперь посмотрим на разборку:

objdump -D a.out

который содержит:

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 

Обратите внимание на полезные комментарии автоматически добавлен objdump«S искусственного интеллекта модуль .

Если вы запустите эту программу несколько раз через GDB, вы увидите, что:

  • канарейка каждый раз получает разные случайные значения
  • последний цикл myfuncименно то, что изменяет адрес канарейки

Канарейка рандомизируется путем установки его с помощью %fs:0x28, который содержит случайное значение, как описано в:

Попытки отладки

С этого момента мы модифицируем код:

    myfunc(arr, len + 1);

быть вместо:

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

быть более интересным

Затем мы попытаемся выяснить, сможем ли мы точно определить + 1вызов преступника с помощью более автоматизированного метода, чем просто чтение и понимание всего исходного кода.

gcc -fsanitize=address включить Google Sanitizer адреса (ASan)

Если вы перекомпилируете с этим флагом и запустите программу, она выдаст:

#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

с последующим более цветным выводом.

Это ясно указывает на проблемную линию 12.

Исходный код для этого находится по адресу: https://github.com/google/sanitizers, но, как мы видели из примера, он уже передан в GCC.

ASan также может обнаруживать другие проблемы с памятью, такие как утечки памяти: как найти утечку памяти в коде / проекте C ++?

Valgrind SGCheck

Как уже упоминалось , Вальгринд не очень хорошо решает подобные проблемы.

У него есть экспериментальный инструмент под названием SGCheck :

SGCheck - это инструмент для поиска переполнения стека и глобальных массивов. Он работает с использованием эвристического подхода, основанного на наблюдении за вероятными формами доступа к стекам и глобальным массивам.

Поэтому я не очень удивился, когда не нашел ошибку:

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

Сообщение об ошибке должно выглядеть следующим образом: ошибка Valgrind отсутствует

GDB

Важным наблюдением является то, что если вы запускаете программу через GDB или просматриваете coreфайл после факта:

gdb -nh -q a.out core

затем, как мы видели на сборке, GDB должен указать вам на конец функции, которая выполняла канарейку:

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

И, следовательно, проблема, скорее всего, в одном из вызовов этой функции.

Затем мы попытаемся точно определить точный сбойный вызов, сделав первый шаг сразу после установки канарейки:

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

и смотреть адрес:

(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

Теперь, это оставляет нас в правильном нарушении инструкции: len = 5и i = 4, и в данном конкретном случае, указало нам на виновную линию 12.

Однако обратная трассировка повреждена и содержит некоторое количество мусора. Правильная обратная трассировка будет выглядеть так:

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

так что, возможно, это может повредить стек и помешать вам увидеть след.

Кроме того, этот метод требует знания того, что является последним вызовом функции проверки канарейки, в противном случае у вас будут ложные срабатывания, что не всегда возможно, если вы не используете обратную отладку .


16

Пожалуйста, посмотрите на следующую ситуацию:

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

Когда я отключил защиту от разрушения стека, ошибок не обнаружено, что должно было произойти, когда я использовал «./a.out wepassssssssssssssssss»

Таким образом, чтобы ответить на ваш вопрос выше, было отображено сообщение «** smashing dismashing: xxx», потому что ваш защитник разрушения стека был активен и обнаружил, что в вашей программе переполнение стека.

Просто выясните, где это происходит, и исправьте это.


7

Вы можете попытаться отладить проблему, используя valgrind :

В настоящее время дистрибутив Valgrind включает шесть инструментов качества производства: детектор ошибок памяти, два детектора ошибок потока, профилировщик кэша и прогнозирования ветвлений, профилировщик кэша, генерирующий граф вызовов, и профилировщик кучи. Он также включает два экспериментальных инструмента: детектор переполнения кучи / стека / глобального массива и генератор векторов базовых блоков SimPoint. Он работает на следующих платформах: X86 / Linux, AMD64 / Linux, PPC32 / Linux, PPC64 / Linux и X86 / Darwin (Mac OS X).


2
Да, но Valgrind не работает хорошо для переполнения выделенных в стеке буферов, о чем свидетельствует это сообщение об ошибке.
DW

4
Как мы можем использовать этот детектор переполнения массива стека ? Можете ли вы уточнить?
Крейг МакКуин

@CraigMcQueen Я попытался использовать экспериментальный эвристический детектор разрушения стека SGCheck от Valgrind на минимальном примере: stackoverflow.com/a/51897264/895245, но это не удалось.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

4

Это означает, что вы записали некоторые переменные в стеке недопустимым образом, скорее всего, в результате переполнения буфера .


9
Переполнение стека - это стек, врезавшийся во что-то другое. Здесь все наоборот: что-то врезалось в стек.
Питер Мортенсен

5
На самом деле, нет. Это одна часть стека, врезавшаяся в другую часть. Так что это действительно переполнение буфера, но не поверх стека, а «только» в другой части стека.
Бас Вийнен

2

Каковы могут быть возможные причины этого и как мне это исправить?

Один сценарий будет в следующем примере:

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

В этой программе вы можете перевернуть строку или ее часть, если вы, например, вызываете reverse() что-то вроде этого:

reverse( arr + 2 );

Если вы решили передать длину массива следующим образом:

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

Тоже работает нормально.

Но когда вы делаете это:

revSTR( arr + 2, len );

Вы получаете:

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

И это происходит потому, что в первом коде длина arrпроверяется внутри, revSTR()что хорошо, но во втором коде, где вы передаете длину:

revSTR( arr + 2, len );

Длина теперь больше, чем фактическая длина, которую вы передаете, когда говорите arr + 2.

Длина strlen ( arr + 2 )! = strlen ( arr ).


1
Мне нравится этот пример, потому что он не опирается на стандартные библиотечные функции, такие как getsи scrcpy. Интересно, могли бы мы минимизировать, если дальше. Я бы хотя бы избавился от string.hс size_t len = sizeof( arr );. Протестировано на gcc 6.4, Ubuntu 16.04. Я также привел бы неудачный пример с тем, arr + 2чтобы минимизировать вставку копий.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

1

Повреждения стека, обычно вызванные переполнением буфера. Вы можете защищаться от них, программируя в обороне.

Всякий раз, когда вы обращаетесь к массиву, ставьте перед ним assert, чтобы гарантировать, что доступ не выходит за пределы. Например:

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

Это заставляет вас думать о границах массива, а также о добавлении тестов для их запуска, если это возможно. Если некоторые из этих утверждений могут потерпеть неудачу при обычном использовании, превратите их в регулярные if.


0

Я получил эту ошибку при использовании malloc () для выделения некоторой памяти для struct *. После некоторой отладки кода я, наконец, использовал функцию free (), чтобы освободить выделенную память, и впоследствии сообщение об ошибке исчезло :)


0

Другим источником разрушения стека является (неправильное) использование vfork()вместо fork().

Я только что отладил случай, когда дочерний процесс не смог выполнить execve()целевой исполняемый файл и возвратил код ошибки, а не вызов _exit().

Поскольку vfork()порождал этого потомка, он возвращался, в то время как фактически все еще выполнялся в пространстве процесса родителя, не только повреждая стек родителя, но и вызывал печать двух несопоставимых наборов диагностики с помощью «нисходящего» кода.

Изменение vfork()в fork()фиксированные оба проблемы, как и изменение ребенка returnзаявления _exit()вместо этого.

Но поскольку дочерний код предшествует execve()вызову вызовами других подпрограмм (для установки uid / gid, в данном конкретном случае), он технически не соответствует требованиям для vfork(), поэтому изменение его использования fork()здесь правильно.

(Обратите внимание, что проблемный returnоператор на самом деле не был закодирован как таковой - вместо этого был вызван макрос, и этот макрос решил, следует ли использовать глобальную переменную _exit()или returnосновываться на ней. Поэтому не сразу было очевидно, что дочерний код не соответствуетvfork() использования. )

Для получения дополнительной информации см .:

Разница между fork (), vfork (), exec () и clone ()

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.