SELinux旅程Part5-撰寫SELinux政策(Policy)

先附上自製影片, 之後會再持續更新哦!
用 Ubuntu PPA 開啟 SELinux 且運行在 enforcing mode[Youtube]: 
https://www.youtube.com/watch?v=4fdXuIIBxes
Ubuntu PPA(Personal Package Archives)可以讓我們自行製作 Package 並透過 apt 套件管理工具下載。有鑒於社群最近修掉了一個 issue,此 PPA 未來會再進行更新,到時候就不必修改核心(Kernel)囉。

前言

了解完第四篇稽核 SELinux 事件後, 我們知道存取行為會被 SELinux 依據政策(Policy)所檢查,Linux Audit System 再將結果紀錄起來。 
前面幾篇不斷提到 SELinux 政策,到底什麼是政策呢? 終於,在這篇中要來分享何謂 SELinux 政策,政策分別以不同名稱代表其功能,事實上,不同的功能也就是由不同的 SELinux 規則語法所組成,規則語法稱為 SELinux Kernel Language。 
運用規則語法撰寫完成的政策還需要透過 SELinux 工具編譯成政策二進制檔(Binary Policy),才可被載入核心中。 依據不同核心版本所能夠支援的 SELinux 政策版本也不同,核心所能支援的政策版本過舊則無法使用新政策的功能。
另外,在開源世界中有維護一套通用的 SELinux 政策,稱之為 The Reference Policy,簡稱 Refpolicy,根據不同 Linux 發行版本,可以透過微調 Refpolicy 來符合其環境。 最後,我們會分享如何撰寫自己的政策,學會之後即可自由地去調整 SELinux 政策以符合自身需求。

SELinux 政策名稱(Policy Name)

通常在 Linux 發行版(Distro)上,可以看到許多 SELinux 政策名稱,例如 targeted、strict、mls、mcs、minimum 或是 refpolicy 等等,通常是依照其政策功能來區別,基本如上所述,政策功能的不同是因為所用的 SELinux 規則語法組合不同。 
在之前的篇章中有提到,我們將不同功能的政策目錄放置在 /etc/selinux 目錄下,例如 /etc/selinux/targeted/,其中存放著跟此政策相關的設定檔,例如描述如何為資源標上標籤的設定檔 file_contexts。 如果要挑選其中一個政策使用,則將所挑選的政策名稱寫入 SELinux 設定檔 /etc/selinux/config 中的 SELINUXTYPE 欄位。

SELinux 政策版本(Policy Version)

SELinux 政策的版本由其編譯工具(例如 checkpolicy)所檢查,影響著所能夠使用的 SELinux 規則語法,另外也要注意核心(Kernel)所能夠支援的 SELinux 政策版本,如果使用的功能太新,雖然編譯成功產生政策二進制檔,但是核心可能不支援其功能而導致功能無效,如圖1。 如果要查詢核心最高支援的政策版本可以透過 sestatus 指令,"Max kernel policy version"對應到的數值就是核心最高支援的政策版本
圖1. SELinux 政策版本
(來源自 The SELinux Notebook 4th Edition)

SELinux Policy: Monolithic & Modular

我們透過規則語法將政策寫完之後,需要將其編譯成政策二進制檔,依據建構方式的不同可以分為 Monolithic 和 Modular 的方法,Refpolicy 提供的環境支援這兩種建構方法,這邊要注意一件事,雖然建構方式不同,但最後產生出來用來載入核心的政策二進制檔(Binary Policy)是一樣的,二進制檔放在 /etc/selinux/NAME/policy/ 目錄下。

  • Monolithic: 將所有撰寫好的政策集中在一起生成一個檔案,通常稱為 policy.conf,再透過 checkpolicy 工具編譯成政策二進制檔 policy.VERSION。(VERSION 為 checkpolicy 支援的政策版本)
  • Modular: 將所有撰寫好的政策分為基本模組(Base Module)跟選擇載入模組(Optional Loadable Module),所有必須載入核心中的規則包含在基本模組內,剩下的模組都是可以在系統建置時期或是運行時期透過 semodule 工具載入。 如圖2,SELinux 架構圖中上方有展示如何製作 Modular 政策,首先將政策撰寫好後透過 checkmodule 工具編譯成政策中介格式(Intermediate Format),再利用 semodule_package 工具將政策中介格式與其他關於此政策的設定檔一同包裝成可以用來載入的模組檔案。最後,使用 semodule 工具將其載入 SELinux Policy Store 中以作管理並且將剛載入的模組融合進 /etc/selinux/NAME/policy/ 底下的政策二進制檔 policy.VERSION。
圖2. Modular 方法建構政策

開源 SELinux 政策: The Reference Policy

在開源的世界中有維護了一個通用的 SELinux 政策原始檔,在這邊簡稱為 Refpolicy,裡面包含著許多描述各種應用程式的政策,透過設定檔可以將其整體調整以符合特定 Linux 發行版本,再透過 Refpolicy 提供的編譯環境,可以選擇使用 Monolithic 或是 Modular 方法將 SELinux 政策原始檔編譯為 SELinux 政策二進制檔。

Refpolicy 設定檔

將 Refpolicy 下載下來後,在最上層目錄中我們可以看到一個設定檔 build.conf,我們挑選幾個重要的欄位講解一下,詳細內容可以參考其註解。

  • TYPE: 可能的值有 standard、mls 和 mcs,這三者的差別在於需不需要開啟 Multi-Level Security 或 Multi-Category Security,standard 代表都不開啟,選擇 mls 的話會將相關功能開啟,選擇 mcs 亦是如此。
  • NAME: 政策名稱,預設為 refpolicy。
  • DISTRO: 可能的值有 redhat、gentoo、debian、suse 和 rhel4,設定此欄位即可調整符合特定發行版本的政策設定。
  • MONOLITHIC: 可能的值有 y 或 n,即選擇要用哪種方式建構政策二進制檔。
Refpolicy 中的設定檔可不只有 build.conf,在 config/ 目錄下也可以看到其他設定檔,這些設定檔會在安裝的時候被放置在 /etc/selinux/NAME/ 目錄裡面的某個地方,未來我們會再分享有哪些 SELinux 設定檔。

Refpolicy 政策二進制檔建構操作

調整完設定檔後,我們可以使用 make 指令來操作 Refpolicy 所提供的建置方法。 首先,我們需要掃描整個 Refpolicy 政策內容,看看有哪些政策原始檔。
make conf
上述指令會在 policy/ 目錄底下產生 modules.conf 和 booleans.conf,booleans.conf 中的內容描述整個政策中的 boolean 定義與其初始值,而 modules.conf 則描述各個政策名稱與其對應的分類,base 或是 module,也就是上面提到的基本模組(Base Module)和選擇載入模組(Optional Loadable Module),不過當您選擇要以 Monolithic 方式建構時這些選擇都沒差,因為 Monolithic 方式會把所有政策內容都集合在一起,除非把 base 或 module 改為 off。
接著,如果您想先試著建構政策二進制檔而不要安裝到指定路徑的話,可以使用如下指令。
make policy
當確認建構進行順利後,我們準備將此政策二進制檔連同相關設定檔安裝至 /etc/selinux/NAME 目錄底下,執行以下指令。
make install
如果是 Modular 環境,可以使用以下指令來安裝模組。
make load
安裝完成後,我們使用以下指令來為系統資源標上標籤。
make resetlabels
如果不用 resetlabels,則只會改變資源標籤中 Type 的部分。

Refpolicy 政策內容 

了解完 Refpolicy 中的設定檔以及如何建置政策二進制檔後,我們來看看 Refpolicy 中的政策內容,也就是說這些政策原始檔如何被撰寫出來,由淺入深,我們先來看看政策由什麼原始檔組合而成,各自原始檔中又該以什麼 SELinux 規則語法所撰寫。

.te, .if, .fc 檔案

每個 SELinux 政策都是以這三個為副檔名的檔案所組成,這三個檔案有各自的作用,以 C 語言程式來類比。
  • .te = .c
  • .if = .h
在講述為何如此類比之前,我們需要先知道,SELinux 政策可以用來規範存取主體的行為,也可以用來規範系統資源(Object)如何被其他存取主體(Subject)存取。
如果政策有規範存取主體行為的部分,則我們會把描述行為的 SELinux 規則語法寫在 .te 檔案中。 如果政策有規範系統資源如何被其他存取主體存取的部分,則會把規則語法包裝成 Interface 寫在 .if 檔案中,此 Interface 可以被其他政策引用,因此才會說 .if 檔類似 C 語言中的 .h 檔。 而 .fc 檔用來描述系統資源該如何標上標籤,內容跟設定檔 file_contexts 一樣,應該說 file_contexts 檔案就是由所有政策的 .fc 檔所合成。
舉個例子,針對程式 P 的行為來撰寫政策,建立 p.te、p.if、p.fc 三個檔案,我們想讓 P 可以執行 /bin 底下的程式且可以讀 /etc 底下的設定檔,我們在 p.te 中寫下:(假設 P 程序的標籤為 system_u:system_r:p_t 且 /bin 和 /etc 底下的資源標籤為 system_u:object_r:bin_t 和 system_u:object_r:etc_t)

allow p_t bin_t:file { getattr open map read execute execute_no_trans};
allow p_t etc_t:file { getattr open read };
當我們所操作的程序(例如 shell)想使用 signal 來將此 P 程序停止時,我們需要權限存取此 P 程序,我們可以在 p.if 中寫下:
interface(`p_signal', `
    gen_require(`
        type p_t;
    ')
    allow $1 p_t:process signal;
')
然後在我們操作的程序對應的政策中(例如 shell.te)引用此 interface(假設我們操作的程序標籤為 user_u:user_r:user_t)。
p_signal(user_t)
如此一來,當建構政策二進制檔時,這些 interface 會被展開形成:
require{
    type p_t;
}
allow user_t p_t:process signal;
可以注意到 gen_require 也是可以展開的。

Policy Macro 

看完上面的例子可以知道如 interface 或是 gen_require 都可以被展開,因為在 Refpolicy 環境中大量使用 M4 Macro 語法,這些 Macro 語法裡面包裝著 SELinux Kernel Language,也就是 checkpolicy 工具看得懂的語法,所謂的展開其實也就是字串的替換,例如 gen_require,此 Macro 定義在 Refpolicy 中的 policy/support/loadable_module.spt。
define(`gen_require',`
        ifdef(`self_contained_policy',`
                ifdef(`__in_optional_policy',`
                        require {
                                $1
                        } # end require
                ')
        ',`
                require {
                        $1
                } # end require
        ')
')
當我們使用 gen_require Macro,展開時會把 gen_require 替換成裡面的內容,其中的 ifdef 是 M4 語法,這邊的意思是如果有定義 self_contained_policy 則替換成第一個部分,接著再去看是否有定義 __in_optional_policy,如果沒有定義 self_contained_policy 則替換成 require { $1 },此為 SELinux Kernel Language。
Refpolicy 環境中提供了各種不同的政策,這些政策有的主要描述存取主體有的主要描述資源,政策中有許多的 Macro 可以引用,因此當我們在撰寫政策時可以重複使用這些 Macro 增進撰寫的便利性和政策可讀性。

SELinux Kernel Language

SELinux Kernel Language 為 SELinux 政策的規則語法,可以透過如 checkpolicy 或 checkmodule 工具來轉換為核心看得懂的二進制檔,詳細規則語法可以參考官方或是 SELinux Notebook 4th 的第四章。 在撰寫時需要注意,有些語法是不能寫在 if 判斷式、optional 語法或是 require 語法裡面,也要留意撰寫時是寫在 base 還是 module 政策中,語法也會有所差別。 另外,SELinux Kernel Language 之間是有順序的,順序不一樣會導致編譯失敗,基本排序如圖3,由上至下排序,此圖中有標示出何種語法可以用在 base 或 module 政策中,"m"代表必要,也就是必須要載入核心的規則語法,因此標示在 base 政策中,"o"代表可選擇。
圖3. 規則語法在 base 或 module 政策中的使用以及排序

建立自己的 SELinux 政策

了解完 Refpolicy 後,其實就等於擁有撰寫自己政策的能力了,接下來是一連串的實務操作,環境為 CentOS 7 並且使用 Modular 的方式建構,您準備好了嗎? 讓我們開始吧!

任務一:管理資源

首先,使用者登入系統後使用 id 指令來顯示目前狀態。
uid=1000(bighead)...context=unconfined_u:unconfined_r:unconfined_t
在我的環境中 /home/bighead/playground 底下有個一般檔案 demo,管理員希望可以撰寫一個政策,定義一個新的 Type,稱為 demop_home_t,並且讓使用者登入後對於此檔案只能讀取但不能寫入。 記得先將您的 SELinux 環境設定為 permissive 模式。
首先,先建立 demop.te、demop.if、demop.fc 三個檔案,內容分別如下。

demop.te

policy_module(demop,1.0.0)

type demop_home_t;

gen_require(`
        type unconfined_t;
')

allow unconfined_t demop_home_t:file { read_file_perms }; 
policy_module 是一個 Macro,用來定義此政策為一個 module 政策,參數分別為名稱和版本,這邊要注意名稱必須和檔名一樣,皆為 demop。 接著定義新的 Type,語法為"type ...;"。
因為 unconfined_t 定義在其他地方,所以使用 gen_require,gen_require 為 Macro,用來描述所需要的外部資訊(e.g. type、role、class ...),詳細情況可以參考官方的Table3,只要在"require Statement"欄位內有寫 Yes 的規則語法都可以用。
最後一行為 allow 規則,用來描述 unconfined_t 只能夠對標有 demop_home_t 的一般檔案執行讀取的權限,read_file_perms 展開後為"getattr open read ioctl lock"。

demop.if

這個範例沒有用到此檔案,但如果要將此範例改為更加彈性的寫法,可以在這個檔案中寫如下內容,並且在描述 unconfined_t 的政策中引用,例如 demop_read_file(unconfined_t)。
interface(`demop_read_file',`

        gen_require(`
                type demop_home_t;
        ')
        allow $1 demop_home_t:file { read_file_perms };
')

demop.fc

/home/.../playground/demo -- gen_context(system_u:object_r:demop_home_t,s0)
此檔案描述如何將 demo 檔案標上標籤,當此政策載入後我們使用如下指令
restorecon -F ./demo
或是
setfiles -F /etc/selinux/targeted/.../file_contexts /home/.../demo
即可把 demo 標記為 system_u:object_r:demop_home_t。

建構政策二進制檔

寫好上述三個檔案後,執行以下指令。
make -f /usr/share/selinux/devel/Makefile demop.pp
順利的話即可取得包裝好的政策檔案 demop.pp,接著執行指令將其載入核心。
semodule -i demop.pp
可以用 semodule -l 來查看已載入的政策。
/usr/share/selinux/devel/ 底下存放著建構政策二進制檔所需的工具以及可引用的 .if 檔案或其他 Macro。

實際演練

復述一下我們的目的,使用者登入後對於此 demo 檔案只能讀取但不能寫入,首先,使用者登入後的標籤為:
圖4. 使用者狀態
且目前 SELinux 模式為 Enforcing Mode:
圖5. 目前 SELinux 模式
我們利用 cat 指令可以看到 demo 檔案內容:
圖6. demo 檔案標籤和讀取 demo 檔案 
但卻不能對此檔案做寫入,甚至是 root 使用者!
圖7. 無法對 demo 寫入
被 SELinux 阻擋過後,來看一下是因為什麼權限而被擋住:
圖8. 存取 demo 檔案的稽核訊息
從稽核訊息中可以看出,此寫入行為被阻擋是因為被標記為 unconfined_u:unconfined_r:unconfined_t 的使用者對於標記為 system_u:object_r:demop_home_t 的一般檔案(file)沒有 append 權限。

任務二:讓 /bin/ls 以新標籤運行且只能查看 /proc 底下內容

首先,使用者登入系統後使用 id 指令來顯示目前狀態。
uid=1000(bighead)...context=unconfined_u:unconfined_r:unconfined_t
我們想要讓 /bin/ls 運行時的標籤為 ls_t 且只能查看我們允許的地方,這邊定為 /proc 目錄下,和上面一樣先建立三個檔案,名稱為 ls.te、ls.if、ls.fc。

ls.te

此檔案的內容需要分成幾個部分來說明,首先,建立一個 module 政策並且引用 policy_module Macro 和定義新的 type:
policy_module(ls,1.0.0)

type ls_t;
type binls_exec_t;
接著,我們希望標記為 binls_exec_t 的 /bin/ls 運行起來時標記為 ls_t,需要使用 domain transition 的規則語法: (對 domain transition 不熟的讀者,請務必前往SELinux旅程Part3閱讀。)
type_transition unconfined_t binls_exec_t:process ls_t;
allow unconfined_t binls_exec_t:file exec_file_perms;
allow ls_t binls_exec_t:file entrypoint;
allow unconfined_t ls_t:process transition;
要注意使用者執行 /bin/ls 前的標籤為 unconfined_u:unconfined_r:unconfined_t,因為 type_transition 語法只會改變 type 的部分,因此 transition 之後標籤為 unconfined_u:unconfined_r:ls_t。 這邊會有一個問題,政策中 unconfined_r 並沒有跟 ls_t 連結在一起,所以需要使用 role 規則語法來將 unconfined_r 跟 ls_t 連結。
role unconfined_r types ls_t;
順利 transition 之後要處理 Parent 程序與 Child 程序之間的行為,例如 Child 程序繼承 Parent 程序的 file descriptor 還有當 Child 程序執行完畢後傳送 sigchld 給 Parent 程序。
allow ls_t unconfined_t:fd use;
allow ls_t unconfined_t:process sigchld;
transition 部分結束後,/bin/ls 以 ls_t 運行著,並且進行後續 /bin/ls 行為,也就是查看 /etc 底下所需資訊、利用 Dynamic Linker 載入 Library、查看 /sys/fs/selinux 狀態等等。
#/etc/ld.so.cache
allow ls_t etc_t:dir search;
allow ls_t ld_so_cache_t:file { getattr map open read };

#/usr/lib64/ld-2.17.so
allow ls_t usr_t:dir search;
allow ls_t ld_so_t:file read;

#/usr/lib64/lib...
allow ls_t root_t:dir search;  #search from /
allow ls_t lib_t:dir search;
allow ls_t lib_t:file { execute getattr map open read };
allow ls_t lib_t:lnk_file read;

#/sys/fs/selinux
allow ls_t sysfs_t:dir search;
allow ls_t security_t:dir getattr;
allow ls_t security_t:filesystem getattr;

#/usr/lib/locale/locale-archive
allow ls_t locale_t:file { getattr map open read };
我們的目標是只能查看 /proc 底下內容,因此我們要給予適當的權限:
allow ls_t user_devpts_t:chr_file { rw_file_perms };
allow ls_t proc_t:dir list_dir_perms;
allow ls_t proc_t:file read_file_perms;
第一行的權限是為了讓程序能夠把訊息透過 Standard Output 顯示出來。
後兩行允許標有 ls_t 的此程序能夠讀取 /proc 底下的內容。
上方的規則語法可以透過 audit2allow 來幫助您生成,以增加撰寫政策的速度。
(對 audit2allow 不熟的讀者請務必前往SELinux旅程Part4閱讀)

ls.if

這個範例沒有用到此檔案。

ls.fc

為了不影響系統運作,我把 /bin/ls 複製一份到我的環境中 playground/ 目錄底下,因此 ls.fc 內容為:


/home/.../playground/ls -- gen_context(system_u:object_r:binls_exec_t,s0)

與上面例子一樣,我們可以使用 restorecon 或是 setfiles 來將 ls 標記為 "system_u:object_r:binls_exec_t"。

實際演練


復述一下我們的目的,我們想要讓 /bin/ls 運行時的標籤為 ls_t 且只能查看我們允許的地方,這邊定為 /proc 目錄下(補充一下,為了不影響系統上的 /bin/ls,我複製了 /bin/ls 到 playground/ 底下)。
首先,使用者登入後標籤為:
圖9. 使用者狀態
且目前 SELinux 模式為 Enforcing Mode:
圖10. 目前 SELinux 模式
我們直接用 ls 來看看 /proc 的標籤以及底下的內容吧!
圖11. /proc 目錄標籤
圖12. /proc 目錄內容
但我們卻不能用 ls 來查看其他目錄,甚至是 root 使用者!
圖13. 無法存取其他系統目錄
一樣在阻擋過後來看看被阻擋的訊息吧:

圖14. 存取其他系統目錄的稽核訊息
阻擋的原因為,被標記為 unconfined_u:unconfined_r:unconfined_t 的使用者對於標記為 system_u:object_r:var_t 的目錄(dir)沒有 search 權限。(其他目錄以此類推)

Monolithic

如果您想使用 Monolithic 的方式來將自己建立的政策與原有的政策合併,您需要先下載 Distro 所提供的政策原始檔(如果有提供)或是下載 Refpolicy,這邊我選擇使用 Refpolicy。 進入 Refpolicy 後在 policy/modules/ 底下建立一個目錄,例如 mypolicy,此目錄中需要有 metadata.xml 檔案,其內容只需要如下即可:
<summary>Policy modules for mypolicy.</summary>
並且把 .te、.if、.fc 三個檔案放在此目錄下,.if 最上方需要加上如下內容:
## <summary>
## This is a summary for XXX.if.
## </summary>
基本上,加上這些內容最主要是為了符合 make conf 時的需求,找到我們新加入的政策並且在 modules.conf 中顯示。
剩下的步驟就如"Refpolicy 政策二進制檔建構操作"中的說明。

總結

恭喜您,現在可以自由地撰寫符合您需求的政策,為了有好的開發環境,建議下載 Refpolicy(https://github.com/SELinuxProject/refpolicy),裡面支援 Monolithic 和 Modular 的建構環境以及許多由開源社群維護的政策,因此當您需要引用 Macro,只需要查看這些政策中的 .if 檔或是 policy/support/ 底下的輔助檔案即可。 
如果您想了解更多關於政策、Macro 或是 SELinux Kernel Language,可以參考 SELinux Notebook 4th Edition 或是官方 wiki

留言