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ć?
Odpowiedzi:
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:
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-protector
zawsze 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.
źródło
Przykład minimalnej reprodukcji z analizą demontażu
main.c
GitHub w górę .
Skompiluj i uruchom:
zawodzi zgodnie z życzeniem:
Testowane na Ubuntu 16.04, GCC 6.4.0.
Demontaż
Teraz patrzymy na demontaż:
który zawiera:
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:
myfunc
jest dokładnie tym, co modyfikuje adres kanarkaKanarek losowo ustawia się za pomocą
%fs:0x28
, który zawiera losową wartość, jak wyjaśniono w:Próby debugowania
Od teraz modyfikujemy kod:
zamiast tego:
być bardziej interesującym.
Spróbujemy wtedy sprawdzić, czy możemy wskazać
+ 1
wywoł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:
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 :
Więc nie byłem bardzo zaskoczony, gdy nie znalazł błędu:
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
core
plik po fakcie:następnie, jak widzieliśmy na zestawie, GDB powinien skierować cię do końca funkcji, która sprawdziła kanarek:
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:
i obserwując adres:
To pozostawia nam właściwą instrukcję obrażającą:
len = 5
ii = 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:
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 .
źródło
Proszę spojrzeć na następującą sytuację:
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.
źródło
Możesz spróbować debugować problem za pomocą valgrind :
źródło
Oznacza to, że napisałeś do niektórych zmiennych na stosie w nielegalny sposób, najprawdopodobniej w wyniku przepełnienia bufora .
źródło
Jednym ze scenariuszy byłby następujący przykład:
W tym programie możesz odwrócić ciąg znaków lub jego część, jeśli na przykład wywołujesz
reverse()
coś takiego:Jeśli zdecydujesz się przekazać długość tablicy w ten sposób:
Działa też dobrze.
Ale kiedy to zrobisz:
Otrzymasz:
Dzieje się tak, ponieważ w pierwszym kodzie
arr
sprawdzana jest długość, wrevSTR()
którym jest w porządku, ale w drugim kodzie, w którym podajesz długość:Długość jest teraz dłuższa niż długość, którą mówisz
arr + 2
.Długość
strlen ( arr + 2 )
! =strlen ( arr )
.źródło
gets
iscrcpy
. Zastanawiam się, czy moglibyśmy zminimalizować, jeśli dalej. Chciałbym przynajmniej pozbyćstring.h
sięsize_t len = sizeof( arr );
. Testowane na gcc 6.4, Ubuntu 16.04. Podałbym również nieudany przykładarr + 2
minimalizacji wklejania kopii.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:
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
.źródło
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ął :)
źródło
Innym źródłem niszczenia stosu jest (niepoprawne) użycie
vfork()
zamiastfork()
.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()
nafork()
naprawę obu problemów, podobnie jak zmianareturn
oś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życiefork()
jest tutaj poprawna.(Należy pamiętać, że problematyczne
return
oświadczenie nie zostało właściwie oznaczonych jako takie - zamiast makro została wywołana, a makro zdecydował, czy_exit()
lubreturn
na 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 ()
źródło