Een van de meest interessante en meest gebruikte concepten in de x86 architectuur is Protected mode en de ondersteuning daarvan in 4 modi (aka ringen):
Het was een uitdagend idee om te begrijpen en ik zal proberen het in dit bericht zo duidelijk mogelijk uit te leggen. We zullen de volgende concepten behandelen:
- GDT, LDT, IDT.
- Virtual memory translation.
- ASLR en Kernel ASLR (KASLR).
Laten we beginnen met de basis, elke computer heeft op zijn minst (hopelijk) de volgende componenten: CPU, Schijf en RAM. Elk van deze componenten heeft een sleutelrol in de stroom van het systeem. De CPU voert de commando’s en operaties uit op het geheugen (RAM), het RAM bevat de data die we gebruiken en maakt snelle en betrouwbare toegang ertoe mogelijk, de schijf bevat persistente data die we nodig hebben om te blijven bestaan, zelfs na opnieuw opstarten of afsluiten. Ik ga hier van uit omdat, ook al is dit de basis, het belangrijk is om dit in gedachten te houden en terwijl je door dit artikel leest jezelf af te vragen over welke component we het op dat moment hebben.
Het besturingssysteem is de software die dit alles orkestreert, en ook degene die een snelle, gemakkelijke, consistente en efficiënte interface mogelijk maakt om toegang te krijgen tot al zijn mogelijkheden – waarvan sommige toegang geven tot die hardware, en andere het gemak en de prestaties verhogen.
Zoals elke goede software werkt het OS in lagen, de kernel is de eerste laag en – naar mijn mening – de meest belangrijke. Om het belang van de kernel te begrijpen, moeten we eerst zijn acties en uitdagingen begrijpen, dus laten we eens kijken naar enkele van zijn verantwoordelijkheden:
- Handelen van systeemaanroepen (dezelfde interface waar we het over hadden).
- Bronnen toewijzen (RAM, CPU, en nog veel meer) aan de processen/threads in de hand.
- Beveiligen van de uitgevoerde operaties.
- Tussenpersoon tussen de hardware en de software.
Vele van deze acties worden uitgevoerd met de genereuze hulp van de processor, in het geval van x86, Protected mode is de modus die ons in staat stelt om de kracht (instructieset) van de momenteel draaiende uitvoeringscontext te beperken.
Laten we aannemen dat we twee werelden hebben – de wereld van de gebruiker en de wereld van de supervisor. Op elk gegeven moment kun je maar in een van die werelden zijn. Als je in de wereld van de gebruiker bent, zie je de wereld zoals de opzichter wil dat je hem ziet. Laten we eens kijken wat ik daarmee bedoel:
Laten we zeggen dat je een proces bent. Een proces is een container van een of meer threads. Een thread is een uitvoeringscontext, het is de logische eenheid waarvan de machine-instructies worden uitgevoerd. Dit betekent dat wanneer de thread aan het uitvoeren is, laten we zeggen, lezen van het geheugenadres 0x808080, het feitelijk verwijst naar het virtuele adres 0x808080 van het huidige proces. Zoals je kunt raden, zal de inhoud van het adres verschillend zijn tussen twee processen. Nu is de virtuele adresruimte op procesniveau, wat betekent dat alle threads van hetzelfde proces dezelfde adresruimte hebben en toegang hebben tot hetzelfde virtuele geheugen. Om een voorbeeld te geven van een bron die zich op threadniveau bevindt, laten we de beroemde stack gebruiken.
Dus ik heb een thread die de volgende code uitvoert:
Zoals u kunt zien hadden alle threads hetzelfde stacksegment omdat ze dezelfde virtuele adresruimte hebben. De stack pointer voor elke thread is anders omdat elke thread zijn eigen stack heeft om zijn waarden in op te slaan.
Noot over het stack segment – Ik zal meer uitleggen over segment registers in de GDT/LDT sectie – voor nu geloof me op mijn woord.
Dus waarom is dit belangrijk? Op elk willekeurig moment kan de processor de thread bevriezen en de controle aan een andere thread geven. Als onderdeel van de kernel is de scheduler degene die de CPU toewijst aan de op dat moment bestaande (en “gereed” zijnde) threads. Om de threads betrouwbaar en efficiënt te laten werken, is het essentieel dat elke thread zijn eigen stack heeft, waarin hij zijn relevante waarden kan opslaan (lokale variabelen en retouradressen bijvoorbeeld).
Om zijn threads te beheren, houdt het besturingssysteem voor elke thread een speciale structuur bij, genaamd TCB (Thread Control Block), in die structuur slaat het – onder andere – de context van die thread en zijn status op (running / ready / etc…). De context bevat – opnieuw – onder andere, de CPU-registers waarden:
- EBP -> Basisadres van de stack, elke functie gebruikt dit adres als het basisadres van waaruit het offset om lokale variabelen en parameters te openen.
- ESP -> De huidige pointer naar de laatste waarde (eerste naar pop) op de stack.
- General purpose registers -> EAX, EBX, enz…
- Flags register.
- C3 -> bevatten de locatie van de page directory (wordt later besproken).
- EIP – De volgende instructie die wordt uitgevoerd.
Naast threads moet het besturingssysteem nog een heleboel andere dingen bijhouden, waaronder processen. Voor processen bewaart het OS de PCB (Process Control Block) structuur, we zeiden dat er voor elk proces een geïsoleerde adresruimte is. Laten we nu eens aannemen dat er een tabel is die elk virtueel adres aan een fysiek adres koppelt en dat die tabel in het PCB is opgeslagen, het OS is verantwoordelijk voor het bijwerken van die tabel en houdt die bijgewerkt tot de juiste toestand van het fysieke geheugen. Elke keer als de scheduler de uitvoering naar een bepaalde thread schakelt, wordt de tabel die is opgeslagen voor het eigen proces van die thread toegepast op de CPU, zodat hij in staat is om de virtuele adressen correct te vertalen.
Dat is genoeg voor de concepten, laten we begrijpen hoe het werkelijk wordt gedaan. Laten we daarvoor eens naar de wereld kijken vanuit het perspectief van de processor:
Global Descriptor Table
We weten allemaal dat de processor registers heeft die hem helpen berekeningen te maken, sommige registers meer dan andere (;)). Door het ontwerp ondersteunt de x86 meerdere modi, maar de belangrijkste zijn gebruiker en supervisor, de CPU heeft een speciaal register genaamd gdtr (Global Descriptor Table Register) dat het adres bevat van een zeer belangrijke tabel. die tabel wijst elk virtueel adres toe aan de overeenkomstige modus van de processor, het bevat ook de permissies voor dat adres (READ | WRITE | EXECUTE). uiteraard kan dat register alleen worden gewijzigd vanuit de supervisor-modus. Als deel van de uitvoering van de processor, controleert hij welke instructie de volgende is om uit te voeren (en op welk adres die is), hij controleert dat adres met de GDT en op die manier weet hij of het een geldige instructie is, gebaseerd op de gewenste modus (komt overeen met de huidige modus van de CPU en de modus in de GDT) en permissies (indien niet uitvoerbaar – ongeldig). Een voorbeeld is ‘lgdtr’ de instructie die waarde laadt in het gdtr register en deze kan alleen worden uitgevoerd vanuit de bewaakte modus zoals vermeld. Het belangrijkste punt om hier te benadrukken is dat elke bescherming over de geheugen operaties (uitvoeren van instructie / schrijven naar ongeldige plaats / lezen van ongeldige plaats) wordt gedaan door de GDT en LDT (komt hierna) op het niveau van de processor met behulp van deze structuren die werden gebouwd door het OS.
Zo ziet de inhoud van een invoer in GDT / LDT eruit:
http://wiki.osdev.org/Global_Descriptor_Table
Zoals u kunt zien heeft het de reeks adressen waar de entry betrekking op heeft, en de attributen (permissies) zoals je zou verwachten.
Local Descriptor Table
Alles wat we over de GDT hebben gezegd, geldt ook voor LDT, met een klein (maar groot) verschil. Zoals de naam al zegt, wordt de GDT globaal toegepast op het systeem terwijl de LDT lokaal wordt toegepast, wat bedoel ik met globaal en lokaal? De GDT houdt de permissies bij voor alle processen, voor iedere thread en het wordt niet gewijzigd tussen context wisselingen, de LDT daarentegen wel. Het is alleen logisch dat als ieder proces zijn eigen adresruimte heeft, het mogelijk is dat voor het ene proces adres 0x10000000 uitvoerbaar is en voor een ander proces alleen lezen/schrijven. Dit is vooral het geval met ASLR aan (zal later worden besproken). De LDT is verantwoordelijk voor het bijhouden van de permissies die elk proces onderscheidt.
Een ding om op te merken is dat alles wat gezegd is het doel van de structuur is, maar in werkelijkheid kunnen sommige besturingssystemen sommige van de structuur wel of niet gebruiken. Het is bijvoorbeeld mogelijk om alleen de GDT te gebruiken en deze te veranderen tussen context wisselingen en nooit de LDT te gebruiken. Het is allemaal onderdeel van het ontwerpen van het OS en de afwegingen. De ingangen van die tabel lijken op die van de GDT.
Selectors
Hoe weet de processor waar hij moet kijken in de GDT of LDT wanneer hij een specifieke instructie uitvoert? De processor heeft speciale registers die segmentregisters worden genoemd:
https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture
- Stack Segment (SS). Pointer to the stack. - Code Segment (CS). Pointer to the code. - Data Segment (DS). Pointer to the data. - Extra Segment (ES). Pointer to extra data ('E' stands for 'Extra'). - F Segment (FS). Pointer to more extra data ('F' comes after 'E'). - G Segment (GS). Pointer to still more extra data ('G' comes after 'F').
Elk register is 16 bits lang en de structuur ervan is als volgt:
http://www.c-jump.com/CIS77/ASM/Memory/M77_0290_segment_registers_protected.htm
Dus we hebben de index van de GDT/LDT, we hebben ook de bit die zegt of het de LDT of de GDT is, en welke mode het moet zijn (RPL 0 is supervisor, 4 is gebruiker).
Interrupt Descriptor Table
Naast de GDT en LDT hebben we ook nog de IDT (Interrupt Descriptor Table), de IDT is eenvoudigweg een tabel die de adressen bevat van een aantal zeer belangrijke functies, sommige behoren tot het OS, andere tot drivers en fysieke apparaten die op de PC zijn aangesloten. Net als de gdtr hebben we idtr die, zoals je waarschijnlijk al geraden hebt, het register is dat het adres van de IDT bevat. Wat maakt de IDT zo speciaal? Wanneer we een interrupt starten, schakelt de CPU automatisch naar de bewaakte modus, wat betekent dat elke functie in de IDT in bewaakte modus draait. Elke thread in elke modus kan een interrupt triggeren door de ‘int’ instructie te geven, gevolgd door een getal dat de CPU vertelt op welke index de doelfunctie zich bevindt. Met dat gezegd te hebben, is het nu duidelijk dat elke functie in de IDT een potentiële gateway is naar de bewaakte modus.
Dus we weten dat we de GDT/LDT hebben die de CPU de permissies voor elk virtueel adres vertelt en we hebben de IDT die de ‘gateway’ functies naar onze geliefde kernel wijst (die zich uiteraard in de bewaakte sectie van het geheugen bevindt). Hoe gedragen deze structuren zich in een draaiend systeem?
Virtueel geheugen
Voordat we kunnen begrijpen hoe dit alles in elkaar steekt, moeten we nog één concept behandelen – Virtueel geheugen. Weet je nog dat ik zei dat er een tabel is die elk virtueel geheugenadres koppelt aan zijn fysieke adres? Het is eigenlijk een beetje ingewikkelder dan dat. Ten eerste kunnen we niet eenvoudigweg elk virtueel adres mappen omdat dat meer ruimte in beslag neemt dan we eigenlijk hebben, en afgezien van de noodzaak om efficiënt te zijn, het OS kan ook geheugenpagina’s naar de schijf swappen (voor efficiëntie en prestatie), is het mogelijk dat de geheugenpagina van het benodigde virtuele adres op dit moment niet in het geheugen aanwezig is, dus naast het vertalen van het virtuele adres naar het fysieke adres moeten we ook opslaan of het geheugen zich in RAM bevindt en zo niet, waar het zich bevindt (er kan meer dan één paginabestand zijn). De MMU (Memory Management Unit) is de component die verantwoordelijk is voor het vertalen van het virtuele geheugen naar een fysiek geheugen.
Een ding is echt belangrijk om te begrijpen is dat elke instructie in elke modus door het proces van virtueel adres vertaling gaat, zelfs code in de bewaakte modus. Zodra de CPU in beschermde modus, elke instructie die het uit te voeren maakt gebruik van virtuele adres – nooit fysiek (er zijn een aantal trucs die de werkelijke virtuele adres zal altijd vertalen naar exact dezelfde virtuele geheugen, maar dat valt buiten het bestek van deze post).
Dus eenmaal in beschermde modus, hoe de CPU weet waar te kijken als hij nodig heeft om virtuele adres vertalen? het antwoord is CR3 register, dit register houdt het adres van de structuur die de benodigde informatie bevatten – Page directory. De waarde verandert naar gelang het proces dat op dat moment draait (weer een andere virtuele adresruimte).
Hoe ziet deze paginadirectory er dan uit? Als het op efficiëntie aankomt, moeten we deze “tabel” zo snel mogelijk kunnen opvragen, we moeten hem ook zo klein mogelijk maken omdat deze tabel voor elk proces wordt aangemaakt. De oplossing voor dat probleem is niets minder dan briljant. De beste afbeelding die ik kon vinden om het vertaalproces te illustreren is deze (van wikipedia):
De MMU heeft 2 ingangen, het te vertalen virtuele adres en de CR3 (adres van de op dat moment relevante paginadirectory). De x86 specificatie hakt het virtuele adres in 3 stukken:
- 10 bit nummer – index naar de pagina directory.
- 10 bit nummer – index naar de pagina tabel.
- 12 bit nummer – offset naar het fysieke adres zelf.
Dus de processor neemt het eerste 10 bit getal en gebruikt dat als index voor de page directory, voor elke entry in de page directory hebben we page table, die de processor dan gebruikt als index voor het volgende 10 bit getal. Elke entry in de paginatabel wijst naar een 4K grensgeheugenpagina, waarvan de laatste 12bit offset van het virtuele adres wordt gebruikt om de exacte locatie in het fysieke geheugen aan te wijzen. Het briljante in die oplossing is:
- De flexibiliteit dat elk virtueel adres naar een geheel ongerelateerde fysieke lokaliseert.
- De efficiëntie in ruimte van de betrokken structuren is verbazingwekkend.
- Niet elke entry van elke tabel wordt gebruikt, alleen virtuele adressen die daadwerkelijk worden gebruikt en in kaart gebracht door het proces zijn aanwezig in de tabellen.
Het spijt me echt dat ik dit proces niet in meer detail heb uitgelegd, dit is een goed gedocumenteerd proces waar veel mensen hard aan hebben gewerkt om het beter uit te leggen dan ik ooit zou kunnen doen – google het maar.
Kernel vs Gebruiker
Dit is waar het interessant (en magisch als ik mag) wordt.
We begonnen dit artikel met te stellen dat het OS dit alles orkestreert, het doet dat met behulp van de kernel. Zoals gezegd draait de kernel in een geheugensectie die in de GDT voor alle processen alleen als supervised mode is gemapt. Ja, ik weet dat elk proces zijn eigen adresruimte heeft, maar de kernel knipt die adresruimte (meestal de bovenste helft, hangt af van het OS) voor eigen gebruik, niet alleen de adresruimte, maar ook op precies hetzelfde adres voor alle processen. Dit is belangrijk omdat de code van de kernel vast is en elke verwijzing naar variabelen en structuren op dezelfde plaats moet staan voor alle processen. Je kunt de kernel zien als een speciale bibliotheek die voor ieder proces op dezelfde plaats wordt geladen.
Dieper in interrupts
We weten dat de IDT adressen van functies bevat, deze functies worden ISR (Interrupt Service Routine) genoemd, sommige worden uitgevoerd als er zich een hardware gebeurtenis voordoet (een druk op een toets op het toetsenbord) en andere wanneer software de interrupt initieert, bijvoorbeeld om naar de kernel modus over te schakelen.
Windows heeft een cool concept over interrupts en het toekennen van prioriteiten daaraan: Een bijzonder belangrijke interrupt is het tikken van de klok. Met elke tik van de klok is er een interrupt die wordt afgehandeld door zijn ISR. De scheduler van het OS gebruikt deze klok gebeurtenis om te controleren hoeveel tijd een proces draait en of een ander proces al dan niet aan de beurt is. Zoals je kunt begrijpen is deze interrupt super belangrijk en moet zo snel mogelijk worden afgehandeld, maar niet alle ISR’s zijn even belangrijk en dit is waar de prioriteiten tussen de interrupts om de hoek komen kijken. Laten we als voorbeeld de toets indrukken op het toetsenbord nemen en aannemen dat deze de prioriteit 1 heeft, ik heb zojuist een toets op het toetsenbord ingedrukt en zijn ISR wordt uitgevoerd, terwijl de ISR van het toetsenbord wordt uitgevoerd worden alle interrupts met dezelfde prioriteit en lager genegeerd. Tijdens het uitvoeren van de ISR, wordt de klok’s ISR getriggerd met prioriteit 2 (dat is waarom hij niet uitgeschakeld is), er wordt onmiddellijk overgeschakeld naar de klok’s ISR, zodra de klok klaar is gaat de controle terug naar de toetsenbord’s ISR van waar hij gestopt was. Deze interrupts prioriteiten worden IRQLs (Interrupt ReQuest Level) genoemd, naarmate de IRQL van de interrupt hoger is, is zijn prioriteit hoger. De interrupts met de hoogste prioriteit zijn nooit interrupts in het midden, ze lopen tot het einde, altijd. IRQLs is windows specifiek – de IRQL is een nummer tussen 0-31, voor Linux daarentegen bestaat het niet, Linux behandelt elke interrupt met dezelfde prioriteit en schakelt gewoon alle interrupts uit als het echt nodig is dat die specifieke routine niet gestoord wordt. Zoals je kunt zien is het allemaal een kwestie van ontwerp en voorkeuren.
Laten we het allemaal verbinden met onze geliefde User mode . De ISR van die klok gebeurtenis zal worden uitgevoerd ongeacht welke thread er op dat moment draait en zou zelfs kunnen interrumperen naar een andere ISR voor een niet-gerelateerde taak. Dit is een perfect voorbeeld waarom de kernel op hetzelfde adres zit voor alle processen, we willen niet de GDT en de Page Directory (in C3) veranderen elke keer als we een interrupt uitvoeren, want het gebeurt VEEL keer tijdens zelfs maar een enkele functie van een gegeven user mode proces. Er gebeurt veel tussen de regels code die je schrijft wanneer je je gebruikers mode applicatie ontwikkelt (;)).
Een andere manier om naar interrupts te kijken is als externe en onafhankelijke inputs voor ons OS, deze definitie is niet accuraat (niet alle interrupts zijn extern of onafhankelijk) maar het is goed om een punt te maken, een groot deel van de taak van de kernel is om de events die de hele tijd voorkomen van iedere lokatie (invoer apparaten) te begrijpen en van de ene kant om deze event te bedienen en de andere om ervoor te zorgen dat alles correct gecorreleerd is.
Om dit alles duidelijk te maken, laten we beginnen met een eenvoudige gebruikersmodus applicatie die de volgende instructie uitvoert:
0x0000051d push ebp;
Voor elke instructie die de CPU uitvoert, onderzoekt hij eerst het adres van die instructie (in dit geval ‘0x0000051d’) tegen de GDT/LDT met behulp van het code segment register (‘cs’ omdat het een uit te voeren instructie is) om de index te kennen waarnaar hij moet zoeken in de tabel (onthoud dat het segment register de CPU precies vertelt waar hij moet zoeken). Zodra de CPU weet dat de instructie op de uitvoerbare plaats is en wij in de juiste ring (gebruikersmodus/kernelmodus) zitten, gaat hij verder met het uitvoeren van de instructie. In dit geval heeft de ‘push ebp’ instructie niet alleen effect op het register maar ook op de stack van het programma (het duwt de stack de ebp inhoud) dus de CPU controleert ook tegen de GDT/LDT voor het adres in het esp register (het adres van de huidige locatie op de stack, en omdat het de stack locatie is weet de CPU dat hij het stack segment register moet gebruiken om dat te doen) om er zeker van te zijn dat het beschrijfbaar is in die specifieke ring. Merk op dat als deze instructie ook uit geheugen zou lezen, de CPU ook het adres voor leestoegang zou moeten controleren.
Dit is nog niet alles, nadat de CPU alle veiligheidsaspecten heeft gecontroleerd, is het nu zaak om het geheugen te benaderen en te manipuleren, zoals je je herinnert zijn de adressen in hun virtuele formaat. De MMU vertaalt nu elk door de instructie gespecificeerd virtueel geheugen in een fysiek geheugenadres met behulp van het CR3-register dat naar de paginadirectory wijst (die naar de paginatabel wijst) die ons in staat stelt het adres uiteindelijk in een fysiek adres om te zetten. Merk op dat het adres misschien niet in het geheugen is op het moment dat het nodig is, in dat geval genereert het OS een page fault (een uitzondering die een interrupt genereert) en brengt de gegevens naar het fysieke geheugen voor ons en gaat dan verder met de uitvoering (dit is transparant voor de user mode applicatie).
Van gebruiker naar kernel
Elke uitwisseling tussen user mode en kernel mode gebeurt met behulp van de IDT. Vanuit de gebruikersmodus applicatie, zal de instructie ‘int <num>’ de uitvoering overbrengen naar de functie in de IDT op de index num. Wanneer de uitvoering in kernel mode is veranderen veel van de regels, elke thread heeft verschillende stacks voor user en kernel mode, geheugen toegang controles zijn veel gecompliceerder en verplicht, in kernel mode is er weinig dat je niet kunt doen en veel dat je kunt breken.
ASLR en KASLR
Meer wel dan niet is het “slechts” het gebrek aan kennis dat ons verhindert het onmogelijke te bereiken.
ASLR (Address Space Layout Randomization) is een concept dat binnen elk OS anders wordt geïmplementeerd, het concept is om de virtuele adressen van de processen en hun geladen bibliotheken te randomiseren.
Voordat we er in duiken wil ik opmerken dat ik besloten heb om ASLR in dit bericht op te nemen omdat het een mooie manier is om te zien hoe beschermde modus en zijn structuren dit soort mogelijkheden mogelijk maken, ook al is het niet degene die het implementeert of er verantwoordelijk voor is.
Waarom ASLR?
Het waarom is eenvoudig, om aanvallen te voorkomen. Wanneer iemand in staat is om code in een lopend proces te injecteren, kan het niet kennen van de adressen van enkele nuttige functies de aanval doen mislukken.
We hebben al een verschillende adresruimte voor elk proces, dit betekent dat zonder ASLR alle processen dezelfde basisadressen zouden hebben, dit komt omdat wanneer elk proces in zijn eigen virtuele adresruimte zit, we niet op onze hoede hoeven te zijn voor botsingen tussen processen. Wanneer we het programma linken, kiest de linker een vast basisadres waartegen hij het uitvoerbare bestand linkt. Op papier zullen alle uitvoerbare bestanden die door dezelfde linker worden gelinkt met de standaard parameters (het basisadres kan worden geconfigureerd indien nodig) hetzelfde basisadres hebben. Om een voorbeeld te geven: ik heb twee applicaties geschreven, een heet “1.exe” en de tweede “2.exe”, beide zijn verschillende projecten in Visual Studio en toch hebben ze beide hetzelfde basisadres (ik heb exeinfo PE gebruikt om het basisadres in het PE bestand te bekijken):
Niet alleen hebben deze twee executables hetzelfde basisadres ze ondersteunen ook allebei geen ASLR (ik heb het uitgeschakeld):
Je kunt het ook zien in de PE-indeling onder Bestandskenmerken:
Nu laten we beide uitvoerbare bestanden tegelijk uitvoeren en de beide delen hetzelfde basisadres (ik zal vmmap van Sysinternals gebruiken om het basisbeeld te bekijken):
We kunnen zien dat beide processen geen ASLR gebruiken en hetzelfde basisadres van 0x00400000 hebben. Als wij aanvallers waren en toegang hadden tot dit uitvoerbare bestand, hadden we precies kunnen weten welke adressen beschikbaar waren voor dit proces, zodra we een manier hadden gevonden om onszelf te injecteren in de uitvoering ervan. Laten we ASLR inschakelen in ons uitvoerbare bestand 1.exe en de magie ervan bekijken:
Het is veranderd!
KASLR (Kernel ASLR) is hetzelfde als ASLR, alleen werkt het op het niveau van de kernel, wat betekent dat als een aanvaller zich eenmaal in de context van de kernel heeft kunnen injecteren, hij (hopelijk) niet meer kan weten welke adressen welke structuren bevatten (bijvoorbeeld waar de GDT zich in het geheugen bevindt). Een ding dat hier moet worden vermeld is dat ASLR zijn magie werkt bij iedere spawn van een nieuw proces (dat ASLR ondersteunt natuurlijk) terwijl KASLR dit doet bij iedere herstart omdat dit het moment is waarop de kernel wordt “gespawned”.
Hoe werkt ASLR?
Dus hoe werkt het en hoe is het verbonden met protected mode? Degene die verantwoordelijk is voor de uitvoering van ASLR is de loader. Wanneer een proces wordt gestart, is de loader degene die het in het geheugen moet zetten, de relevante structuren moet maken en de thread moet starten. De loader controleert eerst of het uitvoerbare bestand ASLR ondersteunt en zo ja, dan randomiseert hij een basisadres binnen het bereik van de beschikbare adressen (de kernelruimte is bijvoorbeeld niet beschikbaar). Gebaseerd op dat adres initialiseert de loader nu de Page Directory voor dat proces om de gerandomiseerde adresruimte naar de fysieke adresruimte te verwijzen. De flexibiliteit van LDT komt ons ook te hulp daar de loader eenvoudig LDT aanmaakt die overeenkomt met het gerandomiseerde adres met de relevante permissies. Het mooie hier is dat de beschermde modus zich er niet eens van bewust is dat ASLR wordt gebruikt, het is flexibel genoeg om er zich niets van aan te trekken.
Enk interessant implementatie detail is dat in Windows het gerandomiseerde adres voor een specifieke executable vast staat om efficiency redenen. Wat ik daarmee bedoel is dat als we het adres van bijvoorbeeld calc.exe hebben gerandomiseerd, de tweede keer dat het wordt uitgevoerd het basisadres hetzelfde zal zijn. Dus als ik 2 rekenmachines tegelijk open – zullen ze hetzelfde basisadres hebben. Zodra ik beide rekenmachines sluit en ze weer open, zullen ze weer hetzelfde adres hebben, alleen dit zal anders zijn dan het adres van de vorige rekenmachines. Waarom is dit niet efficiënt vraag je je af? Denk aan veel gebruikte DLL’s. Veel processen gebruiken ze en als hun basisadressen verschillend zouden zijn voor elk proces, zou hun code ook verschillend zijn (de code verwijst naar de gegevens met dit basisadres) en als de code verschillend is, moet de DLL in het geheugen worden geladen voor elk proces. In werkelijkheid laadt het OS de images slechts één keer voor alle processen die deze image gebruiken. Dat bespaart ruimte – veel ruimte!
Conclusie
U zou zich nu een beeld moeten kunnen vormen van de kernel en moeten begrijpen hoe alle sleutelstructuren van de x86 architectuur samenwerken tot een groter geheel en ons in staat stellen om mogelijk gevaarlijke toepassingen in gebruikersmodus uit te voeren zonder (of met weinig) angst.