# Le lab `wosuid` — RCE root **sans** bit setuid ``` foowosd un démon volontairement vulnérable qui est root parce qu'il a été *DÉMARRÉ* en tant que root (port 2344, loopback seulement par défaut) foowosc l'exploit : transforme un débordement de pile en un **shell** root en exécutant de la shellcode — les mêmes 23 octets qui ont cassé le démon user-level `food` du lab principal ``` C'est le troisième lab de la série. Même chaîne d'outils d'exploit, même style, une différence fondamentale : | Lab | comment le processus cible devient root | shell `uid=0(root)` ? | |----------|------------------------------------------------|----------------------| | food/fooc | jamais — c'est un démon utilisateur ordinaire | non | | foosd/foosc | le bit SUID (`chmod u+s`) — euid 0, ruid 1000 | oui (exige `setreuid` dans la shellcode, car bash réinitialise euid→ruid) | | **foowosd/foowosc** | **aucun — root *démarre* le démon** (sudo / systemd `User=root`) | **oui (shellcode `execve` ordinaire)** | Le bit setuid est un *moyen de transport* des privilèges — pas les privilèges eux-mêmes. Un démon démarré par root a des uids réel, effectif et sauvegardé tous égaux à 0. Pour le noyau, c'est root, point final ; il ne peut pas et ne veut pas savoir si le processus y est arrivé via `+s` sur un fichier ou via `sudo ./foowosd`. Le débordement d'un démon démarré par root est donc un exploit root — *« je n'ai pas de binaires SUID » n'est pas la même chose que « je ne suis pas exploitable ».* C'est toute la leçon de ce lab. Tout ce qui suit, c'est le mécanisme. --- ## Démarrage rapide (ce que l'utilisateur a demandé) ``` cd wosuid make # compile le démon, l'exploit et la harnesse de test ``` ### Le vrai truc — exécutez le démon en tant que **root** ``` sudo make run-root # démarre foowosd en uid 0 (processus, pas état de fichier) make test-root # chaque technique doit maintenant donner uid=0(root) ``` ### Pas de sudo ? Le chemin noyau identique via un user namespace ``` make run-root-ns # uid 0 dans un user namespace — aucun mot de passe requis make test-root # mêmes verdicts ; utilisé par la CI et tous ceux sans sudo ``` ### Référence — démon comme votre utilisateur normal (root nulle part) ``` make run # foowosd tourne avec vos uids make test # les exploits atterrissent des shells, mais `root` est attendu MISSING ``` ### Rituel de nettoyage (toujours : c'est un lab de shell root) ``` make stop ``` `foowosd` n'est *pas* setuid, et rien dans ce répertoire ne fait jamais `chmod +s` — c'est le but. L'état dangereux est **le processus**, pas le fichier. --- ## Quand on vous dit de poser le bit SUID Ça ne vous sera **pas** demandé. Ce lab n'a délibérément pas de bit SUID : - `foowosd` est compilé, vous appartient et a des permissions ordinaires comme n'importe quel programme. - Il devient root comme les vrais démons — en étant *démarré* par root. - `make run-root` utilise `sudo` pour exactement ça, et `make run-root-ns` obtient un vrai processus uid 0 sans rien de tout cela. Le bit Suid appartient au lab *frère* (`foosd`). Le contraste entre les deux est le programme : 1. Lab SUID : le bit donne **euid 0, mais ruid 1000** → `execve("/bin/sh")` est dégradé par le gardien de bash (`euid != ruid` → reset) → la shellcode doit d'abord appeler `setreuid(0,0)` (payload de 32 octets). 2. Ce lab : root **démarre** le processus → **ruid == euid == 0** → le gardien n'a rien à réinitialiser → la shellcode `execve` ordinaire de 23 octets garde root. Même débordement. Même technique. *Origine* des privilèges différente, forme de payload différente. C'est la leçon en miniature. --- ## Le protocole Quel que soit le type de client qui se connecte, foowosd le salue avec : ``` FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (le banner + les fuites) BUF=0x7ffd… (l'adresse du tampon) ``` `ids=euid/ruid` est le canal *« suis-je root ? »*. foowosc affiche un avertissement sonore quand euid n'est pas 0 (c'est-à-dire que vous avez démarré le démon en utilisateur ordinaire) : la payload atterrit quand même, mais le shell devient un shell utilisateur, et appeler l'exploit « cassé » serait faux — il n'escalade juste pas. > Note d'orthographe : `ids=`, pas `euid=`/`ruid=`. La harnesse de test prouve > un `id` vivant en matchant la forme littérale `uid=NNN(`, donc le banner ne > doit jamais contenir une sous-chaîne qui satisfait elle-même le contrôle. > (Dans le lab SUID, exactement ce piège a produit un spectaculaire faux > positif.) --- ## Les techniques d'exploit (`foowosc -t …`) Les quatre chemins d'exploit ci-dessous fonctionnent contre foowosd. Quand le démon est root, **tout ce qui spawn quelque chose donne root** — contrairement au lab SUID où ret2win/ret2libc étaient silencieusement dégradés en uid 1000 par le gardien de bash. Ici, il n'y a pas de mismatch à surveiller. | `-t` | ce qui se passe | quand le démon est root | |---------------|---------------------------------------------------------------------|---------------------| | `shellcode` | `execve("/bin/sh", NULL, NULL)` de 23 octets s'exécute sur la pile. | **shell root** (par défaut) | | `ret2win` | saut vers `win()` → `execl("/bin/sh")` | **shell root** | | `ret2libc` | ROP : `pop rdi; ret` → `"/bin/sh"` → `system()` | **shell root** | | `demo` | débordement de bourrage uniquement — attendez un SIGSEGV dans le log | crash, par conception | | `leak` | affiche juste les fuites, n'envoie pas de payload | n/a | ``` ./foowosc -t shellcode # interactif ; cible par défaut 127.0.0.1:2344 ./foowosc -t shellcode -n # envoyer et rapporter, pas de session interactive ``` Une session interactive réussie relaie votre terminal vers le shell *sur la victime* — il y a exactement un shell dans le tableau, et c'est `/bin/sh` qui tourne en root à l'intérieur de foowosd. Tapez `id` pour voir `uid=0(root)`. ### Pourquoi il n'y a pas de technique `ret2win-root` ici foosc en avait une — elle sautait vers un `win_root()` qui appelait `setreuid(0,0)` avant l'exec, parce qu'un processus setuid tournait avec un uid réel qui disait encore 1000. Un processus *démarré* par root a déjà l'uid réel à 0 ; il n'y a rien à nettoyer, donc la fonction et la technique supplémentaires n'auraient rien appris. Supprimé. --- ## Ce que fait foowosc, étape par étape 1. **Analyse statique** — `objdump -d` de `./foowosd`. Trouve `vulnerable_handler`, `win()`, le `lea -0x50(%rbp)` qui adresse `buf`, et le premier `ret` nu. À partir du décalage, il calcule `rip_off = 80 + 8 = 88`. Rien n'est hardcodé ; ça survit à une recompilation. 2. **Auto-introspection** — lit son propre `/proc/self/maps` et `dlsym()`e `system`/`read` pour apprendre les *offsets* de la libc. La base libc de la cible est `leaked_read − off_read`, puis `system = base + off_system`, etc. Cette arithmétique de delta est la raison pour laquelle les exploits survivent aux versions de libc. 3. **Connecte** — lit le banner/les fuites (`ids=`, `stack=`, `libc=`, `BUF=`). 4. **Construit la payload** — pour `shellcode` : 23 octets de code machine, du bourrage jusqu'à `rip_off`, puis la RIP sauvegardée = `buf` (pour que le `ret` saute dans le code). Pour `ret2win`/`ret2libc` : des adresses calculées depuis l'analyse — aucune exécution de pile nécessaire. 5. **Le correctif d'alignement** — un `ret` nu détourné donne au callee `rsp ≡ 8 (mod 16)`, et le code SSE2 de glibc fault en `movaps` sur une pile mal alignée (le crash-reporter journalise `si_addr=(nil)` — l'indice). foowosc insère un gadget `ret` supplémentaire avant la vraie cible et rétablit l'invariant. Une technique de gants de soie dans un lab de shellcode, mais c'est la différence entre une payload qui « marche parfois » et une qui marche toujours. 6. **Envoie, puis relaie** — le processus victime *est* le shell ; ce processus ne fait que splisser des octets. Pas de shell local, pas d'autre lecteur — le bug à curseur de lecture unique (un octet mangé par morceau) est documenté dans `become_shell()`. --- ## Les bugs délibérés du démon (tous dans `foowosd.c`, toutes de vraies classes CWE) | # | bug | CWE | note | |---|-----|-----|------| | 1 | `read(fd, buf, 512)` dans un tampon de pile de 64 octets | [CWE-120](https://cwe.mitre.org/data/definitions/120.html) | le débordement : 448 octets au-delà de `buf`, RIP sauvegardée à +88 | | 2 | seulement `snprintf(line, …, "%.*s", …)` ; mais `%` de l'attaquant dans le chemin d'echo | [CWE-134](https://cwe.mitre.org/data/definitions/134.html) | la fuite est ici la vraie payload ; un `%n` dans un processus *root* serait un write-what-where en root | | 3 | les enfants gardent root en traitant des entrées non fiables | [CWE-271](https://cwe.mitre.org/data/definitions/271.html) | le `drop_privs()` correct (setgroups→setgid→setuid, dans cet ordre, avec vérification) est dans le fichier, commenté, *délibérément jamais appelé* | | 4 | `ids=`, `stack=`, `libc=`, `BUF=` révélés à n'importe quel client | [CWE-200](https://cwe.mitre.org/data/definitions/200.html)) | sans ces fuites, les techniques shellcode et ret2libc ne pourraient pas calculer d'adresses (ASLR les vaincrait) | Le handler a exactement la même forme `buf[64]`/`read(512)` que les deux autres labs, donc le pipeline commun de reconnaissance par objdump fonctionne sans changement. --- ## Comment inspecter le démon (parcours d'apprentissage) ``` make status # tourne-t-il ? en quel uid ? l'état du fichier est montré make run-root # ou run / run-root-ns ./foowosc -t leak # voyez le banner et les fuites, n'envoyez rien ./foowosc -t demo # débordement de bourrage -> SIGSEGV, journalisé avec RIP/rsp ./foowosc -t shellcode # le shell root interactif make test-root # matrice complète, toutes les techniques, --must-root ``` Crash-reporter : Au SIGSEGV, le démon journalise l'adresse d'erreur, RIP et RSP. Un `ret` dans un `0x4141…` non canonique fault sur le `ret` lui-même (RIP comme `0x4028xx`, `si_addr=(nil)`) — bon à savoir avant de lire une ligne de log comme une déréférence NULL. --- ## Pourquoi la shellcode fait 23 octets, pas 32 ``` 31 f6 xor esi, esi ; argv = NULL 31 d2 xor edx, edx ; envp = NULL 48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0" 57 push rdi 48 89 e7 mov rdi, rsp 6a 3b push 0x3b ; 59 = execve 58 pop rax 0f 05 syscall ``` Le lab SUID a besoin de `setreuid(0,0)` devant ça. Pas ce lab, pour la raison répétée partout : **ruid est déjà 0**, parce que root a démarré le processus. `make verify` prouve que les octets dans `foowosc.c` sont, octet pour octet, ce que `shellcode.S` assemble. --- ## Contre-mesures — ce que `make hardened` change Build durcie (`-fstack-protector-strong -fPIE -pie -z noexecstack`) : | technique | `foowosd` vulnérable | `foowosd_hardened` durci | |-----------|----------------------|------------------------------| | `shellcode` | shell root (pile exécutable) | SIGSEGV au contrôle de canary / NX | | `ret2win` / `ret2libc` | shell root | la canary abort le `ret` — mais notez : une build *PIE* rend aussi ces adresses aléatoires | | `demo` | SIGSEGV, journalisé | SIGSEGV, journalisé | `make test-hardened` le démontre en direct. L'observation importante n'est pas seulement que les contre-mesures ont tué les techniques — c'est qu'elles n'ont **pas** rendu le démon « non-root ». Une build durcie qui est toujours *démarrée* en root reste un démon root ; la contre-mesure ne fait que relever la barre pour l'attaquant. Moindre privilège (`drop_privs()`) et sécurité mémoire sont deux bugs différents, et un démon qui n'a pas besoin de root ne devrait pas en avoir. --- ## Garde-fous (même politique que le lab SUID) - **Loopback seulement.** foowosd refuse de se lier ailleurs qu'à `127.0.0.1` / `localhost` / `::1`, sauf si vous donnez `-L`. Un démon root sur une vraie interface est un *service root distant*. `-L` existe seulement pour montrer le garde-fou ; ne l'utilisez pas sur quelque chose qui compte. - **L'état est journalisé à voix haute.** Au démarrage, il affiche `ruid/euid` et si c'est un processus root, pour que vous sachiez toujours quel résultat d'exploit attendre. - **Les verdicts viennent du code de sortie** dans `make test*` (code retour de la harnesse pty), jamais d'un grep sur sa sortie — la sortie grepable ment. - **Le pty doit tourner en cooked + ECHO off**, sinon la harnesse s'échoit sa propre ligne de commande et falsifie le marqueur. La harnesse désactive ECHO et garde ECHONL actif. - **Rituel de nettoyage :** `make stop` après chaque session. Si le démon appartient à root, `stop` vous dit de lancer `sudo pkill -x foowosd`. - Ne faites jamais tourner ça sur une machine à laquelle vous tenez. Ça existe pour distribuer des shells `uid=0` sur l'interface loopback. --- ## Exercices 1. Lancez `make run` (démon utilisateur), puis `./foowosc -t shellcode`. Pourquoi le shell n'est-il pas root ? (Vérifiez `ids=` dans le banner — foowosc vous le dit avant même que vous vous connectiez.) 2. `make stop && sudo make run-root && make test-root`. Expliquez à partir de la ligne du banner pourquoi les quatre techniques donnent maintenant `uid=0(root)`. 3. Trouvez `drop_privs()` dans `foowosd.c` et lisez *pourquoi l'ordre* de `setgroups → setgid → setuid` compte. Décidez où dans `main()` il aurait sa place, et ce que devient la surface d'attaque du lab quand il est réellement appelé. 4. Calculez `rip_off` à la main depuis `objdump -d foowosd` : trouvez le `lea -0xNN(%rbp)` de `buf` dans `vulnerable_handler`, puis `NN + 8`. foowosc fait exactement ça ; vérifiez son calcul contre le vôtre. 5. `make hardened && make test-hardened`. Quelle technique tombe sur la canary, et laquelle sur NX ? Pourquoi le durcissement ne change-t-il pas ce que `make status` rapporte sur *le processus* ? 6. Comparez les shellcodes des deux labs : 23 octets ici, 32 pour foosd. Que font les 9 octets supplémentaires, et pourquoi ne sont-ils nécessaires que dans le cas SUID ? 7. Lisez le commentaire de `become_shell()` sur la curseur de lecture unique. Reconstruisez mentalement l'état d'échec : deux lecteurs sur une socket, c'est le shell de connexion qui mange un octet par morceau — « uid=1000… » arrive comme « id=1000… ». Pourquoi un processus relay ne peut-il jamais avoir ce bug ? --- ## Fichiers ``` foowosd.c le démon root vulnérable (chaque ligne commentée) foowosc.c l'exploit (chaque ligne commentée) shellcode.S l'assembleur de référence pour la payload de 23 octets tests/pty_wosuid_test.c la harnesse pty (marqueur + contrôle strict de forme id) Makefile build / run / run-root / run-root-ns / test / test-root / verify / hardened / clean … ``` Labs frères : `../food.c`/`../fooc.c` (baseline user-level, port 2342) et `../suid/` (démon root SUID `foosd`/`foosc`, port 2343). Les ports sont délibérément différents — vous pouvez faire tourner les trois en même temps et croiser leurs lignes `ids=` dans le banner.