M07 · 階梯 3 · L3
App Sandbox 與 seatbelt(SBPL)
一個進程能 open() 什麼、能跟誰 mach-lookup,由 SBPL profile 經 Sandbox policy module 掛在 MACF 上逐操作判定;container 重導向把家目錄虛擬化,sandbox extension 動態鬆綁——sandbox 不是權限,是進程的「能力地圖」。
沙盒不是「關起來」,是「能力地圖」
第一個直覺要先打掉:很多人以為 sandbox 是「把 App 關在一個資料夾裡」。不對。一個 sandboxed 進程跟其他進程跑在同一個 kernel、同一個 uid 下;它沒有被關進獨立的命名空間或容器(這不是 Linux 的 namespaces/cgroups)。它被限制的是每一個系統呼叫當下「能不能做」:這次 open() 准不准、這次 mach_lookup 找不找得到那個 service、這次 connect() 出不出得去網路。
換句話說,sandbox 描述的是這個進程的能力地圖(capability map):預設一片荒地(default deny),profile 在地圖上開出一條條允許通行的路。M05 你看到 SIP 是一道系統層的 MAC 閘擋寫 /System;sandbox 是同一套 MACF 機制,但閘的粒度落到單一進程,而且地圖是逐進程、由它載入的 profile 決定。
⚠ 重點:sandbox 是 MAC,不是 DAC。 你已經是 uid 501、檔案權限也允許你讀某檔——sandbox 仍可獨立把這次
open()擋成EPERM。它和 POSIX 權限(DAC)、SIP、TCC 是各自獨立的閘,任一道 deny 就整體失敗(deny-overrides)。這正是 M09「六道閘門」要收斂的心智模型,sandbox 是其中一道。
SBPL:用 Scheme 寫的 policy
App Sandbox 的規則語言叫 SBPL(Sandbox Profile Language),語法是 Scheme-like 的 S-expression。它長這樣(教學示意,非完整語言):
(version 1)
(deny default) ; 預設全擋——這是 sandbox 的靈魂
(allow file-read* ; 允許「讀類」操作
(subpath "/usr/lib")
(literal "/etc/hosts"))
(allow mach-lookup ; 允許向某些 Mach service 查詢
(global-name "com.apple.system.logger"))
(allow network-outbound ; 允許對外連線
(remote tcp "*:443"))
讀法:每條 rule 是 (動作 操作 過濾器…)。
| 元素 | 角色 | 例子 |
|---|---|---|
| 動作(action) | 允許或拒絕 | allow / deny |
| 操作(operation) | 被攔截的能力類別 | file-read*、file-write*、mach-lookup、network*、process-exec、iokit-open |
| 過濾器(filter) | 把操作收斂到具體目標 | (subpath …)、(literal …)、(global-name …)、(regex …) |
(deny default) | 兜底基線 | 沒被任何 allow 命中就落到這裡 |
幾個關鍵語意:
*是操作家族展開:file-read*同時涵蓋file-read-data、file-read-metadata、file-read-xattr等子操作。寫 profile 時的常見漏放/過放都出在這裡。- 評估順序與 deny-overrides:可以把 SBPL 心智模型化為「default + 規則疊加」,最終以是否命中 allow 決定。重點是預設
deny default——沒明確允許的就是擋。 - filter 是攻防焦點:
(regex …)過濾器寫得太寬(例如想限制某子目錄卻配到鄰接路徑)就是典型的 profile 過放根因——這是規則撰寫邏輯錯誤,不是 kernel 漏洞。
⚠ SBPL 是「教學用真子集」。 本課只討論 SBPL 的一個可讀子集:核心 operation、
allow/deny/default、幾種 filter。真實 SBPL 還有 modifiers、require-*、with、metafilter、entitlement 條件式等大量未公開語法,且未正式文件化、跨版本會變。任何「SBPL 完整文法」的宣稱都該存疑——以本機/System/Library/Sandbox/Profiles/*.sb與/usr/share/sandbox/的實際內容為準(驗證於版本)。
下面的求值器示範一個 profile 怎麼把「能不能做某操作」收斂成 allow/deny——注意 last-match-wins(先 allow 一個子樹、再用更 specific 的 deny 覆寫):
SBPL sandbox 求值器
- (deny default) ← 起點
- #1 (allow file-read* /usr/lib) ← 命中
- #2 (allow file-read* ~/Library/Containers/com.x.app/Data)
- #3 (allow file-write* ~/Library/Containers/com.x.app/Data)
- #4 (allow mach-lookup com.apple.fonts)
- #5 (deny file-read* ~/Library/Containers/com.x.app/Data/secret)
命中規則 #1(allow file-read*)。last-match-wins:後面的規則覆寫前面的。
試 file-read-data ~/Library/Containers/com.x.app/Data/secret/key:先被 allow(Data 子樹)命中、再被後面更specific 的 deny(secret 子樹)覆寫 → 最終 deny。這就是 last-match-wins。SBPL 為教學子集,非完整語言。
Sandbox policy module 掛在 MACF:閘在哪一層
SBPL 文字不會自己生效。流程是:
- 編譯:profile 文字被編譯成一種緊湊的 bytecode(給 kernel 的決策表),不是直接拿字串去比對。App Sandbox 的 profile 通常在進程啟動時由系統套用。
- 掛鉤:sandbox 的 kernel 端是一個 MACF policy module(Sandbox;在 Apple Silicon 上內建於 kernelcache,傳統上以
Sandbox.kext形式存在),它向 **MACF(Mandatory Access Control Framework,TrustedBSD 衍生)**註冊一組 hook。MACF 在 kernel 各個敏感操作點(vnode存取、Mach port 操作、socket等)插入了 policy 呼叫點。 - 逐操作判定:每次該進程觸到一個被 hook 的操作,MACF 就帶著「操作 + 主體(進程的 sandbox 標籤)+ 客體(路徑/service 名…)」去問所有註冊的 policy module;Sandbox policy 拿進程的已編譯 profile 跑決策,回 allow/deny。
使用者進程 ── syscall (open / mach_msg / connect) ──► kernel
│
MACF hook 點(mac_vnode_check_open …)
│ 問各 policy module
┌──────────────────┼───────────────────┐
Sandbox policy AMFI (其他 MAC)
跑此進程的 簽章/權限
已編譯 profile 檢查
│
任一 deny → EPERM(deny-overrides)
⚠ 同一套 MACF,多個 policy module。 M05 的 SIP/rootless、本章的 sandbox、以及 AMFI(程式碼簽章/權限的核心閘)都是掛在 MACF 上的 policy module。它們獨立判定、彼此不知道對方,最後由 kernel 做 deny-overrides 疊加。所以「sandbox 放行」不代表「AMFI 放行」,反之亦然。理解 sandbox 的正確切面是「它是 MACF 上的一個 policy,粒度到單進程」。
Container 重導向:路徑被虛擬化了
App Sandbox 最容易誤判的機制是 container 重導向。一個 sandboxed App(帶 com.apple.security.app-sandbox entitlement)啟動時,系統幫它建立並指向一個 container:
~/Library/Containers/<bundle-id>/Data/
關鍵在於:對這個 App 來說,它以為自己在用 ~/Documents、~/Library 等家目錄路徑,但這些路徑被重導向進它自己的 container Data/ 底下。實際落地時有兩種形態並存:App 私有的目錄(如 Documents、Library)直接實作在 container Data/ 內;而像 Desktop、Downloads、Movies、Music、Pictures 等則常以 symlink 指回真正的 ~/——但這些 symlink 指回去的存取仍受 sandbox profile 與 TCC 把關,並非無條件可讀。它看到的「家目錄」整體是一個虛擬化、私有的視角。
# 觀察某個 sandboxed App 的 container 真實落點(唯讀)
ls -la ~/Library/Containers/com.apple.TextEdit/Data/
# 你會看到 Documents / Library 實作在 container 內部,
# 而 Desktop / Downloads 等是指回真實 ~/ 的 symlink(存取仍受 sandbox / TCC 管)
這帶來兩個常被搞錯的點:
- 路徑欺騙不成立的根因:因為解析發生在 sandbox 層,App 對私有路徑(如
~/Documents/secret)的寫死只會落到自己的 container;對指回真實家目錄的路徑,存取也要逐次過 profile / TCC。所以「猜路徑」對 sandboxed App 沒用——它的路徑命名空間本身被虛擬化、且每次存取都還要過閘。 - 跨 App 資料隔離:每個 bundle-id 一個 container,預設彼此讀不到。共享要走明確機制(App Group 的
~/Library/Group Containers/、或使用者透過 powerbox/開啟對話框授予的 file),這些授予就是下面要講的 sandbox extension。
⚠ container ≠ 完整檔案系統隔離。 container 重導向只覆蓋家目錄類路徑與被 profile 限制的範圍;profile 仍可
allow file-read*到/usr/lib、/System/Library等共用唯讀資源。把 container 想成「家目錄被換成私有視角」,不是「整個檔案系統被換掉」。
Sandbox extension:動態把一扇門打開
靜態 profile 寫死「能讀哪些路徑」,但使用者臨時用開啟對話框選了一個 ~/Downloads/report.pdf——這條路不在 profile 裡,怎麼讀得到?答案是 sandbox extension:一個由有權的一方(例如 powerbox / com.apple.appkit.xpc.openAndSavePanelService,或某個 daemon)發出的動態能力令牌(token)。
機制直覺:
- extension 是一段不可偽造的 token,代表「對某資源的某種存取」這個能力(capability)。
- 進程拿到 token 後**consume(消費/啟用)**它,sandbox 就在那次存取放行該資源;可以是程序生命週期內持有,或用完即棄。
- 這是能力導向(capability-based)授權:不是改 profile,而是在執行期遞交一張票。使用者「同意開啟這個檔」這件事,被翻譯成一張只對這個檔有效的 token。
# 真機觀察 sandbox 的拒絕/授予痕跡(唯讀,看 log)
log show --last 10m --predicate 'sender == "Sandbox"' --info | head -40
# 看 "deny" 行能讀出:哪個進程、哪個 operation、哪個目標被擋
⚠ extension 是攻防上的「動態信任邊界」。 研究視角要關注的是 token 的發放者是否驗對了請求方、token 的範圍是否過寬、是否該用完即收。一個「把過寬 extension 發給低權進程」的 design 缺陷,屬於 confused-deputy 類別(M08 會正式展開)——高權者代低權者行使了它不該得到的能力。這裡只談類別與防禦面,不涉及任何具體繞過步驟。
app-sandbox entitlement 與例外能力
一個 App 進不進 sandbox、進去後能多開哪些門,由它的簽章 entitlements 決定(回顧 M03:entitlement 是簽進 code signature、由 AMFI 認的鍵值)。
| Entitlement(示意) | 意義 |
|---|---|
com.apple.security.app-sandbox | 開啟 App Sandbox(true 才進沙盒) |
com.apple.security.network.client | 例外能力:允許對外網路連線 |
com.apple.security.files.user-selected.read-write | 允許存取使用者透過對話框選取的檔(搭配 powerbox + extension) |
com.apple.security.device.camera | 宣告需要相機(仍受 TCC 把關,見 M06) |
兩個關鍵釐清:
- entitlement 是「申請」,不是「自動放行」。宣告
network.client讓 profile 開出網路能力;但相機這類仍要再過 TCC(M06)。sandbox 與 TCC 是兩道獨立閘——sandbox 決定「這個進程的 profile 准不准碰相機這個 device class」,TCC 決定「使用者授不授權這個 App 用相機」。 - entitlements 受 AMFI 把關。entitlement 不是 App 自己說了算的明文宣告——它簽在 signature 裡,由 AMFI 驗證(且某些受限 entitlement 只發給 Apple 平台二進位)。把 sandbox 例外能力的可信度錨定回 M03 的簽章鏈,是正確的理解。
把模型收口
- sandbox = 掛在 MACF 上、粒度到單進程的一個 policy module(Sandbox policy)。
- 規則用 SBPL(Scheme-lite,
deny default+allow 操作 過濾器)寫成,編成 bytecode 給 kernel 逐操作判定。 - container 重導向把家目錄虛擬化成
~/Library/Containers/<id>/Data/;路徑命名空間被換掉、每次存取仍過閘,所以猜路徑無效。 - sandbox extension 是執行期的能力 token,把「使用者臨時同意」翻成精確、可消費的授權;過寬發放 = confused-deputy 類別。
- sandbox 與 DAC / SIP / TCC / AMFI 各自獨立,最終 deny-overrides 疊加——這是 M09 的伏筆。
🟢/🟡 真機操作卡
下列為唯讀或安全觀察,macOS 26 適用(部分輸出格式可能隨版本變動,標「驗證於版本」):
# 🟢 看某 App 是否啟用 sandbox:抓它的 entitlements
codesign -d --entitlements - /System/Applications/TextEdit.app 2>/dev/null
# 找 com.apple.security.app-sandbox(true 即沙盒)與各例外能力鍵
# 🟢 看 sandboxed App 的 container 真實落點與虛擬化結構
ls -la ~/Library/Containers/com.apple.TextEdit/Data/
# 觀察哪些目錄實作在 container 內、哪些是指回真實 ~/ 的 symlink(家目錄被虛擬化)
# 🟢 看系統內建的 profile 文字(SBPL 真檔,學語法用)
ls /System/Library/Sandbox/Profiles/ 2>/dev/null
ls /usr/share/sandbox/ 2>/dev/null
# 挑一個 .sb 開來讀:看 (deny default) 與 allow 規則怎麼寫(驗證於版本)
# 🟡 看 sandbox 的判定痕跡(哪些 operation 被擋/放)
log show --last 15m --predicate 'sender == "Sandbox"' --info 2>/dev/null | head -40
# "deny(1)" 行 = 哪個進程(pid)對哪個 operation/目標被擋;這是 sandbox 的偵測訊號
# 🟡 即時觀察 sandbox 拒絕事件(Ctrl-C 結束)
log stream --predicate 'sender == "Sandbox" && eventMessage CONTAINS "deny"' --info
# 研究/除錯一個被擋的存取時,這是第一手線索
⚠ 關於
asctl: 早期 macOS 有asctl(App Sandbox Container 工具)可查 container 路徑,但新版 macOS(含 macOS 26)已不再提供此指令。要看 container 真實落點,請用上面的ls -la ~/Library/Containers/<bundle-id>/Data/;container 的 metadata 在現代 macOS 是隱藏檔.com.apple.containermanagerd.metadata.plist(舊版的Container.plist已不存在),可用plutil -p ~/Library/Containers/<bundle-id>/.com.apple.containermanagerd.metadata.plist唯讀檢視(需ls -a才看得到,驗證於版本)。不要依賴已被移除的指令。
⚠
sandbox-exec可用任意 SBPL profile 啟動一個受限進程,是學 SBPL 的好工具,但 Apple 已在 man page 明確標為 DEPRECATED,且自製 profile 只該在你自己的機器 / 受控 VM 上玩、用於理解語意,不要拿來規避他人系統的保護。任何「鬆綁某 App 的 sandbox」「重簽改 entitlement」都會降低該 App 與整機安全,且重簽需有效簽章與授權;本章只示範唯讀觀察。涉及繞過的內容一律框成「機制理解 + 偵測面」,不提供 offset / payload / exploit 步驟。
下一步:M08 Mach IPC / XPC / MIG — sandbox 把進程的 mach-lookup 收窄成「只能找到某些 service」;下一章就看這些 service 怎麼經 Mach port / XPC 連起來,以及高權 daemon 若用 PID 而非 audit token 驗請求方,會怎麼被 confused-deputy 利用。