Le tas (heap) et ses implications en exploitation

La heap permet l’allocation dynamique, indispensable dès qu’un programme manipule des données dont la taille n’est pas connue à la compilation. Derrière ce rappel se cache la nécessité de comprendre les mécanismes internes de l’allocation pour pouvoir raisonner sur les vulnérabilités modernes.

Fonctionnement général

  • La heap repose sur des segments de mémoire gérés par l’allocateur (ptmalloc2 sur Linux).
  • Les allocations ne sont jamais servies directement par le kernel ; le kernel ne sert que des pages via mmap ou brk, et c’est l’allocateur qui sous-divise ces pages en chunks.
  • Toute donnée issue de l’utilisateur qui n’est pas bornée statiquement finit dans la heap.

Pourquoi c’est une alerte méthodologique

Pointe d'examen

Dès qu’un pointeur est retourné par une fonction, les données auxquelles il réfère peuvent avoir été altérées entre deux appels. Ce rappel préfigure les failles de type use-after-free, double free, tcache poisoning.

En reverse, cela impose d’examiner attentivement :

  • l’état des chunks autour de l’adresse retournée ;
  • les écritures postérieures au free ;
  • la structure interne d’un chunk (size, prev_size, drapeaux).

Sections .data et .rodata et leur rôle en reverse

Il est essentiel de distinguer les sections de l’ELF car elles déterminent :

  • ce qui est modifiable ;
  • ce qui persiste ;
  • ce qu’on peut détourner pour un ROP ou un détournement logique.

.data

Variables globales initialisées, modifiables. Permet souvent de stocker ou récupérer des données utiles à l’exploitation (chaîne "sh", compteurs internes, drapeaux logiques).

.rodata

Constantes non modifiables. En reverse, cette section regorge de chaînes permettant d’identifier des branches logiques, du débogage résiduel, etc.

Le point essentiel concernant static

Point piégeux

static dans une fonction ne signifie pas « sur la pile ». Le compilateur place ces variables dans .data ou .bss, souvent sous des noms transformés. Elles persistent entre les appels.

Côté reverse, elles permettent d’identifier une machine à états implicite. Côté exploitation, un débordement dans .data peut corrompre des variables critiques sans déclencher de protections stack-based.

Le modèle de copie par buffers de taille fixe

L’illustration classique est char buf[4096]. Ce n’est pas un exemple quelconque : cela initie à deux phénomènes essentiels.

  1. Le noyau ne traite jamais un fichier de manière atomique : il le segmente en blocs, souvent alignés sur la taille des pages.
  2. Les programmes utilisant ce pattern sont sensibles aux conditions de bord, aux TOCTOU, et aux dépassements liés aux sous-lectures successives.

Notion clé : lecture partielle

Les read et write ne garantissent jamais que la totalité des données sera consommée. Une faille peut apparaître lorsque la logique du programme suppose l’inverse.

Ce point prépare la compréhension des débordements inter-pages et, plus généralement, des comportements qui dépendent du découpage matériel (ou virtuel) de la mémoire.

La granularité fondamentale : la page mémoire

On n’alloue jamais moins de 4096 octets. C’est la granularité imposée par l’architecture matérielle (MMU) pour l’adressage virtuel.

Conséquences

  • Toutes les protections (RWX) s’appliquent à la page entière.
  • La heap peut croître seulement par multiples de pages.
  • Un débordement peut franchir la frontière d’un chunk ou d’une page.

Notion clé : MMU

La MMU traduit une adresse virtuelle en adresse physique en scindant l’adresse en deux : index de page + offset (12 bits). Ce découpage est essentiel pour comprendre les dépassements inter-pages.

Traduction d’adresse : rôle CPU vs rôle kernel

La traduction n’est pas « du code C » dans le kernel, mais un mécanisme hybride matériel/logiciel.

Le kernel :

  • configure les tables de pages ;
  • attribue les permissions (RWX) ;
  • gère les défauts de page.

La MMU :

  • réalise la traduction à chaque accès ;
  • filtre selon les permissions ;
  • exploite le TLB pour accélérer les recherches.

À retenir pour l'examen

Les protections mémoire sont appliquées par le matériel, non par le code du kernel. C’est ce qui explique la robustesse des segments RX, RW, etc., et l’impossibilité de définir des permissions à l’octet.

Interprétation pratique en exploitation binaire

Ces rappels convergent vers plusieurs compétences opérationnelles.

Lire correctement vmmap

Comprendre la disposition mémoire réelle :

  • .text : RX
  • .rodata : R
  • .data / .bss : RW
  • heap : RW
  • stack : RW (NX)

Identifier les pages utiles pour un ROP ou pour injecter des données.

Retrouver une adresse absolue dans un ELF

Les adresses apparaissant dans les instructions sont relatives aux bases de chargement. En présence d’ASLR partiel, seul le binaire non-PIE possède des adresses constantes.

Trouver des cibles d’exploitation

  • Chaînes "sh" dans .rodata ou .data
  • Globales modifiables jouant un rôle logique
  • Pages RW pouvant accueillir des constructions détournées (petits gadgets, données ROP)

Notion clé : ROP

Le ROP repose sur des séquences d’instructions existantes, terminées par ret, et enchaînées via la pile. La compréhension des segments mémoire permet de localiser des gadgets fiables.


Synthèse type fiche d’examen

Granularité

  • Pages de 4096 octets : l’espace d’adressage virtuel d’un processus est divisé en unités de taille fixe appelées pages, souvent de 4 Kio (4096 octets).
  • Droits appliqués par pages : la mémoire est protégée et gérée par le système d’exploitation à la granularité de la page. Chaque page peut avoir des bits de protection qui spécifient les types d’accès autorisés (lecture, écriture, exécution).
  • Traduction via MMU : les adresses virtuelles générées par le programme sont traduites en adresses physiques par l’unité de gestion mémoire (MMU). La MMU utilise les tables de pages pour effectuer cette traduction à chaque référence mémoire.

Heap (tas)

  • Chunks internes : le tas (heap) est utilisé pour l’allocation dynamique de mémoire (par exemple via malloc ou kmalloc). Il est géré en interne par l’OS ou la bibliothèque C (glibc) comme une liste de « morceaux » (chunks) de mémoire allouée ou libre.
  • Vecteurs d’attaque (UAF, double-free, détournement) : les vulnérabilités du tas, comme l’utilisation après libération (Use After Free ou UAF) ou la double libération (double-free), résultent de la manipulation des structures de données internes du gestionnaire de tas. Les attaques visent à corrompre les métadonnées de ces chunks pour détourner l’exécution ou injecter des données, car le tas est une région de données accessible en lecture/écriture (RW).
  • Pointeurs : en C, le tas est manipulé via des pointeurs explicites. C est un langage puissant et efficace, mais il ne vérifie pas les types ni les bornes des tableaux, ce qui rend le code vulnérable si les pointeurs sont mal gérés.

Sections ELF

Les binaires au format ELF (Executable and Linkable Format) définissent logiquement l’espace mémoire d’un processus.

SectionPermissionsRôle
.textRX (lecture, exécution)Contient le code exécutable du programme. Elle est lue et ne doit pas être modifiable.
.rodataR (lecture seule)Contient les données en lecture seule, comme les chaînes de format des fonctions printf.
.data / .bssRW (lecture, écriture)Contient les données initialisées (.data) et non initialisées (.bss). Ce sont des zones de données et ne doivent pas être exécutables (règle W^X).
Variables locales(sur la pile)Les variables locales statiques sont traitées comme des variables globales et résident dans le segment de données ; les variables locales non statiques sont gérées sur la pile (stack).

Chargement mémoire

  • Alignement sur pages : le noyau charge les segments du programme (texte, données) en mémoire virtuelle, en mappant les blocs de l’exécutable sur des pages physiques.
  • ASLR : la randomisation de l’agencement de l’espace d’adressage (ASLR) décale de manière aléatoire l’adresse de base du programme, de la pile et des bibliothèques partagées (libc, .text, .data) entre les exécutions. Cela rend l’exploitation difficile car les attaquants ne connaissent plus l’adresse exacte où injecter du code ou retourner.
  • vmmap : l’organisation mémoire d’un processus est complexe. Des outils comme vmmap (ou l’examen de /proc/<pid>/maps sous Linux) sont cruciaux pour les développeurs pour comprendre l’agencement et l’utilisation réelle de la mémoire.

Exploitation (détournement du flot de contrôle)

Si un attaquant réussit à modifier une adresse de retour ou un pointeur de fonction, il peut détourner le flot d’exécution vers des adresses qu’il contrôle :

  • ret2text : le retour direct à une adresse (ret2text), souvent une fonction déjà présente dans le code du programme (ou du noyau), comme un system("/bin/sh"). L’ASLR rend cette adresse imprévisible.
  • ret2plt : l’attaquant peut cibler la Procedure Linkage Table (PLT) pour exécuter une fonction de bibliothèque partagée (libc).
  • ROP (Return-Oriented Programming) : l’attaquant enchaîne des séquences de quelques instructions (gadgets) qui se terminent par un RET (retour de fonction). En contrôlant les adresses de retour sur la pile, il construit un programme arbitraire à partir d’instructions existantes, contournant le DEP (interdiction d’exécution sur le tas/la pile).
  • Chaînes de données : si le code ciblé est sûr (DEP activé), l’attaquant peut chercher à modifier une variable globale (.data ou .bss) ou une chaîne de caractères en lecture seule (.rodata) pour altérer la logique du programme (attaque de type non-control-flow diverting). Par exemple, si une chaîne "sh" est nécessaire pour un appel système, l’attaquant pourrait chercher à en modifier une existante ou à en fournir une via un autre canal.
flowchart TB

  %% --- Espace utilisateur ---------------------------------------------------
  subgraph UserSpace["Espace utilisateur (processus)"]
    libc["libc / glibc<br/>malloc(), free(), ..."]
    heap["Segment Heap (RW)<br/>zone d'adresses virtuelles"]
    chunks["Liste de chunks<br/>(subdivisions de pages)"]
    ptrs["Pointeurs<br/>retournés au programme"]
  end

  %% --- Noyau / gestion mémoire virtuelle -----------------------------------
  subgraph KernelSpace["Noyau Linux"]
    mm_subsys["Sous-système mémoire<br/>(VM, gestionnaire de pages)"]
    brk_sys["brk() / sbrk()<br/>extension du segment de données"]
    mmap_sys["mmap() anonyme<br/>réservation de nouvelles pages"]
    pages["Pages virtuelles pour le heap<br/>∼ 4096 octets (granularité)"]
    allocator["Allocateur utilisateur<br/>ptmalloc2 (glibc)"]
    pagetable["Table(s) de pages<br/>(VA → PA)"]
  end

  %% --- Matériel -------------------------------------------------------------
  subgraph Hardware["Matériel"]
    mmu["MMU<br/>traduction @virt → @phys"]
    physmem["Mémoire physique<br/>(frames)"]
  end

  %% --- Liens logiques -------------------------------------------------------
  libc -->|"demande de mémoire<br/>malloc()/free()"| allocator
  allocator -->|"gère le heap"| heap
  heap -->|"découpe en"| chunks
  chunks -->|"contiennent les données<br/>et adresses manipulées"| ptrs

  %% Quand le heap manque de place, l'allocateur demande au noyau
  allocator -. "plus de pages nécessaires" .-> mm_subsys
  mm_subsys --> brk_sys
  mm_subsys --> mmap_sys
  brk_sys -->|"fait croître le heap"| pages
  mmap_sys -->|"réserve des pages<br/>supplémentaires"| pages
  pages -->|"zone RW mappée<br/>pour le heap"| heap

  %% Mapping virtuel → physique
  pages --> pagetable
  pagetable --> mmu
  mmu --> physmem
graph TD
    subgraph CPU_Context [Contexte CPU & Registres]
        RIP[Instruction Pointer RIP] --> MMU
        RSP[Stack Pointer RSP]
    end

    subgraph Virtual_Memory [L'Obélisque : Espace Virtuel]
        direction BT
        Text[".text (RX) - Fondations Cristallines"]
        Rodata[".rodata (R) - Données Figées"]
        Data[".data / .bss (RW) - Usine Interne"]
        Heap[".heap (RW) - Chantier Dynamique"]
        LibC["Shared Libs (libc.so) - Étage Mitoyen"]
        Stack[".stack (RW) - Plafond Électrique"]

        Text --> Rodata
        Rodata --> Data
        Data --> Heap
        Heap -.->|Extension via brk/mmap| LibC
        LibC -.-> Stack
    end

    subgraph Hardware_Mechanism [Le Prisme : Mécanisme MMU]
        MMU((MMU))
        TLB[TLB - Cache Rapide]
        PageTable[Table des Pages - La Carte]
        Fault{Défaut de Page ?}
    end

    subgraph Physical_Memory [Le Chaos : Mémoire Physique]
        Frame1[Frame Physique A]
        Frame2[Frame Physique B]
        Frame3[Frame Physique C]
    end

    %% Relations
    RIP -- "Adresse Virtuelle" --> MMU
    MMU -- "Recherche" --> TLB
    TLB -- "Miss" --> PageTable
    PageTable -- "Entrée Valide" --> MMU
    PageTable -- "Entrée Invalide" --> Fault
    Fault -- "Trap" --> Kernel["OS Kernel (mmap, swap in)"]

    MMU -- "Adresse Physique" --> Frame2

    %% Note sur les attaques
    Heap -- "Overflow inter-chunk" --> Heap
    Stack -- "ROP (Return Oriented prog)" --> Text
    Data -- "Corrupt Global State" --> Data

    style Text fill:#aee,stroke:#333,stroke-width:2px
    style Heap fill:#fca,stroke:#f00,stroke-width:2px,stroke-dasharray: 5 5
    style Stack fill:#fca,stroke:#f00,stroke-width:2px
    style MMU fill:#fff,stroke:#333,stroke-width:4px