FreeBDS Mandatory Access Control 框架簡介
簡介
最常見預設在 UNIX-like system 上面的安全模形是 DAC (Discretionary Access Control),讓檔案擁有者決定權限。另外相對的 MAC (Mandatory Access Control) 安全模型,它讓系統管理原決定一個全系統的安全政策,在 MAC 安全模型下甚至 root 都不一定會有全部的權限。
FreeBSD MAC 框架提供一個方式,可以動態載入不同安全模型模組,這避免為了實做所有安全模型在核心,導致核心變得太大太多東西。
FreeBSD 定義很多 KPI 讓 MAC module 使用,很常會把它跟 System call control 搞混,但其實不是,MAC 控制的是更底層的 Kerenl ,像是某個持有 ucred 的 thread 讀取某個 vnode,這種更底層的行為,而不只是更高層的 read system call,並且 System call controll 相關的安全模型很容易遇到 Race condition 相關的問題 (TOCTOU),而 FreeBSD MAC 都有考慮到這些問題並且處理。
MAC Policy
FreeBSD 已經有很多相關的安全模型透過 MAC 實做,像是
- 資訊流模型 (Information Flow Models) – [Day 10] 安全工程 (Security Models and Architecture Design)
- 1973 Bell-LaPadula 模型(BLP),他是一種多級安全 (MLS) 策略,專注於保密性 (避免未經授權的人檢視東西),像是高權限的人不能把資料寫入低權限的檔案,防止洩密
- 1977 Biba 模型,專注於資料的完整性 (避免資料被低權限的人竄改),有點像 MLS 的反面
- 2000 LOMAC 模型,類似 Biba 但改良 Biba 完全禁止讀取的缺點,它允許高權限者讀取低權限資料,但一旦讀取了(被污染了),系統會動態地降低該使用者的權限等級,讓他之後無法再去修改高權限的檔案。這是一種追蹤「汙點」的概念
安全模型
1985 Bobert 在他的提出 Type Enforcement,它定義的 Type 也就是每個東西都有自己的 Label,這有實做在 Bobert 自己的 LOCK 系統,並且現在像是 SELinux and McAfee Sidewinder firewall 都有實做此概念。
另外一個概念在 Trusted MACH 出現,Badger 等人提出 DTE (Dynamic Type Enforcement),雖然在廣義的 TE 裡,所有東西都有標籤(Label),但 DTE 特別強調將標籤分為兩類,讓邏輯更清晰:
- Domain(領域): 專指主動的主體 (Subjects),也就是執行中的 Process
- Type(型別): 專指被動的物件 (Objects),例如檔案、目錄、Socket
DTE 在 Trusted MACH 中引入了一個非常關鍵的概念,叫做 Implicit Domain Transition(隱式領域轉換)。
在舊系統中,如果你是系統管理員(Admin Domain),你想跑一個網頁伺服器,你需要手動切換權限,非常麻煩且容易出錯。DTE 的解法是,它可以設定規則:「當 Admin Domain 的 Process 執行了 httpd_exec_t (網頁伺服器執行檔) 時,自動將新產生的 Process 切換到 Web_Server_Domain。
這讓安全性不僅僅是靜態的擋牆,而是變成一個動態的狀態機(State Machine),SELinux 實際上在寫規則的時候也有提供此類似的規則。但不管是 TE 或者是 DTE 都可以透過 FreeBSD MAC 框架實做,FreeBSD 的 MAC Framework 是一個「通用底座」,它的設計目的就是要能支援各種不同的安全模型。
FreeBSD 的 Base System(基本系統)預設不包含通用的 TE 模組,主要是因為TE/DTE(像 SELinux)不只是一個核心模組,它需要龐大的 Userland(使用者空間)配套工具。
FreeBSD Base 傾向於保持精簡。引入一套完整的 TE 系統(包含數千條預設規則)會讓 Base System 變得極度龐大且難以維護。與其提供一個難以維護的通用 TE,FreeBSD 選擇提供「框架」,讓有需要的廠商(如 McAfee, Juniper)自己去實作專用的 TE。
MAC 細節
架構
系統有幾個跟 MAC 相關的 Components,像是:
- Policy Entry-point KPI,讓 Policy 使用的 KPI,它對應到 Kernel 裡面的各個子系統 (VFS, IPC …) 的 Kernel Services Entry-point KPI
- FreeBSD userspace tool 像是
getfmacorsetfmac… - DTrace Probes in MAC module,讓 DTrace 可以除錯 MAC module
啟動流程
為了符合 “不可被繞過” 的需求,MAC 框架必須在第一個使用者行程 (init) 執行之前就準備好,像是 Biba or MLS 要求所有物件在誕生的時候就要有標籤。
FreeBSD 使用 mac_late flag 表示 MAC Boot 初始化結束,對於像 Biba 或 MLS 這種需要「嚴格全域標籤(Ubiquitous Labeling)」的策略來說,它們必須在 mac_late 變成 1 之前就載入。
如上 SYSINT 定義 MAC Policy 會在 mac_late_init 之前執行 (因為數字比較小)。
註冊
使用 struct mac_policy_conf 設定你的模組,包含,模組名稱、是否允許在開機後載入 (mac_late)、是否允許被卸載 (Unload)。
期中一個重要的成員是 mpc_ops (struct mac_policy_ops),放所有你要 hook 的函式,像是 mpo_init(...) and mpo_destroy(...) 分別對應模組載入跟刪除時,像是你可以在載入的時候初始化你的記憶體 zone,刪除的時候刪除所有 zone。
Chapter 6. The TrustedBSD MAC Framework 定義了所有可以 hook 的函數,但文章內的 function 跟實際上 FreeBSD 可以 hook 的函式有一點不同,因為文章是從別的地方 port 過來的,但基本上函式參數跟函式效果一樣。
mpo_check_vnode_open(...) 函式檢查當你 open vnode 的時候,但在 FreeBSD 叫做 mpo_vnod_check_open(...),基本上只是單字的調換,但函式效果跟參數都一樣,FreeBSD 所有可以 hook 的函式請參考 sys/security/mac/mac_policy.h。MAC Hook Function 設計
Kernel 應該在哪裡安插檢查點(Hooks)?太高層像是 System call,很容易遇到 TOCTOU 問題,太細的話 Policy 會很難寫 (早期是在 UFS/ZFS 檔案系統上面實做),所以 MAC 這邊改成針對 VFS 層的 vnode 做安插,系統上還有很多其他物件也是。
對於 sysctl(系統參數),他選擇不把每個參數都當作獨立物件(因為太繁瑣了),而是把它們視為一個整體。如果你要讀寫 sysctl,主要檢查的是「你的 Process Label 有沒有權限」,而不是去檢查每個 sysctl 節點的 Label。
FreeBSD 對 MAC module 載入/刪除做了一些檢查,假設你的模組正在檢查一個 Process 的權限 (mpo_cred_check_... 正在執行中),這時候如果你執行 kldunload,MAC Framework 會讓指令 “Wait”,它必須等到目前所有正在執行的 Hook 都執行完成,才會真正把你踢掉。
Kernel 在使用 Hook function 時,都會注意做 lock,避免 object 的狀態改變 (TOCTOU),像是 mac_vnode_check_write() 會被 vn_write() 呼叫,在被呼叫的時候會對 vnode 做 lock。對 Credentials 也是,使用 Copy-on-Write 機制。當你讀取它時,核心保證它不會被修改。
Check_write 雖然檢查寫入權限,但它不給你「寫入的資料內容 (Data Pointer)」,是因為那些資料還在 User Space。如果讓 Policy 去讀 User Space 的資料,會產生嚴重的 Race Condition(使用者可能在你檢查完的瞬間偷換資料)。
MAC 的設計,只管「權限」,不管「內容」。
核心視角的 MAC 設計
當多個 Policy 被載入,FreeBSD 使用 Restrict, Not Grant 原則,只要有人說不行就會失敗。
針對多個 Policy 回傳值,Kernel 有相關的 Macro:
| 巨集名稱 | 用途 | 邏輯 |
|---|---|---|
MAC_POLICY_PERFORM() |
事件通知 (如 Socket 建立) | 廣播:通知所有 Policy,不關心回傳值。 |
MAC_POLICY_CHECK() |
存取控制 (如 check_write) |
一票否決:如果 Policy A 回傳 EACCES,Policy B 回傳 0,結果就是 EACCES。 |
MAC_POLICY_BOOLEAN() |
布林判斷 (如 IP 分片) | AND 運算:所有人都要回傳 True 才算 True。 |
MAC_POLICY_GRANT() |
特權賦予 (例外) | OR 運算:只要有一個 Policy 說「給他特權」,他就擁有特權。 |
如果 Policy A 拒絕並回傳 EACCES (權限不足),Policy B 拒絕並回傳 EPERM (操作不允許),核心該回傳哪個給使用者? 核心有一個 mac_error_select() 函數,它有內建的優先順序邏輯,決定哪個錯誤碼比較「重要」,最終回傳那個重要的錯誤碼。
標籤設計
對於複雜的操作(如 setlabel),核心會分多個步驟呼叫 Policy:
- Init (分配記憶體)
- Internalize (解析字串)
- Check (檢查能不能改)
- Set (真的改下去)
- Free (釋放記憶體)
儲存方式: MAC Framework 會在常見的 Kernel 物件(如 vnode, ucred, mbuf)裡面「寄生」一個指標。這個指標指向一塊由 Framework 管理的記憶體(struct label)。
- 對你 (Policy) 來說:它是透明的 (Opaque),你只能用 mac_label_get / mac_label_set 來存取你的專屬 Slot。
- 對 Kernel 來說:它只是一個陣列 (array of uintptr_t)。
分配時機 (Allocation):
- 通常使用 Slab Allocator (UMA):就像你的 zone_casper。這能重用已初始化的物件,效能較好。
- 睡眠與否 (Sleeping):這點非常重要!如果是在中斷處理 (Interrupt) 或持有 Spin Lock 的情況下分配標籤,絕對不能睡覺 (M_NOWAIT)。如果記憶體不足,分配就會失敗,這會導致整個物件建立失敗(例如 Socket 建立失敗)。
「創造」與「關聯」的差別 (Creation vs. Association)
- Object Creation (物件創造):…
- Object Association (物件關聯):…
標籤同步與鎖定 (Label Synchronization): 直接借用物件的鎖
所以不能在裡面使用 namei 或者任何跟 vnode 相關的函式,可能會遇到 Reentrancy Deadlock (重入死鎖),例子像是:
因為正在執行 lookup(查找檔案),為了保證目錄不會在查找過程中被刪除或改名,Kernel 已經對 /etc 這個 Directory Vnode (目錄節點) 上了 獨佔鎖 (LK_EXCLUSIVE)。
你在 hook 裡面呼叫了 vn_open("/etc/config", …)。 vn_open 是一個乖寶寶,它必須遵守 VFS 的規則:為了安全地讀取 /etc 目錄的內容,它必須請求 /etc 的鎖。
vn_open 說:「我要鎖定 /etc 才能繼續工作。但我發現 /etc 已經被鎖住了,所以我睡覺等待 (Sleep),直到鎖被釋放。」 你 (Hook) 說:「我在等待 vn_open 回傳結果給我,我才能結束這個 Hook。」
結果:你在等 vn_open。 vn_open 在等 /etc 的鎖。 /etc 的鎖在你手上。 無限迴圈,系統卡死。
使用者視角:最後一部分解釋了 setfmac, ls, ps 這些工具是如何在「不知道 Policy 細節」的情況下運作的。
- Policy-Agnostic (策略無關): 這些工具只認得字串。它們把標籤當成一串文字處理,例如 “casper/dns,biba/high”
- Kernel 的工作:負責解析這些字串。當 setfmac 傳入字串時,Kernel 會切分它,然後問每一個 Policy:「這是你的嗎?(Internalize)」。
- mac.conf: 管理者可以透過 /etc/mac.conf 設定預設要顯示哪些標籤,這樣 ls -Z 才知道要印出什麼。
假如你真的要開發自己的 MAC 模組,那要注意以下事情:
- 自己評估效能,模組可能會降低系統效能
- 現在系統都是多處理器,所以要注意避免 Race Condition
- 有可能同時會有多個模組同時載入,要注意不要讓他們發生衝突
MAC 例子
Hook Function
這邊實際舉例子看看 Hook funciton 以及他們是幹嘛的,我們以 vnode 為例,它比較複雜一點。
Lable 相關的操作
mpo_init_vnode_labelmpo_destroy_vnode_labelmpo_copy_vnode_labelmpo_externalize_vnode_labelmpo_internalize_vnode_labelmpo_check_vnode_relabel
Init and Destroy 很基本,就是在 vnode 創建和刪除的時候會執行的函式,注意在 label 的時候,就算沒有要幹嘛也要寫出來直接讓它 return (pass)。
Externalize and Internalize 對應當你 setfmac and getfmac 時會做的操作,也就是當你想用字串設定標籤或者從標籤推回字串的時候。像是你有一個叫做 “apple” 的標籤,但在你 MAC 模組可能對應的標籤就是 int label = 1,所以要做轉換。
Copy vnode 就是當你做 getfmac 的時候,我們需要把 vnode 複製一份,在做 externalize,因為我們不希望一直 lock vnode,可以看以下的 src code。
如上,FreeBSD 先寫一個 Kernel function mac_vnode_copy_label,使用 MAC_POLICY_PERFORMA_NOSLEEP macro atomic 的執行 vnode copy。
如上,可以看到會在 sys___mac_get_fd() 函式執行的時候用到,也就是 get vnode label 的時候。
mpo_check_vnode_relabel 則是當複製標籤的時候,注意就連 setfmac 它也會先執行一次,因為 FreeBSD 會把新標籤複製到沒有標籤的檔案上面,也算一種複製。
流程可能是:
- init: 先 init 一個臨時
- internalize: 把字串轉成標籤放在零食標籤
- init: Kernel 為了不影響正在運行的 Process,複製了一份全新的 Process Credential Label
- relabel: 把臨時標籤複製到真正的標籤放面
- destroy: 把臨時標籤刪除
- destroy: 把 Porcess 舊的標籤刪除
Vnode 實際相關的檢查
基本存取與查找相關:
mpo_check_vnode_access:當呼叫access()或系統內部檢查一般讀寫權限時。最基礎的把關者,檢查使用者是否有權限進行讀 (R)、寫 (W) 或執行 (X)。mpo_check_vnode_lookup:當系統解析檔案路徑時。例如存取/usr/bin/ls,系統會逐層檢查/、/usr、/usr/bin是否允許搜索 (Look up)。mpo_check_vnode_open:當呼叫open()打開檔案時。決定是否允許使用者以特定模式(如 Read-Only, Read-Write)取得檔案的 File Descriptor。mpo_check_vnode_stat:當呼叫stat()、lstat()或fstat()時。控制使用者是否可以查看檔案的 Metadata(如大小、時間、權限),即使不打開檔案內容。mpo_check_vnode_readdir:當呼叫readdir()讀取目錄內容時(如ls指令)。控制使用者是否可以列出目錄下的檔案清單。mpo_vnode_check_readlink:當呼叫readlink()時。控制使用者是否可以讀取符號連結 (Symbolic Link) 指向的路徑文字。mpo_vnode_check_poll:當呼叫poll()或select()時。檢查使用者是否可以監控該檔案的 I/O 狀態(如是否有資料可讀)。
檔案生命週期相關:
mpo_vnode_check_create:當建立新檔案時(open帶有O_CREAT旗標或touch)。決定使用者是否有權限在該目錄下產生新檔案。mpo_vnode_check_unlink:當刪除檔案時(rm指令)。Kernel 術語稱為unlink。決定使用者是否可以移除該檔案連結。mpo_vnode_check_rename_from/mpo_vnode_check_rename_to:當重新命名或移動檔案時(mv指令)。rename是一個原子操作,Policy 必須同時檢查「來源檔案 (from) 是否可移走」以及「目的路徑 (to) 是否可寫入」。mpo_vnode_check_link:當建立硬連結 (Hard Link) 時(ln指令)。防止使用者透過建立連結來繞過某些存取限制。mpo_vnode_check_revoke:當呼叫revoke()時。這是一個強制性的系統呼叫,用來切斷所有指向該檔案的現有連線(通常由 Root 使用,如登出時切斷 TTY)。
程式執行與記憶體 (Execution & Memory):
mpo_vnode_check_exec:當呼叫execve()執行程式時。決定該檔案是否被允許執行。mpo_vnode_check_mmap:當呼叫mmap()進行記憶體映射時。通常發生在載入函式庫 (.so) 或映射檔案內容時,檢查權限(如PROT_EXEC)。mpo_vnode_check_mmap_downgrade:當mmap的權限被降級時(例如從 RWX 改為 RX)。通常用於確保降級操作符合安全策略(較少用到,通常預設允許)。mpo_vnode_check_mprotect:當呼叫mprotect()修改記憶體頁面保護屬性時。防止程式在執行期間動態修改記憶體屬性(例如防止 W^X 繞過攻擊)。
屬性修改 (Attribute Modification):
mpo_vnode_check_setmode:當呼叫chmod()修改權限時。決定誰可以更改檔案的 UNIX 權限位元 (如 0755)。mpo_vnode_check_setowner:當呼叫chown()修改擁有者時。決定誰可以移交檔案的所有權。mpo_vnode_check_setutimes:當修改存取/修改時間時(如touch或utimes)。防止偽造檔案時間戳記。mpo_vnode_check_setflags:當呼叫chflags()設定檔案旗標時(如immutable)。檢查是否允許設定系統級別的檔案旗標。
目錄導航 (Navigation):
mpo_vnode_check_chdir:當呼叫chdir()(cd) 時。決定 Process 是否可以進入該目錄作為工作目錄。mpo_vnode_check_chroot:當呼叫chroot()時。決定 Process 是否可以將該目錄設為新的根目錄 (Root Directory)。
擴充屬性與 ACL (ExtAttr & ACL):
mpo_vnode_check_setextattr:當寫入擴充屬性時(如setfmac)。決定是否允許修改安全標籤。mpo_vnode_check_getextattr:當讀取擴充屬性時(如getfmac)。決定是否允許查看安全標籤。mpo_vnode_check_deleteacl:當刪除 ACL 時。檢查是否允許移除檔案的存取控制清單。mpo_vnode_check_getacl:當讀取 ACL 時。檢查是否允許查看 ACL 設定。mpo_vnode_check_setacl:當設定/修改 ACL 時。檢查是否允許修改 ACL 設定。
檔案系統特定事件
因為 Vnode 的標籤跟檔案有關,所以標籤會在 VFS 的 vnode 有一份,硬碟的 FS 裡面的檔案也有一份,所以會有同步的問題,這邊 Hook 的函式跟它有關。
mpo_associate_vnode_devfs: 當/dev底下的裝置節點(如/dev/null,/dev/ada0)被建立或存取時。專門處理裝置檔案。因為/dev是虛擬檔案系統,檔案不會真的存在硬碟上。mpo_associate_vnode_extattr: 當一個位於支援擴充屬性 (Extended Attribute) 的檔案系統(如 UFS, ZFS)上的檔案,被 Kernel 從硬碟讀入記憶體建立 Vnode 時。負責呼叫vn_extattr_get從硬碟的 Metadata 區讀取標籤資料,並填入記憶體中的struct label。若沒實作此函數,重開機後標籤就會消失。mpo_associate_vnode_singlelabel: 當掛載不支援擴充屬性的檔案系統時(如 FAT32, CD-ROM (ISO9660), NFS, MSDOS)。既然硬碟不能存標籤,這個 Hook 會採取「全域設定」策略,直接給該掛載點下的所有檔案貼上同一張預設標籤(例如:光碟機裡的檔案全部視為public)。mpo_create_vnode_extattr: 當建立新檔案時(open(O_CREAT)或mkdir)。負責計算新檔案應該有什麼標籤(通常繼承自父目錄或當前 Process),並立刻呼叫vn_extattr_set將這張新標籤寫入硬碟。mpo_setlabel_vnode_extattr: :當使用者手動修改標籤時(呼叫setfmac或系統呼叫mac_set_file)。只有在通過mpo_vnode_check_setextattr檢查後才會執行。負責將使用者指定的新標籤寫入硬碟 (ExtAttr),並同步更新記憶體中的 Vnode 標籤。