Reverse-concevoir ou exploiter un binaire suppose de savoir, à chaque adresse, ce qu’on a le droit d’en faire. C’est tout l’enjeu de la mémoire virtuelle : elle organise l’espace d’adressage d’un processus en zones aux droits distincts, et c’est cette cartographie — bien plus que la mémoire physique sous-jacente — qui importe en exploitation.
Du fichier ELF à l’espace virtuel
Un processus Linux dispose d’un espace d’adressage virtuel organisé en pages, chacune porteuse de droits de lecture, écriture, exécution (r, w, x). Le système de mémoire virtuelle permet au noyau d’y mapper des fichiers ou des zones anonymes — tas, pile — avec des droits variés.
Dans un binaire ELF, les sections (.text, .rodata, .data, .bss, .got, .plt) sont regroupées en segments (PT_LOAD notamment) lors de l’édition de liens et du chargement, pour décider comment elles seront mappées. Un segment PT_LOAD peut ainsi contenir .text et .plt (exécutable, lecture seule) tandis qu’un autre regroupe .data, .got.plt et .bss (données, lecture-écriture). Concrètement, lors d’un execve, le noyau lit ces segments PT_LOAD dans la table des program headers et, pour chacun, effectue un mmap du fichier à l’adresse virtuelle p_vaddr, sur la longueur p_filesz / p_memsz, avec les droits p_flags — le tout aligné sur des pages (4 Kio en x86-64). On le retrouve tel quel dans /proc/<pid>/maps :
00400000-0041c000 r-xp 00000000 08:01 /bin/ls # PT_LOAD (R-X)
0061e000-0061f000 rw-p 0001e000 08:01 /bin/ls # PT_LOAD (RW-)Le mapping virtuel vers physique reste, lui, invisible depuis l’espace utilisateur — sauf à être root et à inspecter certaines informations noyau. Mais pour l’exploitation, seul le layout virtuel compte : adresses, permissions, segments.
Ce que contiennent .data et .rodata
Distinguer les sections a un intérêt direct : cela détermine ce qui est modifiable, ce qui persiste, ce qu’on peut détourner.
La section .data héberge les variables globales initialisées, modifiables — souvent utile pour y stocker ou y récupérer des données exploitables (une chaîne "sh", un compteur interne, un drapeau logique). La section .rodata héberge les constantes en lecture seule ; en reverse, elle regorge de chaînes qui trahissent des branches logiques, des messages de débogage résiduels.
Le piège du mot-clé
static
staticdans une fonction ne signifie pas « sur la pile ». Le compilateur place ces variables dans.dataou.bss, souvent sous des noms transformés, et elles persistent entre les appels successifs de la fonction. Côté reverse, elles trahissent une machine à états implicite ; côté exploitation, un débordement dans.datapeut corrompre des variables critiques sans déclencher les protections propres à la pile.
Le tas et l’allocation dynamique
Le tas (heap) sert l’allocation dynamique, indispensable dès qu’un programme manipule des données dont la taille n’est pas connue à la compilation. Le noyau ne sert jamais directement une allocation : il ne fournit que des pages, via brk ou mmap, et c’est l’allocateur (ptmalloc2 sur Linux/glibc) qui les subdivise en chunks. Toute donnée utilisateur non bornée statiquement finit, tôt ou tard, dans le tas.
Une vigilance méthodique
Dès qu’un pointeur est retourné par une fonction d’allocation, les données qu’il désigne peuvent avoir été altérées entre deux appels. C’est le terreau des failles use-after-free, double-free et tcache poisoning — qui exigent d’examiner l’état des chunks autour de l’adresse retournée, les écritures postérieures à un
free, et la structure interne d’un chunk (taille, taille précédente, drapeaux).
La page, unité de granularité incompressible
On n’alloue jamais moins de 4096 octets : c’est la granularité imposée par l’architecture matérielle (la MMU) pour l’adressage virtuel. Toutes les protections mémoire (r/w/x) s’appliquent à la page entière — jamais à l’octet — et le tas ne peut croître que par multiples de pages. Un débordement peut ainsi franchir la frontière d’un chunk ou d’une page entière, avec des conséquences très différentes selon le cas.
La MMU traduit chaque adresse virtuelle en adresse physique en la scindant en un index de page et un offset (12 bits pour des pages de 4 Kio). Ce découpage explique les débordements inter-pages. Le partage des rôles est net : le noyau configure les tables de pages, attribue les permissions et gère les défauts de page ; la MMU, elle, réalise la traduction à chaque accès mémoire, filtre selon les permissions, et s’appuie sur le TLB pour accélérer la recherche.
IMPORTANT
Les protections mémoire sont appliquées par le matériel, non par le code du noyau. C’est ce qui explique la robustesse des segments RX/RW et l’impossibilité de définir des permissions à l’octet près.
Lire la mémoire d’un processus vivant
Ces éléments convergent vers une compétence opérationnelle : savoir lire vmmap (ou /proc/<pid>/maps) pour reconstituer la disposition réelle — .text en RX, .rodata en R, .data/.bss et le tas en RW, la pile en RW mais non exécutable (NX). C’est cette carte qui permet d’identifier les pages utiles à un ROP ou à l’injection de données, et de retrouver une adresse absolue dans un ELF — sachant que les adresses des instructions sont relatives à la base de chargement, et que seul un binaire non-PIE garantit des adresses constantes d’une exécution à l’autre (voir PIE et ASLR).
flowchart TB subgraph UserSpace["Espace utilisateur (processus)"] libc["libc / glibc : malloc(), free(), ..."] heap["Segment heap (RW)"] chunks["Chunks (subdivisions de pages)"] end subgraph KernelSpace["Noyau Linux"] brk_sys["brk() / sbrk()"] mmap_sys["mmap() anonyme"] pages["Pages virtuelles pour le heap (~4096 octets)"] pagetable["Table des pages (VA → PA)"] end subgraph Hardware["Matériel"] mmu["MMU : traduction @virt → @phys"] physmem["Mémoire physique"] end libc -->|malloc/free| heap heap -->|découpe en| chunks heap -. "plus de pages nécessaires" .-> brk_sys heap -. "plus de pages nécessaires" .-> mmap_sys brk_sys -->|fait croître| pages mmap_sys -->|réserve| pages pages --> pagetable --> mmu --> physmem