Z minulých dílů seriálu jste se dozvěděli, jak deskriptory virtuálních adres vypadají. Dnešní díl navazuje informacemi o významu některých položek struktur MMVAD_XXX.
Velmi zajímavou částí struktur deskriptorů virtuálních adres tvoří pole příznaků. Definují je struktury MMVAD_FLAGS, MMVAD_FLAGS2 a MMVAD_FLAGS3. Krátké deskriptory, reprezentující obvykle privátní paměť procesu, obsahují pouze první a třetí skupinu. Přitom právě příznaky první skupiny dokáží uchovat všechny údaje, potřebné pro obyčejný blok privátní paměti. Význam příznaků ze třetí skupiny mi zůstává bohužel neznámý. Výjimku tvoří položka Teb, kterou správce paměti nastavuje na jedničku v případě, že blok paměti reprezentovaný daným deskriptorem slouží k uložení struktury TEB pro nějaké vlákno. U aplikací běžících v režimu WOW64 jsem toto chování ale nepozoroval.
Normální a dlouhé deskriptory již disponují všemi třemi skupinami příznaků. Díky tomu dokáží reprezentovat i bloky virtuálních stránek patřící paměťově mapovaným souborům. Začněme ale pěkně od počátku.
První skupina příznaků
Asi nejdůležitější položkou z první skupiny je člen VadType, který udává, k jakému bližšímu účelu blok paměti reprezentovaný daným deskriptorem slouží. Jak jste si mohli přečíst v minulém díle, správce paměti si v případě stránek alokovaných či rezervovaných běžným způsobem vystačí s deskriptory typu VadNone. To samé platí i o paměťově mapovaných souborech nepatřících do kategorie knihoven DLL či jiných entit dodržujících formát PE (a mapovaných jako soubory tohoto formátu). Další informace o jednotlivých typech deskriptorů najdete v příslušné tabulce v minulém dílu seriálu.
Příznak PrivateMemory lze použít k rozlišení toho, zda deskriptor reprezentuje blok privátní paměti, který nelze sdílet mezi více adresovými prostory (od toho také disponuje přídavným jménem privátní), nebo paměťově mapovaný soubor. Všechny bloky privátní paměti mají tento příznak nastaven na jedničku, paměťově mapované soubory na nulu.
Položka CommitCharge udává, kolik stránek daný blok paměti ubírá z kvóty svého procesu. Tato položka však může být nulová například u rezervované paměti, nebo pokud proces za daný blok „zaplatí“ dříve než při alokaci virtuálních stránek. Tato situace nastává při alokaci stránek fyzické paměti pomocí rozšíření Address Windowing Extension (rutina AllocateUserPhysicalPages). U paměťově mapovaných souborů (včetně souborů formátu PE) a obyčejných bloků privátní paměti obsah položky CommitCharge odpovídá počtu alokovaných stránek. Nezapomínejte na fakt, že oblast paměti reprezentovaná jedním deskriptorem může sestávat jak z alokovaných tak rezervovaných oblastí. A z obsahu deskriptoru nepoznáte, které jsou které. V rozlišování alokované a rezervované paměti nepomáhá ani příznak MemCommit, který v případě deskriptoru popisujícího oblast privátní paměti udává, zda při prvním volání rutiny VirtualAlloc (či VirtualAllocEx) došlo rovnou k alokaci (commit), nebo pouze k rezervování.
Při alokaci virtuální paměti či mapování souboru do adresového prostoru procesu musíte uvést oprávnění, jakým budou nově namapované virtuální stránky disponovat. Jeho hodnota se ukládá do položky Protection. Nominálně neodpovídá konstantám PAGE_XXX známým z dokumentace; uvnitř jádra dochází k převodu do jiného formátu. Hodnoty položky Protection odpovídající některým konstantám z dokumentace ukazuje tabulka níže. Další zajímavost tohoto členu struktury prvních příznaků spočívá v tom, že neinformuje o aktuálně nastavených oprávněních virtuálních stránek v oblasti paměti popisované deskriptorem. Ani to není možné, protože jeden deskriptor, stejně jako v případě oblasti tvořené mixem alokovaných a rezervovaných stránek, reprezentuje celý blok, nehledě na oprávnění jeho jednotlivých součástí nastavených po alokaci pomocí některé z rutin VirtualProtect či VirtualProtectEx.
Hodnota pole Protection | Konstanta PAGE_XXX | Typ |
1 | PAGE_READONLY, PAGE_WRITECOPY | Oprávnění |
2 | PAGE_READWRITE | Oprávnění |
3 | PAGE_EXECUTE_READ, PAGE_EXECUTE_WRITECOPY | Oprávnění |
4 | PAGE_READWRITE | Oprávnění |
6 | PAGE_EXECUTE_READWRITE | Oprávnění |
8 | PAGE_NOCACHE | Příznak |
16 | PAGE_GUARD | Příznak |
24 | PAGE_WRITECOMBINE | Příznak |
24 | PAGE_NOACCESS | Oprávnění |
Z tabulky je vidět, že po přemapování je výsledkem hodnot PAGE_NOACCESS a PAGE_WRITECOMBINE stejná konstanta, která má navíc společný bit s hodnotami vzniklými po přemapování příznaků PAGE_GUARD a PAGE_NOCACHE. Tato „kolize“ však nezpůsobuje žádné problémy, protože PAGE_NOACCESS není příznak, ale oprávnění. Navíc jej nelze kombinovat s ostatními v tabulce uvedenými příznaky. Dále, PAGE_WRITECOMBINE nelze kombinovat s PAGE_NOCACHE či PAGE_GUARD.
V první skupině již zbývá pouze popsat, k jakému účelu slouží člen NoChange. Jeho význam mi není do detailu znám, ale moje pozorování ukazují, že je nastaven na jedničku, pokud je daný blok paměti zabezpečen proti změně oprávnění či odmapování. Správce paměti jej nastavuje na jedničku v případě paměťově mapovaných souborů vytvořených s příznakem SEC_NO_CHANGE a bloků paměti zabezpečených pomocí funkce MmSecureVirtualMemory.
typedef struct _MMVAD_FLAGS { ULONG_PTR CommitCharge : COMMIT_SIZE; ULONG_PTR NoChange : 1; ULONG_PTR VadType : 3; ULONG_PTR MemCommit: 1; ULONG_PTR Protection : 5; ULONG_PTR Spare : 2; ULONG_PTR PrivateMemory : 1; } MMVAD_FLAGS; |
Druhá a třetí skupina příznaků
Příznaky druhé skupiny používá správce paměti pro bližší popis paměťově mapovaného souboru, pro rozlišení mezi normálním a dlouhým deskriptorem a pro ukládání části informací o zabezpečení daného bloku stránek.
Pro paměťově mapované soubory jsou důležité položky FileOffset, SecNoChange a Inherit. První udává offset od počátku souboru, kde začíná namapovaná oblast. Jednotkou jsou 64KB velké bloky, z čehož vyplývá teoretické omezení na velikost mapovaného souboru na 1 TB (240 bajtů). Příznak Inherit určuje, co se stane s namapovanou oblastí v případě, že daný proces vytvoří nový proces. Pokud je tento příznak nastaven na 1, namapovaná oblast se namapuje také do nově vzniklého adresového prostoru. Poslední jmenovaná položka již trochu zasahuje do zabezpečení, ačkoliv se stále týká paměťově mapovaných souborů. Hodnota 1 znamená, že byl příslušný objekt paměťově mapovaného souboru vytvořen s nastavením SEC_NO_CHANGE, které zakazuje měnit oprávnění stránek na namapovaných částech tohoto objektu. Jedná se tedy o přísnější formu zabezpečení poskytovaného MmSecureVirtualMemory s tím rozdílem, že odmapování je povoleno.
Poznámka: Pro vytvoření takto zabezpečeného paměťově mapovaného souboru musíte použít funkci NtCreateSection, která dovoluje příznak SEC_NO_CHANGE nastavit. Rutina CreateFileMapping jej totiž považuje za neplatný. Obdobná situace platí i pro příznak SEC_BASED, jehož význam jde však nad rámec tohoto dokumentu.
Rozlišit normální a dlouhé deskriptory lze pomocí výmluvně nazvaného příznaku LongVad. Hodnota 0 implikuje normální VAD, hodnota 1 dlouhý. Nezapomeňte, že tento bit lze použít až v případě, že vám deskriptor vychází jako normální (struktura MMVAD). A poznat normální deskriptor od krátkého dá více práce. Z mých pozorování jsem byl schopen odvodit pouze heuristiku.
Zabezpečení se týkají hlavně příznaky OneSecured a MultipleSecured. Pokud některý z nich disponuje hodnotou 1, znamená to, že blok paměti reprezentovaný deskriptorem, nebo pouze část tohoto bloku či několik jeho částí, je zabezpečen pomocí rutiny MmSecureVirtualMemory. Oba příznaky by nikdy jedničkou disponovat neměly. Mé zdroje informací dále ukazují, že k zabezpečení patří i položka ReadOnly, ale její úlohu jsem zatím dostatečně nepochopil.
Na doplnění celého výčtu chybí ještě příznaky CopyOnWrite a ExtendableFile. První se zdá být nastaven na jedničku, kdykoliv je na obsah daného bloku paměti aplikována optimalizace copy on write. Význam druhého z nich mi zatím není jasný.
typedef struct _MMVAD_FLAGS2 { ULONG_PTR FileOffset : 24; ULONG_PTR SecNoChange : 1; ULONG_PTR OneSecured : 1; ULONG_PTR MultipleSecured : 1; ULONG_PTR ReadOnly : 1; ULONG_PTR LongVad : 1; ULONG_PTR ExtendableFile : 1; ULONG_PTR Inherit : 1; ULONG_PTR CopyOnWrite : 1; } MMVAD_FLAGS2; |
Třetí skupina příznaků představuje více neznámého než obě předešlé skupiny dohromady. Pouze význam příznaku Teb se zdá jasný; je nastaven na 1 u deskriptorů, které popisují stránky paměti sloužící jako úložiště struktur TEB pro jednotlivá vlákna.
typedef struct _MMVAD_FLAGS3 { ULONG_PTR PreferredNode : 6; ULONG_PTR Teb : 1; ULONG_PTR Spare : 1; ULONG_PTR SequentialAccess : 1; ULONG_PTR LastSequentialTrim : 15; ULONG_PTR Spare2 : 8; } MMVAD_FLAGS3; |
Zabezpečení
Z pojednání o druhé skupině příznaků již víte, jak poznat, zda blok paměti popisovaný deskriptorem obsahuje jednu, nebo více zabezpečených oblastí, nebo zda zabezpečení vůbec nepodléhá. Pro získání kompletního obrazu o této oblasti chybí pouze informace, jak je reprezentován seznam těchto zabezpečených oblastí a kde je uložen. Odpověď na druhou otázku zní: je uložen v členu u3 ve struktuře dlouhého deskriptoru. Z definice vidíte, že tato položka se může chovat buď jako záznam typu MMADDRESS_LIST, jehož definici vidíte níže, nebo jako hlava spojového seznamu.
Pokud blok paměti disponuje pouze jednou zabezpečenou oblastí, položka u3 příslušného dlouhého deskriptoru se chová jako záznam MMADDRESS_LIST. Hranice této oblasti uchovává v položkách StartVpn (první stránka) a EndVpn (poslední zabezpečená stránka).
typedef struct _MMADDRESS_LLIST { ULONG_PTR StartVpn; ULONG_PTR EndVpn; } MMADDRESS_LIST, *PMMADDRESS_LIST; |
V případě, že blok paměti obsahuje více zabezpečených oblastí, položka u3 v jeho deskriptoru se chová jako hlava spojového seznamu, jehož prvky jsou definovány jako struktury MMSECURE_ENTRY. Každá z nich obsahuje informace o počáteční a koncové stránce jedné zabezpečené oblasti (opět v položkách StartVpn a EndVpn) a samozřejmě i odkazy na předka a následníka v seznamu.
typedef struct _MMSECURE_ENTRY { ULONG_PTR StartVpn; ULONG_PTR EndVpn; LIST_ENTRY List; } MMSECURE_ENTRY, *PMMSECURE_ENTRY; |
Rozlišování krátkých, normálních a dlouhých deskriptorů
Jak bylo řečeno výše, správce paměti popisuje jednotlivé bloky virtuálního adresového prostoru procesu pomocí tří různých typů deskriptorů virtuálních adres: krátkých, normálních a dlouhých. Rozlišit normální deskriptory od těch dlouhých lze podle členu LongVad z druhé skupiny příznaků popsané výše. Dlouhé deskriptory mají tento příznak nastavený na 1, zatímco krátké ne. Hodnotu tohoto příznaku však lze zkoumat pouze v případě, že již víme, že daný deskriptor patří alespoň do kategorie normálních. Krátké deskriptory totiž druhou skupinou příznaků nedisponují.
Rozlišení krátkých deskriptorů od normálních bohužel není tak jednoduché. Při svém výzkumu jsem došel pouze k něčemu na způsob heuristiky, která zatím funguje ve všech testovaných případech. Důležité jsou hodnoty příznaků PrivateMemory a NoChange z první skupiny. Krátké deskriptory se vyznačují tím, že mají první jmenovaný příznak nastavený na 1 a druhý na 0. Popisují tedy bloky privátní paměti nezabezpečené proti restriktivní změně oprávnění a dealokaci.
V článku he VAD tree: A process-eye view of physical memory, jsem se dočetl, že druh virtuálního deskriptoru lze rozpoznat také pomocí tagu, který bloku paměti, ve kterém je struktura MMVAD_XXX uložena, přiřadil správce paměti. Tabulka níže ukazuje pozorované hodnoty tagu a druh deskriptoru k němu příslušný. Bohužel, autoři článku se zabývali pouze systémy Windows 2000 a Windows XP. Na novějších verzích operačního systému se objevuje více druhů tagů, než uvádějí v článku.
Tag | Druh deskriptoru |
VadS | Krátký |
Vad | Normální |
Vadl | Dlouhý |
Vadm | V článku he VAD tree: A process-eye view of physical memory, tento tag není zmiňován. Můj výzkum naznačuje, že se jedná pravděpodobně o dlouhý deskriptor reprezentující namapovanou část paměťově mapovaného souboru. To znamená, že tento deskriptor se v minulosti svou strukturou řadil mezi normální. |
A touto tabulkou ukončíme tento díl seriálu o deskriptorech virtuálních adres. Příště se podíváme na to, jaké deskriptory vznikají při alokaci či rezervaci paměti pro různé účely (alokace privátní paměti, mapování souboru, AWE) a celý seriál zakončíme.