M08 · 階梯 3 · L3
Mach IPC / XPC / MIG
進程之間如何「彼此呼叫」——Mach port 與 port rights 是 XNU 的 IPC 底層、launchd/bootstrap 把 service 名對應到 port、XPC 與 NSXPCConnection 疊在其上做序列化 RPC;而所有信任缺陷的共同根因是「高權 service 用 PID 而非 audit token 認呼叫方」,這正是 confused-deputy 類別的核心,本章只講類別、根因與防禦/偵測,不給任何可武器化步驟。
為什麼要先懂 Mach port
到目前為止(M01 看 XNU = Mach + BSD,M02 看 Mach-O 怎麼被 dyld 載進一個進程),我們談的都是單一進程內部的事。但 macOS 是一個高度進程間協作的系統:Dock、WindowServer、tccd、launchd、各種 daemon 之間時時刻刻在互相請求。這些請求不是走 POSIX socket 或 signal 為主,而是走 XNU 的原生 IPC——Mach IPC。
理解 Mach IPC 是後面所有「跨進程信任」討論(confused-deputy、TCC attribution、sandbox 的 mach-lookup)的共同前置。先把最底層的物件講清楚。
Mach port 與 port rights
Mach port 是由 kernel 管理的一個單向訊息佇列(unidirectional message queue)的端點。進程之間不直接「看見」彼此的記憶體;它們交換的是對 port 的權利(port right)。權利由 kernel 持有與轉移,進程手上只有一個不透明的整數 handle(mach_port_name_t),指向 kernel 內部真正的 port 物件。
三種核心 right:
| Right | 角色 | 關鍵限制 |
|---|---|---|
| receive right | 「伺服端」:能從這個 port 的佇列收訊息 | 同一個 port 同時只能有一個 receive right 持有者 |
| send right | 「客戶端」:能往這個 port 送訊息 | 可同時存在多份;可被複製、傳遞給別的進程 |
| send-once right | 只能送一次的 send right | 常用於「回覆 port」(reply port),送完即消耗 |
幾個常被搞錯的點:
- handle 是進程私有的。同一個 kernel port 物件,在進程 A 手上叫
0x1234、在進程 B 手上可能叫0x5678——名字(name)是 per-task 的,不能拿一個進程的 port name 去另一個進程用。 - right 可以隨訊息傳遞。Mach 訊息(
mach_msg)除了帶資料,還能在訊息描述符裡夾帶 port right(把一個 send right 送給對方)。這是「把能力交出去」的底層機制——能力導向(capability-based)IPC 的本質。 - 訊息是非同步佇列,不是函式呼叫。同步 RPC 的「呼叫→回覆」語意是上層(MIG / XPC)用 reply port 疊出來的。
⚠ Mach port 是「能力」,不是「地址」。 拿到一個 service 的 send right,就等於拿到「能對它送請求」這個能力。所以 macOS 安全的很大一塊是在管「誰能拿到哪個 port 的 send right」——sandbox 的
mach-lookup規則(M07)就是在收窄一個進程能查到哪些 service 的 send right。
bootstrap 與 launchd:service 名怎麼對應到 port
進程怎麼拿到別人的 receive port 的 send right?它不可能憑空知道。答案是透過一個已知的中介——bootstrap server,在現代 macOS 上就是 launchd(PID 1)。
機制直覺:
- 一個 daemon/agent 啟動時(由
launchd依其.plist拉起),向launchd註冊一個 Mach service 名(例如com.apple.example.helper),把該 service 的 receive right 的歸屬交給launchd管理(plist 裡的MachServices鍵宣告)。 - 客戶端要連線時,呼叫
bootstrap_look_up(底層)/ 直接用 XPC(上層)以 service 名查詢,launchd回給它一個指向該 service 的 send right。 - 之後客戶端就能往那個 send right 送 Mach 訊息——連線建立。
daemon ──註冊 service 名 + receive right──► launchd (PID 1, bootstrap server)
▲
client ──bootstrap_look_up("com.apple.…")─────────┘
◄────────── 回一個 send right ───────────
client ──mach_msg(send right) ──► daemon 的 receive port(連線成立)
這也說明了 launchd 的 on-demand 啟動:service 不必常駐,client 一查名、launchd 才把對應的 daemon 拉起來。launchctl 看到的那張「service 表」,本質就是這個 名→port 的註冊表。
⚠ bootstrap 命名空間有分層。 大致分 system(PID 1 root bootstrap,給 daemon)與 per-login-session / per-user(給 GUI agent)。一個進程「查得到哪些 service 名」受它所在的 bootstrap 子集與 sandbox
mach-lookup規則雙重限制——這是「低權進程能不能找到某個高權 service」的決定因素之一。
XPC 與 NSXPCConnection:疊在 Mach 上的 RPC
直接寫 mach_msg 很痛苦(要自己處理序列化、reply port、權利傳遞)。所以日常開發用的是 XPC:
- libxpc(C API,
xpc_*):提供型別化的字典/陣列訊息物件(xpc_object_t)、連線(xpc_connection_t)、以及和launchd整合的 service 生命週期管理。XPC 訊息底層仍是 Mach 訊息,XPC 幫你做序列化與連線狀態機。 NSXPCConnection(Objective-C / Swift 高層 API):把 XPC 包裝成「呼叫一個遠端物件的方法」。你定義一個@protocol、用NSXPCInterface宣告,框架在兩端自動 marshalling/unmarshalling。
XPC service 的兩種常見形態:
| 形態 | 註冊/生命週期 | 例子 |
|---|---|---|
| bundled XPC service | 放在 App 的 Contents/XPCServices/,由 launchd 隨需拉起,與主 App 同信任域 | App 把高風險解析(如剖析不可信檔案)隔離到一個低權 XPC service |
| launchd daemon/agent | 系統範圍或登入會話範圍的常駐服務,自己的 .plist + MachServices | 系統 helper、特權服務 |
⚠ 「把危險工作隔離到 XPC service」是防禦設計,不是攻擊面。 把不可信輸入的解析丟進一個低權、沙盒化的 XPC service,即使該 service 被打爆,攻擊者拿到的也是一個低權上下文——這是 privilege separation 的正規用法(瀏覽器 renderer、各種 parser 都這樣做)。本章關注的不是「怎麼打爆它」,而是它與高權端之間的信任邊界怎麼劃。
核心:用 audit token 驗呼叫方,不要用 PID
這是 M08 的心臟,也是整個第三階梯「分析權限決策」的關鍵一課。
當一個高權 service 收到請求,它常常需要決定:「我該不該為這個呼叫方做這件事?」 它可能想檢查呼叫方的 entitlement、code-signing 身分、uid、或 sandbox 狀態。問題出在它怎麼識別呼叫方。
錯誤做法(confused-deputy 的根因):用呼叫方的 PID 來識別,再拿 PID 去查身分。例如「拿到 PID → 用 PID 取得路徑 / code signature → 據此放行」。
為什麼用 PID 是錯的(根因,非操作步驟):
- PID 會被回收重用(PID reuse)。一個 PID 在進程結束後可被分配給新進程。「我查到 PID 1234 是受信任的 App」與「現在送訊息進來的真的是當初那個 1234」之間有一個時間差(TOCTOU,time-of-check-to-time-of-use)。在這個窗口內,原進程若已結束、PID 被另一個進程取得,身分判斷就對錯了對象。
- PID 不是 kernel 對「這條訊息的發送者」的擔保。它只是一個你後來自己去查的數字,查的當下未必還對應發訊息的那個進程。
正確做法:用 audit token。
audit_token_t是 kernel 在這一條 Mach 訊息的 trailer 裡附帶並擔保的不可偽造識別資訊。它由 kernel 填寫,呼叫方無法竄改,且對應的就是送出這條訊息的那個 task。- 它包含
auid、euid、egid、ruid、rgid、pid、asid,以及關鍵的pidversion(也寫作 PID generation)——這個版本號讓「PID + pidversion」能唯一辨別一個進程世代,回收重用的新進程不會撞到舊世代,因此沒有 PID reuse 的 TOCTOU 問題。 - 在 XPC 層用
xpc_connection_get_audit_token取得連線的 audit token(避免改用xpc_connection_get_pid);要驗 sandbox 用sandbox_check_by_audit_token;要驗 code-signing 身分用SecCodeCreateWithAuditToken搭配SecCodeCheckValidity/SecRequirementCreateWithString釘一條 client requirement(而不是SecCodeCreateWithPID)。
呼叫方 ── XPC/mach_msg ──► 高權 service
│
┌────────────────────┴─────────────────────┐
❌ 用 PID 識別 ✅ 用 audit token 識別
pid = get_pid(conn) tok = get_audit_token(conn)
→ 後查身分(可被 PID reuse → kernel 擔保的訊息發送者
的 TOCTOU 對錯對象) (含 pidversion,無重用歧義)
→ confused-deputy 風險 → 對 tok 驗 sandbox / code requirement
⚠ 這就是 confused-deputy 類別的本質。 confused deputy(被混淆的代理人)= 一個高權實體被一個低權實體誘導,代它行使了它本不該得到的能力。在 XPC 場景,缺陷類別是「高權 service 沒有正確地把『該不該為這個呼叫方做事』綁定到 kernel 擔保的呼叫方身分(audit token)上」,於是低權呼叫方借到了高權能力。防禦的關鍵就是一句話:驗 audit token,不驗 PID。 本章只講到這個類別、根因(PID reuse / TOCTOU)與防禦面,不提供任何特定的提權鏈步驟、offset、payload 或 PoC。
下面的實驗室把這件事演出來:切換 service 用 PID 還是 audit token 認呼叫方,看 confused-deputy 在什麼條件下成立、以及 audit token 為何擋得住:
XPC 呼叫方驗證(confused-deputy 根因 vs 防禦)
- 高權 service 收到請求,需決定「該不該為這個呼叫方做事」。
- 用 PID 識別呼叫方 → 再拿 PID 去查身分/entitlement。
- PID 在 check 後、use 前被回收給另一個(低權)進程(TOCTOU 窗口)。
- service 對「原本那個 PID 的身分」放行,實際服務的卻是新進程 → 身分對錯對象。
根因:用 PID 認呼叫方(check 與 use 之間 PID 可被回收 → 身分對錯對象)。防禦一句話:驗 audit token,不驗 PID(含 pidversion,kernel 擔保)。本實驗室只示範類別與防禦,不含提權步驟。
MIG:Mach 訊息的「介面產生器」
很多系統 service(含 kernel 子系統)不是手寫 mach_msg,而是用 MIG(Mach Interface Generator)。你寫一份 .defs 介面定義(宣告 subsystem、routine、參數型別),MIG 產生兩端的 marshalling stub:client stub 把參數打包成 Mach 訊息、server stub 收訊息後拆包並分派到你的實作函式。
要點:
- 每個 MIG subsystem 有一個 base message ID,每條 routine 是 base + 偏移。server 端的 demux 函式據 message ID 分派。
- MIG 自動產生的拆包碼歷史上是型別混淆 / 邊界檢查類錯誤的來源類別(例如對收到的訊息欄位信任過度)。理解這點的價值在於:收訊息端必須把所有來自對端的欄位當成不可信輸入——這是和上一節同一個主題(信任邊界)的另一個面向。
- 從研究/防禦角度,看一個 service 用了哪些 MIG subsystem、demux 怎麼分派,有助於畫出它的攻擊面與信任邊界;這屬於結構理解,與「如何利用某個具體 bug」是兩回事。
⚠ 本章對 MIG 只談「類別與信任邊界」。 不列任何特定 subsystem 的可利用細節、不給觸發序列。要記住的心智模型是:MIG/XPC 的 server 端 = 一個信任邊界;對端送來的每個欄位都不可信;身分判斷一律綁 audit token。
把模型收口
- Mach port 是 kernel 管理的單向訊息佇列端點;進程持有的是 port right(receive / send / send-once)的不透明 handle,right 可隨訊息傳遞——這是能力導向 IPC 的底層。
launchd(PID 1)= bootstrap server:daemon 註冊 service 名→receive right,client 用bootstrap_look_up/ XPC 查名拿 send right 後連線;on-demand 啟動與 sandboxmach-lookup都建立在此。- XPC /
NSXPCConnection疊在 Mach 訊息上做型別化序列化 RPC;把危險解析隔離到低權 XPC service 是 privilege separation 的正規防禦。 - 信任缺陷的共同根因 = 用 PID 識別呼叫方(PID reuse → TOCTOU)。防禦 = 用 kernel 擔保的 audit token(含
pidversion),並對它驗 sandbox(sandbox_check_by_audit_token)/ code requirement(SecCodeCreateWithAuditToken)。這正是 confused-deputy 類別的防線。 - MIG 自動產生 marshalling stub;server 端是信任邊界,對端欄位一律不可信。
- 這些都是 M09「六道閘門 / 權限分層」裡 confused-deputy 拼圖的共同前置(與 M07 sandbox 的
mach-lookup收窄、M06 TCC 的 attribution 互相呼應)。
🟢 真機操作卡
下列為唯讀觀察,macOS 26 適用(部分輸出格式會隨版本變動,標「驗證於版本」):
# 🟢 看系統範圍註冊了哪些 Mach service(名→管理它的 launchd 網域)
sudo launchctl print system 2>/dev/null | grep -A2 -i "services" | head -40
# 這張表就是 bootstrap 的「service 名→port」註冊全景(驗證於版本)
# (唯讀;只是讀 launchd 目前的狀態,不改任何設定)
# 🟢 看單一 launchd 服務的細節:它宣告了哪些 MachServices、跑在哪個網域
sudo launchctl print system/com.apple.example.label 2>/dev/null | head -60
# 把 label 換成你在上一條看到的真實 label;觀察 endpoints / 狀態(驗證於版本)
# 🟢 看某個進程當前持有的 Mach port 與 right(純列舉,唯讀)
sudo lsmp -p <PID> 2>/dev/null | head -40
# 能看到 send/receive right、reply port 等;理解「進程手上的 IPC 能力」最直觀的視窗
# 🟢 看某個 App 內建的 XPC service(隔離高風險工作的低權子進程)
ls -la /Applications/Safari.app/Contents/XPCServices/ 2>/dev/null
# 每個 .xpc bundle 就是一個 bundled XPC service;可再對它抽 entitlements 看信任設定
# 🟢 對一個 XPC service 抽 entitlements / sandbox 設定(回顧 M03/M07)
codesign -d --entitlements - /Applications/Safari.app/Contents/XPCServices/*.xpc 2>/dev/null | head -40
# 觀察它是否 app-sandbox、宣告了哪些能力——這界定了「被打爆也只拿到低權」的邊界
# 🟢 看 XPC / launchd 相關的活動痕跡(偵測面,唯讀讀 log)
log show --last 10m --predicate 'subsystem == "com.apple.xpc"' --info 2>/dev/null | head -40
# 研究連線建立 / service on-demand 拉起 / 連線失敗的訊號(驗證於版本)
⚠ 上述全部是唯讀觀察:
launchctl print、lsmp、codesign -d、log show都只讀取狀態,不改變系統。本章不提供任何攻擊 service、偽造呼叫方、或串接提權的步驟——涉及信任缺陷的內容一律框成「類別 + 根因 + 防禦(驗 audit token 不驗 PID)+ 偵測面」。要在自己機器上實驗 XPC server/client 的正確寫法,請用你自己的程式碼與受控環境,並把身分驗證一律綁在 audit token 上。
下一步:M09 六道閘門 / 權限分層 — 把 M03 簽章、M05 SIP、M06 TCC、M07 sandbox、與本章的 XPC audit-token 驗證收斂成一張「一個請求要過哪些獨立閘」的疊加圖;confused-deputy 拼圖大關要求你同時持 Sandbox 逃逸類別、XPC audit-token、SIP confused-deputy 三張理解憑證,才看得出低權 actor 如何借高權 helper 之力——並給出對應防禦。