Suzu macOS Playground

M10 · 階梯 3 · L3

launchd、持久化與現代核可

把一台機器上「能讓程式在重開機/登入後再跑」的位置一次盤點成全景——LaunchAgents/Daemons、login items、現代的 SMAppService(取代已棄用的 SMJobBless/SMLoginItemSetEnabled)、Ventura+ 的 BTM(Background Task Management)、cron、Configuration Profile/MDM——並對每一類說出它的偵測訊號與盲區。框架是防禦:持久化全景盤點 + 偵測,不是教你怎麼裝持久化。

●● 前置:M03

持久化,是把「跑一次」變成「一直能跑」

前面的模組大多在問「攻擊者怎麼過一道閘」——簽章(M03)、Gatekeeper(M04)。但攻擊者過閘之後最想要的,是讓自己的程式在重開機、重新登入、甚至使用者完全沒注意時還能再跑起來。這件事叫持久化(persistence)。

本章的視角從頭到尾是防禦:我們不教你怎麼「裝」一個持久化,而是教你把一台機器上所有能達成持久化的位置一次盤點成全景,並對每一類位置說出兩件事——

  1. 它長什麼樣(哪個目錄、哪個 plist 欄位、哪個 database 條目);
  2. 它的偵測訊號與盲區在哪(新增時系統在哪裡留痕、哪些位置容易被忽略)。

⚠ 本章定位(必讀):這是「持久化全景盤點 + 偵測」。所有操作卡都是唯讀盤點,不含任何安裝/植入步驟。理解攻擊面是為了盤得全、偵得到,不是為了複製。

launchd:macOS 上一切常駐程式的根

launchd 是 macOS 的 init(PID 1),也是所有服務與常駐程式的總管。它用 property list(plist)描述「要跑什麼、何時跑、跑在哪個 domain」。對持久化來說,最經典的兩個位置就是 launchd 的 job 目錄:

類型目錄執行身分 / 時機
LaunchDaemons/Library/LaunchDaemons、/System/Library/LaunchDaemons系統層、開機即起,通常以 root 跑,無使用者 session
LaunchAgents/Library/LaunchAgents、~/Library/LaunchAgents、/System/Library/LaunchAgents使用者層,登入後在使用者 session 內跑

/System/... 下的屬於 Apple、受 SIP(M05)保護不可改;/Library/... 與 ~/Library/... 則是第三方與使用者可寫的範圍——盤點時這兩處才是重點。

一個 launchd job 的 plist 關鍵欄位決定它「為什麼會自己跑」:

欄位(plist key)意義盤點時看什麼
Labeljob 的唯一識別對不上任何已知軟體的 label 即可疑
Program / ProgramArguments實際要執行的可執行檔與參數落點在 /tmp、家目錄隱藏路徑、或冒名系統路徑
RunAtLoad載入時立即跑一次開機/登入即觸發的常見手法
KeepAlive被殺後自動重啟「殺不掉」的常駐特徵
StartInterval / StartCalendarInterval定時觸發週期性回連的訊號

⚠ errata / 觀念:plist 本身只是「描述」。讓 job 真正生效的是 launchd 把它載入到某個 domain(system / gui / user)。所以盤點不能只看目錄裡有哪些 plist 檔,還要看目前實際載入了什麼——兩者可能不一致(檔在但沒載、或載了但檔被刪)。

launchctl list                 # 目前實際載入的 job(盤點起點,非看目錄而是看狀態)
launchctl print-disabled system  # 哪些 service 被停用(被改過狀態的訊號)

現代核可:SMAppService 取代了 SMJobBless / SMLoginItemSetEnabled

過去 app 要安裝特權 helper 或登入項,走的是 ServiceManagement 框架的舊 API:

  • SMJobBless(安裝特權 helper daemon)
  • SMLoginItemSetEnabled(啟用登入項 / 背景 agent)

⚠ errata(核心事實,必記):SMJobBless 與 SMLoginItemSetEnabled 都已被 Apple 棄用(deprecated)。從 macOS 13(Ventura)起,現代且正確的 API 是 SMAppService。它把「daemon / agent / login item」統一成由 app 內捆綁的服務,用 SMAppService.daemon(plistName:) / .agent(plistName:) / .loginItem(identifier:) 註冊,狀態(enabled / requiresApproval / notRegistered)可被查詢與管理。

SMAppService 帶來兩個對防禦很重要的改變:

  1. 使用者可見、可撤銷:透過它註冊的背景項目會集中顯示在「系統設定 → 一般 → 登入項目與擴充功能(Login Items & Extensions)」,使用者能看到、能關掉。舊 API 那種「悄悄裝個 helper」的不透明度被大幅收斂。
  2. 由 BTM 統一記錄(見下節)——這正是現代 macOS 把持久化「可盤點化」的關鍵基礎建設。

⚠ 注意別搞混:SMAppService 是開發者用來合法註冊自家背景服務的 API;對防禦者/盤點者而言,你不會去呼叫它,而是去讀它在系統留下的紀錄(BTM、系統設定面板)。本章只走後者。

BTM(Background Task Management):Ventura+ 的持久化登記簿

從 macOS 13(Ventura) 起,系統新增了 BTM(Background Task Management)。它的角色是:把所有背景項目(透過 SMAppService 註冊的 daemon/agent/login item、以及偵測到的 LaunchAgents/Daemons、login items)集中登記,並在新增一個背景項目時通知使用者(你會看到「某 App 已加入背景項目」的系統通知)。

這對防禦是一次結構性升級:以前持久化散落在好幾個目錄與 database,現在 BTM 提供一個較集中的登記簿。盤點與偵測都能受益:

sudo sfltool dumpbtm            # 傾印 BTM 紀錄(背景項目全景;唯讀檢視)

⚠ errata(事實校正):

  • BTM 是 Ventura(macOS 13)及之後才有的機制,講「macOS 一直都有 BTM」是錯的。
  • 檢視 BTM 的主路徑有二:(1) 指令 sfltool dumpbtm(macOS 26 仍可用,需 root,唯讀傾印);(2) 系統設定 → 一般 → 登入項目與擴充功能面板,使用者可見可管理。
  • sfltool 帶有會改狀態的子命令(如 resetbtm、clear)。本章只用唯讀的 dumpbtm;重置類操作會改動系統狀態,不在盤點範圍。

對偵測來說,BTM 的高價值點是:新增持久化會被記錄並通知。在 M14(DFIR)會看到對應的 Endpoint Security 事件 ES_EVENT_TYPE_NOTIFY_BTM_LAUNCH_ITEM_ADD——「新出現一個背景項目」是極強的偵測訊號。

其餘持久化面:login items、cron、Configuration Profile/MDM

全景盤點不能只看 launchd 與 BTM。還有幾條獨立的軌:

  • 傳統 login items:使用者登入時自動開的 app。現代多由 SMAppService/BTM 收編並顯示在系統設定面板,但盤點時仍應對照面板確認。
  • cron:古老的 Unix 定時任務(crontab)。macOS 上不是預設啟用的主路徑,但仍存在、仍能被用作週期觸發,盤點時不可遺漏。launchd 的 StartCalendarInterval 是其現代對應。
  • Configuration Profile / MDM:.mobileconfig 設定描述檔可下發各種設定,MDM 管理更可遠端推送 payload。這是「合法管理」與「被濫用持久化/控制」之間的雙面刃——一台被納管的機器,其信任邊界延伸到 MDM 伺服器。盤點時要看裝了哪些 profile、是否受 MDM 管理。
profiles status -type enrollment   # 是否受 MDM 納管(唯讀)
profiles list -type configuration  # 已安裝的設定描述檔(唯讀)
crontab -l 2>/dev/null             # 目前使用者的 cron 任務(唯讀;多半為空)

⚠ errata / 觀念:這些面彼此獨立——一條沒在 BTM、不在 LaunchAgents、卻藏在 cron 或某個 profile 裡的觸發點,全景盤點才抓得到。「以為 BTM 是唯一真相來源」是常見的盤點盲區。

下面的盤點板列出持久化向量與偵測訊號,並讓你對一個「背景項目」勾選觀察到的特徵——看為何單一特徵不是鐵證、特徵的組合才是規則:

持久化盤點 + 偵測 triage

① 持久化向量參考
向量位置偵測訊號
LaunchAgents / Daemons/Library/Launch*、~/Library/LaunchAgentslaunchctl list 狀態、對 Launch* 目錄的 ES WRITE
SMAppService 註冊項app 捆綁的 daemon/agent/login itemBTM 紀錄、登入項面板、ES BTM_LAUNCH_ITEM_ADD
傳統 login items系統設定 登入項目BTM、系統設定面板
croncrontab、/etc/crontab、/var/atcrontab -l(多半為空,不可遺漏)
Configuration Profile / MDM.mobileconfig、MDM 推送profiles list / status、納管狀態異常
② 背景項目 triage:勾選觀察到的特徵
分數 0 · 看似正常(仍需對照基線)

重點:單一特徵(例如「label 看起來怪」)不是鐵證;高可疑來自特徵的組合。最強訊號是「某背景項目剛剛出現」(事件偵測),靜態盤點只告訴你「現在有什麼」。純教學評分,非真實掃描。

把全景盤點變成偵測:根因、類別、訊號

不要把持久化偵測想成「背一串路徑」。用類別 → 觸發根因 → 偵測訊號的三段式思考,才能對「沒見過的新手法」也有判斷力:

持久化類別觸發根因(為什麼會自己跑)偵測訊號 / 盤點位置
LaunchAgents / LaunchDaemonslaunchd 載入 plist 並依 RunAtLoad/KeepAlive/間隔觸發launchctl list 狀態、/Library/Launch* 與 ~/Library/LaunchAgents 新增/異常 plist、ES WRITE 到這些路徑
SMAppService 註冊項app 捆綁的 daemon/agent/login item 被啟用BTM 紀錄、系統設定登入項面板、ES BTM_LAUNCH_ITEM_ADD
傳統 login items登入時自動開系統設定面板、BTM
croncrond 依排程觸發crontab -l、/var/at/、/etc/crontab
Configuration Profile / MDMprofile payload / MDM 推送profiles list / status、納管狀態異常

⚠ 偵測心智模型(接 M14):最強的訊號不是「某個檔存在」,而是「某個背景項目剛剛出現」。靜態盤點告訴你現在有什麼;事件偵測(BTM 通知、ES BTM_LAUNCH_ITEM_ADD、對 Launch* 目錄的 WRITE)告訴你什麼時候多出來的。兩者合起來才是完整的偵測面。把單一欄位(例如「label 看起來怪」)當鐵證會誤判;特徵的組合(不明 label + 落點在 /tmp + 父程序非安裝器 + 剛新增)才是規則。

各面的盲區(防禦現實)

  • 檔在 vs 載入:目錄裡有 plist 不代表它在跑;它在跑也不代表目錄裡找得到對應檔。盤點要同時看狀態(launchctl list)與位置(目錄/BTM/profile)。
  • BTM 不等於全部:cron、某些 profile payload、非標準觸發點不一定反映在 BTM。
  • SIP 保護的 /System 路徑不是盤點重點(不可被第三方寫),把注意力放在可寫範圍。
  • 時間維度:靜態快照看不出「何時新增」。要回答這點得靠事件來源(M14 的 ES/Unified Log/FSEvents)與 timeline 重建。

🟢/🟡 真機操作卡

以下皆為唯讀盤點,在你自己的機器上安全執行(macOS 26 適用)。需要 root 的會標 sudo。不含任何安裝/植入步驟。

# ── launchd 狀態(盤點起點:看實際載入了什麼,而非只看目錄)──
launchctl list                       # 目前載入的 job(label / PID / 最後退出碼)
launchctl print-disabled system      # 被停用的 system service(狀態被改過的訊號)
launchctl print-disabled user/$(id -u)  # 使用者 domain 的停用清單

# ── launchd job 目錄(可寫範圍才是重點;唯讀瀏覽)──
ls -la /Library/LaunchDaemons /Library/LaunchAgents ~/Library/LaunchAgents 2>/dev/null
# 看某個 plist 的關鍵欄位(Program / RunAtLoad / KeepAlive)——唯讀讀取
plutil -p /Library/LaunchAgents/<某檔>.plist 2>/dev/null

# ── BTM 背景項目全景(Ventura+;唯讀傾印)──
sudo sfltool dumpbtm | head          # 透過 SMAppService 等註冊的背景項目登記簿
# 圖形對照:系統設定 → 一般 → 登入項目與擴充功能(使用者可見可管理)

# ── cron(古老定時軌;多半為空,但不可遺漏)──
crontab -l 2>/dev/null               # 目前使用者的 cron 任務

# ── Configuration Profile / MDM(合法管理 vs 被濫用的雙面刃)──
profiles status -type enrollment     # 是否受 MDM 納管
profiles list -type configuration    # 已安裝的設定描述檔

⚠ 危險/授權邊界:本章所有指令皆為唯讀。sfltool 另有 resetbtm/clear 等會改動 BTM 狀態的子命令,不在本章範圍;launchctl bootout/disable、profiles install/remove、編輯 Launch* 目錄裡的 plist 都會改變系統持久化狀態,屬授權的維運/鑑識操作,需明確授權與證物鏈紀錄(DESIGN.md §10)。盤點的目的是看全、看準,不是改動。


下一步:M14 偵測、監控與 DFIR — 把這裡盤到的持久化全景接上事件來源(Endpoint Security 的 BTM_LAUNCH_ITEM_ADD、Unified Log、FSEvents),從「現在有什麼」進化到「什麼時候多出來、留了哪些痕跡」,做 timeline 重建與交叉佐證。