(TAOMF) Gestion de la mémoire

L'ensemble des articles tagués TAOMF ont été écrit à partir d'un ensemble de notes prises suite à la lecture du livre "The Art Of Memory Forensic" écrit par Michael Hale Ligh, Andrew Case, Jamie Levy et Aaron Walters.

Tout les crédits de ces articles leur reviennent donc de droits. Par ailleurs je vous invite vraiment à lire ce livre qui est une mine colossale d'informations.


Un PC est composé d'un ensemble de circuits imprimés qui permettent de connecter différents périphériques (CPU, carte graphique, carte son, lecteur CD etc...). Tous ces périphériques sont reliés sur ce que l'on appelle la carte mère.

Les deux composants principaux d'un PC sont le processeur (CPU) et la mémoire RAM. Le CPU permet d'exécuter des instructions tandis que la RAM permet de stocker de la donnée de manière temporaire (contrairement aux disques durs qui permettent de la stocker de façon quasi permanente). En plus de ces deux types de mémoires on retrouve aussi ce que l'on appelle de la mémoire cache. Ces caches sont des petits espaces mémoires embarqués dans nos CPU's très rapides d'accès mais qui ne peuvent stocker qu'une petite quantité de données. Il existe plusieurs tailles de cache appelés respectivement L1, L2, et L3. Suivant les CPU's ces caches sont présents -ou non- à l'intérieur du CPU ou bien dans un autre espace et leurs tailles varient.

Lorsqu'un CPU a besoin d'une certaine donnée il va vérifier si cette donnée est présente dans un de ces caches. Si c'est le cas il va pouvoir la récupérer assez rapidement et effectuer les opérations demandées. En revanche si la donnée n'est pas présente, il va devoir la récupérer dans la mémoire RAM et là ça devient un peu plus compliqué...

En 2019 la plupart de nos PC's disposent d'au moins 4Go de RAM. Le mien en possède 16 et la plupart des serveurs sur lesquels je travaille en possède 64. Comment est-ce que mon CPU va savoir ou récupérer la donnée qu'il cherche ? Eh bien pour l'aider il dispose de la MMU (Memory Management Unit). Cette MMU est le composant hardware qui va faire la translation entre l'adresse physique de la donnée demandée par le processeur et son adresse virtuelle.

Adresses virtuelles et physiques, kezako ?

Lorsque vous lancez un binaire que ce soit sur Windows ou Linux, le kernel (le noyau de votre OS) va allouer de la mémoire physique à votre processus. Cet espace est choisi de manière plus ou moins aléatoire dans l'espace RAM disponible. On devrait donc s'attendre à ce qu'un binaire exécuté plusieurs fois soit mappé à différents endroits dans la mémoire. Et pourtant non :

mem1.png

Quoiqu'il arrive l'adresse du début de ma fonction main se trouve toujours à l'adresse 0x555555555135 ! En fait ce qu'il faut retenir ici c'est que nous ne manipulons pas de la mémoire physique mais de la mémoire virtuelle !

Alors comment ça fonctionne tout ça ? Eh bien pour commencer prenons notre RAM :

mem2.png

La RAM est découpée en plusieurs sections que l'on appelle des "pages". Sur les architectures 32 bits la taille des pages est de 4Kb tandis que sur les architectures 64 bits la taille de ces pages est de 8Kb :

mem3.png

Suivant la taille de notre binaire (nombres de lignes de code, taille des données à stocker, taille des librairies à charger etc...) le kernel va allouer plus ou moins de pages à notre binaire :

mem4.png

Cette allocation de mémoire aux yeux du processus est invisible. Tout ce qu'il voit c'est qu'un espace mémoire lui a été alloué, que cet espace débute à l'adresse 0x000000000001 et se termine à un certain endroit (dépendant du nombre de pages allouées).

Attention, ici j'ai représenté l'allocation de mémoire comme étant un segment continu. Ce n'est pas toujours le cas, parfois l'allocation se fait sur des range mémoire éparpillées au seins de la mémoire physique.

Ainsi le processus n'a aucune notion de ce qu'est et contient la mémoire physique et n'est capable de traiter que les données contenues dans son espace virtuel.

Pour la culture générale on pourra aussi retenir qu'un binaire ELF (format d'exécutable Linux) ou PE (format d'exécutable Windows) commencera toujours à l'adresse 0x400000 tandis qu'une DLL Windows commencera à l'adresse 0x10000000. Mais bon on y reviendra plus tard quand on parlera des formats d'exécutable.

Si cette allocation virtuelle est faite c'est tout simplement pour compartimenter les processus entre eux et empêcher un processus A d'aller voler des données d'un processus B (ça serait quand même ultra critique si un processus non privilégié pouvait voler de la donnée venant d'un processus à haut privilège). D'ailleurs si un processus tente d'aller lire des données qui ne sont pas les siennes on obtiendra une jolie erreur de type segfault !

Alors où est le problème ? Eh bien un processus peut gérer des adresses virtuelles en revanche tout composant hardware a besoin de traiter des adresses physiques. Nos CPU's sont incapables (tout seul) de distinguer une adresse virtuelle d'une adresse physique. Pour eux toutes adresses est physique.

Heureusement pour nos CPU's il existe ce que le MMU (Memory Management Unit). Le MMU est un petit composant hardware qui est en charge de faire la translation entre une adresse physique et une adresse virtuelle. Ainsi lorsque le CPU voudra accéder à une donnée d'un processus A, il demandera au MMU de faire la translation d'adresses avant de récupérer la donnée.

Étant donné que cette opération est longue, un second composant, le cache TLB (Translation Lookaside Buffer), sera chargé de stocker les résultats des opérations précédemment effectuées.

Schématiquement voilà ce que ça donnera :

[caption id="attachment_10426" align="aligncenter" width="556"]mem5.png Crédit image : wikipédia.org[/caption]

Plus haut je vous ai dit qu'il était important que les processus soient compartimentés afin d'ajouter un niveau de sécurité et de la stabilité. En réalité il existe aussi des sections de mémoire qui sont dites "mémoires partagés (shared memory).  Pour plusieurs raisons il peut être intéressant que deux processus mappés en mémoire virtuelle aient accès à une zone mémoire partagée pour échanger du code ou des données :

mem6.png

Ces zones de mémoire partagées vont permettre à plusieurs processus d'utiliser - par exemple - les mêmes librairies sans pour autant avoir à les charger dans leurs mémoires virtuelles respectives. On gagne ainsi du temps (pas de copie inutile du contenu partagé) ainsi que de l'espace en RAM.

Cependant niveau sécurité c'est pas super top. En effet dans le cas présent si le processus A non privilégié modifie le contenu de la librairie partagée (hijack de librairie) et que le processus B privilégié l'exécute on pourra potentiellement compromettre la machine.

Pour éviter cela, les zones de mémoire partagé sont taguées comme étant "copy-on-write". Concrètement cela veut dire que tant que les pages partagées ne sont pas modifiées, le kernel ne fera pas de copie du contenu. En revanche dès qu'un processus modifiera le contenu d'une page, une copie de cette page sera faites et sera ttribué au processus en question. Ainsi les données ne pourront pas être corrompues.

En 2016 un exploit (DirtyCOW) ultra violent prenait à partie ce tag copy-on-write afin de permettre une élévation de privilèges. Je vous laisse voir comment il fonctionne ;) !