
xspawn は macOS 上で launchd を通じてプログラムを起動し、そのプログラム自体を exec することはありません。目的は、EDR が親としてこのツールや呼び出し元シェルではなく launchd を記録することです。
作者: cenobyte [email protected] 2026
https://github.com/cenobyte-vincit/xspawn
xspawn は xpc_pipe_create_from_port(bootstrap_port) を開き、launchctl が使用するのと同じ非公開 XPC パイプである _xpc_pipe_interface_routine を通じてプログラムの実行をブートストラップし、プログラム自体を exec することはありません。
/bin/launchctl を exec することはありません。gui/<uid> セッションを備えた macOS (Darwin)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>
| サブコマンド | ライフサイクル |
|---|---|
oneshot | ワンショット(RunAtLoad + LaunchOnlyOnce) |
submit | KeepAlive |
load | 呼び出し元所有の plist をそのまま使用 |
remove | ラベル指定でアンロード |
ワンショット(RunAtLoad + LaunchOnlyOnce。0 はスリープなし):
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
-- の後の引数は ProgramArguments です。これにはインラインコード(python3 -c、perl -e)も含まれます。CrowdStrike Falcon for macOS は完全な CommandLine を記録するため、インタプリタを使うインラインコードの使用は控えめにしてください。
./xspawn oneshot -l com.example.py -o /tmp/py.out -- \
/usr/bin/python3 -c "print('hello world')"
KeepAlive ジョブ。launchctl submit と同じライフサイクルです。60 秒スリープするので、CrowdStrike Falcon for macOS と launchctl print はプロセスをまだ確認できます:
./xspawn submit -l com.example.svc \
-o /tmp/out.log -e /tmp/err.log -- /tmp/helloworld 60
launchctl print で確認します(検証専用。このクライアントはこれを呼び出しません):
launchctl print gui/$(id -u)/com.example.svc
成功すると、type = LaunchAgent(Submitted ではない)、絶対パスとしての program、そして state = running または一時的な xpcproxy が表示されます。Submitted は、ジョブがブートストラップ経路を取らなかったことを意味します。
テストジョブを後始末する:
./xspawn remove -l com.example.svc
呼び出し元所有の plist をロードします(応答後も削除されません):
./xspawn load -p /tmp/job.plist
<program> は絶対パスでなければなりません。launchd は $PATH を検索しません。
load -p には .plist で終わる絶対パスが必要です。
-o / -e は相対パスでもかまいません。plist に書き込まれる前に、カレントワーキングディレクトリからの相対パスとして解決されます。-o と -e を省略した場合は /dev/null になります。
oneshot と submit は、一時 plist を書き込む前に gui と user(ディスクリプタ 708)でラベルを調べます。使用済みラベルの場合は label already loaded で終了し、標準出力には何も出力しません。このチェックは、失敗する運命にある 800 が $TMPDIR/XXXXXX/XXXXXX.plist を書き込んだり(DFIR の成果物。CrowdStrike Falcon はパスを ASEPFilePath に保持します)、ジョブ辞書の XML コピーを出力したりしないためにあります。空きラベルの場合は、一時パスを出力し、次にその XML を出力してから 800 を送信します。load -p は、ファイルの Label に対して同じ占有チェックを実行し、その後、呼び出し元パスと XML を出力します。一時ディレクトリはすべての終了時に削除されます。remove はラベル指定です。
| コード | 意味 |
|---|---|
| 0 | ブートストラップまたはブートアウト XPC が成功 |
| 1 | 使用法エラー、無効なラベル、ルート、または launchd/XPC による拒否 |
ビルドホスト(make とテストツリー。多くの場合 gui セッションと同じ場所にあります)。これらのチェックはクリーンなランタイムの証明ではありません:
make
make test
make test-unit
make test-functional
./xspawn oneshot -l com.example.once -- /tmp/helloworld 0
gui/<uid> のみ。ルートは拒否されます。他UIDを対象にしません。sw_vers -buildVersion が変わった場合は固定をやり直してください(ARCHITECTURE.md 参照)。$TMPDIR/XXXXXX/XXXXXX.plist です($TMPDIR は絶対パスである必要があり、それ以外は /tmp)。ディレクトリはすべての終了時に削除されます。使用済みラベルでは、そのファイルは決して作成されません。ProcessRollup2 イベントの Auto-Start Extensibility Point フィールド(ASEPFilePath)に一時 plist のパスを記録します。プロセスの親は launchd のままです。xspawn の実行は、このクライアントとして可視です。つまり、シェル履歴と、このバイナリに対する EDR プロセスイベントです。CrowdStrike Falcon for macOS は完全な CommandLine を記録します。これにはプログラムのパスとその引数が含まれます。そのイメージと argv が特徴的になる場合は、クライアントを他のツールに組み込んでください。埋め込んでも、ASEPFilePath や launchd.log のブートストラップ行は削除されません(ARCHITECTURE.md の Parentage を参照)。/private/var/log/com.apple.xpc.launchd/launchd.log に記録します(ARCHITECTURE.md の Parentage を参照)。XPCService キーを入れないでください。xpcproxy がフォークし、CrowdStrike Falcon for macOS は親を xpcproxy として記録します。launchd は Mach ブートストラップサーバーです。このクライアントは公開 XPC(xpc_connection_create)を使用しません。継承された bootstrap_port 上で xpc_pipe_create_from_port(bootstrap_port, 4) を使って非公開の libxpc パイプを開き、_xpc_pipe_interface_routine を送信します。これらのシンボルは libxpc にあり、SDK ヘッダーにはありません。
ルーチン ID はディスクリプタ引数であり、リクエスト辞書のキーではありません。macOS 26.6.1 build 25G76 では、ロードはディスクリプタ 800、ブートアウトは 801 です。インターフェースフラグは 6 です。gui/<uid> セッションが必要です。継承されたポートが gui launchd ドメインになるのは Aqua ログインセッション内だけであり、このクライアントは handle = uid を持つ type 8 のみを送信します。
ロード(800)は XPC 辞書です。ジョブ定義はメッセージ本文にはありません。
handle uid (uint64)
type 8 (gui)
paths [absolute .plist]
by-cli true
launchd はパスを stat し、plist を解析し、それから xpcproxy を posix_spawn します。xpcproxy は同じ PID 内でプログラムを exec します。成功の条件は、パイプ戻り値 0、xpc-fault なし、error 0、bootstrap-error 0 です。
ブートアウト(801)はラベル指定です:handle、type 8、name、no-einprogress、wait。plist はありません。
このチャネルは先行技術です。Jonathan Levin(launjctl、2015年。Mac OS X and iOS Internals Vol. 1)は、launchctl が非公開の XPC パイプを介して launchd と通信することを示し、dict キー type、handle、subsystem、routine、name を持つ xpc_pipe_create_from_port / xpc_pipe_routine を文書化しました。Patrick Wardle(The Art of Mac Malware Vol. 2)は、後続の送信エントリとして _xpc_pipe_interface_routine を文書化しました。Csaba Fitzl と Brandon Dalton(OBTS)は、同じ dict ファミリーとドメインタイプコード(gui は 8)をマッピングしました。公開スニペットはすでに xpc_pipe_create_from_port(bootstrap_port, 4) を使用していました。
これらの解説はプロトコルのクラスを説明しています。それらは、実際に動作する 25G76 のロード定数を提供していません。Levin の2015年のキャプチャでは、subsystem と routine を辞書内に置き、xpc_pipe_routine を使用していました。25G76 では、これらのキーは存在しません。launchctl bootstrap は、ルーチン ID をディスクリプタ引数として _xpc_pipe_interface_routine に到達します。launchctl の arm64e 静的解析は依然として古い dict 経路のように見え、ロード ID として 703 を示唆します。実機での x86_64 lldb と arm64 クライアントの実行は、どちらもそれらのキーなしで 800 / 801 を使用します。このクライアントは、両方のスライスでその単一の形状を提供します。
レジスタダンプ、lldb の再固定レシピ、および親子関係フィールドのメモは ARCHITECTURE.md にあります。