
Lanza programas de macOS a través de la interfaz XPC privada de launchd sin ejecutarlos mediante exec, haciendo que el EDR registre a launchd como proceso padre. Admite trabajos one-shot, KeepAlive y basados en plist.
xspawn inicia un programa en macOS a través de launchd y nunca ejecuta (exec) ese programa por sí mismo. El objetivo es que un EDR registre a launchd como el proceso padre, no esta herramienta ni el shell que la invoca.
por cenobyte [email protected] 2026
https://github.com/cenobyte-vincit/xspawn
xspawn abre xpc_pipe_create_from_port(bootstrap_port) y arranca (bootstraps) la ejecución de un programa a través de _xpc_pipe_interface_routine, la misma tubería XPC privada que usa launchctl, y nunca ejecuta (exec) el programa por sí mismo.
/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>
| Subcomando | Ciclo de vida |
|---|---|
oneshot | De un solo disparo (RunAtLoad + LaunchOnlyOnce) |
submit | KeepAlive |
load | Plist propiedad del llamante tal como se escribe |
remove | Descargar por etiqueta |
Un solo disparo (RunAtLoad + LaunchOnlyOnce; 0 significa sin espera):
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
Los argumentos después de -- son ProgramArguments. Eso incluye código en línea (python3 -c, perl -e). CrowdStrike Falcon para macOS registra el CommandLine completo, así que usa código en línea con intérpretes con moderación.
./xspawn oneshot -l com.example.py -o /tmp/py.out -- \
/usr/bin/python3 -c "print('hello world')"
Trabajo KeepAlive, el mismo ciclo de vida que launchctl submit. Con sleep 60 para que CrowdStrike Falcon para macOS y launchctl print sigan viendo el proceso:
./xspawn submit -l com.example.svc \
-o /tmp/out.log -e /tmp/err.log -- /tmp/helloworld 60
Inspecciona con launchctl print (solo oráculo; este cliente no lo invoca):
launchctl print gui/$(id -u)/com.example.svc
El éxito muestra type = LaunchAgent (no Submitted), program como ruta absoluta y state = running o brevemente xpcproxy. Submitted significa que el trabajo no tomó la ruta de bootstrap.
Limpia un trabajo de prueba:
./xspawn remove -l com.example.svc
Carga un plist propiedad del llamante (no se elimina después de la respuesta):
./xspawn load -p /tmp/job.plist
<program> debe ser una ruta absoluta. launchd no busca en $PATH.
load -p requiere una ruta absoluta que termine en .plist.
-o / -e pueden ser relativas. Se resuelven contra el directorio de trabajo actual antes de escribirse en el plist. Si se omiten -o y -e, son /dev/null.
oneshot y submit comprueban la etiqueta en gui y user (descriptor 708) antes de escribir el plist temporal. Una etiqueta ocupada sale con label already loaded y sin salida estándar. Esa comprobación existe para que un 800 condenado al fracaso no escriba $TMPDIR/XXXXXX/XXXXXX.plist (un artefacto DFIR; CrowdStrike Falcon conserva la ruta en ASEPFilePath) ni imprima la copia XML del diccionario del trabajo. Una etiqueta libre imprime la ruta temporal, luego ese XML y después envía 800. load -p ejecuta la misma comprobación de ocupación sobre el Label del archivo y luego imprime la ruta del llamante y el XML. El directorio temporal se elimina en cada salida. remove es por etiqueta.
| Código | Significado |
|---|---|
| 0 | El bootstrap o bootout XPC se realizó correctamente |
| 1 | Error de uso, etiqueta no válida, root o rechazo de launchd/XPC |
Host de compilación (make y el árbol de pruebas; a menudo ubicado junto con una sesión gui). Estas comprobaciones no son una prueba de ejecución limpia:
make
make test
make test-unit
make test-functional
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
gui/<uid> del mismo usuario. Se rechaza root. No se puede apuntar a otro UID.sw_vers -buildVersion cambia (ver ARCHITECTURE.md).$TMPDIR/XXXXXX/XXXXXX.plist ($TMPDIR debe ser absoluto; si no, /tmp). El directorio se elimina en cada salida. Una etiqueta ocupada nunca crea ese archivo.ASEPFilePath) en el evento ProcessRollup2. El proceso padre sigue siendo launchd.xspawn es visible como este cliente: historial del shell y un evento de proceso del EDR para este binario. CrowdStrike Falcon para macOS registra el CommandLine completo, que incluye la ruta del programa y sus argumentos. Compila el cliente dentro de otras herramientas cuando esa imagen y argv resulten distintivos. Incrustarlo no elimina ASEPFilePath ni la línea de bootstrap de launchd.log (ver ARCHITECTURE.md, Parentage)./private/var/log/com.apple.xpc.launchd/launchd.log (ver ARCHITECTURE.md, Parentage).XPCService en un plist escrito a mano: entonces xpcproxy hace fork y CrowdStrike Falcon para macOS registra a xpcproxy como padre.launchd es un servidor de bootstrap de Mach. Este cliente no usa XPC público (xpc_connection_create). Abre una tubería privada de libxpc en el bootstrap_port heredado con xpc_pipe_create_from_port(bootstrap_port, 4) y luego envía _xpc_pipe_interface_routine. Esos símbolos están en libxpc y no están en los encabezados del SDK.
El ID de rutina es el argumento descriptor, no una clave en el diccionario de solicitud. En macOS 26.6.1 build 25G76, load es el descriptor 800 y bootout es 801. Los indicadores de interfaz son 6. Se requiere una sesión gui/<uid>: el puerto heredado es el dominio gui de launchd solo dentro de una sesión de inicio de sesión de Aqua, y este cliente solo envía type 8 con handle = uid.
Load (800) es un diccionario XPC. La definición del trabajo no está en el cuerpo del mensaje.
handle uid (uint64)
type 8 (gui)
paths [absolute .plist]
by-cli true
launchd hace stat de la ruta, analiza el plist y luego hace posix_spawn de xpcproxy. xpcproxy hace exec del programa en el mismo PID. El éxito es que la tubería devuelva 0, sin xpc-fault, error 0, bootstrap-error 0.
Bootout (801) es por etiqueta: handle, type 8, name, no-einprogress, wait. Sin plist.
El canal es técnica previa (prior art). Jonathan Levin (launjctl, 2015; Mac OS X and iOS Internals Vol. 1) demostró que launchctl se comunica con launchd a través de una tubería XPC privada y documentó xpc_pipe_create_from_port / xpc_pipe_routine con las claves de diccionario type, handle, subsystem, routine y name. Patrick Wardle (The Art of Mac Malware Vol. 2) documentó _xpc_pipe_interface_routine como la entrada de envío posterior. Csaba Fitzl y Brandon Dalton (OBTS) mapearon la misma familia de diccionarios y los códigos de tipo de dominio (gui es 8). Los fragmentos públicos ya usaban xpc_pipe_create_from_port(bootstrap_port, 4).