Pengungkapan Memori kernel dalam OS modern

Di bawah pemotong adalah terjemahan dari bagian pembukaan dokumen Mendeteksi Pengungkapan Memori Kernel dengan Pelacakan Emulasi dan Pengotoran x86 ( Article Project Zero ) oleh Mateusz Jurczyk .


Di bagian dokumen yang diterjemahkan:


  • Spesifikasi bahasa pemrograman C (sebagai bagian dari masalah ekspansi memori)
  • spesifikasi operasi kernel Windows dan Linux (sebagai bagian dari masalah ekspansi memori)
  • signifikansi pengungkapan memori kernel dan dampaknya pada keamanan OS
  • metode dan teknik yang ada untuk mendeteksi dan melawan pengungkapan memori kernel

Meskipun dokumen ini berfokus pada mekanisme komunikasi antara kernel istimewa dari OS dan aplikasi pengguna, inti dari masalah dapat digeneralisasi untuk setiap transfer data antara domain keamanan yang berbeda: hypervisor adalah mesin tamu, layanan sistem istimewa (daemon) adalah aplikasi GUI, klien jaringan adalah server, dll. .


KDPV


Pendahuluan


Salah satu tugas sistem operasi modern adalah memastikan pemisahan hak istimewa antara aplikasi pengguna dan kernel OS. Pertama, ini termasuk fakta bahwa pengaruh setiap program pada runtime harus dibatasi oleh kebijakan keamanan tertentu, dan kedua, bahwa program hanya dapat mengakses informasi yang diizinkan untuk dibaca. Yang kedua sulit untuk diberikan, mengingat sifat-sifat bahasa C (bahasa pemrograman utama yang digunakan dalam pengembangan kernel), yang membuatnya sangat sulit untuk secara aman mentransfer data antara domain keamanan yang berbeda.


Sistem operasi modern yang berjalan pada platform x86 / x86-64 multi-threaded dan menggunakan model client-server di mana aplikasi mode pengguna (klien) dieksekusi secara independen dan memanggil OS kernel (server) dengan maksud bekerja dengan sumber daya yang dikelola oleh sistem. Mekanisme yang digunakan oleh kode mode pengguna ( dering 3 ) untuk memanggil satu set fungsi kernel yang sudah ditentukan sebelumnya (dering 0) disebut panggilan sistem atau panggilan sys (singkat). Panggilan sistem khas ditunjukkan pada Gambar 1:
Gambar 1: Panggilan Sistem
Gambar 1: Siklus hidup panggilan sistem.


Sangat penting untuk menghindari kebocoran secara tidak sengaja isi memori kernel ketika berinteraksi dengan program mode pengguna. Ada risiko yang signifikan untuk mengungkapkan data kernel sensitif. Data dapat secara implisit ditransmisikan dalam parameter output panggilan sistem yang aman (dari sudut pandang lain).


Pengungkapan memori sistem yang diistimewakan terjadi ketika kernel OS mengembalikan wilayah memori yang lebih besar (kelebihan) daripada yang diperlukan untuk menyimpan informasi yang sesuai (terdapat di dalamnya). Seringkali byte yang redundan berisi data yang dihuni dalam konteks yang berbeda, dan kemudian memori tidak diinisialisasi, yang akan mencegah penyebaran informasi dalam struktur data baru.


Khusus bahasa pemrograman C


Pada bagian ini, kita melihat beberapa aspek bahasa C yang paling penting untuk masalah ekspansi memori.


Status variabel tidak diinisialisasi yang tidak ditentukan


Variabel individual dari tipe sederhana (seperti char atau int), serta anggota struktur data (array, struktur, dan serikat pekerja) tetap dalam keadaan tidak terdefinisi hingga inisialisasi pertama (terlepas dari apakah mereka ditempatkan di stack atau di heap). Kutipan yang relevan dari spesifikasi C11 (ISO / IEC 9899: 201x Committee Draft N1570, April 2011):


6.7.9 Inisialisasi
...
10 Jika objek yang memiliki durasi penyimpanan otomatis tidak diinisialisasi secara eksplisit, nilainya tidak pasti .

7.22.3.4 Fungsi malloc
...
2 Fungsi malloc mengalokasikan ruang untuk objek yang ukurannya ditentukan oleh ukuran dan yang nilainya tidak ditentukan .

7.22.3.5 Fungsi realokasi
...
2 Fungsi realloc mendelokasi objek lama yang ditunjuk oleh ptr dan mengembalikan pointer ke objek baru yang memiliki ukuran yang ditentukan oleh ukuran. Isi objek baru harus sama dengan objek lama sebelum deallokasi, sampai yang lebih kecil dari ukuran baru dan lama. Setiap byte di objek baru di luar ukuran objek lama memiliki nilai tak tentu .

Bagian yang berlaku untuk kode sistem paling relevan dengan objek yang terletak di stack, karena kernel OS biasanya memiliki antarmuka alokasi dinamis dengan semantik mereka sendiri (tidak harus kompatibel dengan pustaka C standar, seperti yang akan dijelaskan nanti).


Sejauh yang kita ketahui, tidak ada satu pun dari tiga kompiler C paling populer untuk Windows dan Linux (Microsoft C / C ++ Compiler, gcc, LLVM) yang membuat kode yang mempri-inisialisasi variabel yang tidak diinisialisasi programmer pada stack dalam mode Release-build (atau yang setara). Ada opsi kompiler untuk menandai frame stack dengan byte - marker khusus (/ RTC di Microsoft Visual Studio, misalnya) tetapi mereka tidak digunakan dalam rilis build karena alasan kinerja. Sebagai hasilnya, variabel yang tidak diinisialisasi pada stack mewarisi nilai lama dari area memori yang sesuai.


Pertimbangkan contoh implementasi standar panggilan sistem Windows fiktif yang mengalikan bilangan input dengan dua dan mengembalikan hasil perkalian (Listing 1). Jelas, dalam kasus khusus (InputValue == 0), variabel OutputValue tetap tidak diinisialisasi dan disalin kembali ke klien. Kesalahan ini memungkinkan Anda untuk membuka empat byte memori tumpukan kernel untuk setiap panggilan.


NTSTATUS NTAPI NtMultiplyByTwo(DWORD InputValue, LPDWORD OutputPointer) { DWORD OutputValue; if (InputValue != 0) { OutputValue = InputValue * 2; } *OutputPointer = OutputValue; return STATUS_SUCCESS; } 

Daftar Kode 1: Memperluas memori melalui variabel lokal tidak diinisialisasi.


Kebocoran melalui variabel lokal tidak diinisialisasi tidak terlalu umum dalam praktek: di satu sisi, kompiler modern sering mendeteksi dan memperingatkan masalah seperti itu, di sisi lain, kebocoran tersebut adalah kesalahan fungsional yang dapat dideteksi selama pengembangan atau pengujian. Namun, contoh kedua (dalam Listing 2) menunjukkan bahwa kebocoran juga dapat terjadi melalui bidang struktur.


Dalam hal ini, bidang struktur yang dicadangkan tidak pernah secara eksplisit digunakan dalam kode, tetapi masih disalin kembali ke mode pengguna dan, oleh karena itu, juga memaparkan empat byte memori kernel ke kode panggilan. Contoh ini dengan jelas menunjukkan bahwa menginisialisasi setiap bidang dari setiap struktur yang dikembalikan ke klien untuk semua cabang dari eksekusi kode bukanlah tugas yang mudah. Dalam banyak kasus, inisialisasi paksa terlihat tidak logis, terutama jika bidang ini tidak memainkan peran praktis. Tetapi fakta bahwa variabel yang tidak diinisialisasi (atau bidang struktur) pada stack (atau pada heap) menerima isi data yang sebelumnya disimpan di area memori ini (dalam konteks operasi lain), terletak di jantung masalah ekspansi memori kernel.


 typedef struct _SYSCALL_OUTPUT { DWORD Sum; DWORD Product; DWORD Reserved; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtArithOperations( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.Product = InputValue * 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

Daftar 2: Memperluas memori melalui bidang struktur yang dicadangkan.


Penjajaran struktur dan byte bantalan


Menginisialisasi semua bidang struktur output adalah awal yang baik untuk menghindari ekspansi memori. Tetapi ini tidak cukup untuk menjamin bahwa dalam representasi level rendah tidak ada byte yang tidak diinisialisasi. Mari kita kembali ke spesifikasi C11:


6.5.3.4 Ukuran operator dan Keselarasan operator
...
4 [...] Ketika diterapkan pada operan yang memiliki tipe struktur atau gabungan, hasilnya adalah jumlah total byte dalam objek tersebut, termasuk padding internal dan trailing .

6.2.8 Penyelarasan objek
1 Jenis objek lengkap memiliki persyaratan pelurusan yang menempatkan pembatasan pada alamat di mana objek jenis itu dapat dialokasikan . Sebuah penyelarasan adalah nilai integer terintegrasi yang didefinisikan implementasi yang mewakili jumlah byte antara alamat berturut-turut di mana objek yang diberikan dapat dialokasikan. [...]

6.7.2.1 Penentu struktur dan serikat pekerja
...
17 Mungkin ada bantalan yang tidak disebutkan namanya di ujung struktur atau gabungan .

Yaitu, kompiler bahasa C untuk arsitektur x86 (-64) menggunakan perataan alami bidang struktur (memiliki tipe primitif): setiap bidang tersebut disejajarkan oleh N byte, di mana N adalah ukuran bidang. Selain itu, seluruh struktur dan gabungan juga disejajarkan ketika dideklarasikan dalam array, dan persyaratan untuk penyelarasan bidang bersarang terpenuhi. Untuk memastikan keselarasan, byte padding implisit dimasukkan ke dalam struktur jika perlu. Meskipun tidak dapat diakses langsung dalam kode sumber, byte ini juga mewarisi nilai lama dari area memori dan dapat mengirimkan informasi ke mode pengguna.


Dalam contoh di Listing 3, struktur SYSCALL_OUTPUT dikembalikan kembali ke kode panggilan. Ini berisi bidang 4 dan 8 byte, dipisahkan oleh 4 byte padding, diperlukan untuk alamat bidang LargeSum menjadi kelipatan 8. Terlepas dari kenyataan bahwa kedua bidang diinisialisasi dengan benar, padding byte tidak diatur secara eksplisit, yang lagi mengarah pada perluasan memori tumpukan kernel. Lokasi spesifik struktur dalam memori ditunjukkan pada Gambar 2.


 typedef struct _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.LargeSum = 0; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

Daftar 3: Memperluas memori dengan menyelaraskan struktur.


Gambar 2: Menyelaraskan struktur
Gambar 2: Representasi struktur dalam memori dengan penyelarasan dalam pikiran.


Kebocoran melalui keberpihakan relatif umum, karena cukup banyak parameter output dari panggilan sistem diwakili oleh struktur. Masalahnya sangat akut untuk platform 64-bit, di mana ukuran pointer, size_t, dan jenis serupa meningkat dari 4 menjadi 8 byte, yang mengarah pada tampilan padding yang diperlukan untuk menyelaraskan bidang struktur seperti itu.


Karena padding byte tidak dapat dialamatkan dalam kode sumber, perlu untuk menggunakan memset atau fungsi serupa untuk mengatur ulang seluruh area memori struktur sebelum menginisialisasi salah satu bidangnya dan menyalinnya ke mode pengguna, misalnya:


  memset(&OutputStruct, 0, sizeof(OutputStruct)); 

Namun, Seacord RC dalam bukunya "The CERT C Coding Standard, Edisi Kedua: 98 Aturan untuk Mengembangkan Sistem yang Aman, Andal, dan Aman. Addison-Wesley Professional" 2014 menyatakan bahwa ini bukan solusi yang ideal karena menambahkan byte ) mungkin masih dirobohkan setelah memanggil memset, misalnya, sebagai efek samping dari operasi dengan bidang yang berdekatan. Kepedulian dapat dibenarkan dengan pernyataan berikut dalam spesifikasi C:


6.2.6 Representasi tipe
6.2.6.1 Umum
...
6 Ketika nilai disimpan dalam objek tipe struktur atau gabungan , termasuk dalam objek anggota, byte representasi objek yang sesuai dengan padding byte mengambil nilai yang tidak ditentukan . [...]

Namun, dalam praktiknya, tidak ada kompiler C yang kami uji baca atau tulis di luar area memori bidang yang dinyatakan secara eksplisit. Tampaknya pendapat ini dibagikan oleh pengembang sistem operasi yang menggunakan memset.


Serikat pekerja dan bidang dengan ukuran berbeda


Bergabung adalah konstruksi bahasa C lain yang kompleks dalam konteks komunikasi dengan kode panggilan yang kurang istimewa. Pertimbangkan bagaimana spesifikasi C11 menggambarkan representasi serikat pekerja dalam memori:


6.2.5 Jenis
...
20 Sejumlah tipe turunan dapat dikonstruksi dari objek dan tipe fungsi, sebagai berikut: [...] Tipe union menggambarkan set nonempty objek anggota yang tumpang tindih , masing-masing memiliki nama yang ditentukan secara opsional dan tipe yang mungkin berbeda.

6.7.2.1 Penentu struktur dan serikat pekerja
...
6 Seperti dibahas dalam 6.2.5, struktur adalah tipe yang terdiri dari urutan anggota, yang penyimpanannya dialokasikan dalam urutan yang dipesan, dan gabungan adalah jenis yang terdiri dari urutan anggota yang penyimpanannya tumpang tindih .
...
16 Ukuran serikat pekerja cukup untuk menampung anggota-anggotanya yang terbesar . Nilai paling banyak dari satu anggota dapat disimpan di objek gabungan kapan saja.

Masalahnya adalah bahwa jika gabungan terdiri dari beberapa bidang dengan ukuran berbeda dan hanya satu bidang dengan ukuran lebih kecil yang diinisialisasi secara eksplisit, maka byte yang tersisa dialokasikan untuk mengakomodasi bidang besar tetap tidak diinisialisasi. Mari kita lihat contoh penangan panggilan sistem hipotetis, ditunjukkan pada Listing 4, bersama dengan alokasi memori gabungan SYSCALL_OUTPUT yang ditunjukkan pada Gambar 3.


 typedef union _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

Daftar Kode 4: Memperluas memori dengan menginisialisasi sebagian serikat pekerja.


Gambar 3: Menyelaraskan gabung
Gambar 3: Representasi penyatuan dalam memori dengan penyelarasan.


Ternyata ukuran total gabungan SYSCALL_OUTPUT adalah 8 byte (karena ukuran bidang LargeSum yang lebih besar). Namun, fungsi hanya menetapkan nilai bidang yang lebih kecil, meninggalkan 4 trailing byte yang tidak diinisialisasi, yang kemudian menyebabkan kebocoran ke aplikasi klien mereka.


Implementasi yang aman hanya akan mengatur bidang Sum di ruang alamat pengguna, dan tidak menyalin seluruh objek dengan area memori yang mungkin tidak digunakan. Perbaikan lain yang berfungsi adalah memanggil fungsi memset untuk membatalkan salinan serikat dalam memori kernel sebelum mengatur salah satu bidangnya dan mentransfernya kembali ke mode pengguna.


Ukuran tidak aman


Seperti yang ditunjukkan pada dua bagian sebelumnya, penggunaan sizeof operator dapat secara langsung atau tidak langsung berkontribusi untuk mengungkapkan memori kernel, menyebabkan lebih banyak data yang akan disalin daripada yang diinisialisasi sebelumnya.


C tidak memiliki peralatan yang diperlukan untuk mentransfer data dengan aman dari kernel ke ruang pengguna - atau, lebih umum, di antara konteks keamanan yang berbeda. Bahasa tidak mengandung metadata runtime yang secara eksplisit dapat menunjukkan byte mana yang diset di setiap struktur data yang digunakan untuk berinteraksi dengan kernel OS. Akibatnya, tanggung jawab terletak pada programmer, yang harus menentukan bagian dari setiap objek yang harus dilewatkan ke kode panggilan. Jika dilakukan dengan benar, Anda harus menulis fungsi salin terpisah untuk setiap struktur output yang digunakan dalam panggilan sistem. Yang pada gilirannya akan menyebabkan mengasapi ukuran kode, kemunduran dalam keterbacaannya, dan secara umum akan menjadi tugas yang membosankan dan memakan waktu.


Di sisi lain, lebih mudah dan sederhana untuk menyalin seluruh area memori kernel dengan satu panggilan memcpy dan argumen sizeof, dan membiarkan klien menentukan bagian mana dari output yang akan digunakan. Ternyata pendekatan ini digunakan hari ini di Windows dan Linux. Dan ketika kasus spesifik kebocoran informasi terdeteksi, tambalan dengan panggilan memset segera disediakan dan didistribusikan oleh produsen OS. Sayangnya, ini tidak menyelesaikan masalah dalam kasus umum.


Spesifik OS


Ada beberapa solusi desain kernel, metode pemrograman, dan pola kode yang memengaruhi seberapa rentan sistem operasi terhadap kerentanan ekspansi memori. Mereka dipertimbangkan dalam subbagian berikut.


Menggunakan kembali memori dinamis


Pengalokasi saat ini dari memori dinamis (baik dalam mode pengguna dan dalam mode kernel) sangat dioptimalkan, karena kinerja mereka memiliki dampak yang signifikan terhadap kinerja seluruh sistem. Salah satu optimasi yang paling penting adalah penggunaan kembali memori: ketika dirilis, memori yang sesuai jarang benar-benar dibuang, sebagai gantinya, disimpan dalam daftar wilayah yang siap untuk dikembalikan saat dialokasikan berikutnya. Untuk menyimpan siklus CPU, area memori default tidak dihapus antara deallokasi dan alokasi baru. Sebagai akibatnya, ternyata dua bagian kernel yang tidak terhubung bekerja dengan rentang memori yang sama untuk waktu yang singkat. Ini berarti bahwa kebocoran isi memori dinamis kernel memungkinkan Anda untuk mengungkapkan data berbagai komponen OS.


Dalam paragraf berikut, kami memberikan ikhtisar singkat tentang pengalokasi yang digunakan di kernel Windows dan Linux, dan kualitas mereka yang paling penting.


Windows
Fungsi utama manajer kumpulan kernel Windows adalah ExAllocatePoolWithTag , yang dapat dipanggil langsung atau melalui salah satu shell yang tersedia: ExAllocatePool {∅, Ex, WithQuotaTag, WithTagPriority}. Tidak satu pun dari fungsi ini menyiram isi memori yang dikembalikan, baik secara default atau melalui flag input apa pun. Sebaliknya, mereka semua memiliki peringatan berikut dalam dokumentasi MSDN masing-masing:


Catatan Memori yang dialokasikan fungsi tidak diinisialisasi. Driver mode kernel harus terlebih dahulu nol memori ini jika ingin membuatnya terlihat oleh perangkat lunak mode pengguna (untuk menghindari kebocoran konten yang berpotensi istimewa).

Kode panggilan dapat memilih salah satu dari enam tipe utama kumpulan: NonPagedPool, NonPagedPoolNx, NonPagedPoolSession, NonPagedPoolSessionNx, PagedPool, dan PagedPoolSession. Masing-masing memiliki wilayah terpisah di ruang alamat virtual, dan oleh karena itu area memori yang dialokasikan hanya dapat digunakan kembali dalam jenis kumpulan yang sama. Frekuensi penggunaan kembali kepingan memori sangat tinggi, dan area yang kosong biasanya dikembalikan hanya jika catatan yang sesuai tidak ditemukan dalam daftar lookaside, atau permintaan begitu besar sehingga diperlukan halaman memori baru. Dengan kata lain, saat ini praktis tidak ada faktor yang mencegah pengungkapan memori kumpulan di Windows, dan hampir setiap kesalahan seperti itu dapat digunakan untuk membocorkan data sensitif dari berbagai bagian kernel.


Linux
Kernel Linux memiliki tiga antarmuka utama untuk mengalokasikan memori secara dinamis:


  • kmalloc - fungsi umum yang digunakan untuk mengalokasikan blok memori dengan ukuran sewenang-wenang (kontinu dalam ruang alamat virtual dan fisik), menggunakan alokasi memori slab .
  • kmem_cache_create dan kmem_cache_alloc - mekanisme khusus untuk mengalokasikan objek dengan ukuran tetap (misalnya, struktur), juga menggunakan alokasi memori slab .
  • vmalloc adalah fungsi alokasi yang jarang digunakan yang mengembalikan daerah yang kontinuitasnya tidak dijamin pada tingkat memori fisik.

Fungsi-fungsi ini (sendiri) tidak menjamin bahwa wilayah yang dipilih tidak akan berisi data lama (berpotensi rahasia), yang memungkinkan untuk membuka memori tumpukan kernel. Namun, ada beberapa cara di mana kode panggilan dapat meminta memori yang dibatalkan:


  • Fungsi kmalloc memiliki analog kzalloc , yang memastikan bahwa memori yang dikembalikan dihapus.
  • Bendera __GFP_ZERO opsional dapat dikirimkan ke kmalloc , kmem_cache_alloc dan beberapa fungsi lainnya untuk mencapai hasil yang sama.
  • kmem_cache_create menerima pointer ke fungsi konstruktor opsional yang dipanggil untuk melakukan inisialisasi awal setiap objek sebelum mengembalikannya ke kode panggilan. Konstruktor dapat diimplementasikan sebagai pembungkus di sekitar memset ke nol area memori yang diberikan.

Kami melihat ketersediaan opsi-opsi ini sebagai kondisi yang menguntungkan untuk keamanan kernel, karena mereka mendorong pengembang untuk membuat keputusan berdasarkan informasi dan memungkinkan mereka untuk hanya bekerja dengan fungsi alokasi memori yang ada alih-alih menambahkan panggilan memset tambahan setelah setiap alokasi memori dinamis.


Memperbaiki Array Ukuran


Akses ke sejumlah sumber daya OS dapat diperoleh dengan nama uji mereka. Variasi sumber daya yang disebutkan di Windows sangat besar, misalnya: file dan direktori, kunci dan nilai kunci registri, windows, font, dan banyak lagi. Untuk beberapa dari mereka, panjang nama terbatas dan dinyatakan oleh konstanta, seperti MAX_PATH (260) atau LF_FACESIZE (32). Dalam kasus seperti itu, pengembang kernel sering menyederhanakan kode dengan mendeklarasikan buffer ukuran maksimum dan menyalinnya secara keseluruhan (misalnya, menggunakan sizeof kata kunci) daripada hanya bekerja dengan bagian yang sesuai dari baris. Ini sangat berguna jika string adalah anggota dari struktur yang lebih besar. Benda-benda seperti itu dapat dengan bebas dipindahkan dalam memori tanpa khawatir mengelola pointer ke memori dinamis.


Seperti yang Anda harapkan, buffer besar jarang digunakan sepenuhnya, dan ruang penyimpanan yang tersisa sering tidak memerah. Hal ini dapat menyebabkan kebocoran parah pada area memori kernel yang berdekatan. Dalam contoh di Listing 5, panggilan sistem menggunakan fungsi RtlGetSystemPath untuk memuat jalur sistem ke buffer lokal, dan jika panggilan berhasil, semua 260 byte diteruskan ke penelepon, terlepas dari panjang garis yang sebenarnya.


 NTSTATUS NTAPI NtGetSystemPath(PCHAR OutputPath) { CHAR SystemPath[MAX_PATH]; NTSTATUS Status; Status = RtlGetSystemPath(SystemPath, sizeof(SystemPath)); if (NT_SUCCESS(Status)) { RtlCopyMemory(OutputPath, SystemPath, sizeof(SystemPath)); } return Status; } 

Daftar 5: Memperluas memori dengan menginisialisasi sebagian string buffer.


Wilayah memori disalin kembali ke ruang pengguna dalam contoh ini ditunjukkan pada Gambar 4.


Gambar 4: Memori Buffer String yang diinisialisasi sebagian
Gambar 4: Memori buffer garis yang diinisialisasi sebagian.


Implementasi yang aman hanya akan mengembalikan jalur yang diminta, dan bukan seluruh buffer yang digunakan untuk penyimpanan. Contoh ini sekali lagi menunjukkan bagaimana memperkirakan ukuran data dengan ukuran operator (digunakan sebagai parameter untuk RtlCopyMemory) dapat sepenuhnya salah sehubungan dengan jumlah data aktual yang harus dilewati kernel ke area pengguna.


Ukuran output panggilan sistem yang sewenang-wenang


Sebagian besar panggilan sistem menerima pointer ke output mode pengguna bersama dengan ukuran buffer. Dalam kebanyakan kasus, informasi ukuran hanya boleh digunakan untuk menentukan apakah buffer yang disediakan cukup untuk menerima output panggilan sistem. Jangan gunakan seluruh ukuran buffer output yang disediakan untuk menentukan jumlah memori yang akan disalin. Namun, kami melihat kasus di mana kernel akan mencoba menggunakan setiap byte buffer output pengguna, tidak menghitung jumlah data aktual yang perlu disalin. Contoh perilaku ini ditunjukkan pada Listing 6.


 NTSTATUS NTAPI NtMagicValues(LPDWORD OutputPointer, DWORD OutputLength) { if (OutputLength < 3 * sizeof(DWORD)) { return STATUS_BUFFER_TOO_SMALL; } LPDWORD KernelBuffer = Allocate(OutputLength); KernelBuffer[0] = 0xdeadbeef; KernelBuffer[1] = 0xbadc0ffe; KernelBuffer[2] = 0xcafed00d; RtlCopyMemory(OutputPointer, KernelBuffer, OutputLength); Free(KernelBuffer); return STATUS_SUCCESS; } 

Daftar 6: Memperluas memori melalui buffer output dengan ukuran sewenang-wenang.


Tujuan dari pemanggilan sistem adalah untuk memberikan kode panggilan dengan tiga nilai khusus 32-bit, yang menempati total 12 byte. Meskipun memeriksa ukuran buffer yang benar di awal fungsi sudah benar, penggunaan argumen OutputLength harus berakhir di sana. Mengetahui bahwa buffer output cukup besar untuk menyimpan hasilnya, kernel dapat mengalokasikan 12 byte memori, mengisinya, dan menyalin konten kembali ke buffer mode pengguna yang disediakan. Sebaliknya, panggilan sistem mengalokasikan blok biliar (apalagi, dengan panjang yang dikontrol pengguna) dan menyalin seluruh memori yang dialokasikan ke ruang pengguna. Ternyata semua byte, kecuali 12 byte pertama, tidak diinisialisasi dan dibuka secara keliru untuk pengguna, seperti yang ditunjukkan pada Gambar 5.


Gambar 5: Memori Buffer Sewenang-wenang
Gambar 5: Memori penyangga ukuran sewenang-wenang.


Skema yang dibahas dalam bagian ini sangat umum untuk Windows. Kesalahan serupa dapat memberi penyerang primitif yang sangat berguna untuk ekspansi memori:


  • , Windows, . , .
  • . , , . , ( — ) .

, . , , .


,


, . , Windows .



, , . , : AddressSanitizer , PageHeap Special Pool . , , - . , . , , , , , . , ( ).


, , , . , .


, API
API, Windows (Win32/User32 API). API , , , . , , , , . .



, . , . , , , . , , .


, , . , KASLR (Kernel Address Space Layout Randomization ), . : Windows, Hacking Team 2015 ( Juan Vazquez. Revisiting an Info Leak ) (derandomize) win32k.sys, . , Matt Tait' Google Project Zero ( Kernel-mode ASLR leak via uninitialized memory returned to usermode by NtGdiGetTextMetrics ) MS15-080 (CVE-2015-2433).



(/) , , (control flow), : , , , , StackGuard Linux /GS Windows . , . , , .


(/)
(/) , , , : , , , . , , . . , ( , ) , , .



#2


Microsoft Windows



2015 Windows. 2015 Matt Tait win32k!NtGdiGetTextMetrics. Windows Hacking Team. , , , 0-day Windows.


2015, WanderingGlitch (HP Zero Day Initiative) ( Acknowledgments – 2015 ). Ruxcon 2016 ( ) "Leaking Windows Kernel Pointers" .


, 2017 fanxiaocao pjf IceSword Lab (Qihoo 360) "Automatically Discovering Windows Kernel Information Leak Vulnerabilities" , , 14 2017 (8 ). Bochspwn Reloaded, , . VMware (Bochs) . , Bochspwn Reloaded, .


, , 2010-2011 , win32k: "Challenge: On 32bit Windows7, explain where the upper 16bits of eax come from after a call to NtUserRegisterClassExWOW()" "Subtle information disclosure in WIN32K.SYS syscall return values" . Windows 8, 2015 Matt Tait , : Google Project Zero Bug Tracker .



( ), , 2017 - Windows -, : Joseph Bialek — "Anyone notice my change to the Windows IO Manager to generically kill a class of info disclosure? BufferedIO output buffer is always zero'd" . , IOCTL- .


, Visual Studio 15.5 POD- , "= {0}", . , padding- () .


Linux


Windows, Linux , 2010 . , ( ) ( ) . , Windows Linux , — , .



, Linux . "Linux kernel vulnerabilities: State-of-the-art defenses and open problems" 2010 2011 28 . 2017- "Securing software systems by preventing information leaks" Lu K. 59 , 2013- 2016-. . : Rosenberg Oberheide 25 , Linux 2009-2010 , . Linux c grsecurity / PaX-hardened . Vasiliy Kulikov 25 2010-2011 , Coccinelle . , Mathias Krause 21 2013 50 .


, , Linux. — -Wuninitialized ( gcc, LLVM), . kmemcheck , Valgrind' . , . , KernelAddressSANitizer KernelMemorySANitizer . KMSAN syzkaller ( ) 19 , .


Linux. 2014 — 2016 Peir´o Coccinelle , Linux 3.12: "Detecting stack based kernel information leaks" International Joint Conference SOCO14-CISIS14-ICEUTE14, pages 321–331 (Springer, 2014) "An analysis on the impact and detection of kernel stack infoleaks" Logic Journal of the IGPL. , . 2016- Lu UniSan — , , : , . , 20% (350 1800), 19 Linux Android.


— (multi-variant program execution), , . , . , KASLR, -, . , 2006 DieHard: probabilistic memory safety for unsafe languages, 2017 — BUDDY: Securing software systems by preventing information leaks. John North "Identifying Memory Address Disclosures" 2015- . , SafeInit (Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities) , , . , , , Linux.



, . , : , . , , - , . .


CONFIG_PAGE_POISONING CONFIG_DEBUG_SLAB, -. -, . , , , Linux.


grsecurity / PaX . , PAX_MEMORY_SANITIZE , slab , ( — ). , PAX_MEMORY_STRUCTLEAK , ( ), . padding- (), 100% . , — PAX_MEMORY_STACKLEAK, . , , . (Kernel Self Protection Project) STACKLEAK .


Linux:


Secure deallocation, Chow , 2005

Chow, Jim and Pfaff, Ben and Garfinkel, Tal and Rosenblum, Mendel. Shredding Your Garbage: Reducing Data Lifetime Through Secure Deallocation. In USENIX Security Symposium, pages 22–22, 2005.


, , ( ) . Linux .


Split Kernel, Kurmus Zippel, 2014

Kurmus, Anil and Zippel, Robby. A tale of two kernels: Towards ending kernel hardening wars with split kernel. In Proceedings of the 2014 ACM SIGSAC Conference on Computer and Communications Security, pages 1366–1377. ACM, 2014.


, .


SafeInit, Milburn , 2017

Milburn, Alyssa and Bos, Herbert and Giuffrida, Cristiano. SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities. In Proceedings of the 2017 Annual Network and Distributed System Security Symposium (NDSS)(San Diego, CA), 2017.


, , .


UniSan, Lu , 2016

Lu, Kangjie and Song, Chengyu and Kim, Taesoo and Lee, Wenke. UniSan: Proactive kernel memory initialization to eliminate data leakages. In Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, pages 920–932. ACM, 2016.


SafeInit , , , , .


, Linux .


( )


, , ( ). : (), , , , ( - ) . , . , , .


, :


  • Bochspwn Reloaded – detection with software x86 emulation
  • Windows bug reproduction techniques
  • Alternative detection methods
  • Other data sinks
  • Future work
  • Other system instrumentation schemes

, :) , .

Source: https://habr.com/ru/post/id415685/


All Articles