Dlaczego jest gets()
niebezpieczny
Pierwszy robak internetowy ( Morris Internet Worm ) uciekł około 30 lat temu (1988-11-02) i wykorzystał gets()
przepełnienie bufora jako jedną ze swoich metod rozprzestrzeniania się z systemu do systemu. Podstawowym problemem jest to, że funkcja nie wie, jak duży jest bufor, więc kontynuuje czytanie, dopóki nie znajdzie nowego wiersza lub nie napotka EOF, i może przekroczyć granice podanego bufora.
Powinieneś zapomnieć, że kiedykolwiek słyszałeś o gets()
istnieniu.
Norma C11 ISO / IEC 9899: 2011 została wyeliminowana gets()
jako funkcja standardowa, którą jest A Good Thing ™ (została formalnie oznaczona jako „przestarzała” i „przestarzała” w ISO / IEC 9899: 1999 / Cor.3: 2007 - Corrigendum techniczne 3 dla C99, a następnie usunięty w C11). Niestety, pozostanie w bibliotekach przez wiele lat (co oznacza „dekady”) ze względu na kompatybilność wsteczną. Gdyby to zależało ode mnie, wdrożenie gets()
byłoby:
char *gets(char *buffer)
{
assert(buffer != 0);
abort();
return 0;
}
Biorąc pod uwagę, że Twój kod i tak się zawiesi, prędzej czy później, lepiej jest usunąć problem wcześniej niż później. Byłbym przygotowany na dodanie komunikatu o błędzie:
fputs("obsolete and dangerous function gets() called\n", stderr);
Nowoczesne wersje systemu kompilacji Linux generują ostrzeżenia, jeśli łączysz gets()
- a także dla niektórych innych funkcji, które również mają problemy z bezpieczeństwem ( mktemp()
,…).
Alternatywy dla gets()
fgets ()
Jak wszyscy inni mówili, kanoniczną alternatywą gets()
jest fgets()
określenie stdin
jako strumień plików.
char buffer[BUFSIZ];
while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
...process line of data...
}
Nikt jeszcze nie wspomniał, że gets()
nie zawiera nowej linii, ale ją fgets()
zawiera. Może być konieczne użycie opakowania, fgets()
które usuwa nowy wiersz:
char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
return buffer;
}
return 0;
}
Albo lepiej:
char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
buffer[strcspn(buffer, "\n")] = '\0';
return buffer;
}
return 0;
}
Ponadto, jak wskazuje caf w komentarzu, a paxdiablo pokazuje w swojej odpowiedzi, a fgets()
ty możesz mieć dane w linii. Mój kod opakowania pozostawia te dane do odczytania następnym razem; możesz go łatwo zmodyfikować, aby pochłonąć resztę wiersza danych, jeśli wolisz:
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
else
{
int ch;
while ((ch = getc(fp)) != EOF && ch != '\n')
;
}
Pozostały problem polega na tym, jak zgłosić trzy różne stany wynikowe - EOF lub błąd, odczyt linii i nie obcięty oraz częściowy odczyt linii, ale dane zostały obcięte.
Ten problem nie występuje, gets()
ponieważ nie wie, gdzie kończy się bufor, i wesoło tratuje za nim, siejąc spustoszenie w pięknie utrzymanym układzie pamięci, często psując stos zwrotny ( przepełnienie stosu ), jeśli bufor jest przydzielony na stos lub deptanie informacji kontrolnych, jeśli bufor jest dynamicznie przydzielany, lub kopiowanie danych przez inne cenne zmienne globalne (lub modułowe), jeśli bufor jest przydzielany statycznie. Żadne z nich nie jest dobrym pomysłem - są one uosobieniem wyrażenia „niezdefiniowane zachowanie”.
Istnieje również TR 24731-1 (Raport techniczny komitetu standardowego C), który zapewnia bezpieczniejsze alternatywy dla różnych funkcji, w tym gets()
:
§6.5.4.1 gets_s
Funkcja
Streszczenie
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);
Ograniczenia w czasie wykonywania
s
nie będzie wskaźnikiem zerowym. n
nie będzie równy zero ani większy niż RSIZE_MAX. Znak odczytu nowej linii, błąd końca pliku lub błąd odczytu wystąpią podczas odczytu
n-1
znaków z stdin
. 25)
3 W przypadku naruszenia ograniczenia środowiska wykonawczego s[0]
jest ustawiany na znak zerowy, a znaki są odczytywane i odrzucane stdin
do momentu odczytania znaku nowej linii lub wystąpienia błędu końca pliku lub błędu odczytu.
Opis
4 gets_s
Funkcja wczytuje najwyżej jeden mniej niż liczbę znaków określoną przez n
ze wskazanego strumienia do stdin
tablicy wskazanej przez s
. Żadne dodatkowe znaki nie są odczytywane po znaku nowej linii (który jest odrzucany) lub po końcu pliku. Odrzucony znak nowej linii nie jest wliczany do liczby odczytanych znaków. Znak zerowy jest zapisywany natychmiast po ostatnim znaku odczytanym do tablicy.
5 Jeśli napotkany zostanie koniec pliku i nie zostaną wczytane żadne znaki do tablicy lub jeśli podczas operacji wystąpi błąd odczytu, wówczas s[0]
ustawiany jest znak pusty, a pozostałe elementy s
przyjmują nieokreślone wartości.
Zalecana praktyka
6 fgets
Funkcja pozwala poprawnie napisanym programom bezpiecznie przetwarzać wiersze wejściowe zbyt długo, aby zapisać je w tablicy wyników. Zasadniczo wymaga to, aby osoby wywołujące fgets
zwracały uwagę na obecność lub brak znaku nowej linii w tablicy wyników. Rozważ użycie fgets
(wraz z niezbędnym przetwarzaniem opartym na znakach nowej linii) zamiast
gets_s
.
25) W gets_s
przeciwieństwie do tej funkcji, gets
naruszenie linii czasu wykonywania dla linii wejściowej przepełnia bufor, aby go zapisać. W przeciwieństwie fgets
, gets_s
utrzymuje relację jeden do jednego między liniami wejściowymi i udanych połączeń do gets_s
. Programy, które używają, gets
oczekują takiej relacji.
Kompilatory Microsoft Visual Studio implementują zbliżenie do standardu TR 24731-1, ale istnieją różnice między podpisami zaimplementowanymi przez Microsoft a podpisami w TR.
Norma C11, ISO / IEC 9899-2011, zawiera TR24731 w załączniku K jako opcjonalną część biblioteki. Niestety rzadko jest implementowany w systemach uniksopodobnych.
getline()
- POSIX
POSIX 2008 zapewnia również bezpieczną alternatywę dla gets()
wywoływanych getline()
. Dynamicznie przydziela miejsce dla linii, więc musisz go zwolnić. Usuwa zatem ograniczenie długości linii. Zwraca również długość odczytanych danych -1
( lub nie EOF
!), Co oznacza, że bajty zerowe na wejściu mogą być obsługiwane niezawodnie. Istnieje również wariant „wybierz własny separator jednoznakowy” getdelim()
; może to być przydatne, jeśli masz do czynienia z danymi wyjściowymi, na find -print0
których końce nazw plików są oznaczone '\0'
na przykład znakiem NUL ASCII .
gets()
Buffer_overflow_attack