Wykryto rozbicie stosu


246

Wykonuję mój plik a.out. Po wykonaniu program działa przez pewien czas, a następnie kończy pracę z komunikatem:

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

Jakie mogą być tego przyczyny i jak to naprawić?


2
Czy możesz zidentyfikować, które części twojego kodu powodują rozbicie stosu i opublikować go? Wtedy prawdopodobnie będziemy w stanie dokładnie wskazać, dlaczego tak się dzieje i jak to naprawić.
Bjarke Freund-Hansen

Myślę, że to synonim błędu przepełnienia. Na przykład, jeśli zainicjujesz i tablica 5 elementów, ten błąd pojawi się podczas próby zapisania 6. elementu lub dowolnego elementu poza granicami tablicy.
DorinPopescu

Odpowiedzi:


349

Smashowanie stosu tutaj jest w rzeczywistości spowodowane mechanizmem ochronnym używanym przez gcc do wykrywania błędów przepełnienia bufora. Na przykład w następującym fragmencie:

#include <stdio.h>

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

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

Kompilator (w tym przypadku gcc) dodaje zmienne ochronne (zwane kanarkami), które mają znane wartości. Łańcuch wejściowy o rozmiarze większym niż 10 powoduje uszkodzenie tej zmiennej, co powoduje, że SIGABRT kończy działanie programu.

Aby uzyskać wgląd, możesz spróbować wyłączyć tę ochronę gcc przy użyciu opcji -fno-stack-protector podczas kompilacji. W takim przypadku pojawi się inny błąd, najprawdopodobniej błąd segmentacji podczas próby uzyskania dostępu do nielegalnej lokalizacji pamięci. Pamiętaj, że -fstack-protectorzawsze należy włączyć kompilacje wersji, ponieważ jest to funkcja bezpieczeństwa.

Możesz uzyskać informacje o punkcie przepełnienia, uruchamiając program z debuggerem. Valgrind nie działa dobrze z błędami związanymi ze stosem, ale jak debugger, może pomóc w ustaleniu lokalizacji i przyczyny awarii.


3
dzięki za tę odpowiedź! Odkryłem, że w moim przypadku nie zainicjowałem zmiennej, do której próbowałem zapisać
Ted Pennings,

5
Valgrind nie działa dobrze w przypadku błędów związanych ze stosem, ponieważ nie może tam dodać czerwonych stref
toasted_flakes

7
Ta odpowiedź jest niepoprawna i zawiera niebezpieczne porady. Po pierwsze, usunięcie ochrony stosu nie jest właściwym rozwiązaniem - jeśli pojawia się błąd polegający na zniszczeniu stosu, prawdopodobnie masz poważną lukę w zabezpieczeniach w kodzie. Prawidłowa odpowiedź to naprawić błędny kod . Po drugie, jak zauważa grasGendarme, zalecenie wypróbowania Valgrind nie będzie skuteczne. Valgrind zazwyczaj nie działa w celu wykrycia nielegalnego dostępu do pamięci danych przydzielonych na stosie.
DW

22
OP pyta o możliwe przyczyny tego zachowania, moja odpowiedź zawiera przykład i sposób, w jaki odnosi się do rozsądnie znanego błędu. Poza tym usunięcie ochraniacza stosu nie jest rozwiązaniem, jest to rodzaj eksperymentu, który można zrobić, aby uzyskać więcej informacji na temat problemu. Rada naprawdę jest jakoś naprawić błąd, dziękuję za wskazanie na temat valgrind, zmienię moją odpowiedź, aby to odzwierciedlić.
sud03r

4
@DW ochrona stosu powinna być wyłączona w wersji Release, ponieważ na początku - komunikat o wykryciu stosu jest pomocny tylko dla programistów; po drugie - aplikacja może mieć jeszcze szansę na przeżycie; i po trzecie - to niewielka optymalizacja.
Cześć Angel

33

Przykład minimalnej reprodukcji z analizą demontażu

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 w górę .

Skompiluj i uruchom:

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

zawodzi zgodnie z życzeniem:

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

Testowane na Ubuntu 16.04, GCC 6.4.0.

Demontaż

Teraz patrzymy na demontaż:

objdump -D a.out

który zawiera:

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 

Zwróć uwagę na poręczne komentarze dodawane automatycznie przez objdump„s sztucznej inteligencji modułu .

Jeśli uruchomisz ten program wiele razy za pośrednictwem GDB, zobaczysz, że:

  • kanarek otrzymuje za każdym razem inną losową wartość
  • ostatnia pętla myfuncjest dokładnie tym, co modyfikuje adres kanarka

Kanarek losowo ustawia się za pomocą %fs:0x28, który zawiera losową wartość, jak wyjaśniono w:

Próby debugowania

Od teraz modyfikujemy kod:

    myfunc(arr, len + 1);

zamiast tego:

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

być bardziej interesującym.

Spróbujemy wtedy sprawdzić, czy możemy wskazać + 1wywołanie winowajcy za pomocą metody bardziej zautomatyzowanej niż tylko czytanie i rozumienie całego kodu źródłowego.

gcc -fsanitize=address włączyć Google Sanitizer Adres (ASan)

Jeśli ponownie skompilujesz tę flagę i uruchomisz program, wyświetli:

#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

a następnie bardziej kolorowe wydruki.

To wyraźnie wskazuje problematyczną linię 12.

Kod źródłowy tego znajduje się na stronie : https://github.com/google/sanitizers, ale jak widzieliśmy na przykładzie, jest już przesłany do GCC.

ASan może również wykryć inne problemy z pamięcią, takie jak wycieki pamięci: jak znaleźć wyciek pamięci w kodzie / projekcie C ++?

Valgrind SGCheck

Jak wspomnieli inni , Valgrind nie jest dobry w rozwiązywaniu tego rodzaju problemów.

Ma eksperymentalne narzędzie o nazwie SGCheck :

SGCheck to narzędzie do znajdowania przekroczeń stosu i globalnych tablic. Działa przy użyciu podejścia heurystycznego opartego na obserwacji prawdopodobnych form dostępu do stosu i globalnej tablicy.

Więc nie byłem bardzo zaskoczony, gdy nie znalazł błędu:

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

Komunikat o błędzie powinien wyglądać następująco: Valgrind brakujący błąd

GDB

Ważną obserwacją jest to, że jeśli uruchomisz program przez GDB lub przejrzysz coreplik po fakcie:

gdb -nh -q a.out core

następnie, jak widzieliśmy na zestawie, GDB powinien skierować cię do końca funkcji, która sprawdziła kanarek:

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

Dlatego problem prawdopodobnie występuje w jednym z wywołań tej funkcji.

Następnie staramy się wskazać dokładną nieudaną rozmowę, podnosząc ją pierwszy raz tuż po ustawieniu kanarka:

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

i obserwując adres:

(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

To pozostawia nam właściwą instrukcję obrażającą: len = 5i i = 4, w tym konkretnym przypadku, wskazało nam linię winowajcy 12.

Jednak ślad jest uszkodzony i zawiera trochę śmieci. Prawidłowy ślad będzie wyglądał następująco:

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

więc może to może uszkodzić stos i uniemożliwić zobaczenie śladu.

Ponadto ta metoda wymaga znajomości ostatniego wywołania funkcji sprawdzania kanarka, w przeciwnym razie będziesz mieć fałszywe alarmy, co nie zawsze będzie możliwe, chyba że użyjesz odwrotnego debugowania .


16

Proszę spojrzeć na następującą sytuację:

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

Kiedy wyłączyłem ochronę przed rozbiciem stosu, nie wykryto żadnych błędów, co powinno się zdarzyć, gdy użyłem „./a.out wepassssssssssssssssss”

Aby odpowiedzieć na powyższe pytanie, wyświetlił się komunikat „** rozbicie stosu wykryte: xxx”, ponieważ program zabezpieczający przed rozbiciem stosu był aktywny i stwierdził, że w programie występuje przepełnienie stosu.

Po prostu dowiedz się, gdzie to się dzieje, i napraw to.


7

Możesz spróbować debugować problem za pomocą valgrind :

Dystrybucja Valgrind obejmuje obecnie sześć narzędzi o jakości produkcyjnej: wykrywacz błędów pamięci, dwa wykrywacze błędów wątków, profil pamięci podręcznej i przewidywania rozgałęzień, profil pamięci podręcznej generujący wykres połączeń oraz profil stosu. Zawiera także dwa narzędzia eksperymentalne: wykrywacz przepełnienia / stosu / macierzy globalnej oraz generator wektorów blokowych podstawowego SimPoint. Działa na następujących platformach: X86 / Linux, AMD64 / Linux, PPC32 / Linux, PPC64 / Linux i X86 / Darwin (Mac OS X).


2
Tak, ale Valgrind nie działa dobrze w przypadku przepełnienia buforów alokowanych na stosie, na co wskazuje ten komunikat o błędzie.
DW

4
Jak możemy użyć tego detektora przekroczenia macierzy stosu ? Czy możesz rozwinąć?
Craig McQueen

@CraigMcQueen Próbowałem użyć eksperymentalnego heurystycznego detektora niszczącego stos SGCheck Valgrinda na minimalnym przykładzie: stackoverflow.com/a/51897264/895245, ale to nie powiodło się.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

4

Oznacza to, że napisałeś do niektórych zmiennych na stosie w nielegalny sposób, najprawdopodobniej w wyniku przepełnienia bufora .


9
Przepełnienie stosu to stos uderzający w coś innego. Tutaj jest na odwrót: coś uderzyło w stos.
Peter Mortensen

5
Nie całkiem. Jedna część stosu rozbija się na inną. Tak naprawdę jest to przepełnienie bufora, po prostu nie ponad górną krawędź stosu, ale „tylko” do innej części stosu.
Bas Wijnen,

2

Jakie mogą być tego przyczyny i jak to naprawić?

Jednym ze scenariuszy byłby następujący przykład:

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

W tym programie możesz odwrócić ciąg znaków lub jego część, jeśli na przykład wywołujesz reverse()coś takiego:

reverse( arr + 2 );

Jeśli zdecydujesz się przekazać długość tablicy w ten sposób:

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

Działa też dobrze.

Ale kiedy to zrobisz:

revSTR( arr + 2, len );

Otrzymasz:

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

Dzieje się tak, ponieważ w pierwszym kodzie arrsprawdzana jest długość, w revSTR()którym jest w porządku, ale w drugim kodzie, w którym podajesz długość:

revSTR( arr + 2, len );

Długość jest teraz dłuższa niż długość, którą mówisz arr + 2.

Długość strlen ( arr + 2 )! = strlen ( arr ).


1
Podoba mi się ten przykład, ponieważ nie opiera się on na standardowych funkcjach biblioteki, takich jak getsi scrcpy. Zastanawiam się, czy moglibyśmy zminimalizować, jeśli dalej. Chciałbym przynajmniej pozbyć string.hsię size_t len = sizeof( arr );. Testowane na gcc 6.4, Ubuntu 16.04. Podałbym również nieudany przykład arr + 2minimalizacji wklejania kopii.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1

Zniszczenia stosu zwykle spowodowane przepełnieniem bufora. Możesz się przed nimi bronić, programując defensywnie.

Ilekroć uzyskujesz dostęp do tablicy, wstaw przed nią aser, aby upewnić się, że dostęp nie jest poza zakresem. Na przykład:

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

To sprawia, że ​​myślisz o granicach tablicy, a także zastanawiasz się nad dodaniem testów, aby je uruchomić, jeśli to możliwe. Jeśli niektóre z tych twierdzeń mogą zawieść podczas normalnego użytkowania, zamień je w zwykłe if.


0

Wystąpił ten błąd podczas używania malloc () do przydzielenia pamięci do struktury * po spędzeniu tego czasu na debugowaniu kodu, w końcu użyłem funkcji free (), aby zwolnić przydzieloną pamięć, a następnie komunikat o błędzie zniknął :)


0

Innym źródłem niszczenia stosu jest (niepoprawne) użycie vfork()zamiast fork().

Właśnie debugowałem przypadek tego, w którym proces potomny nie był w stanie wykonać execve()docelowego pliku wykonywalnego i zwrócił kod błędu zamiast wywoływać _exit().

Ponieważ vfork()odrodziło to dziecko, wróciło podczas wykonywania w przestrzeni procesu rodzica, nie tylko uszkadzając stos rodzica, ale powodując wydrukowanie dwóch różnych zestawów diagnostycznych za pomocą kodu „downstream”.

Zmiana vfork()na fork()naprawę obu problemów, podobnie jak zmiana returnoświadczenia dziecka na _exit().

Ale ponieważ kod potomny poprzedza execve()wywołanie połączeniami z innymi procedurami (w tym konkretnym przypadku w celu ustawienia identyfikatora uid / gid), technicznie nie spełnia wymagań vfork(), więc zmiana go na użycie fork()jest tutaj poprawna.

(Należy pamiętać, że problematyczne returnoświadczenie nie zostało właściwie oznaczonych jako takie - zamiast makro została wywołana, a makro zdecydował, czy _exit()lub returnna podstawie zmiennej globalnej Więc to nie było oczywiste, że kod dziecko zostało stwierdzone: niezgodne z. vfork()Użytkowania. )

Aby uzyskać więcej informacji, zobacz:

Różnica między fork (), vfork (), exec () i clone ()

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.