Mémoire virtuelle, segmentation, droits mémoire et ELF mapping
- Un processus sous Linux (et plus généralement en UNIX) a un espace mémoire virtuel — le virtual address space — organisé en segments/pages avec des droits de lecture, écriture, exécution (r, w, x). Le système de mémoire virtuelle permet au noyau de mapper des fichiers ou des zones anonymes (heap, stack) dans cet espace, avec des droits variés1.
- Dans un binaire ELF (exécutable ou bibliothèque), les sections (comme
.text,.rodata,.data,.bss,.got,.plt) sont groupées en « segments » (segments LOAD, etc.) lors du linkage / chargement, pour décider comment elles seront mappées en mémoire, avec quels droits2. - Par exemple, un segment LOAD peut contenir les sections
.text+.plt(exécutables, lecture seule ou lecture + exécution), un autre peut contenir.data+.got.plt+.bss(données, lecture/écriture)2. - Le mapping virtuel → physique (pages physiques) est abstrait pour l’utilisateur : depuis l’espace utilisateur, on ne sait pas « où » se trouve exactement en RAM physique une page virtuelle (sauf à être root et inspecter certaines informations, ou via des outils du kernel). Mais pour le reverse / exploitation, ce qui importe, c’est le layout virtuel (adresses, droits, segments).
Remarque : «
vmmapne permet pas de savoir dans quelle partie physique se trouve la donnée — hormis root » est correct : on reste dans le domaine de la mémoire virtuelle. Pour l’exploitation, ce qui compte, c’est la mémoire virtuelle (adresses, permissions), pas la page physique.
- L’agencement « mettre ensemble les zones exécutables (rx), les lectures seules (ro), les lectures-écritures (rw) » est exactement ce que fait le loader via les segments LOAD, ce qui permet d’isoler les zones de code, de données, de constantes, etc., pour les droits mémoire2.
Sécurité mémoire et protections « classiques »
Quand on regarde un binaire, il est important d’évaluer quelles protections sont activées. Ces protections modifient le « modèle » d’exploitation. Parmi les plus courantes :
| Protection / mécanisme | Description / effet | Ce qu’elle empêche / atténue | Limites / ce qu’elle n’empêche pas |
|---|---|---|---|
| Stack Canary (canari) | Le compilateur insère une valeur aléatoire (canari) avant l’adresse de retour dans la pile ; avant retour, on vérifie qu’elle est intacte. Si modifiée → abort3. | Écrasement de l’adresse de retour (stack-overflow → retour contrôlé). | N’empêche pas d’écraser d’autres données, de faire du heap overflow, ou d’utiliser des techniques non classiques (format-string, write-what-where, use-after-free). |
| NX / DEP / W^X | Les pages de données (stack, heap, data) ne sont pas marquées exécutable — interdit d’exécuter du shellcode injecté en RW4. | Injection + exécution de shellcode classique (buffer overflow + shellcode). | Si on peut faire du ROP, NX n’empêche pas : on réutilise du code existant (gadgets) dans les zones exécutables5. |
| ASLR (et PIE / PIC) | L’OS (loader) randomise à chaque exécution l’emplacement des segments : stack, heap, bibliothèques partagées, base de l’exécutable si PIE actif6. | Rend difficile de prédire les adresses — compliquant l’écrasement « à l’aveugle » des retours, ROP, ret2libc. | Si on peut fuiter une adresse (leak), on peut bypasser ASLR ; ou utiliser des techniques dérivées (ret2plt, GOT leak, format-string, infoleak)7. |
| RELRO (partial ou full) | Protection de la table GOT (Global Offset Table) → en full RELRO + binding now, la GOT est marquée read-only après relocation4. | Empêche les GOT overwrite (modifier les adresses de fonctions externes pour rediriger les appels). | Ne protège pas contre ROP, heap overflow, corruption d’autres données, use-after-free. C’est une protection ciblée. |
Pourquoi ces protections sont pertinentes (et souvent combinées)
- Ces protections sont cumulatives : un binaire moderne typique aura canari + NX + ASLR + RELRO (+ PIE) — ce qui rend l’exploitation beaucoup plus difficile8.
- En reverse / exploitation, il faut détecter lesquelles sont présentes (via
checksec,readelf,objdump, inspection des segments, droits, etc.) — car elles orientent fortement la stratégie d’attaque8. - Elles modifient ce qu’on peut faire : on doit s’adapter (ROP, infoleak pour bypass ASLR, ret2libc vs GOT overwrite).
Fonctionnements dynamiques : GOT, PLT, dynamic linking
Pour un binaire dynamique (lié à des bibliothèques, e.g. libc), la gestion des appels externes se fait via GOT + PLT + dynamic linker :
- Le PLT (Procedure Linkage Table) est un « trampoline » / stub : quand le binaire appelle une fonction externe (par exemple
printf,malloc), c’est en fait un appel àprintf@plt9. - Le GOT (Global Offset Table) contient des pointeurs vers les adresses « réelles » de ces fonctions — résolues à l’exécution / au chargement dynamique7.
- Au premier appel, le PLT + dynamic linker résout la fonction, écrit l’adresse dans la GOT, puis redirige l’appel. Les appels suivants passent directement via la GOT (à travers le PLT)7.
Implications pratiques en exploitation / reverse :
- Si le binaire est non-PIE (base binaire fixe), alors les offsets PLT / GOT sont constants. Connaissant un offset, on peut calculer les adresses. Très utile pour ret2plt, ret2libc, ROP7.
- Si on a une vulnérabilité d’écriture arbitraire dans la mémoire, et que la GOT / GOT.PLT est modifiable → on peut faire un GOT overwrite : modifier un pointeur de fonction pour le faire pointer vers ce que l’on veut (par exemple
system, un gadget) — la primitive de pwn classique10. - Mais si le binaire est compilé avec full RELRO (et
-z now) → GOT / GOT.PLT sont en lecture seule → le vecteur GOT overwrite est bloqué. Il faut alors passer par d’autres techniques (ROP, ret2libc via PLT, info leak + base libc)4.
C’est pour cela que, dans un contexte CTF / pwn / reverse / exploitation, la présence ou l’absence de ces protections conditionne largement la stratégie.
Buffer overflow / overflows — nuance entre vulnérabilité et exploitabilité
Rappel important : « faille : écrire en dépassement =/= exploitable ».
- Un buffer overflow (stack ou heap) est un bug : écriture hors bornes d’un buffer11.
- Mais qu’il soit exploitable dépend fortement du contexte :
- la présence de canari empêche la corruption de l’adresse de retour via stack overflow ;
- la présence de NX empêche d’exécuter du shellcode injecté sur la pile ;
- ASLR / PIE rendent les adresses imprévisibles (stack, heap, libc, binaire) ;
- RELRO peut empêcher des techniques basées sur la corruption GOT / PLT.
→ Donc même si on a un bug d’overflow, ce bug peut être « amorti » par les protections — rendant l’exploitation bien plus difficile, voire impossible sans fuites d’information, gadgets, etc.
Conseils pour un reverse-engineer / exploit dev
-
Face à un binaire inconnu
- Regarder les protections via
checksec,readelf -l,objdump -h: NX, RELRO, PIE, canari, etc. - Examiner les segments / permissions : quelles zones sont RW, RX, RO — cela donnera l’« attache mémoire » du binaire.
- Regarder les protections via
-
Analyse des appels externes (libc) : repérer les sections
.plt,.got,.got.plt,.dynsym— voir s’il y a des appels dynamiques, ce qui pourrait être utile pour ret2plt / ret2libc. -
Évaluer la surface d’attaque :
- si GOT modifiable → possibilité de GOT-overwrite ; sinon → envisager ROP, ret2libc, info leak ;
- si NX + ASLR + PIE → shellcode traditionnel improbable, nécessité de gadgets / dynamique.
-
Quand un overflow est présent :
- au minimum, tester si on peut écraser la return-address / saved RIP (stack overflow), ou des pointeurs, ou des structures ;
- si canari présent → besoin d’un info leak pour le contourner ;
- si NX + pas de shellcode → prévoir ROP ;
- si ASLR/PIE → prévoir un leak d’adresse pour rebâtir le layout mémoire.
Corrections et nuances sur certaines formulations
- « On retient l’adresse des fonctions de la libc afin d’avoir à scanner à chaque fois cette dernière lors de l’appel » : ce n’est pas tout à fait exact — on retient les pointeurs dynamiques dans la GOT, pour éviter de scanner à chaque appel. C’est un mécanisme d’indirection + résolution dynamique.
- « On scanne toutes les versions en cherchant celle où la fonction et l’offset correspondent » : c’est une approche de brute-forçage / matching d’adresses dans la libc, mais en exploitation moderne, on préfère souvent fuiter une adresse via un info-leak (printf leak, GOT leak, format-string, heap leak), puis calculer les autres adresses via les offsets — plus fiable que le brute-force.
- La GOT / GOT.PLT est modifiable si le binaire n’a pas RELRO (ou partial RELRO) — ce n’est pas « toujours accessible » : ça dépend des protections. L’hypothèse « on est capable d’écrire dans data et arbitraire à un certain emplacement » est valable seulement dans certains contextes.
Briques de base à garder en tête
- La mémoire virtuelle + mapping ELF segments/sections est la base de la modélisation d’un binaire en mémoire. Il faut savoir quelle zone correspond à quoi (code, données, got, plt, rodata, heap, stack).
- Les protections mémoire + runtime (NX, ASLR, PIE, RELRO, canari) modifient fortement la surface d’attaque — il ne suffit pas d’un bug, il faut le bon contexte pour l’exploiter.
- Le linking dynamique (libc, dynlibs) via GOT/PLT est un vecteur majeur pour l’exploitation — GOT overwrite, ret2plt, ret2libc, ROP over GOT/PLT, info leaks.
- Exploiter un bug, c’est souvent combiner plusieurs techniques : overflow / write-what-where / leak / gadgets / contournement des protections — une chaîne d’actions, pas un simple bug + shellcode.