M09 · 階梯 3 · L3
權限模型分層與互動
把前面每一道信任邊界疊起來看——POSIX/DAC、ACL、Sandbox、SIP、AMFI/code signing、TCC 各自管什麼、在什麼時點觸發。釐清「root 不是全能、也不短路 DAC」「通過一層 ≠ 整體放行」,並把 confused-deputy/提權路徑當成「層間借力」的能力組合來理解其根因與防禦,而非可操作的攻擊步驟。
為什麼要把「閘」疊起來看
到這裡你已經單獨學過六種機制: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()- 1POSIX (DAC)uid 0(root)滿足擁有者寫入 → DAC 過。
- 2ACL不適用此操作。
- 3Sandbox不適用此操作。
- 4SIP (rootless)/System 帶 SF_RESTRICTED,非 rootless 授權的二進位 → EPERM。連 root 都擋。
- 5AMFI / CodeSigning不適用此操作。
- 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 上這是錯的,而且要分兩件事講清楚,避免常見誤解:
- root 不短路 DAC。root 仍然要過 POSIX/DAC 這一關——它通常過得了(owner/權限位元對 uid 0 多半放行),但 DAC 仍會評估,而且有例外:沒有 execute bit 的檔案 root 也不能直接
exec;帶SF_IMMUTABLE/SCHG之類 BSD flags 的檔案,連 root 改不動。「root 自動跳過 DAC」是錯誤心智模型。 - 就算過了 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——任一道擋下,整體就拒;要放行,得每一道都過。
| 閘 | 名稱 | 它在判什麼 | 接哪一章 |
|---|---|---|---|
| 1 | POSIX / DAC | owner/group + 權限位元;root 通常過但仍受 execute bit、SF_*IMMUTABLE 制約 | 本章 |
| 2 | ACL | POSIX 之上的延伸存取清單;ACE 的 deny 優先於 allow,可比 mode 更細 | 本章 |
| 3 | Sandbox(seatbelt / MACF) | 該 process 的 SBPL profile 決策樹;default-deny、container 重導向 | M07 |
| 4 | SIP / rootless(MACF) | 目標是否帶 SF_RESTRICTED/protected path;非帶 com.apple.rootless.* 的平台二進位即擋 | M05 |
| 5 | AMFI / code signing | exec 與 dylib 載入時驗 csflags、library validation、get-task-allow 對 task_for_pid 的閘控 | M03 |
| 6 | TCC | 隱私資源(相機/麥克風/文件/全磁碟…)的授權判定,由 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 signing | exec() 與 page-in(程式碼頁載入)、dylib 載入、task_for_pid 等 | kernel(AMFI,userspace amfid 協同) | kernel + amfid |
| TCC | client 嘗試存取隱私資源時,向 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),以及這些動作各自留下什麼偵測訊號。