Aracılığıyla paylaş


Kullanıcı modunda erişimciler

Kullanıcı modu erişimcileri (UMA), çekirdek modu kodundan kullanıcı modu belleğine güvenli bir şekilde erişmek ve belleği işlemek için tasarlanmış bir DDI kümesidir. Bu DDI'ler, çekirdek modu sürücüleri kullanıcı modu belleğine eriştiğinde oluşabilecek yaygın güvenlik açıklarını ve programlama hatalarını giderir.

Kullanıcı modu belleğine erişen/işleyen çekirdek modu kodu yakında UMA kullanmak için gerekli olacaktır.

Çekirdek modundan kullanıcı modu belleğine erişirken karşılaşılan olası sorunlar

Çekirdek modu kodunun kullanıcı modu belleğine erişmesi gerektiğinde çeşitli zorluklar ortaya çıkar:

  • Kullanıcı modu uygulamaları, çekirdek modu koduna kötü amaçlı veya geçersiz işaretçiler geçirebilir. Düzgün doğrulama olmaması bellek bozulmasına, kilitlenmelere veya güvenlik açıklarına neden olabilir.

  • Kullanıcı modu kodu çoklu iş parçacıklı. Sonuç olarak, farklı iş parçacıkları aynı kullanıcı modu belleğini farklı çekirdek modu erişimleri arasında değiştirebilir ve büyük olasılıkla çekirdek belleğinin bozulmasına neden olabilir.

  • Çekirdek modu geliştiricileri genellikle kullanıcı modu belleğine erişmeden önce araştırmayı unutur. Bu bir güvenlik sorunudur.

  • Derleyiciler, tek iş parçacıklı yürütmeyi varsayar ve yedekli gibi görünen bellek erişimlerini optimize ederek ortadan kaldırabilir. Bu tür iyileştirmelerin farkında olmayan programcılar güvenli olmayan kod yazabilir.

Aşağıdaki kod parçacıkları bu sorunları göstermektedir.

Örnek 1: Kullanıcı modunda çoklu iş parçacığı kullanımı nedeniyle olası bellek bozulması

Kullanıcı modu belleğine erişmesi gereken çekirdek modu kodu, belleğin geçerli olduğundan emin olmak için bunu bir __try/__except blok içinde yapmalıdır. Aşağıdaki kod parçacığı, kullanıcı modu belleğine erişmek için tipik bir desen gösterir:

// 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
    }
}

Belleği ilk önce yoklayan bu kod parçacığı, önemli fakat sıkça göz ardı edilen bir adımdır.

Ancak, bu kodda oluşabilecek bir sorun, kullanıcı modunda çoklu iş parçacığı kullanımı nedeniyle oluşur. Özel olarak, Ptr->SizeExAllocatePool2 çağrısından sonra ancak RtlCopyMemory çağrısından önce değişebilir ve bu da çekirdekte bellek bozulmasına neden olabilir.

Örnek 2: Derleyici iyileştirmelerinden kaynaklanan olası sorunlar

Örnek 1'deki çoklu iş parçacığı sorununu giderme girişimi, ayırma ve kopyalamadan önce yerel bir değişkene kopyalamak Ptr->Size olabilir:

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 (…) {}
}

Bu yaklaşım çoklu iş parçacığı kullanımının neden olduğu sorunu hafifletse de, derleyici birden fazla iş parçacığının farkında olmadığından ve bu nedenle tek bir iş parçacığı yürütmesini varsaydığından güvenli değildir. Bir iyileştirme olarak, derleyici, Ptr->Size tarafından işaret edilen değerin yığını üzerinde zaten bir kopyasına sahip olduğunu görebilir ve bu nedenle kopyalamayı LocalSize gerçekleştirmeyebilir.

Kullanıcı modu erişimci çözümü

UMA arabirimi, çekirdek modundan kullanıcı modu belleğine erişirken karşılaşılan sorunları çözer. UMA şu bilgileri sağlar:

  • Otomatik yoklama: Tüm UMA işlevleri adres güvenliğini sağladığından, açık yoklama (ProbeForRead/ProbeForWrite) artık gerekli değildir.

  • Geçici erişim: Tüm UMA DDI'leri, derleyici iyileştirmelerini önlemek için geçici semantiği kullanır.

  • Taşınabilirlik kolaylığı: Kapsamlı UMA DDI'leri kümesi, müşterilerin mevcut kodlarını UMA DDI'lerini kullanacak şekilde taşımasını kolaylaştırır ve kullanıcı modu belleğe güvenli ve doğru bir şekilde erişilmesini sağlar.

UMA DDI kullanma örneği

Daha önce tanımlanmış kullanıcı modu yapısını kullanarak aşağıdaki kod parçacığı, kullanıcı modu belleğine güvenli bir şekilde erişmek için UMA'nın nasıl kullanılacağını gösterir.

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 (…) {}
}

UMA uygulaması ve kullanımı

UMA arabirimi, Windows Sürücü Seti'nin (WDK) bir parçası olarak sunulur:

  • İşlev bildirimleri usermode_accessors.h üst bilgi dosyasında bulunur.
  • İşlev uygulamaları umaccess.lib adlı statik bir kitaplıkta bulunur.

UMA, yalnızca en son sürümlerde değil, windows'un tüm sürümlerinde çalışır. İşlev bildirimlerini ve uygulamalarını sırasıyla usermode_accessors.h ve umaccess.lib'den almak için en son WDK'yi kullanmanız gerekir. Sonuçta elde edilen sürücü Windows'un eski sürümlerinde sorunsuz çalışır.

Umaccess.lib , tüm DDI'ler için güvenli, alt düzey bir uygulama sağlar. Windows çekirdeğinin UMA kullanan sürümlerinde, sürücülerin tüm işlevleri ntoskrnl.exe'de uygulanan daha güvenli bir sürüme yönlendirilir.

Kullanıcı modu belleğine erişirken olası özel durumlar nedeniyle tüm kullanıcı modu erişimci işlevleri yapılandırılmış bir özel durum işleyicisi (SEH) içinde yürütülmelidir.

Kullanıcı modu erişimci DDI türleri

UMA, farklı kullanıcı modu bellek erişimi türleri için çeşitli DDI'ler sağlar. Bu DDI'lerin çoğu BOOLEAN, ULONG ve işaretçiler gibi temel veri türlerine yöneliktir. Ayrıca, UMA toplu bellek erişimi, dize uzunluğu alma ve birbirine bağlı işlemler için DDI'ler sağlar.

Temel veri türleri için genel DDI'ler

UMA, basit veri türlerini okumak ve yazmak için altı işlev çeşidi sağlar. Örneğin, BOOLEAN değerleri için aşağıdaki işlevler kullanılabilir:

İşlev Adı Description
KullanıcıdanBooleanOku Kullanıcı modu belleğinden bir değer okuyun.
ReadBooleanFromUserAcquire Bellek düzenlemesi için edinme semantiği ile kullanıcı mod belleğinden bir değer okuyun.
ReadBooleanFromMode Mod parametresine göre kullanıcı modu veya çekirdek modu belleğinden okuma yapılır.
WriteBooleanToUser Kullanıcı modu belleğine bir değer yazın.
WriteBooleanToUserRelease Bellek sıralama için yayın semantiğiyle kullanıcı modu belleğine bir değer yazın.
WriteBooleanToMode Bir mod parametresine göre kullanıcı modu veya çekirdek modu belleğine yazma.

ReadXxxFromUser işlevleri için Source parametresi kullanıcı modu sanal adres alanına (VAS) işaret etmelidir. Aynı durum, ReadXxxFromMode sürümlerinde de geçerlidir Mode == UserMode olduğunda.

ReadXxxFromMode için, Mode == KernelModeSource parametresi çekirdek mod VAS'ye işaret etmelidir. Önişlemci tanımlaması DBG olarak yapılmışsa, işlem FAST_FAIL_KERNEL_POINTER_EXPECTED koduyla hızlı bir şekilde başarısız olur.

XxxToUser Yaz işlevlerinde Hedef parametresinin kullanıcı modu VAS'ye işaret etmesi gerekir. Aynı durum, YazmaXxxToMode sürümlerinde Mode == UserMode olduğunda da geçerlidir.

Kopyalama ve bellek işleme DDI'leri

UMA, kullanıcı ve çekirdek modları arasında bellek kopyalama ve taşıma işlevleri sağlar ve bunun içine zamandaşırı ve hizalanmış bellek kopyaları için varyantlar da dahildir. Bu işlevler, olası SEH özel durumlarını ve IRQL gereksinimlerini (en fazla APC_LEVEL) belirten ek açıklamalarla işaretlenir.

Örnek olarak CopyFromUser, CopyToMode ve CopyFromUserToMode verilebilir.

CopyFromModeAligned ve CopyFromUserAligned gibi makrolar, kopyalama işlemini gerçekleştirmeden önce güvenlik için hizalama yoklama içerir.

CopyFromUserNonTemporal ve CopyToModeNonTemporal gibi makrolar, önbellek kirliliğini önleyen kritik olmayan kopyalar sağlar.

Okuma/yazma makrolarını yapılandırma

Modlar arasında okuma ve yazma yapılarına yönelik makrolar, boyut ve mod parametreleriyle yardımcı işlevleri çağırarak tür uyumluluğunu ve hizalamayı sağlar. Örnek olarak WriteStructToMode, ReadStructFromUser ve bunların hizalanmış değişkenleri verilebilir.

Dolgu ve sıfır bellek işlevleri

DDI'ler, kullanıcı veya mod adres uzaylarında belleği doldurmak veya sıfırlamak için sağlanır ve parametreler hedefi, uzunluğu, doldurma değerini ve modu belirtir. Bu işlevler ayrıca SEH ve IRQL ek açıklamalarını da taşır.

FillUserMemory ve ZeroModeMemory örnekleri verilebilir.

Kilitlenmiş işlemler

UMA, eş zamanlı ortamlarda iş parçacığı güvenli bellek işlemeleri için gerekli olan atomik bellek erişimi için kilitlenmiş işlemler içerir. DDI'ler hem 32 bit hem de 64 bit değerler için sağlanır ve sürümler kullanıcı veya mod belleğini hedefler.

Örnek olarak InterlockedCompareExchangeToUser, InterlockedOr64ToMode ve InterlockedAndToUser verilebilir.

Dize uzunluğu DDI'leri

Kullanıcı veya mod belleğinden dize uzunluklarını güvenli bir şekilde belirlemeye yönelik işlevler eklenmiştir ve hem ANSI hem de geniş karakterli dizeleri destekler. Bu işlevler, güvenli olmayan bellek erişiminde özel durumlar oluşturacak şekilde tasarlanmıştır ve IRQL kısıtlanır.

StringLengthFromUser ve WideStringLengthFromMode örnekleri verilebilir.

Büyük tamsayı ve Unicode dize erişimcileri

UMA, kullanıcı ve mod belleği arasında LARGE_INTEGER, ULARGE_INTEGER ve UNICODE_STRING türlerini okumak ve yazmak için DDI'ler sağlar. Varyantlar, güvenlik ve doğruluk için mod parametreleriyle semantiği alır ve serbest bırakır.

ReadLargeIntegerFromUser, WriteUnicodeStringToMode ve WriteULargeIntegerToUser örnekleri verilebilir.

Edinim ve bırakma semantiği

ARM gibi bazı mimarilerde CPU, bellek erişimlerini yeniden sıralayabilir. Kullanıcı modu erişimi için bellek erişimlerinin yeniden sıralanmadığını garanti etmeniz gerekiyorsa genel DDI'lerin tümü bir Alma/Yayın uygulamasına sahiptir.

  • Alma semantiği, yükün diğer bellek işlemlerine göre yeniden sıralanmasını önler.
  • Yayın semantiği, depolama işlemi ile diğer bellek işlemlerinin tekrar sıralanmasını önler.

UMA'daki alma ve sürüm semantiğine örnek olarak ReadULongFromUserAcquire ve WriteULongToUserRelease verilebilir.

Daha fazla bilgi için bkz. Alma ve Yayın Semantiği.

En iyi yöntemler

  • Çekirdek kodundan kullanıcı modu belleğine erişirken her zaman UMA DDI'lerini kullanın.
  • Özel durumları uygun __try/__except.
  • Kodunuz hem kullanıcı modu hem de çekirdek modu belleğini işleyebilirken mod tabanlı DDI'leri kullanın.
  • Kullanım örneğinde bellek sıralaması önemli olduğunda alma/bırakma semantiğini göz önünde bulundurun.
  • Tutarlılığı sağlamak için kopyalanan verileri çekirdek belleğine kopyaladıktan sonra doğrulayın.

Gelecekteki donanım desteği

Kullanıcı modu erişimcileri, gelecekteki donanım güvenliği özelliklerini destekleyecek şekilde tasarlanmıştır:

  • SMAP (Gözetmen Modu Erişim Önleme): Çekirdek kodunun UMA DDI'leri gibi belirlenmiş işlevler dışında kullanıcı modu belleğine erişmesini engeller.
  • ARM PAN (Privileged Access Never): ARM mimarilerinde benzer koruma.

UMA DDI'leri tutarlı bir şekilde kullanıldığında, sürücüler gelecekteki Windows sürümlerinde etkinleştirildiğinde bu güvenlik geliştirmeleriyle uyumlu olacaktır.