Mengapa fungsinya begitu berbahaya sehingga tidak boleh digunakan?

229

Ketika saya mencoba mengkompilasi kode C yang menggunakan gets()fungsi dengan GCC, saya mendapatkan peringatan ini:

(.text + 0x34): peringatan: fungsi `mendapat 'berbahaya dan tidak boleh digunakan.

Saya ingat ini ada hubungannya dengan perlindungan dan keamanan tumpukan, tapi saya tidak yakin persis mengapa.

Bagaimana saya bisa menghapus peringatan ini dan mengapa ada peringatan seperti itu tentang penggunaan gets()?

Jika gets()sangat berbahaya lalu mengapa kita tidak bisa menghapusnya?

vinit dhatrak
sumber
penjelasan lebihgets()
lanjut_Bufferoverflow_C

Jawaban:

179

Untuk menggunakan getsdengan aman, Anda harus tahu persis berapa banyak karakter yang akan Anda baca, sehingga Anda dapat membuat buffer Anda cukup besar. Anda hanya akan tahu bahwa jika Anda tahu persis data apa yang akan Anda baca.

Alih-alih menggunakan gets, Anda ingin menggunakan fgets, yang memiliki tanda tangan

char* fgets(char *string, int length, FILE * stream);

( fgets, jika membaca seluruh baris, akan meninggalkan '\n'string; Anda harus menghadapinya.)

Itu tetap merupakan bagian resmi dari bahasa hingga standar ISO C 1999, tetapi secara resmi dihapus oleh standar 2011. Sebagian besar implementasi C masih mendukungnya, tetapi setidaknya gcc mengeluarkan peringatan untuk kode apa pun yang menggunakannya.

Thomas Owens
sumber
79
Sebenarnya bukan gcc yang memperingatkan, glibc yang berisi pragma atau atribut gets()yang menyebabkan kompiler memancarkan peringatan saat digunakan.
fuz
@ Fuz sebenarnya, bukan hanya kompiler yang memperingatkan: peringatan yang dikutip dalam OP dicetak oleh linker!
Ruslan
163

Kenapa gets()berbahaya?

Worm internet pertama ( Morris Internet Worm ) melarikan diri sekitar 30 tahun yang lalu (1988-11-02), dan menggunakan gets()buffer overflow sebagai salah satu metode penyebaran dari sistem ke sistem. Masalah dasarnya adalah bahwa fungsi tersebut tidak tahu seberapa besar buffer, sehingga terus membaca sampai menemukan baris baru atau bertemu EOF, dan mungkin meluap batas buffer yang diberikan.

Anda harus lupa bahwa Anda pernah mendengar yang gets()ada.

Standar C11 ISO / IEC 9899: 2011 dihilangkan gets()sebagai fungsi standar, yaitu A Good Thing ™ (secara resmi ditandai sebagai 'usang' dan 'tidak digunakan lagi' dalam ISO / IEC 9899: 1999 / Cor.3: 2007 - Technical Corrigendum 3 untuk C99, dan kemudian dihapus di C11). Sayangnya, itu akan tetap di perpustakaan selama bertahun-tahun (yang berarti 'dekade') karena alasan kompatibilitas. Jika terserah saya, implementasi gets()akan menjadi:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}

Mengingat kode Anda akan macet, cepat atau lambat, lebih baik untuk mengatasi masalah lebih cepat daripada nanti. Saya siap menambahkan pesan kesalahan:

fputs("obsolete and dangerous function gets() called\n", stderr);

Versi modern dari sistem kompilasi Linux menghasilkan peringatan jika Anda menautkan gets()- dan juga untuk beberapa fungsi lain yang juga memiliki masalah keamanan ( mktemp(), ...).

Alternatif untuk gets()

fgets ()

Seperti orang lain berkata, alternatif kanonik untuk gets()yang fgets()menentukan stdinsebagai file streaming.

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}

Apa yang belum disebutkan oleh orang lain adalah yang gets()tidak termasuk baris baru tetapi fgets()tidak. Jadi, Anda mungkin perlu menggunakan pembungkus fgets()yang menghapus baris baru:

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

Atau lebih baik:

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

Juga, seperti yang ditunjukkan oleh caf dalam komentar dan paxdiablo menunjukkan dalam jawabannya, dengan fgets()Anda mungkin memiliki data yang tersisa pada satu baris. Kode pembungkus saya membiarkan data itu untuk dibaca lain kali; Anda dapat dengan mudah memodifikasinya untuk melahap sisa data jika Anda lebih suka:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }

Masalah residual adalah bagaimana melaporkan tiga status hasil yang berbeda - EOF atau kesalahan, pembacaan baris dan tidak terpotong, dan sebagian pembacaan baris tetapi data terpotong.

Masalah ini tidak muncul dengan gets()karena tidak tahu di mana buffer Anda berakhir dan riang menginjak-injak ujungnya, mendatangkan malapetaka pada tata letak memori cenderung indah Anda, sering mengacaukan tumpukan kembali ( Stack Overflow ) jika buffer dialokasikan pada tumpukan, atau menginjak-injak informasi kontrol jika buffer dialokasikan secara dinamis, atau menyalin data ke variabel global (atau modul) berharga lainnya jika buffer dialokasikan secara statis. Tak satu pun dari ini adalah ide yang baik - mereka melambangkan ungkapan 'perilaku tidak terdefinisi`.


Ada juga TR 24731-1 (Laporan Teknis dari Komite Standar C) yang memberikan alternatif yang lebih aman untuk berbagai fungsi, termasuk gets():

§6.5.4.1 gets_sFungsi

Ringkasan

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Runtime-kendala

stidak akan menjadi pointer nol. ntidak boleh sama dengan nol atau lebih besar dari RSIZE_MAX. Karakter baris baru, end-of-file, atau kesalahan baca akan muncul dalam membaca n-1karakter dari stdin. 25)

3 Jika ada pelanggaran runtime-constraint, s[0]diatur ke karakter nol, dan karakter dibaca dan dibuang dari stdinhingga karakter baris baru dibaca, atau end-of-file atau kesalahan baca terjadi.

Deskripsi

4 gets_sFungsi membaca paling banyak satu kurang dari jumlah karakter yang ditentukan oleh n dari aliran menunjuk ke stdin, ke dalam array yang ditunjuk oleh s. Tidak ada karakter tambahan dibaca setelah karakter baris baru (yang dibuang) atau setelah akhir file. Karakter baris baru yang dibuang tidak dihitung terhadap jumlah karakter yang dibaca. Karakter nol ditulis segera setelah karakter terakhir membaca ke dalam array.

5 Jika akhir file ditemukan dan tidak ada karakter yang telah dibaca ke dalam array, atau jika kesalahan baca terjadi selama operasi, maka s[0]diatur ke karakter nol, dan elemen lain dari smengambil nilai yang tidak ditentukan.

Latihan yang disarankan

6 fgetsFungsi ini memungkinkan program yang ditulis dengan benar untuk memproses jalur input dengan aman terlalu lama untuk disimpan dalam array hasil. Secara umum ini mengharuskan penelepon fgetsmemperhatikan ada atau tidak adanya karakter baris baru di array hasil. Pertimbangkan untuk menggunakan fgets(bersamaan dengan pemrosesan yang diperlukan berdasarkan karakter baris baru) alih-alih gets_s.

25) The gets_sfungsi, seperti gets, membuat pelanggaran runtime-kendala untuk garis input meluap buffer untuk menyimpannya. Tidak seperti fgets, gets_smempertahankan hubungan satu-ke-satu antara jalur input dan panggilan yang berhasil gets_s. Program yang menggunakan getsmengharapkan hubungan seperti itu.

Kompiler Microsoft Visual Studio menerapkan perkiraan pada standar TR 24731-1, tetapi ada perbedaan antara tanda tangan yang diterapkan oleh Microsoft dan yang ada di TR.

Standar C11, ISO / IEC 9899-2011, termasuk TR24731 dalam Lampiran K sebagai bagian opsional dari perpustakaan. Sayangnya, ini jarang diimplementasikan pada sistem mirip Unix.


getline() - POSIX

POSIX 2008 juga menyediakan alternatif yang aman untuk gets()dipanggil getline(). Ini mengalokasikan ruang untuk garis secara dinamis, sehingga Anda akhirnya harus membebaskannya. Karena itu menghilangkan batasan pada panjang garis. Ini juga mengembalikan panjang data yang telah dibaca, atau -1(dan tidak EOF!), Yang berarti bahwa byte nol dalam input dapat ditangani dengan andal. Ada juga variasi 'pilih pembatas karakter tunggal Anda' yang disebut getdelim(); ini dapat berguna jika Anda berurusan dengan output dari find -print0mana ujung nama file ditandai dengan '\0'karakter ASCII NUL , misalnya.

Jonathan Leffler
sumber
8
Ini juga layak untuk ditunjukkan fgets()dan fgets_wrapper()versi Anda akan meninggalkan bagian tambahan dari garis yang terlalu panjang di buffer input, untuk dibaca oleh fungsi input selanjutnya. Dalam banyak kasus, Anda ingin membaca dan membuang karakter-karakter ini.
caf
5
Saya bertanya-tanya mengapa mereka tidak menambahkan alternatif () yang memungkinkan seseorang untuk menggunakan fungsinya tanpa harus membuat panggilan strlen konyol. Misalnya, varian gadget yang mengembalikan jumlah byte yang dibaca ke dalam string akan memudahkan kode untuk melihat apakah byte terakhir yang dibaca adalah baris baru. Jika perilaku melewatkan pointer nol untuk buffer didefinisikan sebagai "baca dan buang hingga n-1 byte hingga baris baru berikutnya", itu akan memungkinkan kode untuk dengan mudah membuang ekor dari garis yang terlalu panjang.
supercat
2
@supercat: Ya, saya setuju - sangat disayangkan. Pendekatan terdekat untuk itu mungkin POSIX getline()dan relatifnya getdelim(), yang mengembalikan panjang 'baris' yang dibaca oleh perintah, mengalokasikan ruang yang diperlukan untuk dapat menyimpan seluruh baris. Bahkan itu dapat menyebabkan masalah jika Anda berakhir dengan file JSON satu baris yang beberapa gigabytes; dapatkah Anda mendapatkan semua ingatan itu? (Dan sementara kita berada di sana, dapatkah kita memiliki strcpy()dan strcat()varian yang mengembalikan pointer ke byte nol di akhir? Dll)
Jonathan Leffler
4
@supercat: masalah lainnya fgets()adalah jika file tersebut berisi byte nol, Anda tidak dapat menentukan berapa banyak data yang ada setelah byte nol hingga akhir baris (atau EOF). strlen()hanya dapat melaporkan hingga byte nol dalam data; setelah itu, ini adalah dugaan dan karena itu hampir pasti salah.
Jonathan Leffler
7
"lupakan kamu pernah dengar itu gets()ada." Ketika saya melakukan ini, saya bertemu dengannya lagi dan kembali ke sini. Apakah Anda meretas stackoverflow untuk mendapatkan upvotes?
candied_orange
21

Karena getstidak melakukan pemeriksaan apa pun saat mendapatkan byte dari stdin dan meletakkannya di suatu tempat. Contoh sederhana:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);

Sekarang, pertama-tama Anda diperbolehkan memasukkan berapa banyak karakter yang Anda inginkan, getstidak akan mempedulikannya. Kedua byte di atas ukuran array di mana Anda meletakkannya (dalam hal ini array1) akan menimpa apa pun yang mereka temukan di memori karena getsakan menuliskannya. Pada contoh sebelumnya, ini berarti bahwa jika Anda memasukkan "abcdefghijklmnopqrts"mungkin, tidak dapat diprediksi, itu akan menimpa juga array2atau apa pun.

Fungsi ini tidak aman karena mengasumsikan input yang konsisten. TIDAK PERNAH MENGGUNAKANNYA!

Mendongkrak
sumber
3
Apa yang membuat getsoutright tidak dapat digunakan adalah tidak memiliki parameter panjang / jumlah array yang diperlukan; seandainya itu ada, itu hanya akan menjadi fungsi standar C biasa.
legends2k
@ legends2k: Saya ingin tahu untuk apa penggunaan yang dimaksudkan gets, dan mengapa tidak ada varian fiting standar yang dibuat nyaman untuk kasus penggunaan di mana baris baru tidak diinginkan sebagai bagian dari input?
supercat
1
@supercat gets, seperti namanya, dirancang untuk mendapatkan string dari stdin, namun alasan untuk tidak memiliki parameter ukuran mungkin berasal dari semangat C : Trust the programmer. Fungsi ini telah dihapus di C11 dan penggantian yang diberikan gets_smengambil ukuran buffer input. Saya tidak tahu tentangfgets bagian itu.
legends2k
@ legends2k: Satu-satunya konteks yang dapat saya lihat gets mungkin dapat dimaafkan adalah jika seseorang menggunakan sistem I / O buffer-line perangkat keras yang secara fisik tidak mampu mengirimkan garis pada panjang tertentu, dan masa hidup program yang dimaksudkan. lebih pendek dari masa pakai perangkat keras. Dalam hal ini, jika perangkat keras tidak mampu mengirimkan garis lebih dari 127 byte, mungkin dibenarkan untuk getsmenjadi buffer 128-byte, meskipun saya akan berpikir keuntungan dari dapat menentukan buffer yang lebih pendek ketika mengharapkan input yang lebih kecil akan lebih dari membenarkan biaya.
supercat
@ legends2k: Sebenarnya, apa yang mungkin ideal adalah memiliki "penunjuk string" mengidentifikasi byte yang akan memilih di antara beberapa format string / buffer / buffer-info yang berbeda, dengan satu nilai byte awalan yang menunjukkan sebuah struct yang berisi byte awalan [plus padding], ditambah ukuran buffer, ukuran yang digunakan, dan alamat teks yang sebenarnya. Pola seperti itu akan memungkinkan kode untuk melewati substring sewenang-wenang (bukan hanya ekor) dari string lain tanpa harus menyalin apa pun, dan akan memungkinkan metode seperti getsdanstrcat menerima dengan aman sebanyak yang sesuai.
supercat
16

Anda tidak boleh menggunakannya getskarena tidak ada cara untuk menghentikan buffer overflow. Jika pengguna mengetik lebih banyak data daripada yang dapat ditampung di buffer Anda, kemungkinan besar Anda akan berakhir dengan korupsi atau lebih buruk.

Bahkan, ISO telah benar-benar mengambil langkah menghapus gets dari standar C (pada C11, meskipun sudah tidak digunakan dalam C99) yang, mengingat seberapa tinggi mereka menilai kompatibilitas ke belakang, harus menjadi indikasi seberapa buruk fungsi itu.

Hal yang benar untuk dilakukan adalah menggunakan fgetsfungsi dengan stdinpegangan file karena Anda dapat membatasi karakter yang dibaca dari pengguna.

Tetapi ini juga memiliki masalah seperti:

  • karakter tambahan yang dimasukkan oleh pengguna akan diambil pada waktu berikutnya.
  • tidak ada pemberitahuan cepat bahwa pengguna memasukkan terlalu banyak data.

Untuk itu, hampir setiap kode C di beberapa titik dalam karir mereka akan menulis pembungkus yang lebih berguna fgetsjuga. Ini milik saya:

#include <stdio.h>
#include <string.h>

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[strlen(buff)-1] = '\0';
    return OK;
}

dengan beberapa kode uji:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        printf ("No input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long\n");
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

Ini memberikan perlindungan yang sama seperti fgetsyang mencegah buffer overflows tetapi juga memberi tahu pemanggil tentang apa yang terjadi dan membersihkan karakter berlebih sehingga tidak mempengaruhi operasi input Anda berikutnya.

Jangan ragu untuk menggunakannya sesuai keinginan, saya dengan ini merilisnya di bawah lisensi "melakukan apa yang Anda inginkan" :-)

paxdiablo
sumber
Sebenarnya, standar C99 asli tidak secara eksplisit mencela gets()baik di bagian 7.19.7.7 di mana ia didefinisikan atau di bagian 7.26.9 Arah perpustakaan masa depan dan sub-bagian untuk <stdio.h>. Bahkan tidak ada catatan kaki di situ yang berbahaya. (Karena itu, saya melihat "Sudah usang dalam ISO / IEC 9899: 1999 / Cor.3: 2007 (E))" dalam jawaban oleh Yu Hao .) Tapi C11 memang menghapusnya dari standar - dan tidak sebelum waktu!
Jonathan Leffler
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)menyembunyikan size_tuntuk intkonversi sz. sz > INT_MAX || sz < 2akan menangkap nilai aneh sz.
chux - Reinstate Monica
if (buff[strlen(buff)-1] != '\n') {adalah eksploitasi peretas karena karakter pertama pengguna jahat yang dimasukkan bisa berupa karakter nol render yang melekat pada buff[strlen(buff)-1]UB. while (((ch = getchar())...memiliki masalah jika pengguna memasukkan karakter nol.
chux - Reinstate Monica
12

uang .

Untuk membaca dari stdin:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */
Thiago Silveira
sumber
6

Anda tidak dapat menghapus fungsi API tanpa merusak API. Jika Anda mau, banyak aplikasi tidak lagi dapat dikompilasi atau dijalankan.

Inilah alasan yang diberikan oleh satu referensi :

Membaca garis yang meluap-luap oleh array yang ditunjukkan oleh hasil s dalam perilaku yang tidak ditentukan. Dianjurkan untuk menggunakan fgets ().

Gerd Klima
sumber
4

Baru-baru ini saya membaca, di pos USENETcomp.lang.c , yang gets()dihapus dari Standar. WOO HOO

Anda akan senang mengetahui bahwa komite baru saja memilih (dengan suara bulat, ternyata) untuk menghapus get () dari draft juga.

pmg
sumber
3
Sangat baik dihapus dari standar. Namun, sebagian besar implementasi akan menyediakannya sebagai 'ekstensi tidak standar sekarang' untuk setidaknya 20 tahun ke depan, karena kompatibilitas ke belakang.
Jonathan Leffler
1
Ya benar, tetapi ketika Anda mengkompilasi dengan gcc -std=c2012 -pedantic ...mendapat () tidak akan berhasil. (Saya baru saja membuat -stdparameter)
PMG
4

Di C11 (ISO / IEC 9899: 201x), gets()telah dihapus. (Sudah usang dalam ISO / IEC 9899: 1999 / Cor.3: 2007 (E))

Selain itu fgets(), C11 memperkenalkan alternatif aman baru gets_s():

C11 K.3.5.4.1 gets_sFungsi

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Namun, di bagian Praktik yang disarankan , fgets()masih lebih disukai.

The fgetsFungsi memungkinkan benar-ditulis program untuk aman jalur input proses terlalu lama ke toko di array hasil. Secara umum ini mengharuskan penelepon fgetsmemperhatikan ada atau tidak adanya karakter baris baru di array hasil. Pertimbangkan untuk menggunakan fgets(bersamaan dengan pemrosesan yang diperlukan berdasarkan karakter baris baru) alih-alih gets_s.

Yu Hao
sumber
3

gets()berbahaya karena mungkin bagi pengguna untuk crash program dengan mengetik terlalu banyak ke prompt. Itu tidak dapat mendeteksi akhir dari memori yang tersedia, jadi jika Anda mengalokasikan sejumlah memori terlalu kecil untuk tujuan itu, itu dapat menyebabkan kesalahan seg dan crash. Kadang-kadang tampaknya sangat tidak mungkin bahwa pengguna akan mengetik 1000 huruf menjadi prompt yang dimaksudkan untuk nama seseorang, tetapi sebagai programmer, kita perlu membuat program kita antipeluru. (Ini juga bisa menjadi risiko keamanan jika pengguna dapat merusak program sistem dengan mengirimkan terlalu banyak data).

fgets() memungkinkan Anda menentukan berapa banyak karakter yang dikeluarkan dari buffer input standar, sehingga mereka tidak membanjiri variabel.

Aradhana Mohanty
sumber
Perhatikan bahwa bahaya yang sebenarnya adalah tidak bisa crash program Anda, tetapi mampu membuatnya menjalankan kode arbitrer . (Secara umum, mengeksploitasi perilaku yang tidak terdefinisi .)
Tanz87
2

Saya ingin menyampaikan undangan yang sungguh-sungguh kepada semua pengelola perpustakaan C di luar sana yang masih termasuk getsdi perpustakaan mereka "kalau-kalau ada yang masih bergantung padanya": Silakan ganti implementasi Anda dengan yang setara dengan

char *gets(char *str)
{
    strcpy(str, "Never use gets!");
    return str;
}

Ini akan membantu memastikan tidak ada yang masih bergantung padanya. Terima kasih.

Steve Summit
sumber
2

Fungsi C mendapat berbahaya dan merupakan kesalahan yang sangat mahal. Tony Hoare memilihnya untuk disebutkan secara spesifik dalam ceramahnya "Null Referensi: The Billion Dollar Kesalahan":

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

Seluruh jam layak ditonton tetapi untuk tampilan komentarnya dari 30 menit dengan spesifik mendapat kritik sekitar 39 menit.

Semoga ini membangkitkan selera Anda untuk seluruh pembicaraan, yang menarik perhatian pada bagaimana kita membutuhkan lebih banyak bukti kebenaran formal dalam bahasa dan bagaimana perancang bahasa harus disalahkan atas kesalahan dalam bahasa mereka, bukan pada programmer. Ini tampaknya telah menjadi alasan yang meragukan bagi perancang bahasa yang buruk untuk menyalahkan para programmer dengan kedok 'kebebasan programmer'.

pengguna3717661
sumber