Mengapa printf dengan satu argumen (tanpa penentu konversi) tidak digunakan lagi?

102

Dalam buku yang saya baca, tertulis bahwa printfdengan satu argumen (tanpa penentu konversi) tidak berlaku lagi. Ini merekomendasikan untuk mengganti

printf("Hello World!");

dengan

puts("Hello World!");

atau

printf("%s", "Hello World!");

Bisakah seseorang memberi tahu saya mengapa printf("Hello World!");salah? Ada tertulis di buku yang berisi kerentanan. Kerentanan apa ini?

StackUser
sumber
34
Catatan: printf("Hello World!")ini tidak sama dengan puts("Hello World!"). puts()menambahkan a '\n'. Sebaliknya dibandingkan printf("abc")denganfputs("abc", stdout)
chux - Pulihkan Monica
5
Buku apa itu? Menurut saya tidak printfberlaku lagi dengan cara yang sama seperti misalnya getstidak berlaku lagi di C99, jadi Anda dapat mempertimbangkan untuk mengedit pertanyaan Anda agar lebih tepat.
el.pescado
14
Sepertinya buku yang Anda baca tidak terlalu bagus - buku yang bagus seharusnya tidak hanya mengatakan sesuatu seperti ini "usang" (itu sebenarnya salah kecuali penulis menggunakan kata tersebut untuk menggambarkan pendapat mereka sendiri) dan harus menjelaskan kegunaannya sebenarnya tidak valid dan berbahaya daripada menampilkan kode aman / valid sebagai contoh dari sesuatu yang "tidak boleh Anda lakukan".
R .. GitHub STOP HELPING ICE
8
Bisakah Anda mengidentifikasi buku itu?
Keith Thompson
7
Harap tentukan judul buku, penulis dan referensi halaman. Terima kasih.
Greenonline

Jawaban:

122

printf("Hello World!"); apakah IMHO tidak rentan tetapi pertimbangkan ini:

const char *str;
...
printf(str);

Jika strkebetulan menunjuk ke string yang berisi penentu %sformat, program Anda akan menunjukkan perilaku tidak terdefinisi (kebanyakan crash), sedangkan puts(str)hanya akan menampilkan string apa adanya.

Contoh:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"
Jabberwocky
sumber
21
Selanjutnya yang menyebabkan program macet, ada banyak eksploitasi lain yang mungkin dilakukan dengan string format. Lihat di sini untuk info lebih lanjut: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan
9
Alasan lainnya adalah putsmungkin lebih cepat.
edmz
38
@black: puts"agaknya" lebih cepat, dan ini mungkin alasan lain orang merekomendasikannya, tetapi sebenarnya tidak lebih cepat. Saya baru saja mencetak "Hello, world!"1.000.000 kali, dua arah. Dengan printfitu butuh waktu 0,92 detik. Dengan putsitu butuh waktu 0,93 detik. Ada beberapa hal yang perlu dikhawatirkan dalam hal efisiensi, tetapi printfvs. putsbukan salah satunya.
Steve Summit
10
@KonstantinWeitz: Tetapi (a) saya tidak menggunakan gcc, dan (b) tidak masalah mengapa klaim " putslebih cepat" itu salah, itu tetap salah.
Steve Summit
6
@KonstantinWeitz: Klaim yang saya berikan bukti adalah (kebalikan dari) klaim yang dibuat oleh pengguna hitam. Saya hanya mencoba menjelaskan bahwa programmer tidak perlu khawatir menelepon putskarena alasan ini. (Tetapi jika Anda ingin memperdebatkannya: Saya akan terkejut jika Anda dapat menemukan kompiler modern untuk mesin modern mana putspun yang jauh lebih cepat daripada printfdalam keadaan apa pun.)
Steve Summit
75

printf("Hello world");

baik-baik saja dan tidak memiliki kerentanan keamanan.

Masalahnya terletak pada:

printf(p);

dimana ppointer ke input yang dikontrol oleh pengguna. Ini rentan terhadap serangan format string : pengguna dapat memasukkan spesifikasi konversi untuk mengambil kendali program, misalnya, %xmembuang memori atau %nmenimpa memori.

Perhatikan bahwa puts("Hello world")tidak setara dalam behaviour to printf("Hello world")but to printf("Hello world\n"). Kompiler biasanya cukup pintar untuk mengoptimalkan panggilan terakhir untuk menggantikannya puts.

ouah
sumber
10
Tentu saja printf(p,x)akan sama bermasalahnya jika pengguna memiliki kendali atas p. Jadi masalahnya bukanlah penggunaan printfdengan hanya satu argumen melainkan dengan format string yang dikontrol pengguna.
Hagen von Eitzen
2
@HagenvonEitzen Itu benar secara teknis, tetapi hanya sedikit yang sengaja menggunakan string format yang disediakan pengguna. Ketika orang menulis printf(p), itu karena mereka tidak menyadari bahwa itu adalah format string, mereka hanya berpikir mereka sedang mencetak literal.
Barmar
33

Lebih jauh ke jawaban lain, printf("Hello world! I am 50% happy today")adalah bug yang mudah dibuat, berpotensi menyebabkan segala macam masalah memori yang buruk (ini UB!).

Ini hanya lebih sederhana, lebih mudah dan lebih kuat untuk "mengharuskan" pemrogram untuk benar-benar jelas ketika mereka menginginkan string kata demi kata dan tidak ada yang lain .

Dan itulah yang printf("%s", "Hello world! I am 50% happy today")membuatmu. Ini sepenuhnya sangat mudah.

(Steve, tentu saja printf("He has %d cherries\n", ncherries)sama sekali bukan hal yang sama; dalam hal ini, pemrogram tidak memiliki pola pikir "string verbatim"; ia berada dalam pola pikir "format string".)

Balapan Ringan dalam Orbit
sumber
2
Ini tidak layak untuk dijadikan argumen, dan saya mengerti apa yang Anda katakan tentang pola pikir string verbatim vs. format, tapi, yah, tidak semua orang berpikir seperti itu, yang merupakan salah satu alasan aturan satu ukuran untuk semua dapat mengganggu. Mengatakan "jangan pernah mencetak string konstan dengan printf" hampir persis seperti mengatakan "selalu tulis if(NULL == p). Aturan ini mungkin berguna untuk beberapa pemrogram, tetapi tidak semua. Dan dalam kedua kasus ( printfformat yang tidak cocok dan persyaratan Yoda), kompiler modern tetap memperingatkan tentang kesalahan, jadi aturan buatan bahkan kurang penting.
Steve Summit
1
@Steve Jika tidak ada keuntungan sama sekali dalam menggunakan sesuatu, tetapi ada beberapa kerugian, maka ya tidak ada alasan untuk menggunakannya. Kondisi Yoda di sisi lain memang memiliki sisi negatifnya yaitu membuat kode lebih sulit untuk dibaca (Anda secara intuitif akan mengatakan "jika p adalah nol" bukan "jika nol adalah p").
Voo
2
@Voo printf("%s", "hello")akan menjadi lebih lambat dari printf("hello"), jadi ada sisi negatifnya. Yang kecil, karena IO hampir selalu jauh lebih lambat daripada pemformatan sederhana seperti itu, tetapi sisi negatifnya.
Yakk - Adam Nevraumont
1
@Yakk Saya ragu itu akan lebih lambat
MM
gcc -Wall -W -Werrorakan mencegah konsekuensi buruk dari kesalahan tersebut.
chqrlie
17

Saya hanya akan menambahkan sedikit informasi mengenai bagian kerentanan di sini.

Dikatakan rentan karena kerentanan format string printf. Dalam contoh Anda, di mana string di-hardcode, itu tidak berbahaya (bahkan jika string hardcode seperti ini tidak pernah sepenuhnya disarankan). Tapi menentukan tipe parameter adalah kebiasaan yang baik. Ambil contoh ini:

Jika seseorang menempatkan karakter format string di printf Anda daripada string biasa (katakanlah, jika Anda ingin mencetak program stdin), printf akan mengambil apa pun yang dia bisa di tumpukan.

Itu (dan masih) sangat digunakan untuk mengeksploitasi program untuk menjelajahi tumpukan untuk mengakses informasi tersembunyi atau melewati otentikasi misalnya.

Contoh (C):

int main(int argc, char *argv[])
{
    printf(argv[argc - 1]); // takes the first argument if it exists
}

jika saya masukkan sebagai input program ini "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

Ini menginstruksikan fungsi printf untuk mengambil lima parameter dari tumpukan dan menampilkannya sebagai angka heksadesimal berlapis 8 digit. Jadi, keluaran yang mungkin terlihat seperti:

40012980 080628c4 bffff7a4 00000005 08059c04

Lihat ini untuk penjelasan lebih lengkap dan contoh lainnya.

P1kachu
sumber
13

Memanggil printfdengan string format literal aman dan efisien, dan tersedia alat untuk memperingatkan Anda secara otomatis jika pemanggilan printfdengan string format yang disediakan pengguna tidak aman.

Serangan paling parah printfmemanfaatkan %npenentu format. Berbeda dengan semua penentu format lainnya, misalnya %d, %nsebenarnya menulis nilai ke alamat memori yang disediakan di salah satu argumen format. Ini berarti bahwa penyerang dapat menimpa memori dan dengan demikian berpotensi mengambil kendali program Anda. Wikipedia memberikan lebih banyak detail.

Jika Anda menelepon printfdengan string format literal, penyerang tidak dapat menyelinap %nke dalam string format Anda, dan Anda aman. Faktanya, gcc akan mengubah panggilan Anda printfmenjadi panggilan ke puts, jadi tidak ada perbedaan apa pun (uji ini dengan menjalankan gcc -O3 -S).

Jika Anda menelepon printfdengan format string yang disediakan pengguna, penyerang berpotensi menyelinap %nke dalam string format Anda, dan mengendalikan program Anda. Kompiler Anda biasanya akan memperingatkan Anda bahwa kompilernya tidak aman, lihat -Wformat-security. Ada juga alat yang lebih canggih yang memastikan bahwa pemanggilan printfaman bahkan dengan string format yang disediakan pengguna, dan mereka bahkan mungkin memeriksa bahwa Anda meneruskan nomor dan jenis argumen yang tepat printf. Misalnya untuk Java ada Rawan Kesalahan Google dan Kerangka Pemeriksa .

Konstantin Weitz
sumber
12

Ini adalah nasihat yang salah arah. Ya, jika Anda memiliki string run-time untuk dicetak,

printf(str);

cukup berbahaya, dan Anda harus selalu menggunakannya

printf("%s", str);

sebaliknya, karena secara umum Anda tidak pernah tahu apakah strmungkin mengandung sebuah %tanda. Namun, jika Anda memiliki string konstan waktu kompilasi , tidak ada yang salah dengan

printf("Hello, world!\n");

(Antara lain, itu adalah program C paling klasik yang pernah ada, secara harfiah dari buku pemrograman C Genesis. Jadi siapa pun yang mencela penggunaan itu menjadi agak sesat, dan saya sendiri akan agak tersinggung!)

Steve Summit
sumber
because printf's first argument is always a constant stringSaya tidak begitu yakin tentang apa yang Anda maksud dengan itu.
Sebastian Mach
Seperti yang saya katakan, "He has %d cherries\n"adalah string konstan, artinya itu adalah konstanta waktu kompilasi. Namun, agar adil, saran penulis bukanlah "jangan berikan string konstan sebagai printfargumen pertama", melainkan "jangan berikan string tanpa argumen pertama %sebagai printf".
Steve Summit
literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- Anda belum benar-benar membaca K&R dalam beberapa tahun terakhir. Ada banyak saran dan gaya pengkodean di sana yang tidak hanya usang tetapi juga praktik yang buruk akhir-akhir ini.
Voo
@Voo: Baiklah, anggap saja tidak semua yang dianggap praktik buruk sebenarnya praktik buruk. (Nasihat untuk "jangan pernah menggunakan polos int" muncul dalam pikiran.)
Steve Summit
1
@Steve Saya tidak tahu di mana Anda mendengar yang satu itu, tapi tentu saja itu bukan jenis praktik buruk (buruk?) Yang sedang kita bicarakan di sana. Jangan salah paham, untuk saat ini kodenya baik-baik saja, tetapi Anda benar-benar tidak ingin melihat k & r terlalu banyak tetapi sebagai catatan sejarah akhir-akhir ini. "Ada dalam k & r" bukan merupakan indikator kualitas yang baik akhir-akhir ini, itu saja
Voo
9

Aspek yang agak buruk dari printfadalah bahwa bahkan pada platform di mana memori yang tersesat membaca hanya dapat menyebabkan kerusakan terbatas (dan dapat diterima), salah satu karakter pemformatan %n,, menyebabkan argumen berikutnya ditafsirkan sebagai penunjuk ke bilangan bulat yang dapat ditulis, dan menyebabkan jumlah keluaran karakter sejauh ini untuk disimpan ke variabel yang diidentifikasi dengan demikian. Saya tidak pernah menggunakan fitur itu sendiri, dan terkadang saya menggunakan metode gaya printf ringan yang telah saya tulis untuk hanya menyertakan fitur yang benar-benar saya gunakan (dan tidak menyertakan yang satu atau yang serupa) tetapi memberi makan string fungsi printf standar yang diterima dari sumber yang tidak dapat dipercaya dapat mengekspos kerentanan keamanan di luar kemampuan membaca penyimpanan sewenang-wenang.

supercat
sumber
8

Karena tidak ada yang menyebutkan, saya akan menambahkan catatan tentang penampilan mereka.

Dalam keadaan normal, dengan asumsi tidak ada pengoptimalan kompilator yang digunakan (yaitu printf()sebenarnya panggilan printf()dan tidak fputs()), saya berharap printf()untuk melakukan kurang efisien, terutama untuk string yang panjang. Ini karena printf()harus mengurai string untuk memeriksa apakah ada penentu konversi.

Untuk mengonfirmasi ini, saya telah menjalankan beberapa tes. Pengujian dilakukan di Ubuntu 14.04, dengan gcc 4.8.4. Mesin saya menggunakan cpu Intel i5. Program yang diuji adalah sebagai berikut:

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

Keduanya dikompilasi dengan gcc -Wall -O0. Waktu diukur menggunakan time ./a.out > /dev/null. Berikut ini adalah hasil dari lari biasa (Saya telah menjalankannya lima kali, semua hasil dalam 0,002 detik).

Untuk printf()varian:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

Untuk fputs()varian:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

Efek ini diperkuat jika Anda memiliki string yang sangat panjang.

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    }
    fflush(stdout);
    return 0;
}

Untuk printf()varian (berlari tiga kali, plus / minus nyata 1.5s):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

Untuk fputs()varian (berlari tiga kali, plus / minus nyata 0.2s):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

Catatan: Setelah memeriksa assembly yang dihasilkan oleh gcc, saya menyadari bahwa gcc mengoptimalkan fputs()panggilan ke fwrite()panggilan, bahkan dengan -O0. ( printf()Panggilan tetap tidak berubah.) Saya tidak yakin apakah ini akan membuat pengujian saya tidak valid, karena kompilator menghitung panjang string fwrite()pada waktu kompilasi.

pengguna12205
sumber
2
Ini tidak akan membuat pengujian Anda menjadi tidak valid, seperti fputs()yang sering digunakan dengan konstanta string dan peluang pengoptimalan adalah bagian dari poin yang ingin Anda buat. Artinya, menambahkan pengujian yang dijalankan dengan string yang dibuat secara dinamis dengan fputs()dan fprintf()akan menjadi titik data tambahan yang bagus .
Patrick Schlüter
@ PatrickSchlüter Pengujian dengan string yang dihasilkan secara dinamis tampaknya mengalahkan tujuan pertanyaan ini meskipun ... OP tampaknya hanya tertarik pada literal string yang akan dicetak.
pengguna12205
1
Dia tidak menyatakannya secara eksplisit meskipun contohnya menggunakan literal string. Sebenarnya, saya pikir kebingungannya tentang nasihat buku ini adalah akibat penggunaan literal string dalam contoh. Dengan string literal, nasihat buku entah bagaimana meragukan, dengan string dinamis itu adalah nasihat yang baik.
Patrick Schlüter
1
/dev/nullsemacam membuat ini menjadi mainan, karena biasanya ketika menghasilkan output yang diformat, tujuan Anda adalah agar output tersebut pergi ke suatu tempat, bukan dibuang. Setelah Anda menambahkan waktu "sebenarnya tidak membuang data", bagaimana perbandingannya?
Yakk - Adam Nevraumont
7
printf("Hello World\n")

secara otomatis dikompilasi ke yang setara

puts("Hello World")

Anda dapat memeriksanya dengan membongkar file yang dapat dieksekusi:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

menggunakan

char *variable;
... 
printf(variable)

akan menyebabkan masalah keamanan, jangan pernah menggunakan printf seperti itu!

jadi buku Anda sebenarnya benar, menggunakan printf dengan satu variabel sudah tidak digunakan lagi tetapi Anda masih dapat menggunakan printf ("string saya \ n") karena secara otomatis akan menjadi put

Ábrahám Endre
sumber
12
Perilaku ini sebenarnya bergantung sepenuhnya pada kompiler.
Jabberwocky
6
Ini menyesatkan. Anda menyatakan A compiles to B, tetapi pada kenyataannya yang Anda maksud A and B compile to C.
Sebastian Mach
6

Untuk gcc, dimungkinkan untuk mengaktifkan peringatan khusus untuk pemeriksaan printf()dan scanf().

Dokumentasi gcc menyatakan:

-Wformattermasuk dalam -Wall. Untuk kontrol lebih atas beberapa aspek format memeriksa, pilihan -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, dan -Wformat=2yang tersedia, tetapi tidak termasuk dalam -Wall.

Yang -Wformatdiaktifkan dalam -Wallopsi tidak mengaktifkan beberapa peringatan khusus yang membantu menemukan kasus berikut:

  • -Wformat-nonliteral akan memperingatkan jika Anda tidak mengirimkan string litteral sebagai penentu format.
  • -Wformat-securityakan memperingatkan jika Anda mengirimkan string yang mungkin berisi konstruksi berbahaya. Ini adalah bagian dari -Wformat-nonliteral.

Saya harus mengakui bahwa mengaktifkan -Wformat-securitymengungkapkan beberapa bug yang kami miliki di basis kode kami (modul logging, modul penanganan kesalahan, modul output xml, semua memiliki beberapa fungsi yang dapat melakukan hal-hal yang tidak ditentukan jika mereka dipanggil dengan% karakter dalam parameternya. Untuk info, basis kode kami sekarang berusia sekitar 20 tahun dan bahkan jika kami menyadari masalah semacam ini, kami sangat terkejut ketika kami mengaktifkan peringatan ini berapa banyak dari bug ini yang masih ada di basis kode).

Patrick Schlüter
sumber
1

Di samping jawaban lain yang dijelaskan dengan baik dengan kekhawatiran sampingan apa pun, saya ingin memberikan jawaban yang tepat dan ringkas untuk pertanyaan yang diberikan.


Mengapa printfdengan satu argumen (tanpa penentu konversi) tidak digunakan lagi?

Sebuah printfpanggilan fungsi dengan argumen tunggal pada umumnya tidak usang dan memiliki juga tidak ada kerentanan ketika digunakan dengan benar karena Anda selalu harus kode.

C Pengguna di seluruh dunia, dari status pemula hingga ahli status menggunakan printfcara itu untuk memberikan frase teks sederhana sebagai output ke konsol.

Selanjutnya, Seseorang harus membedakan apakah argumen satu-satunya ini adalah string literal atau penunjuk ke string, yang valid tetapi umumnya tidak digunakan. Untuk yang terakhir, tentu saja, dapat terjadi keluaran yang tidak nyaman atau segala jenis Perilaku Tidak Terdefinisi , ketika penunjuk tidak diatur dengan benar untuk menunjuk ke string yang valid tetapi hal ini juga dapat terjadi jika penentu format tidak cocok dengan argumen masing-masing dengan memberikan banyak argumen.

Tentu saja, ini juga tidak benar dan tepat bahwa string, yang disediakan sebagai satu-satunya argumen, memiliki format atau penentu konversi, karena tidak ada konversi yang akan terjadi.

Yang mengatakan, memberikan string literal sederhana seperti "Hello World!"hanya argumen tanpa penentu format apa pun di dalam string itu seperti yang Anda berikan dalam pertanyaan:

printf("Hello World!");

adalah tidak usang atau " praktek buruk " sama sekali tidak memiliki kerentanan apapun.

Nyatanya, banyak programmer C mulai dan mulai belajar dan menggunakan C atau bahkan bahasa pemrograman secara umum dengan program HelloWorld dan printfpernyataan ini sebagai yang pertama dari jenisnya.

Mereka tidak akan menjadi seperti itu jika mereka dicela.

Dalam buku yang saya baca, tertulis bahwa printfdengan satu argumen (tanpa penentu konversi) tidak berlaku lagi.

Nah, kemudian saya akan mengambil fokus pada buku atau penulis itu sendiri. Jika seorang penulis benar-benar melakukan seperti itu, menurut pendapat saya, pernyataan yang salah dan bahkan mengajarkan bahwa tanpa secara eksplisit menjelaskan mengapa dia melakukannya (jika pernyataan itu benar-benar setara dengan yang diberikan dalam buku itu), saya akan menganggapnya sebagai buku yang buruk . Sebuah buku yang bagus , sebagai lawan dari itu, akan menjelaskan mengapa menghindari jenis metode atau fungsi pemrograman tertentu.

Menurut apa yang saya katakan di atas, menggunakan printfhanya dengan satu argumen (string literal) dan tanpa penentu format apa pun tidak akan usang atau dianggap sebagai "praktik buruk" .

Anda harus bertanya kepada penulis, apa yang dia maksud dengan itu atau bahkan lebih baik, ingatkan dia untuk mengklarifikasi atau mengoreksi bagian relatif untuk edisi berikutnya atau cetakan pada umumnya.

RobertS mendukung Monica Cellio
sumber
Anda bisa menambahkan bahwa printf("Hello World!");adalah tidak setara dengan puts("Hello World!");pula, yang menceritakan sesuatu tentang penulis rekomendasi.
chqrlie