Bagikan melalui


Pengakses mode pengguna

Aksesor mode pengguna (UMA) adalah sekumpulan DDI yang dirancang untuk mengakses dan memanipulasi memori mode pengguna dengan aman dari kode mode kernel. DDI ini mengatasi kerentanan keamanan umum dan kesalahan pemrograman yang dapat terjadi ketika driver mode kernel mengakses memori mode pengguna.

Kode mode kernel yang mengakses/memanipulasi memori mode pengguna akan segera diperlukan untuk menggunakan UMA.

Kemungkinan masalah saat mengakses memori mode pengguna dari mode kernel

Ketika kode mode kernel perlu mengakses memori mode pengguna, beberapa tantangan muncul:

  • Aplikasi mode pengguna dapat meneruskan penunjuk berbahaya atau tidak valid ke kode mode kernel. Kurangnya validasi yang tepat dapat menyebabkan kerusakan memori, crash, atau kerentanan keamanan.

  • Kode dalam mode pengguna mendukung multi-threading. Akibatnya, utas yang berbeda dapat memodifikasi memori mode pengguna yang sama antara akses mode kernel terpisah ke dalamnya, mungkin mengarah ke memori kernel yang rusak.

  • Pengembang mode kernel sering lupa untuk memeriksa memori mode pengguna sebelum mengaksesnya, yang merupakan masalah keamanan.

  • Kompiler mengasumsikan eksekusi berulir tunggal dan mungkin mengoptimalkan apa yang tampaknya menjadi akses memori yang berlebihan. Programmer tidak menyadari pengoptimalan tersebut dapat menulis kode yang tidak aman.

Cuplikan kode berikut menggambarkan masalah ini.

Contoh 1: Kemungkinan kerusakan memori karena multithreading dalam mode pengguna

Kode mode kernel yang perlu mengakses memori mode pengguna harus melakukannya dalam sebuah blok __try/__except untuk memastikan memori valid. Cuplikan kode berikut menunjukkan pola umum untuk mengakses memori mode pengguna:

// User-mode structure definition
typedef struct _StructWithData {
    ULONG Size;
    CHAR* Data[1];
} StructWithData;

// Kernel-mode call that accesses user-mode memory
void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);

        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, Ptr->Size);
        
        // Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr->Data, Ptr->Size);
    } __except (…) {
        // Handle exceptions
    }
}

Cuplikan ini memeriksa memori terlebih dahulu, yang merupakan langkah pertama yang penting tetapi sering diabaikan.

Namun, salah satu masalah yang dapat muncul dalam kode ini adalah akibat dari multithreading dalam mode pengguna. Secara khusus, Ptr->Size mungkin berubah setelah panggilan ke ExAllocatePool2 tetapi sebelum panggilan ke RtlCopyMemory, berpotensi menyebabkan kerusakan memori di kernel.

Contoh 2: Kemungkinan masalah karena pengoptimalan kompilator

Upaya untuk mengatasi masalah multithreading di Contoh 1 mungkin adalah menyalin Ptr->Size ke dalam variabel lokal sebelum dialokasikan dan disalin.

void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);
        
        // Read Ptr->Size once to avoid possible memory change in user mode
        ULONG LocalSize = Ptr->Size;
        
        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr, LocalSize);
    } __except (…) {}
}

Meskipun pendekatan ini mengurangi masalah yang disebabkan oleh multithreading, itu masih tidak aman karena kompilator tidak menyadari beberapa utas dan dengan demikian mengasumsikan satu utas eksekusi. Sebagai pengoptimalan, pengkompilasi mungkin melihat bahwa ia sudah memiliki salinan nilai yang Ptr->Size tunjuk pada stack dan karena itu tidak menyalin ke LocalSize.

Solusi untuk akses di mode pengguna

Antarmuka UMA memecahkan masalah yang dihadapi saat mengakses memori mode pengguna dari mode kernel. UMA menyediakan:

  • Pemeriksaan otomatis: Pemeriksaan eksplisit (ProbeForRead/ProbeForWrite) tidak lagi diperlukan, karena semua fungsi UMA memastikan keamanan alamat.

  • Akses volatil: Semua DDI UMA menggunakan semantik volatil untuk mencegah pengoptimalan kompilator.

  • Kemudahan portabilitas: Serangkaian DDI UMA yang komprehensif memudahkan pelanggan untuk memindahkan kode yang ada untuk menggunakan DDI UMA, memastikan bahwa memori mode pengguna diakses dengan aman dan benar.

Contoh menggunakan UMA DDI

Dengan menggunakan struktur mode pengguna yang ditentukan sebelumnya, cuplikan kode berikut menunjukkan cara menggunakan UMA untuk mengakses memori mode pengguna dengan aman.

void MySysCall(StructWithData* Ptr) {
    __try {

        // This UMA call probes the passed user-mode memory and does a
        // volatile read of Ptr->Size to ensure it isn't optimized away by the compiler.
        ULONG LocalSize = ReadULongFromUser(&Ptr->Size);
        
        // Allocate memory in the kernel.
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //This UMA call safely copies UM data into the KM heap allocation.
        CopyFromUser(&LocalData, Ptr, LocalSize);
        
        // To be safe, set LocalData->Size to be LocalSize, which was the value used
        // to make the pool allocation just in case LocalData->Size was changed.
        ((StructWithData*)LocalData)->Size = LocalSize;

    } __except (…) {}
}

Implementasi dan penggunaan UMA

Antarmuka UMA dikirim sebagai bagian dari Windows Driver Kit (WDK):

  • Deklarasi fungsi ditemukan dalam file header usermode_accessors.h .
  • Implementasi fungsi ditemukan di pustaka statis bernama umaccess.lib.

UMA berfungsi pada semua versi Windows, bukan hanya yang terbaru. Anda perlu menggunakan WDK terbaru untuk mendapatkan deklarasi fungsi dan implementasi dari usermode_accessors.h dan umaccess.lib. Pengandar yang dihasilkan akan berjalan dengan baik pada versi Windows yang lebih lama.

Umaccess.lib menyediakan implementasi tingkat bawah yang aman untuk semua DDI. Pada versi kernel Windows yang sadar akan UMA, driver akan mengalihkan semua fungsinya ke versi yang lebih aman yang diimplementasikan dalam ntoskrnl.exe.

Semua fungsi akses mode pengguna harus dijalankan dalam penangan pengecualian terstruktur (SEH) karena kemungkinan pengecualian saat mengakses memori mode pengguna.

Jenis DDI akses mode pengguna

UMA menyediakan berbagai DDI untuk berbagai jenis akses memori mode pengguna. Sebagian besar DDI ini untuk jenis data dasar, seperti BOOLEAN, ULONG, dan pointer. Selain itu, UMA menyediakan DDI untuk akses memori massal, pengambilan panjang dari string, dan operasi interlock.

DDI generik untuk jenis data dasar

UMA menyediakan enam varian fungsi untuk membaca dan menulis jenis data sederhana. Misalnya, fungsi berikut tersedia untuk nilai BOOLEAN:

Nama Fungsi Description
BacaBooleanDariPengguna Membaca nilai dari memori mode pengguna.
ReadBooleanFromUserAcquire Baca nilai dari memori mode pengguna dengan memperoleh semantik untuk pengurutan memori.
ReadBooleanFromMode Baca dari mode pengguna atau memori mode kernel berdasarkan parameter mode.
WriteBooleanToUser Tulis nilai ke memori mode pengguna.
WriteBooleanToUserRelease Tulis nilai ke memori mode pengguna dengan semantik rilis untuk urutan memori.
WriteBooleanToMode Tulis ke memori mode pengguna atau ke mode kernel berdasarkan parameter mode.

Untuk fungsi ReadXxxFromUser , parameter Sumber harus menunjuk ke ruang alamat virtual mode pengguna (VAS). Hal yang sama berlaku dalam versi ReadXxxFromMode saat Mode == UserMode.

Untuk ReadXxxFromMode, ketika Mode == KernelMode, parameter Source harus menunjuk ke mode kernel VAS. Jika definisi pra-prosesor DBG ditentukan, operasi cepat gagal dengan kode FAST_FAIL_KERNEL_POINTER_EXPECTED.

Dalam fungsi WriteXxxToUser , parameter Tujuan harus menunjuk ke VAS mode pengguna. Situasi yang sama berlaku pada versi WriteXxxToMode ketika Mode == UserMode.

DDI salin dan manipulasi memori

UMA menyediakan fungsi untuk menyalin dan memindahkan memori antara mode pengguna dan kernel, termasuk varian untuk salinan nontemporal dan selaras. Fungsi-fungsi ini ditandai dengan anotasi yang menunjukkan potensi pengecualian SEH dan persyaratan IRQL (APC_LEVEL maks).

Contohnya termasuk CopyFromUser, CopyToMode, dan CopyFromUserToMode.

Makro seperti CopyFromModeAligned dan CopyFromUserAligned termasuk pemeriksaan penyelarasan untuk keamanan sebelum melakukan operasi penyalinan.

Makro seperti CopyFromUserNonTemporal dan CopyToModeNonTemporal menyediakan salinan nontemporal yang menghindari polusi cache.

Struktur makro baca/tulis

Makro untuk membaca dan menulis struktur antar mode memastikan kompatibilitas jenis dan perataan, memanggil fungsi pembantu dengan parameter ukuran dan mode. Contohnya termasuk WriteStructToMode, ReadStructFromUser, dan varian yang selaras.

Fungsi pengisian dan pembersihan memori

DDI disediakan untuk mengisi atau mengosongkan memori dalam ruang alamat pengguna atau mode, dengan parameter yang menentukan tujuan, panjang, nilai pengisian, dan mode. Fungsi-fungsi ini juga membawa anotasi SEH dan IRQL.

Contohnya termasuk FillUserMemory dan ZeroModeMemory.

Operasi saling terhubung

UMA mencakup operasi yang saling mengunci untuk akses memori atomik, yang sangat penting untuk manipulasi memori aman utas di lingkungan bersamaan. DDI disediakan untuk nilai 32-bit dan 64-bit, dengan versi yang menargetkan memori pengguna atau mode.

Contohnya termasuk InterlockedCompareExchangeToUser, InterlockedOr64ToMode, dan InterlockedAndToUser.

Panjang string DDI

Fungsi yang menentukan panjang string dengan aman dari memori pengguna atau memori mode disertakan, mendukung string ANSI dan string berkarakter lebar. Fungsi-fungsi ini dirancang untuk meningkatkan pengecualian pada akses memori yang tidak aman dan dibatasi IRQL.

Contohnya termasuk StringLengthFromUser dan WideStringLengthFromMode.

Aksesor bilangan bulat besar dan string Unicode

UMA menyediakan DDI untuk membaca dan menulis jenis LARGE_INTEGER, ULARGE_INTEGER, dan UNICODE_STRING antara memori pengguna dan memori mode. Varian telah memperoleh dan melepaskan semantik dengan parameter mode untuk keamanan dan kebenaran.

Contohnya termasuk ReadLargeIntegerFromUser, WriteUnicodeStringToMode, dan WriteULargeIntegerToUser.

Memperoleh dan melepaskan semantik

Pada beberapa arsitektur seperti ARM, CPU dapat menyusun ulang akses memori. DDI generik semuanya memiliki implementasi Acquire/Release jika Anda memerlukan jaminan bahwa akses memori tidak diurutkan ulang untuk akses mode pengguna.

  • Memperoleh semantik mencegah pengurutan ulang beban relatif terhadap operasi memori lainnya.
  • Semantik rilis mencegah pengurutan ulang penyimpanan relatif terhadap operasi memori lainnya.

Contoh semantik akuisisi dan rilis di UMA termasuk ReadULongFromUserAcquire dan WriteULongToUserRelease.

Untuk informasi selengkapnya, lihat Memperoleh dan Merilis Semantik.

Praktik terbaik

  • Selalu gunakan DDI UMA saat mengakses memori mode pengguna dari kode kernel.
  • Mengelola pengecualian dengan blok yang sesuai __try/__except.
  • Gunakan DDI berbasis mode saat kode Anda mungkin menangani memori mode pengguna dan mode kernel.
  • Pertimbangkan semantik acquire/release saat pengurutan memori penting untuk kasus penggunaan Anda.
  • Validasi data yang disalin setelah menyalinnya ke memori kernel untuk memastikan konsistensi.

Dukungan perangkat keras di masa mendatang

Aksesor mode pengguna dirancang untuk mendukung fitur keamanan perangkat keras di masa mendatang seperti:

  • SMAP (Supervisor Mode Access Prevention): Mencegah kode kernel mengakses memori mode pengguna kecuali melalui fungsi yang ditunjuk seperti UMA DDI.
  • ARM PAN (Privileged Access Never): Perlindungan serupa pada arsitektur ARM.

Dengan menggunakan DDI UMA secara konsisten, driver akan kompatibel dengan peningkatan keamanan ini ketika diaktifkan di versi Windows mendatang.