Suzu macOS Playground

M09 · 階梯 3 · L3

權限模型分層與互動

把前面每一道信任邊界疊起來看——POSIX/DAC、ACL、Sandbox、SIP、AMFI/code signing、TCC 各自管什麼、在什麼時點觸發。釐清「root 不是全能、也不短路 DAC」「通過一層 ≠ 整體放行」,並把 confused-deputy/提權路徑當成「層間借力」的能力組合來理解其根因與防禦,而非可操作的攻擊步驟。

● 前置:M03、M04、M05、M06、M07、M08

為什麼要把「閘」疊起來看

到這裡你已經單獨學過六種機制:code signing(M03)、Gatekeeper(M04)、SIP(M05)、TCC(M06)、Sandbox(M07)、Mach IPC/XPC(M08)。每一章都在回答「這一道機制管什麼、怎麼判」。

M09 不再教新機制,而是把它們疊在一起問一個收斂性的問題:

當某個 process 想做某件事(exec 一支二進位、open 相機、write 進 /System、要求 helper 代為操作),到底有幾道獨立的檢查會介入?任一道擋下會回什麼?哪些是 root 能影響的、哪些連 root 都擋得住?

這是整個信任模型的綜整節點。學會這一層,你才有能力對「某操作為什麼成功/失敗」給出機制級的解釋,也才看得懂提權與 confused-deputy 類問題的根因長什麼樣。

下面的實驗室把這六道閘畫成一條 deny-overrides 管線。選不同情境、或自己切換每道閘,看「誰先擋下、回什麼 errno」——但務必讀面板下方的模型邊界免責:這個固定線性順序是教學抽象,不是真實核心評估順序。

六道閘門決策實驗室

write()
結果拒絕(EPERM)首先擋下:SIP
  1. 1POSIX (DAC)uid 0(root)滿足擁有者寫入 → DAC 過。
  2. 2ACL不適用此操作。
  3. 3Sandbox不適用此操作。
  4. 4SIP (rootless)/System 帶 SF_RESTRICTED,非 rootless 授權的二進位 → EPERM。連 root 都擋。
  5. 5AMFI / CodeSigning不適用此操作。
  6. 6TCC不適用此操作。

點每道閘的圖示可切換 pass/deny/n/a 自由探索。deny-overrides:任一道擋下即整體拒絕——通過一層不等於整體放行。

⚠ 模型邊界:這條「固定線性六閘」是教學模型,不是真實核心評估順序。真實上 AMFI/code signing 在 exec 與 page-in 時點觸發、TCC 是 userspace tccd 的 XPC 決策(非 kernel MAC 閘)、SIP/Sandbox 是 MACF policy 但無此嚴格全域線性序。root 也不短路 DAC。

第一個關鍵直覺:root 不是神,而且 root 不短路 DAC

Unix 的素樸直覺是「root(uid 0)無所不能」。在現代 macOS 上這是錯的,而且要分兩件事講清楚,避免常見誤解:

  1. root 不短路 DAC。root 仍然要過 POSIX/DAC 這一關——它通常過得了(owner/權限位元對 uid 0 多半放行),但 DAC 仍會評估,而且有例外:沒有 execute bit 的檔案 root 也不能直接 exec;帶 SF_IMMUTABLE/SCHG 之類 BSD flags 的檔案,連 root 改不動。「root 自動跳過 DAC」是錯誤心智模型。
  2. 就算過了 DAC,後面還有一排 MAC 閘。sudo touch /System/Library/x 會回 Operation not permitted(EPERM)——DAC 那關 root 過了,是另一道獨立的 MAC(Mandatory Access Control)閘把它擋下:SIP/rootless(M05)。
# sudo touch /System/Library/x
touch: /System/Library/x: Operation not permitted   ← EPERM(MAC 擋),不是 EACCES(DAC 擋)

EPERM 與 EACCES 的區別在本章是貫穿線索:EACCES 多半是 DAC(權限不足)擋的,EPERM 多半是某道 MAC policy(Sandbox/SIP/AMFI)擋的。看到 EPERM 就該問「是哪一道 MAC?」而不是「我權限不夠嗎?」。

教學模型:六道閘門「疊加」(必讀的模型邊界)

為了把多道檢查講成一個好記的心智模型,本課用一張「六道閘門」圖:把一次操作想像成依序經過六道閘,採 deny-overrides——任一道擋下,整體就拒;要放行,得每一道都過。

閘名稱它在判什麼接哪一章
1POSIX / DACowner/group + 權限位元;root 通常過但仍受 execute bit、SF_*IMMUTABLE 制約本章
2ACLPOSIX 之上的延伸存取清單;ACE 的 deny 優先於 allow,可比 mode 更細本章
3Sandbox(seatbelt / MACF)該 process 的 SBPL profile 決策樹;default-deny、container 重導向M07
4SIP / rootless(MACF)目標是否帶 SF_RESTRICTED/protected path;非帶 com.apple.rootless.* 的平台二進位即擋M05
5AMFI / code signingexec 與 dylib 載入時驗 csflags、library validation、get-task-allow 對 task_for_pid 的閘控M03
6TCC隱私資源(相機/麥克風/文件/全磁碟…)的授權判定,由 userspace tccd 經 XPC 決策M06

⚠ P0 模型邊界(必讀,每個用到此模型的地方都會重述):這條「固定線性六閘管線」是教學模型,不是真實的核心評估順序。真實情況是:

  • AMFI / code signing 不是排在某個固定位置的一道閘,而是在 exec() 與 page-in(分頁載入程式碼頁)時點由 kernel 觸發驗證;
  • TCC 不是 kernel MAC 閘,而是 userspace tccd 透過 XPC 做的決策(client 發請求、tccd 查表/比對 csreq、必要時提示使用者),它在使用者空間,不在 MACF policy chain 裡;
  • SIP 與 Sandbox 都是 MACF(Mandatory Access Control Framework)policy,會在相關 kernel hook 被呼叫時各自表態,但並沒有一個嚴格的全域線性序把六者排成 1→6;
  • 多道 MACF policy 對同一個 hook 的結果,由 MACF 以**「任一拒即拒」的方式合成——這才是「deny-overrides」在真實系統裡的對應,而不是**「依序走過六個 stage」。

換句話說:「deny-overrides、通過一層 ≠ 整體放行」這個結論是真的;「六者排成固定一條線」只是幫助記憶的抽象。 評估時點、執行空間(kernel vs userspace)才是真實面貌。本章每次用到六閘圖,請同時記得這張「真實觸發時點對照」。

真實觸發時點對照(與上面六閘圖並讀)

機制真實觸發時點執行空間強制者
POSIX/DAC、ACL每次 open/exec/write 等 VFS 操作kernel(BSD VFS)kernel
Sandbox受監管 process 命中相關 MACF hook 時kernel(Sandbox.kext,MACF policy)kernel
SIP / rootless觸及 restricted 路徑/資源的操作命中 MACF hook 時kernel(MACF policy)kernel
AMFI / code signingexec() 與 page-in(程式碼頁載入)、dylib 載入、task_for_pid 等kernel(AMFI,userspace amfid 協同)kernel + amfid
TCCclient 嘗試存取隱私資源時,向 tccd 發 XPC 請求userspace(tccd)tccd(非 kernel 閘)

讀法:六閘圖回答「有哪些獨立檢查、任一擋下即拒」;這張表回答「它們各自在何時、在哪一層真的被觸發」。兩張一起看,才不會把教學抽象誤當成核心實作。

平台二進位 vs 第三方:身分決定能借到什麼

很多權限差異不取決於「你是不是 root」,而取決於「這支程式碼是誰簽的、是不是 Apple 平台二進位」。

  • 平台二進位(platform binary):Apple 隨系統出貨、由 platform 簽署的二進位。它們可帶 com.apple.rootless.* 等 restricted entitlements,因此能做一般程式(包含 root 跑的第三方程式)做不到的事——例如系統更新流程能寫入 SIP-restricted 路徑。判定來源是 kernel 對該 process 的 code signing 旗標(csflags 的 platform-binary 位元 / is_platform_binary),不是「執行身分」。
  • 第三方二進位:即使以 root 執行,也拿不到 restricted entitlements,因此過不了 Gate 4(SIP)對 restricted 路徑的保護。

⚠ 判定來源要分清楚:is_platform_binary 是 kernel 依 code signing 決定的屬性(ESF 事件、csops 回報的 csflags 是權威來源)。codesign -dvvv 輸出裡的 Platform identifier=NN 是個方便的旁證,不要把它和 flags=0x... 裡的位元混為一談——前者是 SDK/平台標記,真正的閘控依據是 kernel csflags。

這就是為什麼「身分(identity)比權限位元(uid)更能決定能力」——而這正是下一節 confused-deputy 的根。

Authorization Services 與 securityd:「請別人代為決定」的那一層

除了上述六閘,macOS 還有一層 Authorization Services:當一個程式想做需要管理者核可的事(安裝、改系統設定),它不是自己判斷,而是向系統的授權機制申請一個 right(例如 system.privilege.admin),由系統依授權政策資料庫(/var/db/auth.db,政策可用 security authorizationdb read <right> 唯讀檢視)決定要不要彈出認證視窗、要不要放行。

⚠ errata(修正常見過時說法):舊資料常說授權由獨立的 _securityd 使用者帳號處理。在現代 macOS(含 macOS 26)並沒有 _securityd 這個使用者帳號(查無此 record)。Authorization Services 由 securityd 提供(/usr/sbin/securityd,以 root 執行;另有 securityd_system),憑證/keychain 相關另有 trustd(以 _trustd 執行)等 daemon 協同。要描述這層時請用 securityd / Authorization Services,不要寫成 _securityd 使用者。

Authorization 的重點觀念:它是**「把決策外包給有權限的系統元件」**的機制。這個「外包」模式很方便,但也正是 confused-deputy 類問題的溫床——下一節說明。

confused-deputy 與「層間借力」:根因與防禦(非攻擊步驟)

把六閘 + Authorization 疊起來後,會自然浮現一類邏輯問題,理解它是本章階梯 3 的核心能力。這裡只談類別、根因、防禦、偵測,不給任何可操作的利用步驟或 PoC。

confused deputy(被混淆的代理人)是什麼:一個本身有較高權限的元件(deputy,例如帶 restricted entitlement 的特權 helper、或能代為向 securityd 申請 right 的服務),被一個低權限的請求方說服去代替它做事。系統若只看「是誰在執行這個動作」(deputy 的高權限身分),而沒有驗證「這個請求最初是誰發起的、是否該被允許」,低權方就「借」到了 deputy 的能力。這不是記憶體破壞,而是信任歸屬(attribution)錯置的邏輯缺陷。

為什麼六閘模型讓它「自然湧現」:閘是依動作執行者的身分判定的。低權 actor 自己過不了 Gate 4(沒有 restricted entitlement),但若它能讓一個帶 heritable restricted entitlement 的特權 helper 代為發起該動作,閘看到的是 helper 的身分 → 放行。能力被「層間借力」地組合起來,沒有任何一道閘被「打破」——每道閘都如實依其所見的身分作答,問題出在「所見的身分不是真正該負責的那個」。

根因(共通模式):

  • 特權元件對外暴露了過於泛用的操作介面(「幫我寫這個檔」「幫我跑這個」),卻沒有把「誰有資格要求」收斂;
  • 以 PID 等可被回收/搶用的識別來歸屬請求方,而非以 audit token(接 M08)這種綁定到連線、不可偽冒的責任憑證;
  • entitlement 設計得過寬或可繼承(heritable),使能力外溢。

防禦(這才是學習目標):

  • 驗證責任方而非執行方:特權服務在執行敏感操作前,用 audit token 確認「發起這次請求的 client 究竟是誰」,拒絕不該有此能力的請求方(直接呼應 M08「audit token vs PID 的責任驗證」)。
  • 最小化與收斂介面:特權 helper 只暴露語意明確、範圍最小的操作,不提供「萬用代跑/代寫」原語。
  • 收緊 entitlement:restricted/heritable entitlement 只給真正必要的二進位,避免能力可被繼承外溢。
  • Launch Constraints(接 M03):限制「誰能啟動/以什麼脈絡啟動」某支特權二進位,縮小可被借力的入口。

偵測(接 M14):用 Endpoint Security 觀察「低權/第三方父程序 → 觸發特權 helper 代為操作敏感資源」這類身分與動作不相稱的 exec/IPC 組合;is_platform_binary=false 的請求方驅動了 platform helper 的高權動作,就是值得寫成偵測規則的特徵組合。

⚠ 本章邊界:以上全部停留在機制根因與防禦設計。本課不提供 offset、payload、特定 helper 的可利用清單、或任何「照做就能提權」的步驟——那不是學習權限模型的必要,反而會掩蓋「驗證責任歸屬」這個真正該內化的防禦原則。

🟢 真機操作卡

以下皆為唯讀觀察,在你自己的機器上安全執行(macOS 26 適用)。不會更動任何系統狀態。

# ── 閘 1/4:DAC 與 SIP-restricted 旗標(為什麼 root 也寫不進 /System)──
ls -ldO /System /usr /usr/local /Applications     # 看 restricted(SIP)vs sunlnk vs 一般
id                                                 # 你目前的 uid/gid 與所屬群組(admin 等)

# ── 閘 5:平台二進位 vs 第三方(身分決定能力)──
codesign -dvvv /bin/ls 2>&1 | grep -E 'Platform|TeamIdentifier|flags'
codesign -dvvv /usr/bin/codesign 2>&1 | grep -E 'Platform|TeamIdentifier|flags'
# 對照一個第三方/Homebrew 二進位(若有):TeamIdentifier 會是某 Team 而非平台標記
# 真正權威的 is_platform_binary 來自 kernel csflags(ESF 事件),上面是方便旁證

# ── Authorization Services:securityd 的授權政策(唯讀檢視 right 的規則)──
security authorizationdb read system.privilege.admin   # 看這個 right 的授權政策(class/group/rule)
security authorizationdb read system.install.software  # 安裝軟體的授權政策

# ── 哪些系統 daemon 在跑、以誰的身分跑(理解「層間借力」的潛在 deputy)──
ps -axo user,pid,comm | grep -E 'securityd|trustd|tccd|amfid|syspolicyd' | grep -v grep
dscl . -read /Users/_securityd 2>&1 | head -1     # 觀察:現代 macOS 查無此 record(_securityd 不存在)

⚠ 危險/授權邊界:本章卡片全為唯讀。不要用 sudo touch /System/... 之類去「硬幹」觀察 EPERM——看 ls -ldO 的 restricted 旗標即可理解,無需真的對受保護路徑發動寫入。任何停用 SIP(csrutil disable,需 recoveryOS)、修改 authorizationdb(security authorizationdb write)、或調整 entitlement/啟動限制的操作,都會降低系統安全且超出本章唯讀範圍,僅應在你有權限的研究機上、依授權流程進行(DESIGN.md §10)。


下一步:M10 launchd、持久化與現代核可 — 既然你已經看懂「能力如何疊加與借力」,下一站看攻擊者如何把立足點寫進開機/登入流程(LaunchAgents/Daemons、SMAppService、BTM),以及這些動作各自留下什麼偵測訊號。