Uno dei concetti più interessanti e comunemente usati nell’architettura x86 è la modalità protetta e il suo supporto in 4 modalità (aka anelli):

È stata un’idea difficile da afferrare e cercherò di spiegarla il più chiaramente possibile in questo post. Tratteremo i seguenti concetti:

  • GDT, LDT, IDT.
  • Traduzione della memoria virtuale.
  • ASLR e Kernel ASLR (KASLR).

Partiamo dalle basi, ogni computer ha almeno (si spera) i seguenti componenti: CPU, Disco e RAM. Ognuno di questi componenti ha un ruolo chiave nel flusso del sistema. La CPU esegue i comandi e le operazioni sulla memoria (RAM), la RAM tiene i dati che stiamo usando e permette un accesso veloce e affidabile ad essi, il disco tiene i dati persistenti di cui abbiamo bisogno per esistere anche dopo il riavvio o lo spegnimento. Parto da questo perché, anche se questo è molto basilare, è importante tenerlo a mente e mentre leggete questo articolo chiedetevi di quale componente stiamo parlando in quel momento.

Il sistema operativo è il software che orchestra il tutto, e anche quello che permette un’interfaccia veloce, conveniente, coerente ed efficiente per accedere a tutte le sue capacità – alcune delle quali sono l’accesso all’hardware, e altre sono per migliorare la comodità e le prestazioni.

Come ogni buon software il sistema operativo lavora a strati, il kernel è il primo strato e – a mio parere – il più importante. Per capire l’importanza del kernel dobbiamo prima capire le sue azioni e le sfide che deve affrontare, quindi vediamo alcune delle sue responsabilità:

  • Gestire le chiamate di sistema (la stessa interfaccia di cui abbiamo parlato).
  • Allocare le risorse (RAM, CPU, e molto altro) ai processi/threads in mano.
  • Sicurare le operazioni eseguite.
  • Intermediare tra l’hardware e il software.

Molte di queste azioni eseguite con il generoso aiuto del processore, nel caso di x86, la modalità protetta è la modalità che ci permette di limitare la potenza (set di istruzioni) del contesto di esecuzione in corso.

Immaginiamo di avere due mondi: il mondo dell’utente e il mondo del supervisore. In qualsiasi momento potete essere solo in uno di questi mondi. Quando sei nel mondo dell’utente vedi il mondo come il supervisore vuole che tu lo veda. Vediamo cosa intendo con questo:

Diciamo che siete un processo. Un processo è un contenitore di uno o più thread. Un thread è un contesto di esecuzione, è l’unità logica di cui vengono eseguite le istruzioni della macchina. Significa che quando il thread sta eseguendo, diciamo, la lettura dall’indirizzo di memoria 0x80808080, si riferisce effettivamente all’indirizzo virtuale 0x80808080 del processo corrente. Come potete immaginare, il contenuto dell’indirizzo sarà diverso tra due processi. Ora, lo spazio di indirizzo virtuale è a livello di processo, il che significa che tutti i thread dello stesso processo hanno lo stesso spazio di indirizzo e possono accedere alla stessa memoria virtuale. Per dare un esempio di risorsa che è a livello di thread usiamo il famoso stack.

Allora ho un thread che esegue il seguente codice:

Il nostro thread esegue la funzione principale che chiamerà la nostra funzione “func”. Diciamo che interrompiamo il thread alla linea 9. la disposizione dello stack sarà la seguente:

  1. variabile_a.
  2. parametro.
  3. indirizzo di ritorno – indirizzo della linea 20.
  4. variabile_b.

Per illustrare:

Nel codice dato creiamo 3 thread per il nostro processo e ognuno di essi stampa il suo id, segmento di stack e puntatore di stack.

Un possibile output di quel programma è:

Come potete vedere tutti i thread hanno lo stesso segmento di stack perché hanno lo stesso spazio di indirizzo virtuale. Il puntatore allo stack per ognuno è diverso perché ognuno ha il proprio stack in cui memorizzare i propri valori.

Nota a margine sul segmento dello stack – spiegherò di più sui registri di segmento nella sezione GDT/LDT – per ora prendete la mia parola per questo.

Perché questo è importante? In qualsiasi momento, il processore può congelare il thread e dare il controllo a qualsiasi altro thread voglia. Come parte del kernel, lo scheduler è quello che alloca la CPU ai thread attualmente esistenti (e “pronti”). Affinché i thread siano in grado di funzionare in modo affidabile ed efficiente, è essenziale che ciascuno abbia il proprio stack in cui può salvare i suoi valori rilevanti (variabili locali e indirizzi di ritorno, per esempio).

Per gestire i suoi thread, il sistema operativo mantiene una struttura speciale per ogni thread chiamata TCB (Thread Control Block), in tale struttura salva – tra le altre cose – il contesto di quel thread e il suo stato (in esecuzione / pronto / ecc …). Il contesto contiene – di nuovo – tra le altre cose, i valori dei registri della CPU:

  • EBP -> Indirizzo di base dello stack, ogni funzione usa questo indirizzo come indirizzo di base da cui ha compensato per accedere alle variabili locali e ai parametri.
  • ESP -> Il puntatore corrente all’ultimo valore (primo a pop) sullo stack.
  • Registri di uso generale -> EAX, EBX, ecc…
  • Registro delle flags.
  • C3 -> contiene la posizione della directory della pagina (sarà discussa più avanti).
  • EIP – La prossima istruzione da eseguire.

Oltre ai threads il sistema operativo ha bisogno di tenere traccia di molte altre cose, inclusi i processi. Per i processi il sistema operativo salva la struttura PCB (Process Control Block), abbiamo detto che per ogni processo c’è uno spazio di indirizzi isolato. Per ora supponiamo che ci sia una tabella che mappa ogni indirizzo virtuale ad uno fisico e che la tabella sia salvata nel PCB, il sistema operativo è responsabile dell’aggiornamento di quella tabella e la tiene aggiornata allo stato corretto della memoria fisica. Ogni volta che lo scheduler passa l’esecuzione a un determinato thread, la tabella salvata per il processo proprietario di quel thread viene applicata alla CPU in modo che sia in grado di tradurre correttamente gli indirizzi virtuali.

Questo è abbastanza per i concetti, cerchiamo di capire come viene fatto in realtà. Per questo guardiamo il mondo dal punto di vista del processore:

Global Descriptor Table

Sappiamo tutti che il processore ha registri che lo aiutano a fare calcoli, alcuni registri più di altri (;)). Per progettazione l’x86 supporta più modalità ma le più importanti sono utente e supervisionato, la CPU ha un registro speciale chiamato gdtr (Global Descriptor Table Register) che contiene l’indirizzo di una tabella molto importante. quella tabella mappa ogni indirizzo virtuale alla sua modalità corrispondente del processore, contiene anche i permessi per quell’indirizzo (READ | WRITE | EXECUTE). ovviamente quel registro può essere cambiato solo dalla modalità supervisor. Come parte dell’esecuzione del processore, controlla quale istruzione eseguire dopo (e a quale indirizzo si trova), controlla quell’indirizzo con il GDT e in questo modo sa se è un’istruzione valida in base alla modalità desiderata (abbina la modalità corrente della CPU alla modalità nel GDT) e ai permessi (se non eseguibile – non valida). Un esempio è ‘lgdtr’ l’istruzione che carica il valore nel registro gdtr e può essere eseguita solo dalla modalità supervisionata come detto. Il punto chiave da sottolineare qui è che qualsiasi protezione sulle operazioni di memoria (esecuzione di istruzioni / scrittura in posizioni non valide / lettura da posizioni non valide) è fatta dal GDT e LDT (in arrivo) a livello di processore utilizzando queste strutture che sono state costruite dal SO.

Questo è l’aspetto del contenuto di una voce in GDT / LDT:

http://wiki.osdev.org/Global_Descriptor_Table

Come puoi vedere ha la gamma di indirizzi a cui la voce si riferisce, e i suoi attributi (permessi) come ci si aspetta.

Tabella dei descrittori locali

Tutto quello che abbiamo detto sulla GDT è vero anche per la LDT con piccole (ma grandi) differenze. Come suggerisce il nome, la GDT è applicata globalmente al sistema mentre la LDT è locale, cosa intendo per globalmente e localmente? Il GDT tiene traccia dei permessi per tutti i processi, per ogni thread e non cambia tra i cambi di contesto, l’LDT invece sì. Ha solo senso che se ogni processo ha il proprio spazio di indirizzamento, è possibile che per un processo l’indirizzo 0x10000000 sia eseguibile e per un altro sia di sola lettura/scrittura. Questo è particolarmente vero con ASLR attivo (sarà discusso più avanti). L’LDT è responsabile di mantenere i permessi che distinguono ogni processo.

Una cosa da notare è che tutto ciò che è stato detto è lo scopo della struttura, ma in realtà alcuni sistemi operativi potrebbero usare o non usare affatto alcune delle strutture. per esempio è possibile usare solo il GDT e cambiarlo tra i cambi di contesto e non usare mai l’LDT. Fa tutto parte della progettazione del sistema operativo e dei compromessi. Le voci di quella tabella sono simili a quelle del GDT.

Selettori

Come fa il processore a sapere dove guardare nel GDT o LDT quando esegue una specifica istruzione? Il processore ha registri speciali chiamati registri di segmento:

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').

Ogni registro è lungo 16 bit e la sua struttura è la seguente:

http://www.c-jump.com/CIS77/ASM/Memory/M77_0290_segment_registers_protected.htm

Quindi abbiamo l’indice del GDT/LDT, abbiamo anche il bit che dice se è il LDT o il GDT, e quale modalità deve essere (RPL 0 è supervisore, 4 è utente).

Interrupt Descriptor Table

Oltre al GDT e LDT abbiamo anche l’IDT (Interrupt Descriptor Table), l’IDT è semplicemente una tabella che contiene gli indirizzi di funzioni molto importanti, alcune di esse appartengono al sistema operativo, altre ai driver e ai dispositivi fisici collegati al PC. Come il gdtr abbiamo l’idtr che, come probabilmente avrete capito, è il registro che contiene l’indirizzo dell’IDT. Cosa rende l’IDT così speciale? Quando avviamo un interrupt, la CPU passa automaticamente alla modalità supervisionata, il che significa che ogni funzione all’interno dell’IDT è in esecuzione in modalità supervisionata. Ogni thread di ogni modalità può innescare un interrupt emettendo l’istruzione ‘int’ seguita da un numero che dice alla CPU su quale indice risiede la funzione di destinazione. Detto questo, è ora ovvio che ogni funzione all’interno dell’IDT è un potenziale gateway in modalità supervisionata.

Quindi sappiamo che abbiamo il GDT/LDT che dice alla CPU i permessi per ogni indirizzo virtuale e abbiamo l’IDT che punta le funzioni ‘gateway’ al nostro amato kernel (che ovviamente risiede nella sezione supervisionata della memoria). Come si comportano queste strutture in un sistema in funzione?

Memoria virtuale

Prima di poter capire come tutto questo gioca insieme dobbiamo coprire un altro concetto – la memoria virtuale. Ricordate quando ho detto che c’è una tabella che mappa ogni indirizzo di memoria virtuale al suo indirizzo fisico? In realtà è un po’ più complicato di così. Per prima cosa non possiamo semplicemente mappare ogni indirizzo virtuale in quanto richiederebbe più spazio di quello che abbiamo effettivamente, e mettendo da parte la necessità di essere efficienti, il sistema operativo può anche scambiare pagine di memoria nel disco (per efficienza e prestazioni), è possibile che la pagina di memoria dell’indirizzo virtuale necessario non sia in memoria al momento, quindi oltre a tradurre l’indirizzo virtuale a quello fisico abbiamo anche bisogno di salvare se la memoria è in RAM e se no, dove si trova (potrebbe esserci più di un file di pagina). La MMU (Memory Management Unit) è il componente responsabile di tradurre la memoria virtuale in una fisica.

Una cosa davvero importante da capire è che ogni istruzione in ogni modalità passa attraverso il processo di traduzione dell’indirizzo virtuale, anche il codice in modalità supervisionata. Una volta che la CPU in modalità protetta, ogni istruzione che esegue utilizza l’indirizzo virtuale – mai fisico (ci sono alcuni trucchi che l’indirizzo virtuale attuale si tradurrà sempre nella stessa esatta memoria virtuale, ma questo è al di fuori dello scopo di questo post).

Quindi una volta in modalità protetta, come la CPU sa dove guardare quando ha bisogno di tradurre l’indirizzo virtuale? la risposta è il registro CR3, questo registro contiene l’indirizzo della struttura che contiene le informazioni richieste – Page directory. Il suo valore cambia con il processo in corso (di nuovo, spazio di indirizzi virtuali diverso).

Come è fatta questa Page Directory? Quando si tratta di efficienza abbiamo bisogno di essere in grado di interrogare questa “tabella” il più velocemente possibile, abbiamo anche bisogno che sia il più piccolo possibile perché questa tabella sarà creata per ogni processo. La soluzione a questo problema è niente meno che brillante. La migliore immagine che ho trovato per illustrare il processo di traduzione è questa (da wikipedia):

La MMU ha 2 input, l’indirizzo virtuale da tradurre e il CR3 (indirizzo della directory della pagina attualmente rilevante). La specifica x86 taglia l’indirizzo virtuale in 3 pezzi:

  • 10 bit numero – indice della directory di pagina.
  • 10 bit numero – indice della tabella di pagina.
  • 12 bit numero – offset dell’indirizzo fisico stesso.

Quindi il processore prende il primo numero di 10 bit e lo usa come indice per la tabella delle pagine, per ogni voce nella tabella delle pagine abbiamo la tabella delle pagine, che poi il processore usa il successivo numero di 10 bit come indice. Ogni voce della tabella di directory punta alla pagina di memoria 4K che poi l’ultimo offset di 12bit dall’indirizzo virtuale è usato per puntare alla posizione esatta nel fisico. La genialità in questa soluzione è:

  • La flessibilità che ogni indirizzo virtuale individua un indirizzo fisico completamente indipendente.
  • L’efficienza nello spazio delle strutture coinvolte è sorprendente.
  • Non tutte le voci di ogni tabella sono usate, solo gli indirizzi virtuali che sono effettivamente usati e mappati dal processo esistono nelle tabelle.

Sono veramente dispiaciuto per non aver spiegato questo processo in modo più dettagliato, questo è un processo ben documentato che molte persone hanno lavorato duramente per spiegarlo meglio di quanto potrei mai fare io – cercatelo su Google.

Kernel vs Utente

Ecco dove diventa interessante (e magico se posso).

Abbiamo iniziato questo articolo affermando che il SO orchestra tutto, lo fa usando il kernel. Come già detto il kernel è in esecuzione in una sezione di memoria che è mappata come modalità supervisionata solo nel GDT per tutti i processi. Sì, so che ogni processo ha il proprio spazio di indirizzi, ma il kernel sta tagliando quello spazio di indirizzi (di solito la metà superiore, dipende dal sistema operativo) per il suo uso personale, non solo tagliando lo spazio di indirizzi ma anche allo stesso indirizzo per tutti i processi. Questo è importante perché il codice del kernel è fisso e ogni riferimento a variabili e strutture deve essere nella stessa posizione per tutti i processi. Si può considerare il kernel come una libreria speciale caricata in ogni processo nella stessa posizione.

Più a fondo negli interrupt

Sappiamo che l’IDT contiene indirizzi di funzioni, queste funzioni chiamate ISR (Interrupt Service Routine), alcune vengono eseguite quando si verifica un evento hardware (pressione di un tasto sulla tastiera) e altre quando il software avvia l’interrupt, per esempio per passare alla modalità kernel.

Windows ha un bel concetto sugli interrupt e sulla loro priorità: Un interrupt particolarmente importante è il ticchettio dell’orologio. Con ogni ticchettio dell’orologio c’è un interrupt che viene gestito dal suo ISR. Lo scheduler del sistema operativo usa questo evento dell’orologio per controllare quanto tempo ogni processo è in esecuzione e se è o meno il turno di un altro. Come potete capire questo interrupt è super importante e deve essere servito non appena accade, non tutti gli ISR hanno la stessa importanza ed è qui che entrano in gioco le priorità tra gli interrupt. Prendiamo ad esempio la pressione di un tasto sulla tastiera e supponiamo che abbia la priorità 1, ho appena premuto un tasto sulla tastiera e il suo ISR è in esecuzione, mentre si esegue l’ISR della tastiera tutti gli interrupt della stessa priorità o inferiori vengono ignorati. Durante l’esecuzione dell’ISR, l’ISR dell’orologio viene attivato con priorità 2 (che è il motivo per cui non è stato disabilitato), il passaggio immediato si verifica all’ISR dell’orologio, una volta che l’orologio termina restituisce il controllo all’ISR della tastiera da dove si è fermato. Queste priorità di interrupt chiamate IRQL (Interrupt ReQuest Level), come l’IRQL dell’interrupt sale la sua priorità è maggiore. Gli interrupt con la priorità più alta non sono mai interrupt nel mezzo, vengono eseguiti fino alla fine, sempre. IRQL è specifico di Windows – l’IRQL è un numero tra 0-31, per Linux, invece, non esiste, Linux gestisce ogni interrupt con la stessa priorità e semplicemente disabilita tutti gli interrupt quando ha davvero bisogno che quella specifica routine non sia disturbata. Come potete vedere è tutta una questione di design e preferenze.

Colleghiamo il tutto al nostro amato User mode . L’ISR di quell’evento di clock sta per essere eseguito indipendentemente dal thread attualmente in esecuzione e potrebbe anche interrompere ad un altro ISR per un compito non correlato. questo è un perfetto esempio del perché il kernel è allo stesso indirizzo per tutti i processi che non vogliamo cambiare il GDT e il Page Directory (in C3) ogni volta che eseguiamo l’interrupt come accade MOLTE volte durante anche una singola funzione di qualsiasi processo in modalità utente. Un sacco di cose stanno accadendo tra quelle righe di codice che scrivete quando sviluppate la vostra applicazione in modalità utente (;)).

Un altro modo di guardare agli interrupt è come input esterni e indipendenti per il nostro sistema operativo, questa definizione non è accurata (non tutti gli interrupt sono esterni o indipendenti) ma è buona per fare un punto, gran parte del lavoro del kernel è quello di dare un senso agli eventi che si verificano tutto il tempo da ogni luogo (dispositivi di input) e da un lato per servire quegli eventi e l’altro per assicurarsi che tutto sia correlato correttamente.

Per dare un senso a tutto ciò, iniziamo con una semplice applicazione in modalità utente che esegue la seguente istruzione:

0x0000051d push ebp;

Per ogni istruzione che la CPU sta eseguendo, prima esamina l’indirizzo di quell’istruzione (in questo caso ‘0x0000051d’) rispetto al GDT/LDT usando il registro del segmento di codice (‘cs’ perché è l’istruzione da eseguire) per conoscere l’indice da cercare nella tabella (ricordate che il registro del segmento dice alla CPU esattamente dove guardare). Una volta che la CPU sa che l’istruzione è nella posizione eseguibile e che siamo nel ring giusto (modalità utente/modalità kernel) ora continua ad eseguire l’istruzione. In questo caso l’istruzione ‘push ebp’ non agisce solo sul registro ma anche sullo stack del programma (spinge sullo stack il contenuto di ebp) quindi la CPU controlla anche il GDT/LDT per l’indirizzo all’interno del registro esp (l’indirizzo della posizione corrente sullo stack, e poiché è la posizione dello stack la CPU sa di usare il registro del segmento dello stack per farlo) per assicurarsi che sia scrivibile in quello specifico anello. Notate che se questa istruzione fosse anche in lettura dalla memoria la CPU avrebbe controllato anche il relativo indirizzo per l’accesso in lettura.

Questo non è tutto, dopo che la CPU ha controllato tutti gli aspetti di sicurezza è ora necessario accedere e manipolare la memoria, come si ricorda gli indirizzi sono nel loro formato virtuale. La MMU ora traduce ogni memoria virtuale specificata dall’istruzione in un indirizzo di memoria fisica utilizzando il registro CR3 che punta al page directory (che punta alla tabella delle pagine) che ci permette di tradurre eventualmente l’indirizzo in quello fisico. Si noti che l’indirizzo potrebbe non essere in memoria al momento del bisogno, in questo caso il sistema operativo genererà un page fault (un’eccezione che genera un interrupt) e porterà i dati nella memoria fisica per noi e poi continuerà l’esecuzione (questo è trasparente per l’applicazione in modalità utente).

Dall’utente al kernel

Ogni scambio tra la modalità utente alla modalità kernel avviene utilizzando l’IDT. Dall’applicazione in modalità utente, l’istruzione ‘int <num>’ trasferisce l’esecuzione alla funzione nell’IDT all’indice num. Quando l’esecuzione è in modalità kernel molte regole cambiano, ogni thread ha stack diversi per la modalità utente e kernel, i controlli di accesso alla memoria sono molto più complicati e obbligatori, in modalità kernel c’è molto poco che non si può fare e molto che si può rompere.

ASLR e KASLR

il più delle volte è “solo” la mancanza di conoscenza che ci impedisce di raggiungere l’impossibile.

ASLR (Address Space Layout Randomization) è un concetto che viene implementato in modo diverso all’interno di ogni OS, il concetto è quello di randomizzare gli indirizzi virtuali dei processi e delle loro librerie caricate.

Prima di tuffarci volevo notare che ho deciso di includere ASLR in questo post perché è un bel modo per vedere come la modalità protetta e le sue strutture hanno abilitato questo tipo di capacità anche se non è quella che la implementa o ne è responsabile.

Perché ASLR?

Il perché è facile, per prevenire gli attacchi. Quando qualcuno è in grado di iniettare codice in un processo in esecuzione, non conoscere gli indirizzi di alcune funzioni utili è ciò che può far fallire l’attacco.

Abbiamo già diversi spazi di indirizzo per ogni processo, questo significa che senza ASLR tutti i processi avrebbero gli stessi indirizzi di base, questo perché quando ogni processo nel proprio spazio di indirizzo virtuale non dobbiamo diffidare delle collisioni tra processi. Quando colleghiamo il programma, il linker sceglie un indirizzo di base fisso su cui collegare l’eseguibile. Sulla carta tutti i file eseguibili collegati dallo stesso linker con i parametri di default (l’indirizzo di base può essere configurato se necessario) avranno lo stesso indirizzo di base. Per fare un esempio ho scritto due applicazioni una chiamata “1.exe” e la seconda “2.exe”, entrambi sono progetti diversi in Visual Studio eppure entrambi hanno lo stesso indirizzo di base (ho usato exeinfo PE per vedere l’indirizzo di base nel file PE):

Non solo questi due eseguibili hanno lo stesso indirizzo base ma entrambi non supportano ASLR (l’ho disabilitato):

Lo puoi vedere anche incluso nel formato PE sotto Caratteristiche file:

Ora eseguiamo entrambi gli eseguibili allo stesso tempo ed entrambi condividono lo stesso indirizzo di base (userò vmmap di Sysinternals per visualizzare l’immagine di base):

Possiamo vedere che entrambi i processi non usano ASLR e hanno lo stesso indirizzo base di 0x00400000. Se fossimo degli attaccanti e avessimo accesso a questo eseguibile, avremmo potuto sapere esattamente quali indirizzi saranno disponibili per questo processo una volta trovato via per iniettarci nella sua esecuzione. abilitiamo ASLR nel nostro eseguibile 1.exe e vediamo la sua magia:

È cambiato!

KASLR (Kernel ASLR) è la stessa cosa di ASLR, solo che lavora a livello del kernel, il che significa che una volta che un attaccante è stato in grado di iniettarsi nel contesto del kernel non potrà (si spera) sapere quali indirizzi contengono quali strutture (per esempio dove si trova il GDT in memoria). Una cosa da menzionare qui è che ASLR fa la sua magia ad ogni spawn di un nuovo processo (che supporta ASLR, ovviamente) mentre KASLR la fa ad ogni riavvio perché questo è quando il kernel viene “spawnato”.

Come funziona ASLR?

Come funziona e come è collegato alla modalità protetta? Il responsabile dell’implementazione di ASLR è il caricatore. Quando un processo viene lanciato, il caricatore è quello che deve metterlo in memoria, creare le relative strutture e lanciare il suo thread. Il caricatore prima controlla se l’eseguibile supporta l’ASLR e, in caso affermativo, randomizza qualche indirizzo base all’interno della gamma di indirizzi disponibili (lo spazio del kernel per esempio non è disponibile, ovviamente). Sulla base di quell’indirizzo il caricatore ora inizializza la Page Directory per quel processo per puntare lo spazio di indirizzi randomizzato a quello fisico. La flessibilità di LDT viene anche in nostro soccorso, poiché il caricatore crea semplicemente LDT che corrisponde all’indirizzo randomizzato con i relativi permessi. La bellezza qui è che la modalità protetta non è nemmeno consapevole che ASLR è in uso, è abbastanza flessibile da non preoccuparsene.

Alcuni interessanti dettagli di implementazione sono che in Windows l’indirizzo randomizzato per specifici eseguibili è fisso per ragioni di efficienza. Quello che voglio dire è che se abbiamo randomizzato l’indirizzo per, diciamo, calc.exe, la seconda volta che viene eseguito l’indirizzo base sarà lo stesso. Quindi se apro 2 calcolatrici allo stesso tempo – avranno lo stesso indirizzo di base. Quando chiuderò entrambe le calcolatrici e le riaprirò, avranno di nuovo lo stesso indirizzo, solo che questo sarà diverso da quello delle calcolatrici precedenti. Perché non è efficiente, vi chiederete? Pensate alle DLL di uso comune. Molti processi le usano e se i loro indirizzi di base fossero diversi per ogni istanza di processo, anche il loro codice sarebbe diverso (il codice fa riferimento ai dati usando questo indirizzo di base) e se il codice è diverso la DLL dovrà essere caricata in memoria per ogni processo. In realtà il sistema operativo carica le immagini solo una volta per tutti i processi che usano questa immagine. Si risparmia spazio – molto spazio!

Conclusione

A questo punto dovreste essere in grado di immaginare il kernel al lavoro e capire come tutte le strutture chiave dell’architettura x86 giocano insieme in un quadro più grande e ci permettono di eseguire applicazioni possibilmente pericolose in modalità utente senza (o con poca) paura.

Articles

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.