
Spawna programas macOS por meio da interface XPC privada do launchd, sem executá-los via exec, fazendo o EDR registrar o launchd como pai. Suporta jobs one-shot, KeepAlive e baseados em plist.
O xspawn inicia um programa no macOS através do launchd e nunca executa esse programa por conta própria. O objetivo é que um EDR registre o launchd como pai, e não esta ferramenta ou o shell que a invocou.
por cenobyte [email protected] 2026
https://github.com/cenobyte-vincit/xspawn
O xspawn abre xpc_pipe_create_from_port(bootstrap_port) e inicializa a execução de um programa através de _xpc_pipe_interface_routine, o mesmo pipe XPC privado usado pelo launchctl, e nunca executa o programa por conta própria.
/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 | One-shot (RunAtLoad + LaunchOnlyOnce) |
submit | KeepAlive |
load | plist de propriedade do chamador, tal como escrita |
remove | Descarregar por label |
One-shot (RunAtLoad + LaunchOnlyOnce; 0 significa sem pausa):
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
Os argumentos após -- são ProgramArguments. Isso inclui código inline (python3 -c, perl -e). O CrowdStrike Falcon para macOS registra a CommandLine completa, portanto use código inline com interpretadores com moderação.
./xspawn oneshot -l com.example.py -o /tmp/py.out -- \
/usr/bin/python3 -c "print('hello world')"
Job KeepAlive, com o mesmo ciclo de vida do launchctl submit. Use sleep 60 para que o CrowdStrike Falcon para macOS e o launchctl print ainda vejam o processo:
./xspawn submit -l com.example.svc \
-o /tmp/out.log -e /tmp/err.log -- /tmp/helloworld 60
Inspecione com launchctl print (apenas oracle; este cliente não o chama):
launchctl print gui/$(id -u)/com.example.svc
O sucesso exibe type = LaunchAgent (não Submitted), program como caminho absoluto e state = running ou, brevemente, xpcproxy. Submitted significa que o job não seguiu o caminho de bootstrap.
Remova um job de teste:
./xspawn remove -l com.example.svc
Carregue uma plist de propriedade do chamador (não excluída após a resposta):
./xspawn load -p /tmp/job.plist
<program> deve ser um caminho absoluto. O launchd não pesquisa $PATH.
load -p exige um caminho absoluto que termine em .plist.
-o / -e podem ser relativos. Eles são resolvidos em relação ao diretório de trabalho atual antes de serem gravados na plist. -o e -e omitidos são /dev/null.
oneshot e submit verificam o label em gui e user (descritor 708) antes de escrever a plist temporária. Um label já em uso faz o comando sair com label already loaded e sem stdout. Essa verificação existe para que um 800 condenado não grave $TMPDIR/XXXXXX/XXXXXX.plist (um artefato de DFIR; o CrowdStrike Falcon mantém o caminho em ASEPFilePath) nem imprima a cópia em XML do dicionário do job. Um label livre imprime o caminho temporário, depois esse XML e então envia o 800. load -p executa a mesma verificação de ocupação no Label do arquivo e então imprime o caminho do chamador e o XML. O diretório temporário é removido em todas as saídas. remove é por label.
| Código | Significado |
|---|---|
| 0 | Bootstrap ou bootout XPC bem-sucedido |
| 1 | Erro de uso, label inválido, root ou rejeição do launchd/XPC |
Ambiente de compilação (make e a árvore de testes; frequentemente no mesmo local que uma sessão gui). Estas verificações não são uma prova de runtime limpo:
make
make test
make test-unit
make test-functional
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
gui/<uid> do mesmo usuário. Root é recusado. Sem alvo para outro UID.sw_vers -buildVersion mudar (veja ARCHITECTURE.md).$TMPDIR/XXXXXX/XXXXXX.plist ($TMPDIR deve ser absoluto, caso contrário /tmp). O diretório é removido em todas as saídas. Um label já em uso nunca cria esse arquivo.ASEPFilePath) no evento ProcessRollup2. O pai do processo permanece launchd.xspawn é visível como este cliente: histórico do shell e um evento de processo no EDR para este binário. O CrowdStrike Falcon para macOS registra a CommandLine completa, que inclui o caminho do programa e seus argumentos. Compile o cliente dentro de outras ferramentas quando essa imagem e argv forem distintivos. A incorporação não remove ASEPFilePath nem a linha de bootstrap no launchd.log (veja ARCHITECTURE.md, Parentage)./private/var/log/com.apple.xpc.launchd/launchd.log (veja ARCHITECTURE.md, Parentage).XPCService em uma plist escrita à mão: o xpcproxy então faz fork e o CrowdStrike Falcon para macOS registra o pai como xpcproxy.O launchd é um servidor de bootstrap Mach. Este cliente não usa XPC público (xpc_connection_create). Ele abre um pipe privado da libxpc na bootstrap_port herdada com xpc_pipe_create_from_port(bootstrap_port, 4) e então envia _xpc_pipe_interface_routine. Esses símbolos estão na libxpc e não estão nos cabeçalhos do SDK.
O ID da rotina é o argumento descritor, não uma chave no dicionário da requisição. No macOS 26.6.1 build 25G76, load é o descritor 800 e bootout é 801. Os flags de interface são 6. Uma sessão gui/<uid> é obrigatória: a porta herdada é o domínio gui do launchd apenas dentro de uma sessão de login Aqua, e este cliente só envia type 8 com handle = uid.
Load (800) é um dicionário XPC. A definição do job não está no corpo da mensagem.
handle uid (uint64)
type 8 (gui)
paths [absolute .plist]
by-cli true
O launchd executa stat no caminho, faz o parse da plist e então executa posix_spawn do xpcproxy. O xpcproxy executa o programa no mesmo PID. O sucesso é retorno do pipe 0, sem xpc-fault, error 0, bootstrap-error 0.
Bootout (801) é por label: handle, type 8, name, no-einprogress, wait. Sem plist.
O canal é técnica anterior. Jonathan Levin (launjctl, 2015; Mac OS X and iOS Internals Vol. 1) mostrou que o launchctl conversa com o launchd por um pipe XPC privado e documentou xpc_pipe_create_from_port / xpc_pipe_routine com as chaves de dicionário type, handle, subsystem, routine e name. Patrick Wardle (The Art of Mac Malware Vol. 2) documentou _xpc_pipe_interface_routine como a entrada de envio posterior. Csaba Fitzl e Brandon Dalton (OBTS) mapearam a mesma família de dicionários e os códigos de tipo de domínio (gui é 8). Snippets públicos já usavam xpc_pipe_create_from_port(bootstrap_port, 4).