Виявлено розбиття стека


246

Я виконую свій файл a.out. Після виконання програма деякий час запускається, а потім виходить із повідомленням:

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

Які можуть бути можливі причини цього і як це виправити?


2
Не могли б ви визначити, які частини коду викликають розбиття стека, і опублікувати його? Тоді ми, мабуть, зможемо точно вказати, чому це відбувається, і як це виправити.
Bjarke Freund-Hansen

Я думаю, що це синонім з помилкою переповнення. Наприклад, якщо ви ініціалізуєте та масив із 5 елементів, ця помилка з’явиться при спробі записати 6-й елемент або будь-який елемент поза межами масиву.
DorinPopescu

Відповіді:


349

Стек Smashing тут насправді викликаний завдяки механізму захисту, який використовується 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, я відредагую свою відповідь, щоб це відобразити.
sud03r

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

33

Приклад мінімального відтворення при аналізі демонтажу

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 щоб увімкнути Sanitizer адреси Google (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 або вивчаєте 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.

Однак, backtrace зіпсований і містить трохи сміття. Правильне відступлення виглядатиме так:

#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"

Отже, щоб відповісти на ваше запитання вище, повідомлення "** stack smashing: 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, але це не вдалося.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

4

Це означає, що ви писали деякі змінні в стеку незаконним шляхом, швидше за все, в результаті переповнення буфера .


9
Переповнення стека - це стек, що перетворюється на щось інше. Ось навпаки: щось урізалось у стопку.
Пітер Мортенсен

5
Не зовсім. Це одна частина стека, що розбивається на іншу. Таким чином, це справді переповнення буфера, тільки не над вершиною стека, а "лише" в іншу частину стека.
Bas Wijnen

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мінімізацією вставки копії.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1

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

Щоразу, коли ви отримуєте доступ до масиву, поставте аргумент перед ним, щоб переконатися, що доступ не виходить за межі. Наприклад:

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

Це змушує задуматися про межі масиву, а також змушує задуматися над додаванням тестів, щоб запустити їх, якщо це можливо. Якщо деякі з цих тверджень під час звичайного використання можуть вийти з ладу, перетворіть їх на звичайні if.


0

Я отримав цю помилку під час використання malloc () для виділення деякої пам’яті структурі *, витративши цю частину налагодження коду, я нарешті використав функцію 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.