foo/wosuid/README.NL.md
2026-09-29 09:39:24 +02:00

14 KiB
Raw Blame History

Het wosuid-lab — root-RCE zonder setuid-bit

  foowosd   een bewust kwetsbare daemon die root is omdat hij als *GESTART* als
            root  (poort 2344, standaard alleen loopback)
  foowosc   het exploit: verandert een stack-overloop in een **root**-shell door
            shellcode uit te voeren — dezelfde 23 bytes die de user-level-
            `food`-daemon in het hoofdlab kraakten

Dit is het derde lab in de serie. Dezelfde exploit-werktuigketen, dezelfde stijl, één fundamenteel verschil:

Lab hoe het doelproces root wordt uid=0(root)-shell?
food/fooc nooit — het is een gewone user-daemon nee
foosd/foosc de SUID-bit (chmod u+s) — euid 0, ruid 1000 ja (vereist setreuid in de shellcode, want bash reset euid→ruid)
foowosd/foowosc geen — root start de daemon (sudo / systemd User=root) ja (gewone execve-shellcode)

De setuid-bit is een transportmiddel voor privileges — niet de privileges zelf. Een daemon die door root is gestart, heeft reële, effectieve en opgeslagen uid allemaal 0. Voor de kernel is dat root, punt uit; hij kan en wil niet weten of het proces daar via +s op een bestand of via sudo ./foowosd is gekomen. De overloop in een root-gestarte daemon is dus een root-exploit — "ik heb geen SUID-binaries" is niet hetzelfde als "ik ben niet exploiteerbaar".

Dat is de hele les van dit lab. Al het andere hieronder is het mechanisme.


Snelle start (wat de gebruiker vroeg)

cd wosuid
make                 # bouwt de daemon, het exploit en de test-harness

Het echte werk — draai de daemon als root

sudo make run-root      # start foowosd als uid 0 (proces, geen bestandstoestand)
make test-root          # elke techniek moet nu uid=0(root) geven

Geen sudo? Het identieke kernelpad via een user-namespace

make run-root-ns        # uid 0 in een user-namespace — geen wachtwoord nodig
make test-root          # dezelfde oordelen; gebruikt door CI en iedereen zonder sudo

Baseline — daemon als je normale gebruiker (nergens root)

make run                # foowosd draait met jouw uids
make test               # exploits landen shells, maar `root` wordt verwacht als MISSING

Opruimritueel (altijd: dit is een root-shell-lab)

make stop

foowosd is niet setuid, en niets in deze map doet ooit chmod +s — dat is het punt. De gevaarlijke toestand is het proces, niet het bestand.


Wanneer je wordt gezegd de SUID-bit te zetten

Dat zul je niet worden. Dit lab heeft bewust geen SUID-bit:

  • foowosd wordt gebouwd, is eigendom van jou en heeft gewone toestanden als elk ander programma.
  • Hij wordt root zoals echte daemons dat doen — door gestart te worden door root.
  • make run-root gebruikt sudo voor precies dat, en make run-root-ns regelt een echte uid-0-proces zonder ook maar iets daarvan.

De Suid-bit hoort bij het zuster-lab (foosd). Het contrast tussen de twee is de leerstof:

  1. SUID-lab: de bit geeft euid 0, maar ruid 1000 → execve("/bin/sh") wordt gedegradeerd door de wacht van bash (euid != ruid → reset) → de shellcode moet eerst setreuid(0,0) aanroepen (32-byte-payload).
  2. Dit lab: root start het proces → ruid == euid == 0 → de wacht heeft niets om te resetten → de gewone 23-byte execve-shellcode houdt root.

Dezelfde overloop. Dezelfde techniek. Andere oorsprong van privileges, andere payload-vorm. Dat is de les in het klein.


Het protocol

Welke client er ook verbindt, foowosd begroet hem met:

FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f…      (banner + leaks)
BUF=0x7ffd…                                             (het bufferadres)

ids=euid/ruid is het "ben ik root?"-kanaal. foowosc print een luide waarschuwing wanneer euid niet 0 is (dus je startte de daemon als gewone gebruiker): de payload landt nog steeds, maar de shell wordt een gebruiker-shell, en het exploit "kapot" noemen zou fout zijn — hij escaleert alleen niet.

Spellingsnoot: ids=, niet euid=/ruid=. De test-harness bewijst een levende id door de letterlijke vorm uid=NNN( te matchen, dus het banner mag nooit een deelstring bevatten die zelf aan de controle voldoet. (In het SUID-lab produceerde precies die val een spectaculaire fout-positief.)


De exploit-technieken (foowosc -t …)

Alle vier de exploit-paden hieronder werken tegen foowosd. Wanneer de daemon root is, geeft alles dat iets spawnt root — in tegenstelling tot het SUID-lab, waar ret2win/ret2libc stilletjes door de wacht van bash naar uid 1000 werden gedegradeerd. Hier is er geen mismatch om te bewaken.

-t wat er gebeurt wanneer de daemon root is
shellcode 23-byte execve("/bin/sh", NULL, NULL) draait op de stack. root-shell (standaard)
ret2win sprong naar win() → execl("/bin/sh") root-shell
ret2libc ROP: pop rdi; ret → "/bin/sh" → system() root-shell
demo alleen rommel-overloop — verwacht een SIGSEGV in de daemonlog crash, volgens ontwerp
leak print alleen leaks, stuurt geen payload n.v.t.
./foowosc -t shellcode       # interactief; standaardtarget 127.0.0.1:2344
./foowosc -t shellcode -n    # sturen en rapporteren, geen interactieve sessie

Een succesvolle interactieve sessie schakelt je terminal door naar de shell op het slachtoffer — er is precies één shell in het beeld, en dat is /bin/sh die als root binnenin foowosd draait. Typ id om uid=0(root) te zien.

Waarom er hier geen ret2win-root-techniek is

foosc had er één — die sprong naar een win_root() die setreuid(0,0) aanriep vóór exec, omdat een setuid-proces draaide met een reële uid die nog 1000 zei. Een root-gestart proces heeft de reële uid al 0; er valt niets op te ruimen, dus de extra functie en techniek zouden niets leren. Verwijderd.


Wat foowosc doet, stap voor stap

  1. Statische analyse — objdump -d van ./foowosd. Vindt vulnerable_handler, win(), de lea -0x50(%rbp) die buf adresseert, en de eerste kale ret. Uit de verplaatsing berekent het rip_off = 80 + 8 = 88. Niets is hardcoded; het overleeft een herbouw.
  2. Zelf-introspectie — leest zijn eigen /proc/self/maps en dlsym()t system/read om de libc-offsets te leren. De libc-base van het doelwit is leaked_read − off_read, daarna system = base + off_system, enz. Die delta-rekenkunde is de reden dat exploits libc-versies overleven.
  3. Verbind — leest het banner/leaks (ids=, stack=, libc=, BUF=).
  4. Bouw de payload — voor shellcode: 23 bytes machinecode, padding tot rip_off, daarna de opgeslagen RIP = buf (zodat de ret op de code springt). Voor ret2win/ret2libc: adressen berekend uit de analyse — geen stackuitvoering nodig.
  5. De uitlijnfix — een gekaapte kale ret geeft de callee rsp ≡ 8 (mod 16), en glibc's SSE2-code faalt met movaps op een niet-uitgelijnde stack (de crash-reporter logt si_addr=(nil) — de hint). foowosc plaatst één extra ret-gadget vóór het echte doelwit en herstelt de invariant. Een zijdenhandschoentechniek in een shellcode-lab, maar het is het verschil tussen een payload die "soms werkt" en een die altijd werkt.
  6. Stuur, daarna schakel door — het doelproces is de shell; dit proces splist alleen bytes. Geen lokale shell, geen andere lezer — de single-read-cursor-bug (één opgegeten byte per chunk) is gedocumenteerd in become_shell().

De bewuste bugs van de daemon (allemaal in foowosd.c, allemaal echte CWE-klassen)

# bug CWE notitie
1 read(fd, buf, 512) in een 64-byte stack-buffer CWE-120 de overloop: 448 bytes voorbij buf, opgeslagen RIP op +88
2 alleen snprintf(line, …, "%.*s", …); maar aanvaller-% in het echo-pad CWE-134 het lek is hier de echte payload; een %n in een root-proces zou write-what-where als root zijn
3 kinderen behouden root terwijl ze onbetrouwbare input afhandelen CWE-271 de correcte drop_privs() (setgroups→setgid→setuid, in die volgorde, met verificatie) staat in het bestand, gecommentarieerd, bewust nooit aangeroepen
4 ids=, stack=, libc=, BUF= onthuld aan elke client CWE-200 zonder deze leaks konden de shellcode- en ret2libc-technieken geen adressen berekenen (ASLR zou ze verslaan)

De handler heeft precies dezelfde buf[64]/read(512)-vorm als de twee andere labs, dus de gedeelde objdump-gebaseerde herkenningspipeline werkt ongewijzigd.


Zo inspecteer je de daemon (leerpad)

make status            # draait hij? als welke uid? bestandstoestand wordt getoond
make run-root          # of run / run-root-ns
./foowosc -t leak      # zie het banner en leaks, stuur niets
./foowosc -t demo      # rommel-overloop -> SIGSEGV, gelogd met RIP/rsp
./foowosc -t shellcode # de interactieve root-shell
make test-root         # volledige matrix, alle technieken, --must-root

Crash-reporter: Bij SIGSEGV logt de daemon het foutadres, RIP en RSP. Een ret in een niet-canonieke 0x4141… faalt bij de ret zelf (RIP als 0x4028xx, si_addr=(nil)) — goed om te weten vóór je een logregel als NULL-dereferentie verkeerd leest.


Waarom de shellcode 23 bytes is, niet 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

Het SUID-lab heeft setreuid(0,0) vóór dit nodig. Dit lab niet, om de reden die overal herhaald wordt: ruid is al 0, omdat root het proces startte. make verify bewijst dat de bytes in foowosc.c byte-voor-byte zijn wat shellcode.S assembleren tot.


Tegenmaatregelen — wat make hardened verandert

Geharde build (-fstack-protector-strong -fPIE -pie -z noexecstack):

techniek kwetsbare foowosd geharde foowosd_hardened
shellcode root-shell (uitvoerbare stack) SIGSEGV bij de canary-controle / NX
ret2win / ret2libc root-shell canary aborted de ret — maar merk: een PIE-build maakt deze adressen ook willekeurig
demo SIGSEGV, gelogd SIGSEGV, gelogd

make test-hardened demonstreert het live. De belangrijke observatie is niet alleen dat de tegenmaatregelen de technieken doodden — het is dat ze de daemon niet "niet-root" maakten. Een geharde build die nog steeds als root gestart wordt, is nog steeds een root-daemon; de tegenmaatregel verhoogt alleen de lat voor de aanvaller. Minste privilege (drop_privs()) en geheugenveiligheid zijn twee verschillende bugs, en een daemon die geen root nodig heeft, zou het niet moeten hebben.


Veiligheidsleuningen (zelfde beleid als het SUID-lab)

  • Alleen loopback. foowosd weigert iets anders dan 127.0.0.1 / localhost / ::1 te binden, tenzij je -L geeft. Een root-daemon op een echte interface is een externe root-dienst. -L bestaat alleen om de wacht te tonen; gebruik het niet op iets dat ertoe doet.
  • De toestand wordt luid gelogd. Bij de start print hij ruid/euid en of dit een root-proces is, zodat je altijd weet welk exploit-resultaat je moet verwachten.
  • Oordelen komen uit exit-status in make test* (de retourcode van de pty-harness), nooit uit het greppen van zijn stdout — grepbare output liegt.
  • De pty moet cooked + ECHO off draaien, anders echoët de harness zijn eigen commandoregel en vervalst de markering. De harness zet ECHO uit en houdt ECHONL aan.
  • Opruimritueel: make stop na elke sessie. Als de daemon root-bezeten is, zegt stop je sudo pkill -x foowosd te draaien.
  • Draai dit nooit op een host waar je om geeft. Het bestaat om uid=0-shells over de loopback-interface uit te delen.

Oefeningen

  1. Draai make run (user-daemon), daarna ./foowosc -t shellcode. Waarom is de shell niet root? (Check ids= in het banner — foowosc zegt het je vóór je überhaupt verbindt.)
  2. make stop && sudo make run-root && make test-root. Verklaar aan de hand van de bannerregel waarom alle vier de technieken nu uid=0(root) geven.
  3. Vind in foowosd.c drop_privs() en lees waarom de volgorde van setgroups → setgid → setuid ertoe doet. Bepaal waar in main() hij thuis zou horen, en wat het aanvalsoppervlak van het lab wordt wanneer hij daadwerkelijk wordt aangeroepen.
  4. Bereken rip_off met de hand uit objdump -d foowosd: vind de lea -0xNN(%rbp) van buf binnenin vulnerable_handler, daarna NN + 8. foowosc doet precies dat; controleer zijn rekensom tegen de jouwe.
  5. make hardened && make test-hardened. Welke techniek valt voor de canary, en welke voor NX? Waarom verandert harden niet wat make status over het proces rapporteert?
  6. Vergelijk de shellcodes van de twee labs: 23 bytes hier, 32 voor foosd. Wat doen de extra 9 bytes, en waarom zijn ze alleen in het SUID-geval nodig?
  7. Lees de commentaar van become_shell() over de enkele lees-cursor. Maak de faaltoestand mentaal na: twee lezers op één socket betekent dat de login-shell één byte per chunk eet — "uid=1000…" arriveert als "id=1000…". Waarom kan een relay-proces deze bug nooit hebben?

Bestanden

foowosd.c                 de kwetsbare root-daemon (elke regel gecommentarieerd)
foowosc.c                 het exploit (elke regel gecommentarieerd)
shellcode.S               referentie-assembly voor de 23-byte-payload
tests/pty_wosuid_test.c   de pty-harness (markering + strikte id-vorm-controle)
Makefile                  build / run / run-root / run-root-ns / test /
                          test-root / verify / hardened / clean …

Zuster-labs: ../food.c/../fooc.c (user-level-baseline, poort 2342) en ../suid/ (SUID-root-daemon foosd/foosc, poort 2343). De poorten zijn bewust verschillend — je kunt alle drie tegelijk draaien en hun ids=-regels in het banner kruislings controleren.