Escape - Insomni'hack 2026
Challenge par drp3ab0dy. Un binaire qui te donne une fuite mémoire, un overflow, et absolument aucun moyen d'afficher quoi que ce soit. Le genre de situation où il faut devenir créatif.
Contexte
Premier Insomni'hack pour moi. En arrivant sur ce challenge, j'ai tout de suite vu que le seccomp bloquait write. Du coup impossible d'exfiltrer le flag de manière classique. Pendant un moment j'ai tourné en rond en cherchant un bypass seccomp, avant de réaliser que la vraie question n'était pas "comment afficher le flag ?" mais "comment savoir si j'ai le bon caractère sans rien afficher ?".
C'est là que l'idée de l'oracle aveugle est apparue : transformer un timeout en "oui" et un crash en "non". Le reste, c'est de la plomberie ROP.
INS{No_wr1te!?_n0_Str3ss_g4dGet_3verywh3re!}Sommaire
- Recon
- Seccomp, le mur
- Le plan d'attaque
- Leak libc
- Les gadgets qui font tout
- Pourquoi __strverscmp et pas strcmp
- La chaîne ROP complète
- Le cauchemar du bruit réseau
- Conclusion
1. Recon
On commence par les classiques. Le binaire est assez permissif côté protections :
checksec
Arch: amd64
NX: enabled
PIE: disabled (non-PIE)
Canary: disabled
RELRO: partialPas de PIE, pas de canary, RELRO partiel : on va pouvoir écrire dans la GOT et utiliser des adresses fixes. Bon début.
Le programme se décompose en deux phases bien distinctes :
- Avant le seccomp : il lit un entier avec
scanf("%lld", &ptr)puis affiche la valeur à cette adresse. Autrement dit, on a une lecture arbitraire de 8 octets. Un seul tir, mais c'est suffisant. - Après le seccomp : il appelle
read(0, buf, 0x1000)sur un buffer de pile ridiculement petit. L'offset jusqu'au RIP sauvegardé est de 40 octets. Overflow trivial.
2. Seccomp, le mur
Le filtre est brutal. Voici la liste complète de ce qu'on a le droit de faire :
syscalls autorisés
read (0)
open (2)
openat (257)
exit (60)Vous avez bien lu : pas de write, pas de sendto, pas de mmap. On peut ouvrir le fichier flag, on peut le lire en mémoire, mais on n'a strictement aucun moyen de renvoyer son contenu.
3. Le plan d'attaque
L'idée est de ne jamais essayer d'afficher le flag, mais de le deviner caractère par caractère en observant le comportement du programme :
- Fuiter l'adresse de
putsvia la GOT pour calculer la base libc - Construire une ROP chain avec
ret2csu - Ouvrir le flag avec
syscall(SYS_open, ...) - Lire le flag en mémoire
- Comparer un octet du flag avec un caractère deviné en utilisant
__strverscmp - Si égal : le programme reste en vie (timeout). Si différent : le programme crash.
4. Leak libc
La primitive de lecture avant le seccomp nous donne un seul coup. On le dépense sur puts@got :
adresses
PUTS_GOT = 0x404028
puts offset dans la libc : 0x84420Le programme nous crache la valeur pointée, et on en déduit directement la base :
calcul
libc_base = leak_puts - 0x84420À partir de là, on peut calculer toutes les adresses dont on a besoin :
offsets libc (remote)
puts : 0x84420
syscall : 0x118940
__strverscmp : 0x9f2605. Les gadgets qui font tout
Comme le binaire n'est pas PIE, on peut utiliser les gadgets directement par adresse. Le classique ret2csu est présent et exploitable :
ret2csu - setup
0x40147a : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; retret2csu - call
0x401460 : call [r15 + rbx*8]
rdi = r12d
rsi = r13
rdx = r14Avec ça, on peut appeler n'importe quelle fonction dont on a l'adresse en mémoire, en contrôlant les trois premiers arguments. C'est suffisant pour read, open et __strverscmp.
Mais le vrai bijou, c'est celui-ci :
gadget oracle @ 0x4011c3
eax == 0 → exécution continue → tombe sur un read → timeout
eax != 0 → saut invalide → crash / EOF__strverscmp, eax vaut 0 si les chaînes sont identiques. Le gadget traduit ça en un comportement observable de l'extérieur : le programme reste vivant ou il meurt.6. Pourquoi __strverscmp et pas strcmp
Premier réflexe : utiliser strcmp ou memcmp. Sauf que dans cette version de la libc, ces fonctions sont optimisées avec des instructions vectorielles qui compliquent l'exploitation (la valeur de retour n'est pas un simple 0 / non-zero dans eax comme on le voudrait).
__strverscmp est beaucoup plus coopérative :
- Retourne 0 si les chaînes sont égales
- Retourne non-zero sinon
- Résultat proprement dans
eax, sans optimisations exotiques
Exactement ce qu'il faut pour alimenter le gadget conditionnel.
7. La chaîne ROP complète
Chaque tentative de caractère envoie un payload complet qui fait tout ça d'un coup. C'est une seule ROP chain assez longue, mais chaque étape est logique :
- Préparer la GOT : écrire l'adresse de
syscall()dansEXIT_GOTet celle de__strverscmp()dansPRINTF_GOT. On détourne des entrées GOT existantes pour y stocker nos pointeurs. - Écrire le chemin :
/home/pwn/flagdans la section.datadu binaire. - Ouvrir le flag :
syscall(SYS_open, path, 0). Le fd retourné sera 3. - Avancer dans le fichier : consommer
indexoctets avecread(3, ...)pour se positionner sur le caractère qu'on veut tester. - Lire le caractère cible : lire 1 octet du flag dans
EXIT_GOT(préalablement remis à zéro). - Écrire le candidat : lire l'octet deviné depuis stdin dans
DUMMY. - Comparer :
__strverscmp(EXIT_GOT, DUMMY) - Oracle : le gadget conditionnel fait le reste.
8. Le cauchemar du bruit réseau
En théorie, l'oracle est binaire : timeout ou crash. En pratique, le remote était instable, surtout vers la fin du CTF. Des connexions qui timeout alors qu'elles n'auraient pas dû, des crashs aléatoires sur le bon caractère... bref, du bruit.
Pour stabiliser l'extraction, j'ai dû ajouter pas mal de logique autour de l'oracle brut :
- Un prefilter pour éliminer rapidement les caractères impossibles
- Plusieurs tours de vérification par caractère
- Des seuils de majorité : un caractère n'est accepté que s'il passe une majorité des tentatives
- Des tests de suffixe pour les derniers octets, plus sujets aux erreurs
- Un test d'EOF pour détecter la fin du flag
\n. Le vrai flag s'arrête juste avant. Si on ne le sait pas, on cherche un caractère fantôme.9. Conclusion
Ce challenge m'a beaucoup plu parce qu'il force à sortir des sentiers battus. On a toutes les primitives classiques (leak, overflow, ROP) mais l'absence de write rend la dernière étape non triviale.
La solution repose sur une idée simple : si tu ne peux pas parler, fais du bruit. En l'occurrence, la différence entre un programme qui vit et un programme qui meurt suffit à transmettre de l'information, un bit à la fois.
Les briques techniques ( open, read, __strverscmp, ret2csu) ne sont que de la plomberie. Le vrai saut, c'est le passage mental de "je dois afficher le flag" à "je dois deviner le flag".
Flag final
INS{No_wr1te!?_n0_Str3ss_g4dGet_3verywh3re!}