SELinux 安全模型——TE

Rand_CS發表於2024-06-08
  • 首發公號:Rand_cs

SELinux 安全模型——TE

透過前面的示例策略,大家對 SELinux 應該有那麼點感覺認識了,從這篇開始的三篇文章講述 SELinux 的三種安全模型,會涉及一些程式碼,旨在敘述 SELinux 內部的原理

SELinux 提供了 3 種安全模型:

RBAC:Role Based Access Control<基於角色的許可權訪問控制,它根據使用者的角色和職責來管理對系統資源的訪問許可權。RBAC 將使用者分配到不同的角色中,每個角色被賦予一組特定的許可權,使用者透過被分配到相應的角色來獲得相應的許可權,從而實現對系統資源的安全訪問和管理。

TE:Type Enforcement,SELinux 最主要的安全模型,每一個主體客體都被分配一個型別,且使用白名單策略決定指定型別之間的訪問許可權。

MLS:Multi-Level Security,用於保護敏感和機密資訊。這是 SELinux 對 BLP(Bell-La Padula Model) 模型的實現,編寫策略可實現 "no read up, no write down"

本篇文章講述 SELinux 最重要的安全模型:TE,Type Enforcement,針對型別的一種強制訪問控制模式。

兩種規則及其資料結構

它是 SELinux 的基石,百分之九十九的規則都是建立在 TE 之上的。TE 這種安全模型主要有兩種規則,在之前示例策略種也說過,本文再來複習一遍:

  • Access Vector Rules,簡單理解為 allow、neverallow、dontaudit 這些規則屬於 AV 規則。其語法為 allow source target : class { perms },表示 source 對 class 類別的 target 的訪問許可權。
  • Type Rules,這類規則涉及型別轉換,總共有 3 個,type_transition、type_change、type_member,後兩個先不用管,重點知道 type_transion 就行。其語法規則為 allow a_t b_exec_t : process b_t,表示 a_t 這個型別的程序執行 b_exec_t 這個型別的可執行程式後,型別轉換為 b_t。

雖然 TE 分為兩大類規則,但是從形式上來講,它們是統一的,都是 規則名 源型別 目標型別 目標類別 許可權/轉換後型別,可以看出,只有最後一部分是不一樣的。但終歸形式統一,所以在記憶體種這兩大類規則對應的資料結構都是一樣的。

我們可以將前面部分當作 key,最後的許可權/轉換後型別當作 value,如此,所有的 TE 規則實際上都以鍵值對存放在記憶體當中。

struct avtab_key {
    u16 source_type;    /* source type */
    u16 target_type;    /* target type */
    u16 target_class;   /* target object class */
#define AVTAB_ALLOWED       0x0001
#define AVTAB_AUDITALLOW    0x0002
#define AVTAB_AUDITDENY     0x0004
#define AVTAB_AV        (AVTAB_ALLOWED | AVTAB_AUDITALLOW | AVTAB_AUDITDENY)
#define AVTAB_TRANSITION    0x0010
#define AVTAB_MEMBER        0x0020
#define AVTAB_CHANGE        0x0040
#define AVTAB_TYPE      (AVTAB_TRANSITION | AVTAB_MEMBER | AVTAB_CHANGE)
/* extended permissions */
#define AVTAB_XPERMS_ALLOWED    0x0100
#define AVTAB_XPERMS_AUDITALLOW 0x0200
#define AVTAB_XPERMS_DONTAUDIT  0x0400
#define AVTAB_XPERMS        (AVTAB_XPERMS_ALLOWED | \
                AVTAB_XPERMS_AUDITALLOW | \
                AVTAB_XPERMS_DONTAUDIT)
#define AVTAB_ENABLED_OLD   0x80000000 /* reserved for used in cond_avtab */
#define AVTAB_ENABLED       0x8000 /* reserved for used in cond_avtab */
    u16 specified;  /* what field is specified */
};

上述為 key 值定義,可以看出 型別 type,類別 class 在記憶體中都是一個 16 位的無符號整數,可以看作它們的 ID 值,所以理論上來說最多隻能定義 65535 個型別。

specified 指明當前是哪種具體的 TE 規則

struct avtab_datum {
    union {
        u32 data; /* access vector or type value */
        struct avtab_extended_perms *xperms;
    } u;
};

key 值對應的資料如上所示,只有一個元素 32 bit 無符號,擴充套件屬性暫時不用瞭解,基本沒人用

對於 AV 規則,data 為一組許可權向量,比如說:

#define FILE__IOCTL                               0x00000001UL
#define FILE__READ                                0x00000002UL
#define FILE__WRITE                               0x00000004UL
#define FILE__CREATE                              0x00000008UL
#define FILE__GETATTR                             0x00000010UL
#define FILE__SETATTR                             0x00000020UL
#define FILE__LOCK                                0x00000040UL
.......

上述是 file 這個類別的許可權位定義,在核心裡面搜尋會發現並沒有上述定義,這些宏是核心編譯的時候自動生成的,生成指令碼對應著 linux/scripts/selinux/genheaders

上述可以看出,每個許可權都是 32 bit 中的某一位, data 中某一位元位為 1 表示擁有該許可權,為 0 表示沒有該許可權許可權。舉個例子,如果存在規則 allow a_t b_file_t : file read; 那麼當查詢 a_t 型別的程序 對 b_file_t 型別的檔案有什麼訪問許可權時,返回結果 data 值中對應 FILE__READ 那個位元位應該為 1

對於 Type 規則,也就是型別轉換類的規則,data 表示轉換後型別的 ID 值

AVC

TE 規則當中又數 AV 規則使用的最頻繁,為了加快查詢速度,核心設計了 AVC,Access Vector Cache。

struct avc_entry {
    u32         ssid;
    u32         tsid;
    u16         tclass;
    struct av_decision  avd;
    struct avc_xperms_node  *xp_node;
};

struct avc_node {
    struct avc_entry    ae;
    struct hlist_node   list; /* anchored in avc_cache->slots[i] */
    struct rcu_head     rhead;
};

struct avc_cache {
    struct hlist_head   slots[AVC_CACHE_SLOTS]; /* head for avc_node->list */
    spinlock_t      slots_lock[AVC_CACHE_SLOTS]; /* lock for writes */
    atomic_t        lru_hint;   /* LRU hint for reclaim scan */
    atomic_t        active_nodes;
    u32         latest_notif;   /* latest revocation notification */
};

整個 cache 就是一個雜湊表,由 avc_node 組成,每個 avc_node 又由 key 值(ssid, tsid, tclass)和 value(avd) 組成。

這裡的 key 值有三個,ssid、tsid、tclass,我們對雜湊表增刪查改需要的 hash 值也是由這三個值算出來。sid,全稱 security id,對於 sid 後面會詳細講述,這裡先簡單說一說。之前有提到過,在 SELinux 中,每個主體和客體都有一個安全上下文(標籤),由 4 部分組成(user:role:type:mls),核心中由 struct context 來表示,而 sid 則是與 context 對應的一個 id 值,context 和 sid 一一對應

value 值為 av_decision,其結構體表示如下:

struct av_decision {
    u32 allowed;      
    u32 auditallow;
    u32 auditdeny;
    u32 seqno;
    u32 flags;
};

AV 規則有 4 種語句,allow,auditallow,dontaudit,neverallow,前三個與上述的定義對應,最後一個 neverallow 語句是在編譯期間起作用,所以核心沒有相關定義

對於每一種 AV 規則,核心都定義了一組許可權向量,但其實只有 allowed 對應的向量才表示許可權授予與否,其他的都是指示當前訪問是否應該被日誌記錄。

比如說對於 FILE__WRITE,如果在 allowed 中對應的位元位為 0,表示沒有許可權寫;如果在 auditdeny 中對應的位元位為 1,即使在 allowed 中表示沒有許可權寫,但是也不會記錄在日誌中。

上述就是對 AVC 涉及的資料結構進行介紹,其他一些函式大多是雜湊表常見的增刪改查函式,這裡不做詳細說明,可以自己閱讀相關核心程式碼,比較簡單。這裡主要說明許可權檢查函式,avc_has_perm。

int avc_has_perm(u32 ssid, u32 tsid, u16 tclass,
         u32 requested, struct common_audit_data *auditdata)
{
    //將存放許可權查詢結果
    struct av_decision avd; 
    int rc, rc2;
    
    //呼叫此函式來進行真正的許可權查詢,查詢結果存放在 avd 中
    rc = avc_has_perm_noaudit(ssid, tsid, tclass, requested, 0,
                  &avd);
    
    //日誌記錄相關
    rc2 = avc_audit(ssid, tsid, tclass, requested, &avd, rc,
            auditdata);
    if (rc2)
        return rc2;
    return rc;
}

inline int avc_has_perm_noaudit(u32 ssid, u32 tsid,
                u16 tclass, u32 requested,
                unsigned int flags,
                struct av_decision *avd)
{
    u32 denied;
    struct avc_node *node;

    if (WARN_ON(!requested))
        return -EACCES;

    rcu_read_lock();
    // 根據鍵值在 avb 中查詢 avd_node
    node = avc_lookup(ssid, tsid, tclass);
    // 如果 node 不存在,則會去查詢 security server,然後分配 node,填充node,插入雜湊表等操作
    if (unlikely(!node)) {
        rcu_read_unlock();
        return avc_perm_nonode(ssid, tsid, tclass, requested,
                       flags, avd);
    }
    // ae.avd.allowed 記錄著allowed許可權向量,request 表示要查詢許可權對應的位元位
    // 一通位操作下來,denied 為 1 表示沒有該許可權,反之有該許可權
    denied = requested & ~node->ae.avd.allowed;
    memcpy(avd, &node->ae.avd, sizeof(*avd));
    rcu_read_unlock();
    
    // 一般不太可能為 denied,這麼想,如果一個系統的 denied 很多,多半策略有問題,而且系統也不能正常執行
    // 如果為 denied=1,則 SELinux 還有其他配置讓它變為 allowed,比如說如果當前開啟了 permissive 模式
    // 所以這裡還需要呼叫 avc_denied 再次判斷當前模式、策略下是否真的沒有該許可權
    if (unlikely(denied))
        return avc_denied(ssid, tsid, tclass, requested, 0, 0,
                  flags, avd);
                  
    // 返回 0 表示有許可權
    return 0;
}

舉個例子

這一小節用兩個例子說明核心裡面到底是如何進行 SELinux 許可權檢查和型別轉換的

假如我們正在執行某個 exec 呼叫,需要檢查當前程序是否對該檔案有執行許可權。核心裡面是如何做檢查的呢?首先是 DAC 檢查,也就是檢查是該檔案是否有 x 許可權位。

當我們訪問檔案的時候,核心裡面經常會呼叫 inode_permission(idmap, inode, mask) 檢查許可權。比如說這裡想要檢查是否有 exec 許可權,便會呼叫inode_permission(idmap, nd->inode, MAY_EXEC);之後會存在如下的呼叫路徑:

inode_permission
    security_inode_permission
        selinux_inode_permission
            //將想要查詢的許可權用 SELinux 中的向量表示
            file_mask_to_av
                // 這裡的mode就是inode中的mode元素,作用之一就是指示當前檔案型別
                if (!S_ISDIR(mode)){    
                    // mask 可以看作上層想要查詢的許可權,之後轉換為SELinux中對應的許可權
                    if (mask & MAY_EXEC)
                        av |= FILE__EXECUTE;
                } else {
                    //如果訪問的是目錄,想要檢查是否有 exec 許可權,那麼實際上是想要檢查是否有搜尋許可權
                    if (mask & MAY_EXEC)  
                        av |= DIR__SEARCH;
                return av
            // 呼叫 avc_has_perm 來查詢許可權,查詢結果存放在 &avd 結構中
            avc_has_perm_noaudit(sid, isec->sid, isec->sclass, perms, 0, &avd);

在 SELinux 中,對於目錄和檔案的執行許可權有不同解釋,對於普通檔案,那就是真的檢查是否可執行。但是對於目錄檔案來說檢查是否能夠執行其實指的是能否對該目錄進行搜尋。

其實在 DAC 中,對於目錄和普通檔案的 x 許可權位解釋也是不一樣的,一個檔案如果有 x,說明該檔案可以被執行,如果一個目錄有 x,指的是可以進入這個目錄, 通俗來講可以 cd 進去,就需要該目錄有 x 許可權。

我們上層的種種操作,其背後都需要各種許可權,在 SELinux 安全檢查的時候都會進行 SELinux 許可權檢查。

對於型別轉換的流程,例子先不說了,這玩意兒還有點點複雜,後面單獨來一篇文章說明,好了本文就先到這裡,有什麼問題歡迎來交流討論

  • 首發公號:Rand_cs

相關文章