Mozilla merilis
Quantum CSS untuk Firefox tahun lalu, yang memuncak dalam delapan tahun pengembangan Rust, bahasa pemrograman sistem yang ramah memori. Butuh lebih dari setahun untuk menulis ulang komponen browser utama di Rust.
Sampai sekarang, semua mesin browser utama ditulis dalam C ++, terutama karena alasan efisiensi. Tetapi dengan kinerja luar biasa muncul tanggung jawab besar: programmer C ++ harus mengelola memori secara manual, yang membuka kotak kerentanan Pandora. Rust tidak hanya memperbaiki kesalahan seperti itu, tetapi metodenya juga mencegah
perlombaan data , yang memungkinkan programmer untuk lebih efisien mengimplementasikan kode paralel.
Apa itu keamanan memori?
Ketika kita berbicara tentang membuat aplikasi yang aman, kita sering menyebutkan keamanan memori. Secara tidak resmi, kami bermaksud bahwa dalam keadaan apa pun program tidak dapat mengakses memori yang tidak valid. Penyebab pelanggaran keamanan:
- menyimpan pointer setelah membebaskan memori (gunakan-setelah-gratis);
- mendereferensi pointer nol;
- penggunaan memori yang tidak diinisialisasi;
- upaya program untuk membebaskan sel yang sama dua kali (bebas ganda);
- buffer overflow.
Untuk definisi yang lebih formal, lihat Michael Hicks
'Apa itu Keamanan Memori , serta
artikel ilmiah tentang topik ini.
Pelanggaran semacam itu dapat menyebabkan crash yang tak terduga atau perubahan perilaku yang diharapkan dari program. Konsekuensi potensial: kebocoran informasi, eksekusi kode arbitrer dan eksekusi kode jarak jauh.
Manajemen memori
Manajemen memori sangat penting untuk kinerja dan keamanan aplikasi. Pada bagian ini, kami akan mempertimbangkan model memori dasar. Salah satu konsep utama adalah
pointer . Ini adalah variabel di mana alamat memori disimpan. Jika kita pergi ke alamat ini, kita akan melihat beberapa data di sana. Oleh karena itu, kami mengatakan bahwa pointer adalah referensi ke data ini (atau menunjuk ke mereka). Sama seperti alamat rumah memberi tahu orang-orang di mana menemukan Anda, alamat memori menunjukkan program tempat menemukan data.
Segala sesuatu dalam program ini terletak di alamat memori tertentu, termasuk instruksi kode. Penggunaan pointer yang tidak tepat dapat menyebabkan kerentanan serius, termasuk kebocoran informasi dan eksekusi kode arbitrer.
Alokasi / Rilis
Ketika kita membuat variabel, program harus mengalokasikan ruang yang cukup dalam memori untuk menyimpan data variabel ini. Karena setiap proses memiliki jumlah memori yang terbatas, tentu saja, Anda memerlukan cara untuk
membebaskan sumber daya. Ketika memori dibebaskan, itu menjadi tersedia untuk menyimpan data baru, tetapi data lama tinggal di sana sampai sel ditimpa.
Buffer
Buffer adalah area memori yang berdekatan di mana beberapa instance dari tipe data yang sama disimpan. Misalnya, frasa "Kucing saya adalah batman" akan disimpan dalam buffer 16 byte. Buffer ditentukan oleh alamat awal dan panjangnya. Agar tidak merusak data di memori tetangga, penting untuk memastikan bahwa kami tidak membaca atau menulis di luar buffer.
Mengontrol aliran
Program terdiri dari rutinitas yang dijalankan dalam urutan tertentu. Di akhir subrutin, komputer pergi ke pointer yang disimpan ke bagian kode selanjutnya (disebut
alamat pengirim ). Ketika Anda pergi ke alamat pengirim, satu dari tiga hal terjadi:
- Proses berlanjut secara normal (alamat pengirim tidak diubah).
- Proses macet (alamat telah diubah dan menunjuk ke memori yang tidak dapat dieksekusi).
- Proses berlanjut, tetapi tidak seperti yang diharapkan (alamat pengirim telah berubah dan aliran kontrol telah berubah).
Bagaimana bahasa memberikan keamanan memori
Semua bahasa pemrograman milik bagian
spektrum yang berbeda . Di satu sisi spektrum adalah bahasa seperti C / C ++. Mereka efektif, tetapi membutuhkan manajemen memori manual. Di sisi lain, bahasa yang ditafsirkan dengan manajemen memori otomatis (misalnya, penghitungan referensi dan pengumpulan sampah), tetapi mereka terbayar dengan kinerja. Bahkan bahasa dengan pengumpulan sampah yang dioptimalkan dengan baik tidak dapat dibandingkan dalam
kinerja dengan bahasa tanpa GC.
Manajemen memori manual
Beberapa bahasa (misalnya, C) memerlukan programmer untuk mengelola memori secara manual: kapan dan berapa banyak memori yang dialokasikan, kapan harus membebaskannya. Ini memberi programmer kontrol penuh atas bagaimana program menggunakan sumber daya, menyediakan kode yang cepat dan efisien. Tetapi pendekatan ini rentan kesalahan, terutama dalam basis kode yang kompleks.
Kesalahan yang mudah dilakukan:
- lupakan bahwa sumber dayanya gratis dan cobalah untuk menggunakannya;
- jangan mengalokasikan ruang yang cukup untuk penyimpanan data;
- baca memori di luar buffer.
Instruksi keselamatan yang cocok untuk mereka yang mengelola memori secara manualPointer pintar
Pointer pintar memberikan informasi tambahan untuk mencegah manajemen memori yang tidak tepat. Mereka digunakan untuk manajemen memori otomatis dan pemeriksaan perbatasan. Tidak seperti pointer biasa, pointer pintar dapat merusak diri sendiri dan tidak akan menunggu programmer untuk menghapusnya secara manual.
Ada berbagai opsi untuk konstruksi seperti itu, yang membungkus pointer asli dalam beberapa abstraksi yang berguna. Beberapa pointer cerdas
menghitung referensi ke setiap objek, sementara yang lain menerapkan kebijakan pelingkupan untuk membatasi masa pakai pointer ke kondisi tertentu.
Saat menghitung tautan, sumber daya dibebaskan ketika referensi terakhir ke objek dihapus. Implementasi penghitungan referensi dasar mengalami kinerja yang buruk, peningkatan konsumsi memori, dan sulit digunakan di lingkungan multi-utas. Jika objek merujuk satu sama lain (tautan sirkuler), maka jumlah referensi untuk setiap objek tidak akan pernah mencapai nol, sehingga diperlukan metode yang lebih kompleks.
Pengumpulan sampah
Beberapa bahasa (mis. Java, Go, Python) menerapkan
pengumpulan sampah . Bagian dari lingkungan runtime, yang disebut pengumpul sampah (GC), melacak variabel dan mengidentifikasi sumber daya yang tidak dapat diakses dalam grafik tautan antar objek. Segera setelah objek menjadi tidak tersedia, GC membebaskan memori dasar untuk digunakan kembali di masa depan. Setiap alokasi dan pembebasan memori terjadi tanpa perintah programmer eksplisit.
Meskipun GC memastikan bahwa memori selalu digunakan dengan benar, itu tidak membebaskan memori dengan cara yang paling efisien - kadang-kadang penggunaan terakhir suatu objek terjadi jauh lebih awal daripada pengumpul sampah akan membebaskan memori. Biaya kinerja adalah penghalang untuk aplikasi mission-critical: kadang-kadang Anda perlu menggunakan memori 5 kali lebih banyak untuk menghindari penurunan kinerja.
Kepemilikan
Rust menggunakan kepemilikan untuk memastikan kinerja tinggi dan keamanan memori. Secara lebih formal, ini adalah contoh
pengetikan afinitas . Semua kode Rust mengikuti aturan tertentu yang memungkinkan kompiler mengelola memori tanpa kehilangan waktu eksekusi:
- Setiap nilai memiliki variabel yang disebut pemilik.
- Hanya satu pemilik yang dapat melakukannya sekaligus.
- Ketika pemilik bergerak keluar dari ruang lingkup, nilainya dihapus.
Nilai dapat
ditransfer atau
dipinjam dari satu variabel ke variabel lainnya. Aturan-aturan ini berlaku untuk bagian dari kompiler yang disebut peminjam pinjaman.
Ketika sebuah variabel keluar dari ruang lingkup, Rust membebaskan memori ini. Dalam contoh berikut, variabel
s1
dan
s2
melampaui ruang lingkup, keduanya mencoba membebaskan memori yang sama, yang mengarah ke kesalahan bebas-ganda. Untuk mencegah hal ini, saat mentransfer nilai dari variabel, pemilik sebelumnya menjadi tidak valid. Jika programmer kemudian mencoba menggunakan variabel yang tidak valid, kompiler akan menolak kode tersebut. Ini dapat dihindari dengan membuat salinan data yang mendalam atau menggunakan tautan.
Contoh 1 : Transfer kepemilikan
let s1 = String::from("hello"); let s2 = s1;
Seperangkat aturan pemeriksa pinjaman terkait dengan masa pakai variabel. Rust melarang penggunaan variabel tidak diinisialisasi dan menggantung pointer ke objek yang tidak ada. Jika Anda mengkompilasi kode dari contoh di bawah ini,
r
akan merujuk ke memori yang dibebaskan ketika
x
keluar dari ruang lingkup: terjadi penunjuk menggantung. Compiler memonitor semua area dan memeriksa validitas semua transfer, kadang-kadang membutuhkan programmer untuk secara eksplisit menunjukkan umur variabel.
Contoh 2 : Hanging Pointer
let r; { let x = 5; r = &x; } println!("r: {}", r);
Model kepemilikan memberikan dasar yang kuat untuk akses yang benar ke memori, mencegah perilaku yang tidak terdefinisi.
Kerentanan memori
Konsekuensi utama dari memori yang rentan:
- Kecelakaan : Mengakses memori yang tidak valid dapat menyebabkan aplikasi berhenti tiba-tiba.
- Kebocoran informasi : penyediaan data pribadi yang tidak disengaja, termasuk informasi rahasia, seperti kata sandi.
- Eksekusi Kode Sewenang-wenang (ACE) : Memungkinkan penyerang untuk mengeksekusi perintah sewenang-wenang pada mesin target. Jika ini terjadi melalui jaringan, kami menyebutnya Remote Code Execution (RCE).
Masalah lain adalah
kebocoran memori ketika memori yang dialokasikan tidak dibebaskan setelah program berakhir. Jadi, Anda dapat menggunakan semua memori yang tersedia: kemudian permintaan sumber daya diblokir, yang akan mengarah pada penolakan layanan. Ini adalah masalah memori yang tidak dapat diselesaikan pada level PL.
Dalam kasus terbaik, dengan kesalahan memori, aplikasi akan macet. Dalam skenario terburuk, seorang penyerang mendapatkan kendali atas suatu program melalui kerentanan (yang dapat menyebabkan serangan lebih lanjut).
Pelanggaran memori yang dibebaskan (bebas digunakan, bebas ganda)
Subkelas kerentanan ini terjadi saat sumber daya dibebaskan, tetapi tautan ke alamatnya tetap dipertahankan. Ini adalah
metode hacker yang kuat yang dapat menyebabkan akses di luar jangkauan, kebocoran informasi, eksekusi kode, dan banyak lagi.
Bahasa dengan pengumpulan sampah dan penghitungan referensi mencegah penggunaan pointer yang tidak valid, menghancurkan hanya objek yang tidak dapat diakses (yang dapat menyebabkan penurunan kinerja), dan bahasa yang dikontrol secara manual rentan terhadap kerentanan ini (terutama di basis kode yang kompleks). Alat pemeriksa pinjaman di Rust tidak memungkinkan benda untuk dihancurkan saat direferensikan, sehingga bug ini dihapus pada tahap kompilasi.
Variabel tidak diinisialisasi
Jika variabel digunakan sebelum inisialisasi, maka data ini dapat berisi data apa pun, termasuk sampah acak atau data yang sebelumnya dibuang, yang mengarah ke kebocoran informasi (kadang-kadang disebut
pointer tidak valid ). Untuk mencegah masalah ini, bahasa manajemen memori sering menggunakan prosedur inisialisasi otomatis setelah mengalokasikan memori.
Seperti dalam C, sebagian besar variabel di Rust pada awalnya tidak diinisialisasi. Tetapi tidak seperti C, Anda tidak dapat membacanya sebelum inisialisasi. Kode berikut tidak dikompilasi:
Contoh 3 : Menggunakan variabel yang tidak diinisialisasi
fn main() { let x: i32; println!("{}", x); }
Pointer kosong
Ketika sebuah aplikasi mereferensi pointer yang ternyata menjadi null, biasanya hanya mengakses sampah dan menyebabkan crash. Dalam beberapa kasus, kerentanan ini dapat menyebabkan eksekusi kode arbitrer (
1 ,
2 ,
3 ). Rust memiliki dua jenis pointer:
tautan dan pointer mentah. Tautan aman, tetapi pointer mentah bisa menjadi masalah.
Rust mencegah penereferensian pointer nol dengan dua cara:
- Hindari petunjuk yang dapat dibatalkan.
- Hindari referensi mentah dereferencing.
Rust menghindari pointer nol dengan menggantinya dengan
Option
khusus. Untuk mengubah nilai null-mungkin dalam jenis
Option
, bahasa mengharuskan programmer untuk secara eksplisit menangani kasus dengan nilai nol, jika tidak program tidak akan dikompilasi.
Apa yang harus dilakukan jika pointer yang memungkinkan nilai nol tidak dapat dihindari (misalnya, ketika berinteraksi dengan kode dalam bahasa lain)? Cobalah untuk mengisolasi kerusakannya. Dereferencing pointer mentah harus terjadi di blok yang tidak aman terisolasi. Ini
melonggarkan aturan Rust dan menyelesaikan beberapa operasi yang dapat menyebabkan perilaku tidak terdefinisi (misalnya, penereferensi pointer mentah).
"Segala sesuatu tentang peminjam pinjaman ... bagaimana dengan tempat gelap itu?"
- Ini adalah blok yang tidak aman. Jangan pernah ke sana, SimbaBuffer overflow
Kami membahas kerentanan yang dapat dihindari dengan membatasi akses ke memori yang tidak ditentukan. Tetapi masalahnya adalah buffer overflow tidak mengakses dengan benar undefined, tetapi mengalokasikan memori secara legal. Seperti bug yang digunakan setelah bebas, akses tersebut dapat menjadi masalah karena mengakses memori yang dibebaskan, yang masih mengandung informasi rahasia yang seharusnya tidak ada lagi.
Buffer overflows berarti akses di luar batas. Karena cara buffer disimpan dalam memori, mereka sering membocorkan informasi yang mungkin berisi data sensitif, termasuk kata sandi. Dalam kasus yang lebih serius, kerentanan ACE / RCE dimungkinkan dengan menimpa penunjuk instruksi.
Contoh 4: Buffer Overflow (Kode C)
int main() { int buf[] = {0, 1, 2, 3, 4};
Perlindungan paling sederhana terhadap buffer overflows adalah untuk selalu memerlukan pemeriksaan perbatasan ketika mengakses elemen, tetapi ini menyebabkan
kinerja yang buruk .
Apa yang dilakukan karat? Tipe buffer bawaan di pustaka standar memerlukan pemeriksaan perbatasan untuk akses acak apa pun, tetapi juga menyediakan API iterator untuk mempercepat panggilan berurutan. Ini memastikan bahwa membaca dan menulis batas luar tidak dimungkinkan untuk jenis ini. Karat mempromosikan pola yang memerlukan pemeriksaan perbatasan hanya di tempat-tempat di mana Anda hampir pasti harus menempatkannya secara manual di C / C ++.
Keamanan memori hanya setengah pertempuran
Pelanggaran keamanan menyebabkan kerentanan seperti kebocoran data dan eksekusi kode jarak jauh. Ada berbagai cara untuk melindungi memori, termasuk smart pointer dan pengumpulan sampah. Anda bahkan dapat
secara resmi membuktikan keamanan memori . Sementara beberapa bahasa telah sepakat dengan penurunan kinerja demi keamanan memori, konsep kepemilikan Rust memberikan keamanan dan meminimalkan overhead.
Sayangnya, kesalahan memori hanya bagian dari cerita ketika kita berbicara tentang menulis kode aman. Pada artikel selanjutnya, kami akan mempertimbangkan keamanan utas dan serangan pada kode paralel.
Memanfaatkan Kerentanan Memori: Sumber Daya Tambahan