Dalam buku yang saya baca, tertulis bahwa printf
dengan 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?
printf("Hello World!")
ini tidak sama denganputs("Hello World!")
.puts()
menambahkan a'\n'
. Sebaliknya dibandingkanprintf("abc")
denganfputs("abc", stdout)
printf
berlaku lagi dengan cara yang sama seperti misalnyagets
tidak berlaku lagi di C99, jadi Anda dapat mempertimbangkan untuk mengedit pertanyaan Anda agar lebih tepat.Jawaban:
printf("Hello World!");
apakah IMHO tidak rentan tetapi pertimbangkan ini:Jika
str
kebetulan menunjuk ke string yang berisi penentu%s
format, program Anda akan menunjukkan perilaku tidak terdefinisi (kebanyakan crash), sedangkanputs(str)
hanya akan menampilkan string apa adanya.Contoh:
sumber
puts
mungkin lebih cepat.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. Denganprintf
itu butuh waktu 0,92 detik. Denganputs
itu butuh waktu 0,93 detik. Ada beberapa hal yang perlu dikhawatirkan dalam hal efisiensi, tetapiprintf
vs.puts
bukan salah satunya.puts
lebih cepat" itu salah, itu tetap salah.puts
karena alasan ini. (Tetapi jika Anda ingin memperdebatkannya: Saya akan terkejut jika Anda dapat menemukan kompiler modern untuk mesin modern manaputs
pun yang jauh lebih cepat daripadaprintf
dalam keadaan apa pun.)printf("Hello world");
baik-baik saja dan tidak memiliki kerentanan keamanan.
Masalahnya terletak pada:
dimana
p
pointer ke input yang dikontrol oleh pengguna. Ini rentan terhadap serangan format string : pengguna dapat memasukkan spesifikasi konversi untuk mengambil kendali program, misalnya,%x
membuang memori atau%n
menimpa memori.Perhatikan bahwa
puts("Hello world")
tidak setara dalam behaviour toprintf("Hello world")
but toprintf("Hello world\n")
. Kompiler biasanya cukup pintar untuk mengoptimalkan panggilan terakhir untuk menggantikannyaputs
.sumber
printf(p,x)
akan sama bermasalahnya jika pengguna memiliki kendali atasp
. Jadi masalahnya bukanlah penggunaanprintf
dengan hanya satu argumen melainkan dengan format string yang dikontrol pengguna.printf(p)
, itu karena mereka tidak menyadari bahwa itu adalah format string, mereka hanya berpikir mereka sedang mencetak literal.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".)sumber
printf
" hampir persis seperti mengatakan "selalu tulisif(NULL == p)
. Aturan ini mungkin berguna untuk beberapa pemrogram, tetapi tidak semua. Dan dalam kedua kasus (printf
format yang tidak cocok dan persyaratan Yoda), kompiler modern tetap memperingatkan tentang kesalahan, jadi aturan buatan bahkan kurang penting.printf("%s", "hello")
akan menjadi lebih lambat dariprintf("hello")
, jadi ada sisi negatifnya. Yang kecil, karena IO hampir selalu jauh lebih lambat daripada pemformatan sederhana seperti itu, tetapi sisi negatifnya.gcc -Wall -W -Werror
akan mencegah konsekuensi buruk dari kesalahan tersebut.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):
jika saya masukkan sebagai input program ini
"%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:
Lihat ini untuk penjelasan lebih lengkap dan contoh lainnya.
sumber
Memanggil
printf
dengan string format literal aman dan efisien, dan tersedia alat untuk memperingatkan Anda secara otomatis jika pemanggilanprintf
dengan string format yang disediakan pengguna tidak aman.Serangan paling parah
printf
memanfaatkan%n
penentu format. Berbeda dengan semua penentu format lainnya, misalnya%d
,%n
sebenarnya 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
printf
dengan string format literal, penyerang tidak dapat menyelinap%n
ke dalam string format Anda, dan Anda aman. Faktanya, gcc akan mengubah panggilan Andaprintf
menjadi panggilan keputs
, jadi tidak ada perbedaan apa pun (uji ini dengan menjalankangcc -O3 -S
).Jika Anda menelepon
printf
dengan format string yang disediakan pengguna, penyerang berpotensi menyelinap%n
ke 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 pemanggilanprintf
aman bahkan dengan string format yang disediakan pengguna, dan mereka bahkan mungkin memeriksa bahwa Anda meneruskan nomor dan jenis argumen yang tepatprintf
. Misalnya untuk Java ada Rawan Kesalahan Google dan Kerangka Pemeriksa .sumber
Ini adalah nasihat yang salah arah. Ya, jika Anda memiliki string run-time untuk dicetak,
cukup berbahaya, dan Anda harus selalu menggunakannya
sebaliknya, karena secara umum Anda tidak pernah tahu apakah
str
mungkin mengandung sebuah%
tanda. Namun, jika Anda memiliki string konstan waktu kompilasi , tidak ada yang salah dengan(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!)
sumber
because printf's first argument is always a constant string
Saya tidak begitu yakin tentang apa yang Anda maksud dengan itu."He has %d cherries\n"
adalah string konstan, artinya itu adalah konstanta waktu kompilasi. Namun, agar adil, saran penulis bukanlah "jangan berikan string konstan sebagaiprintf
argumen pertama", melainkan "jangan berikan string tanpa argumen pertama%
sebagaiprintf
".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.int
" muncul dalam pikiran.)Aspek yang agak buruk dari
printf
adalah 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.sumber
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 panggilanprintf()
dan tidakfputs()
), saya berharapprintf()
untuk melakukan kurang efisien, terutama untuk string yang panjang. Ini karenaprintf()
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:
Keduanya dikompilasi dengan
gcc -Wall -O0
. Waktu diukur menggunakantime ./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:Untuk
fputs()
varian:Efek ini diperkuat jika Anda memiliki string yang sangat panjang.
Untuk
printf()
varian (berlari tiga kali, plus / minus nyata 1.5s):Untuk
fputs()
varian (berlari tiga kali, plus / minus nyata 0.2s):Catatan: Setelah memeriksa assembly yang dihasilkan oleh gcc, saya menyadari bahwa gcc mengoptimalkan
fputs()
panggilan kefwrite()
panggilan, bahkan dengan-O0
. (printf()
Panggilan tetap tidak berubah.) Saya tidak yakin apakah ini akan membuat pengujian saya tidak valid, karena kompilator menghitung panjang stringfwrite()
pada waktu kompilasi.sumber
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 denganfputs()
danfprintf()
akan menjadi titik data tambahan yang bagus ./dev/null
semacam 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?secara otomatis dikompilasi ke yang setara
Anda dapat memeriksanya dengan membongkar file yang dapat dieksekusi:
menggunakan
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
sumber
A compiles to B
, tetapi pada kenyataannya yang Anda maksudA and B compile to C
.Untuk gcc, dimungkinkan untuk mengaktifkan peringatan khusus untuk pemeriksaan
printf()
danscanf()
.Dokumentasi gcc menyatakan:
Yang
-Wformat
diaktifkan dalam-Wall
opsi tidak mengaktifkan beberapa peringatan khusus yang membantu menemukan kasus berikut:-Wformat-nonliteral
akan memperingatkan jika Anda tidak mengirimkan string litteral sebagai penentu format.-Wformat-security
akan memperingatkan jika Anda mengirimkan string yang mungkin berisi konstruksi berbahaya. Ini adalah bagian dari-Wformat-nonliteral
.Saya harus mengakui bahwa mengaktifkan
-Wformat-security
mengungkapkan 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).sumber
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.
Sebuah
printf
panggilan 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
printf
cara 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: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
printf
pernyataan ini sebagai yang pertama dari jenisnya.Mereka tidak akan menjadi seperti itu jika mereka dicela.
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
printf
hanya 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.
sumber
printf("Hello World!");
adalah tidak setara denganputs("Hello World!");
pula, yang menceritakan sesuatu tentang penulis rekomendasi.