Megosztás:


Felhasználói módú hozzáférők

A felhasználói módú tartozékok (UMA) olyan DDI-k, amelyek célja a felhasználói módú memória biztonságos elérése és kezelése a kernel módú kódból. Ezek a DDI-k elhárítják azokat a gyakori biztonsági réseket és programozási hibákat, amelyek akkor fordulhatnak elő, ha a kernel módú illesztőprogramok hozzáférnek a felhasználói módú memóriához.

A felhasználói módú memóriát elérő/módosító kernel módú kódra hamarosan szükség lesz az UMA használatához.

A felhasználói módú memória kernel módból való elérésekor felmerülő lehetséges problémák

Amikor a kernel módú kódnak hozzá kell férnie a felhasználói módú memóriához, számos kihívás merül fel:

  • A felhasználói módú alkalmazások rosszindulatú vagy érvénytelen mutatókat adhatnak át a kernel módú kódnak. A megfelelő ellenőrzés hiánya memóriasérüléshez, összeomlásokhoz vagy biztonsági résekhez vezethet.

  • A felhasználói módú kód többszálú. Ennek eredményeképpen a különböző szálak módosíthatják ugyanazt a felhasználói módú memóriát a különálló kernel módú hozzáférések között, ami valószínűleg sérült kernelmemóriát eredményezhet.

  • A kernel-módú fejlesztők gyakran elfelejtik ellenőrizni a felhasználói módban lévő memóriát a hozzáférés előtt, ami biztonsági probléma.

  • A fordítók feltételezik az egyszálas végrehajtást, és esetleg elhagyhatják azokat a memóriahozzáféréseket, amelyek redundánsnak tűnhetnek. A programozók nem tudnak az ilyen optimalizálásokról, nem biztonságos kódot írhatnak.

Az alábbi kódrészletek ezeket a problémákat szemléltetik.

1. példa: Lehetséges memóriasérülés a felhasználói módban történő többszálúság miatt

A kernel módú kódnak, amely felhasználói módú memóriát kíván elérni, ezt a __try/__except blokkon belül kell megtennie annak biztosítása érdekében, hogy a memória érvényes legyen. Az alábbi kódrészlet a felhasználói módú memória elérésének tipikus mintáját mutatja be:

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

Ez a kódrészlet először a memóriát vizsgáljuk meg, ami egy fontos első, de gyakran figyelmen kívül hagyott lépés.

A kódban előforduló egyik probléma azonban a felhasználói módban zajló többszálú feldolgozás. Ptr->Size Az ExAllocatePool2 hívása után, de az RtlCopyMemory hívása előtt is változhat, ami memóriasérülést eredményezhet a kernelben.

2. példa: A fordítóoptimalizálások miatti lehetséges problémák

Az 1. példában a többszálúsági probléma lehetséges megoldása lehet, ha a Ptr->Size-t egy helyi változóba másoljuk a foglalás és másolás előtt.

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

Bár ez a megközelítés enyhíti a többszálas feldolgozás okozta problémát, még mindig nem biztonságos, mert a fordító nem tud több szálról, és így egyetlen végrehajtási szálat feltételez. Optimalizálásként előfordulhat, hogy a fordító már rendelkezik a veremében azzal az értékkel, amelyre Ptr->Size mutat, ezért nem készíti el a másolatot LocalSize-be.

Felhasználói módú hozzáférők megoldása

Az UMA-felület megoldja a felhasználói módú memória kernel módból való elérésekor felmerülő problémákat. Az UMA a következőt biztosítja:

  • Automatikus szondázás: Az explicit szondázás (ProbeForRead/ProbeForWrite) már nem szükséges, mivel az UMA összes függvénye biztosítja a címbiztonságot.

  • Volatilis hozzáférés: Minden UMA DDI használ illékony szemantikát a fordítóoptimalizálás megakadályozása érdekében.

  • Könnyű hordozhatóság: Az UMA DDI-k átfogó készlete megkönnyíti az ügyfelek számára, hogy a meglévő kódjukat az UMA DDI-k használatára portálják, biztosítva a felhasználói módú memória biztonságos és helyes elérését.

Példa az UMA DDI használatára

A korábban definiált felhasználói módú struktúra használatával az alábbi kódrészlet bemutatja, hogyan használhatja az UMA-t a felhasználói módú memória biztonságos eléréséhez.

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 implementálása és használata

Az UMA interfész a Windows Driver Kit (WDK) részeként érkezik.

  • A függvény deklarációi a usermode_accessors.h fejlécfájlban találhatók.
  • A függvény implementációi egy umaccess.lib nevű statikus kódtárban találhatók.

Az UMA a Windows összes verzióján működik, nem csak a legújabbakon. A usermode_accessors.h és az umaccess.lib függvénydeklarációinak és implementációinak lekéréséhez a legújabb WDK-t kell használnia. Az eredményként kapott illesztőprogram a Windows régebbi verzióiban is jól fog futni.

Az Umaccess.lib biztonságos, alacsonyabb szintű implementációt biztosít az összes DDI számára. A Windows kernel UMA-t támogató verzióiban az illesztőprogramok az összes függvényüket egy biztonságosabb verzióra irányítják át, amely az ntoskrnl.exe-ben van implementálva.

A felhasználói módú kiegészítő függvényeket egy strukturált kivételkezelőben (SEH) kell végrehajtani a felhasználói módú memória elérésekor esetleges kivételek miatt.

Felhasználói módú kiegészítő DDI-k típusai

Az UMA különböző DDI-kat biztosít a felhasználói módú memóriahozzáférés különböző típusaihoz. Ezeknek a DDI-knak a többsége alapvető adattípusokhoz, például BOOLEAN, ULONG és mutatókhoz készült. Az UMA emellett DDI-kat biztosít a tömeges memória-hozzáféréshez, a sztringhossz-lekéréshez és az összekapcsolt műveletekhez.

Általános DDI-k alapvető adattípusokhoz

Az UMA hat függvényvariánst biztosít egyszerű adattípusok olvasásához és írásához. A Boolean értékekhez például a következő függvények érhetők el:

Függvény neve Description
ReadBooleanFromUser Felhasználói módú memóriából származó érték beolvasása.
ReadBooleanFromUserAcquire Olvasson be egy értéket felhasználói módú memóriából, amely a memóriarendezéshez szükséges megszerzési szemantikát alkalmazza.
ReadBooleanFromMode Olvasás felhasználói vagy kernel módú memóriából egy módparaméter alapján.
WriteBooleanToUser Írjon egy értéket a felhasználói módú memóriába.
WriteBooleanToUserRelease Írjon egy értéket a felhasználói módú memóriába a memóriarendezés kiadási szemantikájának használatával.
WriteBooleanToMode Írás felhasználói vagy kernel módú memóriába egy módparaméter alapján.

A XxxFromUser olvasási függvényekhez a Forrás paraméternek a felhasználói módú virtuális címtérre (VAS) kell mutatnia. Ugyanez igaz a ReadXxxFromMode verziókban, amikor Mode == UserMode.

A ReadXxxFromMode használata esetén Mode == KernelMode a Forrás paraméternek a kernel módú VAS-ra kell mutatnia. Ha az előprocesszor definíciója, a DBG, definiálva van, a művelet gyorsan megszakad a FAST_FAIL_KERNEL_POINTER_EXPECTED kóddal.

A Write XxxToUserfüggvényben a Cél paraméternek a felhasználói módú VAS-ra kell mutatnia. Ugyanez igaz a WriteXxxToMode verziókban, amikor Mode == UserMode.

Másolási és memóriakezelési DDI-k

Az UMA függvényeket biztosít a memória másolásához és áthelyezéséhez a felhasználó és a kernel mód között, beleértve a nemtemporális és az igazított másolatok variánsait is. Ezeket a függvényeket széljegyzetek jelölik, amelyek a lehetséges SEH-kivételeket és az IRQL-követelményeket (maximális APC_LEVEL) jelzik.

Ilyenek például a CopyFromUser, a CopyToMode és a CopyFromUserToMode.

Az olyan makrók, mint a CopyFromModeAligned és a CopyFromUserAligned , a másolási művelet végrehajtása előtt az igazítási próbát is tartalmazzák a biztonság érdekében.

Az olyan makrók, mint a CopyFromUserNonTemporal és a CopyToModeNonTemporal , nemtemporális másolatokat biztosítanak, amelyek elkerülik a gyorsítótár-szennyezést.

Strukturált olvasási/írási makrók

A módok közötti olvasási és írási struktúrák makrói biztosítják a típuskompatibilitást és az igazítást, és méret- és módparaméterekkel hívják meg a segédfüggvényeket. Ilyenek például a WriteStructToMode, a ReadStructFromUser és a hozzájuk igazított változatok.

Memória kitöltési és nullázási függvények

A DDI-k a használói vagy üzemmódbeli címterekben lévő memória kitöltésére vagy nullázására szolgálnak, ahol paraméterekkel meghatározható a célhely, hossza, kitöltési értéke és módja. Ezek a függvények SEH- és IRQL-széljegyzeteket is hordoznak.

Ilyen például a FillUserMemory és a ZeroModeMemory.

Összekapcsolt műveletek

Az UMA olyan összekapcsolt műveleteket tartalmaz az atomi memóriához való hozzáféréshez, amelyek nélkülözhetetlenek a szálbiztos memóriakezeléshez egyidejű környezetekben. A DDI-k a 32 bites és a 64 bites értékekhez is elérhetők, a felhasználók vagy üzemmódok memóriáját megcélzó verziókkal.

Ilyenek például az InterlockedCompareExchangeToUser, az InterlockedOr64ToMode és az InterlockedAndToUser.

Sztringhosszúságú DDI-k

A felhasználói vagy módbeli memóriából biztonságosan meghatározható karakterlánchosszokat meghatározó függvények az ANSI-t és a széles karakterű sztringeket is támogatják. Ezek a függvények úgy vannak kialakítva, hogy kivételeket dobjanak a nem biztonságos memóriahozzáférés esetén, és IRQL korlátozások vonatkoznak rájuk.

Ilyenek például a StringLengthFromUser és WideStringLengthFromMode.

Nagy egész szám- és Unicode-karakterlánc-hozzáférők

Az UMA DDI-ket biztosít LARGE_INTEGER, ULARGE_INTEGER és UNICODE_STRING típusok olvasásához és írásához a felhasználó és a mód memóriája között. A variánsok olyan szemantikákat szereznek be és bocsátanak ki, amelyek biztonsági és helyességi módparaméterekkel rendelkeznek.

Ilyen például a ReadLargeIntegerFromUser, WriteUnicodeStringToMode és WriteULargeIntegerToUser.

Szemantikák beszerzése és kiadása

Egyes architektúrákban, például az ARM-ben a processzor átrendezheti a memóriahozzáféréseket. Az általános DDI-k mindegyike rendelkezik beszerzés/kiadás implementációval, ha garantálnia kell, hogy a memóriahozzáférések nem lesznek átrendezve a felhasználói módú hozzáféréshez.

  • Az 'acquire' szemantikák megakadályozzák a betöltési műveletek átrendezését más memóriaműveletek viszonylatában.
  • A kiadás szemantikája megakadályozza a tárolás átrendezését más memóriaműveletekhez képest.

Ilyenek például a ReadULongFromUserAcquire és a WriteULongToUserRelease UMA-ban történő beolvasási és kiadási szemantikák.

További információ: Szemantikák beszerzése és kiadása.

Ajánlott eljárások

  • A felhasználói módú memória kernelkódból való elérésekor mindig használjon UMA DDI-kat.
  • A kivételek kezelése a megfelelő __try/__except blokkokkal.
  • Használjon módalapú DDI-kat , ha a kód felhasználói és kernel módú memóriát is kezelhet.
  • Fontolja meg az elérési/felszabadítási szemantikát, ha a memóriarendezés fontos az adott esetben.
  • A konzisztencia biztosítása érdekében ellenőrizze a másolt adatokat, miután a kernelmemóriára másolta őket.

Jövőbeli hardvertámogatás

A felhasználói módú tartozékokat úgy tervezték, hogy támogassák a jövőbeli hardveres biztonsági funkciókat, például:

  • SMAP (Felügyeleti módú hozzáférés-megelőzés): Megakadályozza, hogy a kernelkód hozzáférjen a felhasználói módú memóriához, kivéve a kijelölt függvényeket, például az UMA DDI-kat.
  • ARM PAN (Privileged Access Never): Hasonló védelem az ARM-architektúrákon.

Az UMA DDI-k következetes használatával az illesztőprogramok kompatibilisek lesznek ezekkel a biztonsági fejlesztésekkel, ha a jövőbeli Windows-verziókban engedélyezve vannak.