
Startet macOS-Programme über die private XPC-Schnittstelle von launchd, ohne sie per exec auszuführen, wodurch EDR launchd als Elternprozess aufzeichnet. Unterstützt One-Shot-, KeepAlive- und plist-basierte Jobs.
xspawn startet ein Programm unter macOS über launchd und führt dieses Programm selbst nie aus. Ziel ist es, dass eine EDR launchd als Elternprozess aufzeichnet, nicht dieses Tool oder die aufrufende Shell.
von cenobyte [email protected] 2026
https://github.com/cenobyte-vincit/xspawn
xspawn öffnet xpc_pipe_create_from_port(bootstrap_port) und bootstrapt die Ausführung eines Programms über _xpc_pipe_interface_routine, dieselbe private XPC-Pipe, die auch launchctl verwendet, und führt das Programm selbst nie aus.
/bin/launchctl nie aus.gui/<uid>-Sitzungcc)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>
Einmalig (RunAtLoad + LaunchOnlyOnce; 0 bedeutet kein Sleep):
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
Argumente nach -- sind ProgramArguments. Das schließt Inline-Code ein (python3 -c, perl -e). CrowdStrike Falcon für macOS zeichnet die vollständige CommandLine auf, verwende Inline-Code mit Interpreten daher sparsam.
./xspawn oneshot -l com.example.py -o /tmp/py.out -- \
/usr/bin/python3 -c "print('hello world')"
KeepAlive-Job, derselbe Lebenszyklus wie launchctl submit. Sleep 60, damit CrowdStrike Falcon für macOS und launchctl print den Prozess weiterhin sehen:
./xspawn submit -l com.example.svc \
-o /tmp/out.log -e /tmp/err.log -- /tmp/helloworld 60
Inspiziere mit launchctl print (nur als Referenz; dieser Client ruft es nicht auf):
launchctl print gui/$(id -u)/com.example.svc
Erfolg zeigt type = LaunchAgent (nicht Submitted), program als absoluten Pfad und state = running oder kurz xpcproxy. Submitted bedeutet, dass der Job den Bootstrap-Pfad nicht genommen hat.
Räume einen Testjob auf:
./xspawn remove -l com.example.svc
Lade eine vom Aufrufer bereitgestellte Plist (wird nach der Antwort nicht gelöscht):
./xspawn load -p /tmp/job.plist
<program> muss ein absoluter Pfad sein. launchd durchsucht $PATH nicht.
load -p erfordert einen absoluten Pfad, der auf .plist endet.
-o / -e dürfen relativ sein. Sie werden vor dem Schreiben in die Plist relativ zum aktuellen Arbeitsverzeichnis aufgelöst. Fehlende -o und -e sind /dev/null.
oneshot und submit prüfen das Label in gui und user (Deskriptor 708), bevor sie die temporäre Plist schreiben. Ein bereits vergebenes Label endet mit label already loaded und ohne stdout. Diese Prüfung existiert, damit ein zum Scheitern verurteilter 800 nicht $TMPDIR/XXXXXX/XXXXXX.plist schreibt (ein DFIR-Artefakt; CrowdStrike Falcon behält den Pfad in ASEPFilePath) oder die XML-Kopie des Job-Wörterbuchs ausgibt. Ein freies Label gibt den temporären Pfad aus, dann dieses XML, dann sendet es 800. load -p führt dieselbe Belegungsprüfung mit dem Label der Datei durch, gibt dann den Aufruferpfad und das XML aus. Das temporäre Verzeichnis wird bei jeder Beendigung entfernt. remove erfolgt nach Label.
| Code | Bedeutung |
|---|---|
| 0 | Bootstrap- oder Bootout-XPC erfolgreich |
| 1 | Nutzungsfehler, ungültiges Label, Root oder launchd/XPC-Ablehnung |
Build-System (make und der Testbaum; oft zusammen mit einer gui-Sitzung). Diese Prüfungen sind kein Beweis für eine saubere Laufzeit:
make
make test
make test-unit
make test-functional
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
gui/<uid> für denselben Benutzer. Root wird abgelehnt. Kein Zielen auf andere UIDs.sw_vers -buildVersion ändert (siehe ARCHITECTURE.md).$TMPDIR/XXXXXX/XXXXXX.plist ($TMPDIR muss absolut sein, sonst /tmp). Das Verzeichnis wird bei jeder Beendigung entfernt. Ein vergebenes Label erstellt diese Datei nie.ASEPFilePath) im ProcessRollup2-Ereignis auf. Der Prozess-Eltern bleibt launchd.xspawn-Lauf ist als dieser Client sichtbar: Shell-Verlauf und ein EDR-Prozessereignis für dieses Binärprogramm. CrowdStrike Falcon für macOS zeichnet die vollständige CommandLine auf, die den Programm-Pfad und seine Argumente enthält. Kompiliere den Client in andere Werkzeuge ein, wenn dieses Image und argv markant wären. Das Einbetten entfernt nicht ASEPFilePath oder die Bootstrap-Zeile in launchd.log (siehe ARCHITECTURE.md, Parentage).launchd ist ein Mach-Bootstrap-Server. Dieser Client verwendet kein öffentliches XPC (xpc_connection_create). Er öffnet eine private libxpc-Pipe auf dem geerbten bootstrap_port mit xpc_pipe_create_from_port(bootstrap_port, 4) und sendet dann _xpc_pipe_interface_routine. Diese Symbole befinden sich in libxpc und nicht in den SDK-Headern.
Die Routine-ID ist das Deskriptor-Argument, kein Schlüssel im Anforderungs-Wörterbuch. Auf macOS 26.6.1 Build 25G76 ist Load Deskriptor 800 und Bootout 801. Interface-Flags sind 6. Eine gui/<uid>-Sitzung ist erforderlich: Der geerbte Port ist die gui-launchd-Domäne nur innerhalb einer Aqua-Anmeldesitzung, und dieser Client sendet nur type 8 mit handle = uid.
Load (800) ist ein XPC-Wörterbuch. Die Job-Definition befindet sich nicht im Nachrichtentext.
handle uid (uint64)
type 8 (gui)
paths [absoluter .plist]
by-cli true
launchd statt den Pfad, parst die Plist und führt dann posix_spawn von xpcproxy aus. xpcproxy exect das Programm in derselben PID. Erfolg ist Pipe-Rückgabe 0, kein xpc-fault, error 0, bootstrap-error 0.
Bootout (801) erfolgt nach Label: handle, type 8, name, no-einprogress, wait. Keine Plist.
Der Kanal ist Vorarbeit. Jonathan Levin (launjctl, 2015; Mac OS X and iOS Internals Bd. 1) zeigte, dass launchctl über eine private XPC-Pipe mit launchd spricht, und dokumentierte xpc_pipe_create_from_port / xpc_pipe_routine mit den Wörterbuch-Schlüsseln type, handle, subsystem, routine und name. Patrick Wardle (The Art of Mac Malware Bd. 2) dokumentierte _xpc_pipe_interface_routine als den späteren Sendeeinstieg. Csaba Fitzl und Brandon Dalton (OBTS) kartierten dieselbe Wörterbuch-Familie und die Domänentyp-Codes (gui ist 8). Öffentliche Schnipsel verwendeten bereits xpc_pipe_create_from_port(bootstrap_port, 4).
Diese Aufschriebe beschreiben die Klasse des Protokolls. Sie liefern nicht die Live-25G76-Load-Konstanten. Levins Aufzeichnung von 2015 setzte subsystem und routine in das Wörterbuch und verwendete xpc_pipe_routine. Auf 25G76 fehlen diese Schlüssel. launchctl bootstrap trifft _xpc_pipe_interface_routine mit der Routine-ID als Deskriptor-Argument. arm64e-Statikanalyse von launchctl sieht immer noch wie der alte Wörterbuch-Pfad aus und legt 703 als Load-ID nahe. Live-x86_64-lldb und ein arm64-Clientlauf verwenden beide 800 / 801 ohne diese Schlüssel. Dieser Client liefert genau diese eine Form auf beiden Slices.
Register-Dumps, das lldb-Re-Pin-Rezept und Feldnotizen zur Parentage befinden sich in ARCHITECTURE.md.
| Unterbefehl | Lebenszyklus |
|---|
oneshot | Einmalig (RunAtLoad + LaunchOnlyOnce) |
submit | KeepAlive |
load | Vom Aufrufer bereitgestellte Plist wie geschrieben |
remove | Entladen nach Label |
/private/var/log/com.apple.xpc.launchd/launchd.log (siehe ARCHITECTURE.md, Parentage).XPCService-Schlüssel in eine handgeschriebene Plist: xpcproxy forkt dann und CrowdStrike Falcon für macOS zeichnet den Elternprozess xpcproxy auf.