M06 · 階梯 3 · L2
TCC 隱私框架(tccd 判定管線)
為何「同一支程式重簽就要重新授權」——雙層 TCC.db 欄位、tccd 在 userspace 以 XPC 做的授權決策、用 audit token(非裸 PID)做責任歸屬、csreq 釘選 designated requirement,以及 auth_value 各態(limited 僅 Photos 等特定服務)。
SIP 保護系統檔,TCC 保護你的資料
M05 你看到 SIP 是一道系統層的 MAC 閘,擋的是對 /System 這類受保護路徑的修改。TCC(Transparency, Consent, and Control)守的是另一條線:你的個人資料與裝置——相機、麥克風、螢幕錄製、通訊錄、行事曆、「文件與桌面」、整顆磁碟(Full Disk Access)。
關鍵差異要先講清楚:TCC 不是 kernel MAC 閘。它是一個跑在 userspace 的 daemon tccd,透過 XPC 接受「某 client 要存取某資源」的查詢,查自己的資料庫、必要時彈出同意視窗,回一個 allow / deny。M09「六道閘門」會把 TCC 畫成一道閘——那是教學抽象,真實上它和 DAC / Sandbox / SIP / AMFI 各自獨立判定,TCC 這一道是 userspace 的 XPC 決策,不是核心強制點。
⚠ TCC 是 userspace 授權,不是 kernel MAC(DESIGN.md §11)。 AMFI、SIP、Sandbox 都是掛在 MACF 上的核心 policy;TCC 是
tccd這個 userspace daemon 的 XPC 決策。它能擋住「App 想開相機」是因為相關 API/framework 會先去問tccd,不是因為 kernel 在 syscall 點攔截。把 TCC 想成「隱私資源的守門人 daemon」,比想成「核心閘」更接近真實。
下面的實驗室模擬 tccd 的判定管線——切換 audit token、csreq 比對、紀錄值,看每一步怎麼決定 allow / deny / prompt:
TCC 判定實驗室
雙層 db:~/Library/.../TCC.db(user)先於 /Library/.../TCC.db(system)。此服務無 limited。
- attribution用 audit token 歸屬到責任 client:com.example.app(非看 PID)。
- lookup雙層 tcc.db 都查無 (service, client) 紀錄 → 視為 unknown。
- prompt無紀錄 → 由系統跳出授權提示(若無 UI 環境則視為拒絕)。
查無紀錄 → 提示使用者授權(首次請求)。
試試:把「目前簽章符合 DR」關掉(模擬冒名/重簽)→ 既有 allowed 紀錄作廢、重新提示。把 audit token 關掉 → 連歸屬都失敗。
模型邊界:tccd 判定為**教學決策模型**(audit token 歸屬 → 雙層查表 → csreq DR → auth_value),非逐欄位實作。TCC.db 受 SIP/FDA 保護,本實驗室用合成資料。驗證於 macOS 26.3.1。
雙層 TCC.db:系統層 vs 每使用者層
TCC 的決策資料存在 SQLite 資料庫,而且是兩層:
| 層 | 路徑 | 管哪些服務 |
|---|---|---|
| 系統層(system) | /Library/Application Support/com.apple.TCC/TCC.db | 跨使用者、需更高權限的類別(如 Full Disk Access、Screen Recording、Accessibility) |
| 每使用者層(per-user) | ~/Library/Application Support/com.apple.TCC/TCC.db | 該登入使用者的相機、麥克風、通訊錄、行事曆、照片等 |
兩個 DB 都被保護:系統層 TCC.db 落在 SIP 保護的範圍,連 root 都不能隨意改;要讀它(或讀別的 App 的權限狀態)本身就需要 Full Disk Access——這是個漂亮的自指:連檢視 TCC 的工具自己都受 TCC 管。
核心資料表 access 的關鍵欄位(教學重點,欄位名以實際版本為準):
| 欄位 | 意義 |
|---|---|
service | 資源類別,kTCCService*(如 kTCCServiceCamera、kTCCServiceScreenCapture、kTCCServiceSystemPolicyAllFiles) |
client | 被授權的主體識別(bundle ID 或可執行路徑) |
client_type | 0 = bundle ID,1 = 絕對路徑 |
auth_value | 授權狀態(見下表) |
auth_reason | 為何是這個狀態(使用者點選 / MDM 設定檔 / entitlement / 系統設定…) |
csreq | 釘選的 designated requirement(見「csreq 釘選」一節) |
auth_value:四個狀態,但 limited 不是通用態
auth_value 的主要狀態值:
| 值 | 狀態 | 意義 |
|---|---|---|
0 | denied | 明確拒絕 |
1 | unknown | 尚未決定(下次存取會觸發詢問流程) |
2 | allowed | 明確允許 |
3 | limited | 部分授權 |
ℹ 這不是窮舉:macOS 版本會新增其他值(社群逆向命名如
4 = allowedAlways;實機 TCC.db 也可見到5等),完整列舉以實際版本為準。0–3 是理解機制的主軸,不是全集。
⚠
limited(3) 不是通用四態(DESIGN.md §11)。 別把auth_value=3 (limited)當成所有kTCCService都有的第四態。limited目前主要用於 Photos(照片的「選取部分照片」)這類特定服務——使用者只授權 App 看選定的相片,而非整個圖庫。絕大多數類別(相機、麥克風、Full Disk Access…)實務上只有 denied / unknown / allowed 的語意。把limited講成「通用四態」是常見錯誤。
tccd 判定管線:一次授權查詢發生什麼
當某 client 第一次(或在狀態為 unknown 時)要碰受保護資源,大致流程是:
client(想用相機)
│ 經 API / framework
▼
向 tccd 發 XPC 查詢:「我要 kTCCServiceCamera」
│
├─ 1. tccd 取呼叫方的 audit token → 解析出責任程序(responsible process)
├─ 2. 用該程序的 code signature 算出 designated requirement
├─ 3. 查 TCC.db:(service, client) 是否已有紀錄?
│ ├─ 有紀錄 → 比對紀錄裡釘選的 csreq 與當前程序是否仍相符
│ └─ 無紀錄且 auth_value=unknown → 視 entitlement / 觸發同意 UI
└─ 4. 回 allow / deny(必要時把使用者決定寫回 DB)
兩個最該記住的設計點,下面分開講。
audit token:用「身分票」而非裸 PID 做責任歸屬
TCC 要回答的不是「哪個 PID 問的」,而是「哪個程式該為這次存取負責」——也就是 responsible process(責任程序)。例如一個 helper 經由某 App 啟動、或透過 XPC 代呼叫,TCC 要把授權歸到真正該負責的那一方,而不是中間的轉手者。
為了可靠地辨識呼叫方,tccd(以及現代特權服務)用的是 audit token,不是裸 PID。
⚠ 為什麼是 audit token 而非 PID(DESIGN.md §11,與 M08 呼應)。 PID 會被重用:程序 A 用 PID 1234 發了請求,A 結束後 PID 1234 可能很快被另一個程序 B 拿到。如果服務只記下「PID 1234」再回頭查驗,就可能把 B 誤認成 A——這是 PID 重用混淆(PID-reuse confusion),一類 confused-deputy 風險的根因。audit token 是核心提供的不可偽造結構,攜帶
pid加上pidversion(以及其他審計欄位),能唯一辨識「就是當時那個程序實例」,避開 PID 被回收後的張冠李戴。防禦面:服務端要做責任歸屬/權限驗證時,應取 audit token 並用基於 token 的介面(而非
pid取得後再查),把驗證錨在不可重用的身分上。這是 M08 confused-deputy 主題的伏筆——本章只談類別與根因,不涉及任何可操作的繞過步驟。
csreq:釘選 designated requirement,換 binary 就失配
TCC.db 的每筆授權都釘了一個 csreq——它是把這次授權綁到一個具體的程式碼簽章身分上的 designated requirement(DR,回顧 M03)。DR 描述「誰可以是這支程式」(例如:特定 Team ID + 特定 bundle ID,或特定 cdhash 的 anchor 條件)。
下次同一個 (service, client) 再來存取,tccd 會重新用當前程序的簽章去比對紀錄裡釘的 csreq:
- 相符 → 沿用既有授權(不再煩使用者)。
- 不符 → 視同沒有有效授權,需要重新走同意流程。
⚠ 換 binary 即失配,需重新授權(DESIGN.md §11)。 因為授權被
csreq(DR)釘在簽章身分上,把可執行檔換掉、改 entitlements、用不同身分重簽,都會讓當前簽章與釘選的 csreq 失配——TCC 不認,舊授權自動失效,得重新取得同意。這把「授權」和「身分」綁在一起:你授權的不是「那個路徑上的檔」,而是「符合那個 DR 的程式」。這也是為什麼 M03 的 CDHash / DR 是 TCC 的底層——授權的可信度,最終回到簽章鏈。
授權來源:使用者點選 vs 預先授權
不是每個 TCC 紀錄都來自彈窗。auth_reason 區分了授權怎麼來的:
- 使用者互動:彈出同意視窗,使用者按了允許/拒絕(最常見)。
- PPPC / MDM:受管理的裝置可由 MDM 下發 PPPC(Privacy Preferences Policy Control)設定檔,預先為特定 client + service 授權,繞過彈窗。這在企業部署很常見——但設定檔本身也釘簽章身分(同樣靠 DR 概念綁定),不是無條件放行。
- Entitlement-gated:少數類別會看呼叫方是否帶特定(多半是 Apple 平台限定)entitlement。
- 系統設定/繼承:使用者在 System Settings > Privacy & Security 手動開關、或某些責任關係的繼承。
⚠ PPPC 不是「萬能鑰匙」。 MDM 預先授權只在受管理且設定檔受信任的前提下生效,且一樣綁定 client 的簽章身分。把它理解成「組織以政策替使用者做了同意決定」,而非「繞過 TCC 的後門」。
把模型收口
- TCC = userspace 的
tccd經 XPC 做的隱私資源授權決策,不是 kernel MAC 閘(和 SIP/Sandbox/AMFI 各自獨立)。 - 決策存在雙層 SQLite:系統層(SIP 保護)+ 每使用者層;讀它需要 Full Disk Access。
auth_value:0 denied / 1 unknown / 2 allowed / 3 limited(limited 僅 Photos 等特定服務,非通用四態)。- 責任歸屬用 audit token(pid + pidversion),不是裸 PID——避免 PID 重用混淆(M08 confused-deputy 根因)。
- 每筆授權用
csreq釘選 designated requirement:換 binary / 改身分即失配,須重新授權——授權綁的是「身分」不是「路徑」。
🟢/🟡 真機操作卡
下列皆為唯讀觀察,macOS 26 適用(部分輸出格式可能隨版本變動,標「驗證於版本」)。讀 TCC.db 與看 tccd 決策都不改任何授權狀態。
# 🟢 認識 tccutil:它唯一支援的子命令是 reset(清某 service 的授權)
tccutil 2>&1
# 輸出僅一行 usage:`tccutil: Usage: tccutil reset SERVICE [BUNDLE_ID]`——
# tccutil 不能「列出」隱私類別。要看系統認得哪些 kTCCService*,得另循它法
# (例如對 TCC.db 取 distinct service,或掃 framework 字串)。本章不示範 reset(會清授權)。
# 🟢 查某支 App 的簽章身分(理解 csreq 釘的是「誰」;唯讀)
codesign -dv --verbose=4 /Applications/Safari.app 2>&1 | grep -iE "Identifier|TeamID|Authority|CandidateCDHashFull"
# TCC 釘選的 designated requirement 就建立在這些身分欄位上
# 🟢 印出某支 App 的 designated requirement(csreq 釘的就是這個;唯讀)
codesign -d -r- /Applications/Safari.app 2>&1
# "designated => ..." 那行即 DR:描述「誰可以是這支程式」
# 🟡 即時看 tccd 的授權決策(Ctrl-C 結束;唯讀觀察 log)
log stream --predicate 'subsystem == "com.apple.TCC"' --info
# 會看到 tccd 對 (Service kTCCService..., client ...) 的決策;client 常顯示 <private>
# 🟡 回看最近的 TCC 決策歷史(唯讀)
log show --last 30m --predicate 'subsystem == "com.apple.TCC"' --info 2>/dev/null | head -40
# 觀察哪個 client 對哪個資源請求/被准/被拒——這是 TCC 的偵測訊號
# 🟡 唯讀檢視「每使用者層」TCC.db 的授權紀錄(需 Full Disk Access;只讀不寫)
# 終端機(或你的 shell host)必須先在 Privacy & Security > Full Disk Access 取得授權
sqlite3 -readonly "$HOME/Library/Application Support/com.apple.TCC/TCC.db" \
"SELECT service, client, client_type, auth_value FROM access;" 2>&1 | head -40
# 驗證於版本:欄位/表結構可能隨 macOS 版本變動。看 auth_value 對照 0/1/2/3 的語意
⚠ 本章只示範唯讀觀察。不要
sudo去動系統層TCC.db(受 SIP 保護,硬改會破壞隱私狀態且可能需重置);tccutil reset會清掉授權屬於改動狀態,除非你刻意要重置某 App 否則別跑。修改/繞過 TCC(停用、注入既有授權、偽造身分過 csreq)都會降低整機隱私保護,本課一律框成「機制理解 + 偵測面」,不提供 offset / payload / exploit 步驟。任何停用見 DESIGN.md §10 風險警示。
下一步:M07 App Sandbox 與 seatbelt — TCC 決定「使用者授不授權這個 App 用相機」,sandbox 決定「這個進程的 profile 准不准碰相機這個 device class」。兩道獨立的閘,下一章看 sandbox 怎麼用 SBPL 把能力收窄到單一進程。