L’un des concepts les plus intéressants et les plus utilisés dans l’architecture x86 est le mode protégé et son support en 4 modes(aka anneaux):

C’était une idée difficile à saisir et je vais essayer de l’expliquer aussi clairement que possible dans ce post. Nous couvrirons les concepts suivants:

  • GDT, LDT, IDT.
  • Translation de mémoire virtuelle.
  • ASLR et Kernel ASLR (KASLR).

Débutons par les bases, tout ordinateur ont au moins (espérons-le) les composants suivants : CPU, Disque et RAM. Chacun de ces composants tient un rôle clé dans le flux du système. Le CPU exécute les commandes et les opérations sur la mémoire (RAM), la RAM contient les données que nous utilisons et permet un accès rapide et fiable à celles-ci, le disque contient les données persistantes dont nous avons besoin pour exister même après un redémarrage ou un arrêt. Je commence par là car même si c’est le très basique, son important est de garder cela en tête et au fur et à mesure que vous lisez cet article, demandez-vous de quel composant nous parlons à cet instant.

Le système d’exploitation est le logiciel qui orchestre tout cela, et aussi celui qui permet une interface rapide, pratique, cohérente et efficace pour accéder à toutes ses capacités – dont certaines sont l’accès à ce matériel, et d’autres sont pour améliorer la commodité et la performance.

Comme tout bon logiciel, le système d’exploitation fonctionne en couches, le noyau est la première couche et – à mon avis – la plus importante. Pour que nous comprenions l’importance du noyau, nous devons d’abord comprendre ses actions et les défis auxquels il est confronté, alors examinons certaines de ses responsabilités :

  • Gérer les appels système (la même interface dont nous avons parlé).
  • Allocation des ressources (RAM, CPU, et bien plus encore) aux processus/threads en cours.
  • Sécuriser les opérations effectuées.
  • Intermédiaire entre le matériel et le logiciel.

Plusieurs de ces actions réalisées avec l’aide généreuse du processeur, dans le cas du x86, le mode protégé est le mode qui nous permet de limiter la puissance (jeu d’instructions) du contexte d’exécution en cours.

Supposons que nous avons deux mondes – le monde de l’utilisateur et le monde du superviseur. A tout moment, vous ne pouvez être que dans l’un de ces mondes. Lorsque vous êtes dans le monde de l’utilisateur, vous voyez le monde tel que le superviseur veut que vous le voyiez. Voyons ce que je veux dire par là:

Disons que vous êtes un processus. Un processus est un conteneur d’un ou plusieurs threads. Un thread est un contexte d’exécution, c’est l’unité logique dont les instructions de la machine sont exécutées. Cela signifie que lorsque le thread exécute, disons, la lecture de l’adresse mémoire 0x80808080, il se réfère à l’adresse virtuelle 0x80808080 du processus actuel. Comme vous pouvez le deviner, le contenu de l’adresse va être différent entre deux processus. L’espace d’adressage virtuel se situe au niveau du processus, ce qui signifie que tous les threads d’un même processus ont le même espace d’adressage et peuvent accéder à la même mémoire virtuelle. Pour donner un exemple de ressource qui est au niveau du thread, laissons utiliser la fameuse pile.

Donc j’ai un thread qui exécute le code suivant:

Notre thread exécute la fonction main qui va appeler notre fonction « func ». Disons que nous cassons le thread à la ligne 9. la disposition de la pile sera la suivante :

  1. variable_a.
  2. paramètre.
  3. adresse de retour – adresse de la ligne 20.
  4. variable_b.

Pour illustrer:

Dans le code donné, nous créons 3 threads pour notre processus et chacun d’eux imprime son id, son segment de pile et son pointeur de pile.

Une sortie possible de ce programme est:

Comme vous pouvez le voir tous les threads avaient le même segment de pile car ils ont le même espace d’adresse virtuel. le pointeur de pile pour chacun est différent parce que chacun a sa propre pile pour y stocker ses valeurs.

Note annexe sur le segment de pile – j’expliquerai plus en détail les registres de segment dans la section GDT/LDT – pour l’instant, croyez-moi sur parole.

Alors pourquoi est-ce important ? A tout moment, le processeur peut geler le thread et donner le contrôle à tout autre thread qu’il veut. En tant que partie du noyau, l’ordonnanceur est celui qui alloue le processeur aux threads actuellement existants (et « prêts »). Pour que les threads puissent fonctionner de manière fiable et efficace, il est essentiel que chacun d’entre eux ait sa propre pile dans laquelle il puisse sauvegarder ses valeurs pertinentes (variables locales et adresses de retour par exemple).

Pour gérer ses threads, le système d’exploitation conserve une structure spéciale pour chaque thread appelée TCB (Thread Control Block), dans cette structure il sauvegarde – entre autres – le contexte de ce thread et son état (en cours d’exécution / prêt / etc…). Le contexte contient – encore une fois – entre autres choses, les valeurs des registres du CPU:

  • EBP -> Adresse de base de la pile, chaque fonction utilise cette adresse comme adresse de base à partir de laquelle elle se décale pour accéder aux variables locales et aux paramètres.
  • ESP -> Le pointeur actuel vers la dernière valeur (première à pop) sur la pile.
  • Registres à usage général -> EAX, EBX, etc…
  • Registre des drapeaux.
  • C3 -> contient l’emplacement du répertoire de la page (sera discuté plus tard).
  • EIP – La prochaine instruction à exécuter.

En dehors des threads, le système d’exploitation doit garder la trace après beaucoup d’autres choses, y compris les processus. Pour les processus l’OS sauve la structure PCB (Process Control Block), nous avons dit que pour chaque processus il y a un espace d’adresse isolé. Pour l’instant, supposons qu’il existe une table qui fait correspondre chaque adresse virtuelle à une adresse physique et que cette table est sauvegardée dans le PCB. Le système d’exploitation est responsable de la mise à jour de cette table et de son actualisation en fonction de l’état correct de la mémoire physique. Chaque fois que l’ordonnanceur bascule l’exécution à un thread donné, la table qui a été sauvegardée pour le processus propriétaire de ce thread est appliquée au CPU afin qu’il soit capable de traduire correctement les adresses virtuelles.

C’est assez pour les concepts, comprenons comment cela se fait réellement. Pour cela regardons le monde du point de vue du processeur:

Table des descripteurs globaux

Nous savons tous que le processeur a des registres qui l’aident à faire des calculs, certains registres plus que d’autres ( ;)). Par conception, le x86 supporte plusieurs modes, mais les plus importants sont le mode utilisateur et le mode supervisé. Le processeur possède un registre spécial appelé gdtr (Global Descriptor Table Register) qui contient l’adresse d’une table très importante. cette table associe chaque adresse virtuelle au mode correspondant du processeur, elle contient également les permissions pour cette adresse (READ | WRITE | EXECUTE). évidemment, ce registre ne peut être modifié que depuis le mode superviseur. Dans le cadre de l’exécution du processeur, il vérifie quelle instruction doit être exécutée ensuite (et à quelle adresse elle se trouve), il vérifie cette adresse par rapport au GDT et de cette façon, il sait s’il s’agit d’une instruction valide en fonction de son mode souhaité (faire correspondre le mode actuel du processeur au mode du GDT) et des permissions (si elle n’est pas exécutable – invalide). Un exemple est ‘lgdtr’, l’instruction qui charge une valeur dans le registre gdtr et qui ne peut être exécutée qu’en mode supervisé comme indiqué. Le point clé à souligner ici est que toute protection sur les opérations de mémoire (exécution de l’instruction / écriture à l’emplacement invalide / lecture de l’emplacement invalide) est faite par le GDT et le LDT (à venir) au niveau du processeur en utilisant ces structures qui ont été construites par l’OS.

Voici à quoi ressemble le contenu d’une entrée dans GDT / LDT :

http://wiki.osdev.org/Global_Descriptor_Table

Comme vous pouvez le voir, il y a la plage d’adresses à laquelle l’entrée se rapporte, et ses attributs (permissions) comme vous vous y attendez.

Table des descripteurs locaux

Tout ce que nous avons dit sur la GDT est également vrai pour la LDT avec une petite (mais grande) différence. Comme son nom l’indique, la GDT est appliquée globalement sur le système alors que la LDT l’est localement, qu’est-ce que j’entends par globalement et localement ? Le GDT garde la trace des permissions pour tous les processus, pour chaque thread et il ne change pas entre les changements de contexte, le LDT par contre le fait. Il est logique que si chaque processus a son propre espace d’adressage, il est possible que pour un processus l’adresse 0x10000000 soit exécutable et pour un autre en lecture/écriture seulement. Ceci est particulièrement vrai avec ASLR activé (nous y reviendrons plus tard). Le LDT est responsable de garder les permissions qui distinguent chaque processus.

Une chose à noter est que tout ce qui a été dit est le but de la structure, mais en réalité certains OS peuvent ou non utiliser une partie de la structure du tout. par exemple il est possible d’utiliser seulement le GDT et de le changer entre les changements de contexte et de ne jamais utiliser le LDT. Tout cela fait partie de la conception du système d’exploitation et des compromis à faire. Les entrées de cette table ressemblent à celle de la GDT.

Selectors

Comment le processeur sait où regarder dans la GDT ou la LDT quand il exécute une instruction spécifique ? Le processeur a des registres spéciaux qui sont appelés registres de segment:

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

Chaque registre a une longueur de 16 bits et sa structure est la suivante :

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

Donc nous avons l’index du GDT/LDT, nous avons aussi le bit qui dit si c’est le LDT ou le GDT, et quel mode il doit être (RPL 0 est superviseur, 4 est utilisateur).

Table des descripteurs d’interruption

A côté du GDT et du LDT, nous avons aussi l’IDT (Interrupt Descriptor Table), l’IDT est simplement une table qui contient les adresses d’une fonction très importante, certaines d’entre elles appartiennent à l’OS, d’autres aux pilotes et aux périphériques physiques connectés au PC. Comme le gdtr, nous avons idtr qui, comme vous l’avez probablement deviné, est le registre contenant l’adresse de l’IDT. Qu’est-ce qui rend l’IDT si spécial ? Lorsque nous déclenchons une interruption, le CPU passe automatiquement en mode supervisé, ce qui signifie que chaque fonction de l’IDT fonctionne en mode supervisé. Chaque thread de chaque mode peut déclencher une interruption en émettant l’instruction ‘int’ suivie d’un nombre qui indique au CPU l’index de la fonction cible. Ceci étant dit, il est maintenant évident que chaque fonction à l’intérieur de l’IDT est une passerelle potentielle vers le mode supervisé.

Donc, nous savons que nous avons le GDT/LDT qui indique au CPU les permissions pour chaque adresse virtuelle et nous avons l’IDT qui pointe les fonctions ‘passerelles’ vers notre cher noyau (qui réside évidemment à l’intérieur de la section supervisée de la mémoire). Comment ces structures se comportent-elles dans un système en fonctionnement ?

Mémoire virtuelle

Avant de comprendre comment tout cela joue ensemble, nous devons couvrir un autre concept – la mémoire virtuelle. Vous vous souvenez quand j’ai dit qu’il y a une table qui fait correspondre chaque adresse de mémoire virtuelle à son adresse physique ? C’est en fait un peu plus compliqué que cela. Tout d’abord, nous ne pouvons pas simplement faire correspondre chaque adresse virtuelle car cela prendrait plus d’espace que ce dont nous disposons réellement, et en mettant de côté le besoin d’être efficace, le système d’exploitation peut également échanger des pages de mémoire sur le disque (pour l’efficacité et la performance), il est possible que la page de mémoire de l’adresse virtuelle requise ne soit pas en mémoire en ce moment, donc en plus de traduire l’adresse virtuelle en adresse physique, nous devons également enregistrer si la mémoire est en RAM et si non, où est-elle (il pourrait y avoir plus d’une page de fichier). Le MMU (Memory Management Unit) est le composant responsable de traduire la mémoire virtuelle en une mémoire physique.

Une chose vraiment importante à comprendre est que chaque instruction dans chaque mode passe par le processus de traduction d’adresse virtuelle, même le code en mode supervisé. Une fois que le CPU en mode protégé, chaque instruction qu’il exécute utilise l’adresse virtuelle – jamais physique (il y a quelques astuces que l’adresse virtuelle réelle se traduira toujours dans la même mémoire virtuelle exacte, mais cela sort du cadre de ce post).

Donc une fois en mode protégé, comment le CPU sait où regarder quand il a besoin de traduire l’adresse virtuelle ? la réponse est le registre CR3, ce registre détient l’adresse de la structure qui contient l’information requise – répertoire de page. Sa valeur change avec le processus en cours d’exécution (encore une fois, espace d’adresse virtuelle différent).

Alors, à quoi ressemble ce répertoire de pages ? En ce qui concerne l’efficacité, nous devons être en mesure d’interroger cette « table » aussi rapidement que possible, nous avons également besoin qu’elle soit aussi petite que possible car cette table va être créée pour chaque processus. La solution à ce problème n’est rien moins que brillante. La meilleure image que j’ai pu trouver pour illustrer le processus de traduction est celle-ci (de wikipedia):

La MMU ont 2 entrées, l’adresse virtuelle à traduire et le CR3 (adresse du répertoire de la page actuellement pertinente). La spécification x86 découpe l’adresse virtuelle en 3 morceaux:

  • numéro de 10 bits – indice du répertoire de pages.
  • numéro de 10 bits – indice de la table des pages.
  • numéro de 12 bits – décalage vers l’adresse physique elle-même.

Donc, le processeur prend le premier numéro de 10 bits et l’utilise comme index du répertoire des pages, pour chaque entrée du répertoire des pages, nous avons la table des pages, qu’ensuite le processeur utilise le prochain numéro de 10 bits comme index. Chaque entrée de la table de répertoire pointe vers une page de mémoire limite de 4K, puis le dernier décalage de 12 bits de l’adresse virtuelle est utilisé pour pointer l’emplacement exact dans la mémoire physique. La brillance de cette solution est :

  • La flexibilité que chaque adresse virtuelle localise à une physique complètement non liée.
  • L’efficacité dans l’espace des structures impliquées est étonnante.
  • Pas toutes les entrées de chaque table sont utilisées, seules les adresses virtuelles qui sont réellement utilisées et mappées par le processus existent dans les tables.

Je suis vraiment désolé de ne pas expliquer ce processus plus en détail, c’est un processus bien documenté que de nombreuses personnes ont travaillé dur pour expliquer mieux que je ne pourrais jamais le faire – googlez-le.

Noyau vs Utilisateur

C’est là que ça devient intéressant (et magique si je peux me permettre).

Nous avons commencé cet article en affirmant que le système d’exploitation orchestre tout cela, il le fait en utilisant le noyau. Comme nous l’avons déjà dit, le noyau s’exécute dans une section de mémoire qui est mappée en mode supervisé uniquement dans le GDT pour tous les processus. Oui, je sais que chaque processus a son propre espace d’adressage, mais le noyau coupe cet espace d’adressage (généralement la moitié supérieure, selon le système d’exploitation) pour son usage personnel, non seulement en coupant l’espace d’adressage mais aussi à la même adresse pour tous les processus. Ceci est important car le code du noyau est fixe et chaque référence aux variables et aux structures doit se trouver au même endroit pour tous les processus. Vous pouvez regarder le noyau comme une bibliothèque spéciale chargée à chaque processus dans le même emplacement.

Plus loin dans les interruptions

Nous savons que l’IDT contient des adresses de fonctions, ces fonctions appelées ISR (Interrupt Service Routine), certaines s’exécutent lorsque l’événement matériel se produit (pression de touche sur le clavier) et d’autres lorsque le logiciel initie l’interruption par exemple pour passer en mode noyau.

Windows a un concept cool sur les interruptions et leur priorisation : Une interruption particulièrement importante est le tic-tac de l’horloge. A chaque tic-tac de l’horloge, il y a une interruption qui est gérée par son ISR. Le planificateur du système d’exploitation utilise cet événement d’horloge pour contrôler combien de temps chaque processus est en cours d’exécution et si c’est ou non le tour d’un autre. Comme vous pouvez le comprendre, cette interruption est très importante et doit être traitée dès qu’elle se produit. Tous les ISR n’ont pas la même importance et c’est là que les priorités entre les interruptions entrent en jeu. Prenons l’exemple de la pression d’une touche du clavier et supposons qu’elle ait la priorité 1, je viens d’appuyer sur une touche du clavier et son ISR est en train de s’exécuter, pendant l’exécution de l’ISR du clavier toutes les interruptions de la même priorité ou d’une priorité inférieure sont ignorées. Pendant l’exécution de l’ISR, l’ISR de l’horloge est déclenché avec une priorité de 2 (c’est pourquoi il n’a pas été désactivé), une commutation immédiate se produit vers l’ISR de l’horloge, une fois que l’horloge termine, elle retourne le contrôle à l’ISR du clavier à partir d’où elle s’est arrêtée. Ces priorités d’interruptions sont appelées IRQL (Interrupt ReQuest Level), comme l’IRQL de l’interruption augmente, sa priorité est plus élevée. Les interruptions ayant la priorité la plus élevée ne sont jamais des interruptions au milieu, elles s’exécutent jusqu’à la fin, toujours. L’IRQL est spécifique à Windows – l’IRQL est un nombre entre 0-31, pour Linux, par contre, il n’existe pas, Linux traite chaque interruption avec la même priorité et désactive simplement toutes les interruptions quand il a vraiment besoin que cette routine spécifique ne soit pas perturbée. Comme vous pouvez le voir, c’est une question de conception et de préférences.

Connectons tout cela à notre mode utilisateur bien-aimé . L’ISR de cet événement d’horloge va s’exécuter quel que soit le thread en cours d’exécution et pourrait même s’interrompre vers un autre ISR pour une tâche non liée. c’est un exemple parfait de la raison pour laquelle le noyau est à la même adresse pour tous les processus nous ne voulons pas changer le GDT et le répertoire de page (en C3) chaque fois que nous exécutons l’interruption comme cela se produit BEAUCOUP de fois pendant même une seule fonction de tout processus en mode utilisateur donné. Beaucoup de choses se passent entre ces lignes de code que vous écrivez lorsque vous développez votre application en mode utilisateur ( ;)).

Une autre façon de regarder les interruptions est comme des entrées externes et indépendantes à notre OS, cette définition n’est pas exacte (toutes les interruptions ne sont pas externes ou indépendantes) mais il est bon de faire un point, une grande partie du travail du noyau est de donner un sens aux événements qui se produisent tout le temps de tous les endroits (périphériques d’entrée) et d’un côté de servir ces événements et l’autre de s’assurer que tout est corrélé correctement.

Alors pour donner du sens à tout cela, commençons par une application simple en mode utilisateur qui exécute l’instruction suivante :

0x0000051d push ebp;

Pour chaque instruction que le CPU exécute, il examine d’abord l’adresse de cette instruction (dans ce cas ‘0x0000051d’) par rapport au GDT/LDT en utilisant le registre de segment de code (‘cs’ parce que c’est une instruction à exécuter) pour connaître l’index à chercher dans la table (rappelez-vous que le registre de segment indique au CPU exactement où chercher). Une fois que le CPU sait que l’instruction est dans un emplacement exécutable et que nous sommes dans le bon anneau (mode utilisateur/mode noyau), il continue à exécuter l’instruction. Dans ce cas, l’instruction ‘push ebp’ n’affecte pas seulement le registre mais aussi la pile du programme (elle pousse le contenu ebp de la pile). Le CPU vérifie donc aussi dans le GDT/LDT l’adresse dans le registre esp (l’adresse de l’emplacement actuel sur la pile, et comme il s’agit de l’emplacement de la pile, le CPU sait qu’il faut utiliser le registre de segment de pile pour le faire) pour s’assurer qu’il est possible d’écrire dans cet anneau spécifique. Veuillez noter que si cette instruction lisait également de la mémoire, le CPU aurait vérifié l’adresse pertinente pour l’accès en lecture aussi.

Ce n’est pas tout, après que le CPU ait vérifié tous les aspects de sécurité, il est maintenant nécessaire d’accéder et de manipuler la mémoire, comme vous le rappelez les adresses sont dans leur format virtuel. Le MMU traduit maintenant chaque mémoire virtuelle spécifiée par l’instruction en une adresse de mémoire physique en utilisant le registre CR3 qui pointe vers le répertoire des pages (qui pointe vers la table des pages) qui nous permet éventuellement de traduire l’adresse en physique. Notez que l’adresse peut ne pas être en mémoire au moment où l’on en a besoin, dans ce cas l’OS va générer un page fault (une exception qui génère une interruption) et va amener les données dans la mémoire physique pour nous et ensuite continuer l’exécution (ceci est transparent pour l’app du mode utilisateur).

De l’utilisateur au noyau

Chaque échange entre le mode utilisateur et le mode noyau se passe en utilisant l’IDT. Depuis l’application en mode utilisateur, l’instruction ‘int <num>’ transfère l’exécution à la fonction dans l’IDT à l’indice num. Lorsque l’exécution est en mode noyau, beaucoup de règles changent, chaque thread a des piles différentes pour le mode utilisateur et le mode noyau, les vérifications d’accès à la mémoire sont beaucoup plus compliquées et obligatoires, en mode noyau, il y a très peu de choses que vous ne pouvez pas faire et beaucoup que vous pouvez casser.

ASLR et KASLR

plus souvent qu’autrement, c’est « seulement » le manque de connaissances qui nous empêche de réaliser l’impossible.

ASLR (Address Space Layout Randomization) est un concept qui est implémenté différemment au sein de chaque OS, le concept est de randomiser les adresses virtuelles des processus et de leurs bibliothèques chargées.

Avant de nous plonger dedans, je voulais noter que j’ai décidé d’inclure ASLR dans ce post parce que c’est une belle façon de voir comment le mode protégé et ses structures ont permis ce genre de capacité même si ce n’est pas celui qui l’implémente ou qui en est responsable d’ailleurs.

Pourquoi ASLR ?

Le pourquoi est facile, pour prévenir les attaques. Lorsque quelqu’un est capable d’injecter du code dans un processus en cours d’exécution, ne pas connaître les adresses de certaines fonctions bénéfiques est ce qui peut faire échouer l’attaque.

Nous avons déjà un espace d’adressage différent pour chaque processus, cela signifie que sans ASLR tous les processus auraient les mêmes adresses de base, c’est parce que lorsque chaque processus dans son propre espace d’adressage virtuel, nous n’avons pas à nous méfier des collisions entre les processus. Lorsque nous lions le programme, l’éditeur de liens choisit une adresse de base fixe sur laquelle il lie l’exécutable. Sur le papier, tous les fichiers exécutables liés par le même éditeur de liens avec les paramètres par défaut (l’adresse de base peut être configurée si nécessaire) auront la même adresse de base. Pour donner un exemple, j’ai écrit deux applications, l’une appelée « 1.exe » et la seconde « 2.exe », les deux sont des projets différents dans Visual Studio et pourtant ils ont tous les deux la même adresse de base (j’ai utilisé exeinfo PE pour voir l’adresse de base dans le fichier PE) :

Non seulement ces deux exécutables ont la même adresse de base mais tous deux ne supportent pas l’ASLR (je l’ai désactivé) :

Vous pouvez également le voir inclus dans le format PE sous Caractéristiques du fichier :

Maintenant, exécutons les deux exécutables en même temps et les deux partagent la même adresse de base (je vais utiliser vmmap de Sysinternals pour voir l’image de base) :

Nous pouvons voir que les deux processus n’utilisent pas l’ASLR et ont la même adresse de base de 0x00400000. Si nous étions des attaquants et que nous avions accès à cet exécutable, nous aurions pu savoir exactement quelles adresses seront disponibles pour ce processus une fois que nous aurons trouvé le moyen de nous injecter dans son exécution. activons ASLR dans notre exécutable 1.exe et voyons sa magie:

Il a changé !

KASLR (Kernel ASLR) est le même que ASLR, sauf qu’il fonctionne au niveau du noyau, ce qui signifie qu’une fois qu’un attaquant a pu s’injecter dans le contexte du noyau, il ne sera (espérons-le) pas capable de savoir quelles adresses contiennent quelles structures (par exemple où se trouve le GDT en mémoire). Une chose à mentionner ici est que ASLR travaille sa magie à chaque spawn d’un nouveau processus (qui supporte ASLR bien sûr) tandis que KASLR le fait à chaque redémarrage car c’est à ce moment que le noyau est « spawn ».

Comment ASLR?

Alors comment fonctionne-t-il et comment est-il connecté au mode protégé ? Celui qui est responsable de la mise en œuvre de l’ASLR est le chargeur. Quand un processus est lancé, le loader est celui qui doit le mettre en mémoire, créer les structures pertinentes et lancer son thread. Le chargeur vérifie d’abord si l’exécutable supporte l’ASLR et si c’est le cas, il choisit au hasard une adresse de base dans la gamme des adresses disponibles (l’espace du noyau, par exemple, n’est évidemment pas disponible). Sur la base de cette adresse, le chargeur initialise alors le répertoire de pages pour ce processus afin de faire pointer l’espace d’adresses aléatoire vers l’espace physique. La flexibilité de LDT vient également à notre secours puisque le chargeur crée simplement LDT correspondant à l’adresse randomisée avec les permissions appropriées. La beauté ici est que le mode protégé n’est même pas conscient que l’ASLR est utilisé, il est assez flexible pour ne pas s’en soucier.

Un détail de mise en œuvre intéressant est que dans windows l’adresse randomisée pour un exécutable spécifique est fixée pour des raisons d’efficacité. Ce que je veux dire par là, c’est que si nous avons randomisé l’adresse pour, disons, calc.exe, la deuxième fois qu’il est exécuté, l’adresse de base sera la même. Ainsi, si j’ouvre deux calculatrices en même temps, elles auront la même adresse de base. Une fois que j’aurai fermé les deux calculatrices et que je les ouvrirai à nouveau, elles auront à nouveau la même adresse, mais celle-ci sera différente de l’adresse des calculatrices précédentes. Pourquoi cela n’est-il pas efficace ? Pensez aux DLL les plus utilisées. De nombreux processus les utilisent et si leurs adresses de base étaient différentes pour chaque instance de processus, leur code serait également différent (le code fait référence aux données en utilisant cette adresse de base) et si le code est différent, la DLL devra être chargée en mémoire pour chaque processus. En réalité, le système d’exploitation charge les images une seule fois pour tous les processus qui utilisent cette image. Cela permet d’économiser de l’espace – beaucoup d’espace !

Conclusion

À présent, vous devriez être en mesure d’imaginer le noyau au travail et de comprendre comment toutes les structures clés de l’architecture x86 jouent ensemble pour une image plus grande et nous permettent d’exécuter des applications éventuellement dangereuses en mode utilisateur sans (ou peu) de crainte.

Articles

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.