
Avvia programmi macOS tramite l'interfaccia XPC privata di launchd senza eseguirli tramite exec, facendo sì che l'EDR registri launchd come processo padre. Supporta job one-shot, KeepAlive e basati su plist.
xspawn avvia un programma su macOS tramite launchd e non esegue mai quel programma direttamente. L'obiettivo è che un EDR registri launchd come processo padre, non questo strumento o la shell chiamante.
di cenobyte [email protected] 2026
https://github.com/cenobyte-vincit/xspawn
xspawn apre xpc_pipe_create_from_port(bootstrap_port) e avvia l'esecuzione di un programma tramite _xpc_pipe_interface_routine, la stessa pipe XPC privata usata da launchctl, e non esegue mai il programma stesso.
/bin/launchctl.gui/<uid>cc)makebrew install cppcheck)make
xspawn oneshot -l <label> [-o <stdout>] [-e <stderr>] [--] <program> [args...]
xspawn submit -l <label> [-o <stdout>] [-e <stderr>] [--] <program> [args...]
xspawn remove -l <label>
xspawn load -p <plist>
| Sottocomando | Ciclo di vita |
|---|---|
oneshot | One-shot (RunAtLoad + LaunchOnlyOnce) |
submit | KeepAlive |
load | plist di proprietà del chiamante così com'è scritta |
remove | Rimozione tramite etichetta |
One-shot (RunAtLoad + LaunchOnlyOnce; 0 significa nessuna attesa):
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
Gli argomenti dopo -- sono ProgramArguments. Questo include codice inline (python3 -c, perl -e). CrowdStrike Falcon per macOS registra l'intera CommandLine, quindi usa il codice inline con gli interpreti con parsimonia.
./xspawn oneshot -l com.example.py -o /tmp/py.out -- \
/usr/bin/python3 -c "print('hello world')"
Job KeepAlive, con lo stesso ciclo di vita di launchctl submit. Attesa 60 così CrowdStrike Falcon per macOS e launchctl print vedono ancora il processo:
./xspawn submit -l com.example.svc \
-o /tmp/out.log -e /tmp/err.log -- /tmp/helloworld 60
Ispeziona con launchctl print (solo come riferimento; questo client non lo invoca):
launchctl print gui/$(id -u)/com.example.svc
In caso di successo viene mostrato type = LaunchAgent (non Submitted), program come percorso assoluto e state = running o brevemente xpcproxy. Submitted significa che il job non ha seguito il percorso di bootstrap.
Rimuovi un job di test:
./xspawn remove -l com.example.svc
Carica una plist di proprietà del chiamante (non eliminata dopo la risposta):
./xspawn load -p /tmp/job.plist
<program> deve essere un percorso assoluto. launchd non cerca in $PATH.
load -p richiede un percorso assoluto che termina con .plist.
-o / -e possono essere relativi. Vengono risolti rispetto alla directory di lavoro corrente prima di essere scritti nella plist. Se -o e -e sono omessi, il valore è /dev/null.
oneshot e submit verificano l'etichetta in gui e user (descrittore 708) prima di scrivere la plist temporanea. Un'etichetta già occupata termina con label already loaded e nessun output su stdout. Questo controllo esiste per evitare che un 800 destinato a fallire scriva $TMPDIR/XXXXXX/XXXXXX.plist (un artefatto DFIR; CrowdStrike Falcon conserva il percorso in ASEPFilePath) o stampi la copia XML del dizionario del job. Con un'etichetta libera viene stampato il percorso temporaneo, poi quell'XML e quindi viene inviato 800. load -p esegue lo stesso controllo di occupazione sulla Label del file, poi stampa il percorso del chiamante e l'XML. La directory temporanea viene rimossa a ogni uscita. remove opera per etichetta.
| Codice | Significato |
|---|---|
| 0 | Bootstrap o bootout XPC riuscito |
| 1 | Errore di utilizzo, etichetta non valida, root, o rifiuto da launchd/XPC |
Host di build (make e l'albero dei test; spesso nella stessa macchina di una sessione gui). Questi controlli non sono una prova di esecuzione pulita:
make
make test
make test-unit
make test-functional
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
gui/<uid> dello stesso utente. Root viene rifiutato. Nessuna modalità per altri UID.sw_vers -buildVersion cambia (vedi ARCHITECTURE.md).$TMPDIR/XXXXXX/XXXXXX.plist ($TMPDIR deve essere assoluto, altrimenti /tmp). La directory viene rimossa a ogni uscita. Un'etichetta già occupata non crea mai quel file.ASEPFilePath) nell'evento ProcessRollup2. Il processo padre rimane launchd.xspawn è visibile come questo client: cronologia della shell e un evento di processo EDR per questo binario. CrowdStrike Falcon per macOS registra l'intera CommandLine, che include il percorso del programma e i suoi argomenti. Compila il client all'interno di altri strumenti quando quell'immagine e argv sarebbero distintivi. L'incorporamento non rimuove ASEPFilePath né la riga di bootstrap in launchd.log (vedi ARCHITECTURE.md, Parentage)./private/var/log/com.apple.xpc.launchd/launchd.log (vedi ARCHITECTURE.md, Parentage).XPCService in una plist scritta a mano: xpcproxy quindi fa fork e CrowdStrike Falcon per macOS registra xpcproxy come padre.launchd è un server bootstrap Mach. Questo client non usa la XPC pubblica (xpc_connection_create). Apre una pipe libxpc privata sul bootstrap_port ereditato con xpc_pipe_create_from_port(bootstrap_port, 4), quindi invia _xpc_pipe_interface_routine. Questi simboli si trovano in libxpc e non sono negli header dell'SDK.
L'ID della routine è l'argomento descrittore, non una chiave nel dizionario della richiesta. Su macOS 26.6.1 build 25G76, load è il descrittore 800 e bootout è 801. I flag di interfaccia sono 6. È richiesta una sessione gui/<uid>: la porta ereditata è il dominio launchd gui solo all'interno di una sessione di login Aqua, e questo client invia solo type 8 con handle = uid.
Load (800) è un dizionario XPC. La definizione del job non è nel corpo del messaggio.
handle uid (uint64)
type 8 (gui)
paths [absolute .plist]
by-cli true
launchd esegue stat sul percorso, analizza la plist, poi esegue posix_spawn di xpcproxy. xpcproxy esegue exec del programma nello stesso PID. Il successo corrisponde a ritorno della pipe 0, nessun xpc-fault, error 0, bootstrap-error 0.
Bootout (801) avviene per etichetta: handle, type 8, name, no-einprogress, wait. Nessuna plist.
Il canale è un precedente noto. Jonathan Levin (launjctl, 2015; Mac OS X and iOS Internals Vol. 1) ha mostrato che launchctl comunica con launchd tramite una pipe XPC privata e ha documentato xpc_pipe_create_from_port / xpc_pipe_routine con le chiavi di dizionario type, handle, subsystem, routine e name. Patrick Wardle (The Art of Mac Malware Vol. 2) ha documentato _xpc_pipe_interface_routine come successiva voce di invio. Csaba Fitzl e Brandon Dalton (OBTS) hanno mappato la stessa famiglia di dizionari e i codici di tipo di dominio (gui è 8). Snippet pubblici usavano già xpc_pipe_create_from_port(bootstrap_port, 4).
Questi scritti descrivono la classe del protocollo. Non includono le costanti live di load della 25G76. La cattura di Levin del 2015 inseriva subsystem e routine nel dizionario e usava xpc_pipe_routine. Sulla 25G76 queste chiavi sono assenti. launchctl bootstrap usa _xpc_pipe_interface_routine con l'ID della routine come argomento descrittore. L'analisi statica arm64e di launchctl assomiglia ancora al vecchio percorso a dizionario e suggerisce 703 come ID di load. Lldb live x86_64 e un client arm64 in esecuzione usano entrambi 800 / 801 con quelle chiavi assenti. Questo client mantiene quella stessa forma su entrambe le slice.
I dump dei registri, la procedura di riancoraggio lldb e le note sul campo parentage sono in ARCHITECTURE.md.