英語

Linux 中的憑據

作者:David Howells <dhowells@redhat.com>

概述

當一個物件對另一個物件執行操作時,Linux 執行的安全檢查有幾個部分

  1. 物件。

    物件是系統中可能被使用者空間程式直接操作的事物。 Linux 有多種可操作的物件,包括

    • 任務

    • 檔案/inode

    • 套接字

    • 訊息佇列

    • 共享記憶體段

    • 訊號量

    • 金鑰

    作為所有這些物件的描述的一部分,有一組憑據。集合中的內容取決於物件的型別。

  2. 物件所有權。

    在大多數物件的憑據中,會有一個子集指示該物件的所有權。 這用於資源核算和限制(例如磁碟配額和任務 rlimit)。

    例如,在標準 UNIX 檔案系統中,這將由 inode 上標記的 UID 定義。

  3. 客觀上下文。

    同樣在這些物件的憑據中,會有一個子集指示該物件的“客觀上下文”。 這可能與 (2) 中的集合相同,也可能不同 - 例如,在標準 UNIX 檔案中,這由 inode 上標記的 UID 和 GID 定義。

    客觀上下文用作對物件執行操作時執行的安全計算的一部分。

  4. 主題。

    主題是對另一個物件執行操作的物件。

    系統中的大多數物件都是非活動的:它們不對系統中的其他物件執行操作。 程序/任務是明顯的例外:它們執行操作;它們訪問和操作事物。

    除任務之外的物件在某些情況下也可能成為主題。 例如,開啟的檔案可以使用任務透過 fcntl(F_SETOWN) 提供給它的 UID 和 EUID 將 SIGIO 傳送到任務。 在這種情況下,檔案結構也將具有主觀上下文。

  5. 主觀上下文。

    主題對其憑據有額外的解釋。 其憑據的子集構成“主觀上下文”。 主觀上下文用作主題執行操作時執行的安全計算的一部分。

    例如,Linux 任務具有 FSUID、FSGID 和補充組列表,用於它對檔案執行操作時 - 這與通常構成任務客觀上下文的實際 UID 和 GID 完全不同。

  6. 操作。

    Linux 有許多可用的操作,主題可以對物件執行。 可用操作集取決於主題和物件的性質。

    操作包括讀取、寫入、建立和刪除檔案; fork 或傳送訊號以及跟蹤任務。

  7. 規則、訪問控制列表和安全計算。

    當主題對物件執行操作時,會進行安全計算。 這涉及獲取主觀上下文、客觀上下文和操作,並搜尋一個或多個規則集,以檢視在給定這些上下文的情況下,是否授予或拒絕主題以所需方式對物件執行操作的許可權。

    規則主要有兩個來源

    1. 自主訪問控制 (DAC)

      有時,物件會將規則集作為其描述的一部分包含在內。 這是一個“訪問控制列表”或“ACL”。 Linux 檔案可能會提供多個 ACL。

      例如,傳統的 UNIX 檔案包括一個許可權掩碼,這是一個簡寫的 ACL,帶有三個固定的主題類(“使用者”、“組”和“其他”),每個類都可以被授予某些特權(“讀取”、“寫入”和“執行” - 無論這些特權對應於所討論的物件)。 但是,UNIX 檔案許可權不允許任意指定主題,因此用途有限。

      Linux 檔案也可能支援 POSIX ACL。 這是授予任意主題各種許可權的規則列表。

    2. 強制訪問控制 (MAC)

      作為一個整體,系統可能有一組或多組規則,這些規則適用於所有主題和物件,而與其來源無關。 SELinux 和 Smack 是這方面的示例。

      在 SELinux 和 Smack 的情況下,每個物件都被賦予一個標籤作為其憑據的一部分。 當請求一個操作時,它們會獲取主題標籤、物件標籤和操作,並查詢一條規則,該規則說明該操作是被授予還是被拒絕。

憑據型別

Linux 核心支援以下型別的憑據

  1. 傳統 UNIX 憑據。

    • 實際使用者 ID

    • 實際組 ID

    UID 和 GID 由大多數(如果不是全部)Linux 物件攜帶,即使在某些情況下必須發明它(例如,從 Windows 派生的 FAT 或 CIFS 檔案)。 這些(主要)定義了該物件的客觀上下文,但在某些情況下,任務略有不同。

    • 有效使用者 ID、已儲存使用者 ID 和 FS 使用者 ID

    • 有效組 ID、已儲存組 ID 和 FS 組 ID

    • 補充組

    這些是僅由任務使用的其他憑據。 通常,EUID/EGID/GROUPS 將用作主觀上下文,而實際 UID/GID 將用作客觀上下文。 對於任務,應注意這並非總是正確。

  2. 功能。

    • 允許的功能集

    • 可繼承的功能集

    • 有效的功能集

    • 功能邊界集

    這些僅由任務攜帶。 它們表示以零星的方式授予任務的卓越功能,普通任務通常不會擁有這些功能。 這些功能透過對傳統 UNIX 憑據的更改隱式地進行操作,但也可以透過 capset() 系統呼叫直接進行操作。

    允許的功能是程序可以透過 capset() 將自身授予其有效集或允許集的 cap。 這個可繼承的集合也可能受到限制。

    有效功能是任務實際允許自己使用的功能。

    可繼承功能是可能在 execve() 中傳遞的功能。

    邊界集限制了可能在 execve() 中繼承的功能,尤其是在執行將以 UID 0 執行的二進位制檔案時。

  3. 安全管理標誌 (securebits)。

    這些僅由任務攜帶。 這些標誌控制上述憑據在某些操作(例如 execve())中的操作和繼承方式。 它們不會直接用作客觀或主觀憑據。

  4. 金鑰和金鑰環。

    這些僅由任務攜帶。 它們攜帶並快取不適合其他標準 UNIX 憑據的安全令牌。 它們用於使網路檔案系統金鑰等內容可用於程序執行的檔案訪問,而無需普通程式瞭解所涉及的安全詳細資訊。

    金鑰環是一種特殊的金鑰。 它們攜帶其他金鑰集,並且可以搜尋所需的金鑰。 每個程序可以訂閱多個金鑰環

    每個執行緒的金鑰 每個程序的金鑰環 每個會話的金鑰環

    當程序訪問金鑰時,如果金鑰尚未存在,它通常會快取在其中一個金鑰環上,以供將來的訪問查詢。

    有關使用金鑰的更多資訊,請參閱 Documentation/security/keys/*

  5. LSM

    Linux 安全模組允許對任務可以執行的操作進行額外的控制。 目前,Linux 支援多個 LSM 選項。

    一些工作透過標記系統中的物件,然後應用一組規則(策略)來說明具有一個標籤的任務可以對具有另一個標籤的物件執行哪些操作。

  6. AF_KEY

    這是一種基於套接字的憑據管理方法,用於網路堆疊 [RFC 2367]。 本文件未對其進行討論,因為它不直接與任務和檔案憑據互動; 而是保留系統級憑據。

開啟檔案時,開啟任務的主觀上下文的一部分記錄在建立的檔案結構中。 這允許使用該檔案結構的操作使用這些憑據,而不是發出該操作的任務的主觀上下文。 這方面的一個例子是在網路檔案系統上開啟的檔案,無論誰實際執行讀取或寫入操作,都應將開啟檔案的憑據呈現給伺服器。

檔案標記

磁碟上或透過網路獲取的檔案可能具有形成該檔案客觀安全上下文的註釋。 根據檔案系統的型別,這可能包括以下一項或多項

  • UNIX UID、GID、模式;

  • Windows 使用者 ID;

  • 訪問控制列表;

  • LSM 安全標籤;

  • UNIX 執行特權提升位 (SUID/SGID);

  • 檔案功能執行特權提升位。

這些將與任務的主觀安全上下文進行比較,並允許或禁止某些操作。 在 execve() 的情況下,特權提升位會發揮作用,並且可能允許生成的程序具有額外的特權,這基於可執行檔案上的註釋。

任務憑據

在 Linux 中,任務的所有憑據都儲存在 (uid, gid) 中,或者透過(組、金鑰、LSM 安全)型別為“struct cred”的引用計數的結構中。 每個任務透過其 task_struct 中的一個名為“cred”的指標指向其憑據。

一旦準備好並提交了一組憑據,就不能更改它們,但以下例外情況除外

  1. 可以更改其引用計數;

  2. 可以更改它指向的 group_info 結構的引用計數;

  3. 可以更改它指向的安全資料的引用計數;

  4. 可以更改它指向的任何金鑰環的引用計數;

  5. 它指向的任何金鑰環都可能被撤銷、過期或更改其安全屬性; 以及

  6. 可以更改它指向的任何金鑰環的內容(金鑰環的全部意義在於一組共享憑據,可由具有適當訪問許可權的任何人修改)。

要更改 cred 結構中的任何內容,必須遵守複製和替換原則。 首先複製一個副本,然後更改副本,然後使用 RCU 更改任務指標以使其指向新的副本。 有一些包裝器可以幫助實現這一點(請參見下文)。

任務只能更改其 _自身_ 的憑據; 不再允許任務更改另一個任務的憑據。 這意味著不再允許 capset() 系統呼叫接受當前程序的 PID 以外的任何 PID。 此外,keyctl_instantiate()keyctl_negate() 函式不再允許附加到請求程序中的程序特定金鑰環,因為例項化程序可能需要建立它們。

不可變憑據

一旦一組憑據公開(例如,透過呼叫 commit_creds()),就必須將其視為不可變的,但以下兩個例外情況除外

  1. 可以更改引用計數。

  2. 雖然不能更改一組憑據的金鑰環訂閱,但可以更改訂閱的金鑰環的內容。

為了在編譯時捕獲意外的憑據更改,struct task_struct 具有指向其憑據集的 _const_ 指標,struct file 也是如此。 此外,某些函式(例如 get_cred()put_cred())對 const 指標進行操作,因此無需強制轉換,但需要暫時放棄 const 限定才能更改引用計數。

訪問任務憑據

任務能夠僅更改自己的憑據,這允許當前程序讀取或替換自己的憑據而無需任何形式的鎖定 - 這大大簡化了事情。 它可以只調用

const struct cred *current_cred()

以獲取指向其憑據結構的指標,並且它無需在之後釋放它。

有一些便捷的包裝器可以檢索任務憑據的特定方面(在這種情況下,值只是被返回)

uid_t current_uid(void)         Current's real UID
gid_t current_gid(void)         Current's real GID
uid_t current_euid(void)        Current's effective UID
gid_t current_egid(void)        Current's effective GID
uid_t current_fsuid(void)       Current's file access UID
gid_t current_fsgid(void)       Current's file access GID
kernel_cap_t current_cap(void)  Current's effective capabilities
struct user_struct *current_user(void)  Current's user account

還有一些便捷的包裝器可以檢索任務憑據的特定關聯對

void current_uid_gid(uid_t *, gid_t *);
void current_euid_egid(uid_t *, gid_t *);
void current_fsuid_fsgid(uid_t *, gid_t *);

這些包裝器透過其引數返回這些值對,然後從當前任務的憑據中檢索它們。

此外,還有一個函式可以獲取對當前程序的當前憑據集的引用

const struct cred *get_current_cred(void);

以及用於獲取對實際上不駐留在 struct cred 中的憑據之一的引用的函式

struct user_struct *get_current_user(void);
struct group_info *get_current_groups(void);

它們分別獲取對當前程序的使用者帳戶結構和補充組列表的引用。

獲得引用後,必須使用 put_cred()free_uid()put_group_info() 適當地釋放它。

訪問另一個任務的憑據

雖然任務可以訪問自己的憑據而無需鎖定,但對於想要訪問另一個任務的憑據的任務來說,情況並非如此。 它必須使用 RCU 讀取鎖和 rcu_dereference()

rcu_dereference()

const struct cred *__task_cred(struct task_struct *task);

包裝。 這應該在 RCU 讀取鎖內使用,如以下示例所示

void foo(struct task_struct *t, struct foo_data *f)
{
        const struct cred *tcred;
        ...
        rcu_read_lock();
        tcred = __task_cred(t);
        f->uid = tcred->uid;
        f->gid = tcred->gid;
        f->groups = get_group_info(tcred->groups);
        rcu_read_unlock();
        ...
}

如果需要長時間持有另一個任務的憑據,並且可能在執行此操作時休眠,則呼叫方應使用以下方式獲取對它們的引用

const struct cred *get_task_cred(struct task_struct *task);

這會在其內部完成所有 RCU 魔術。 當呼叫方完成憑據的使用後,必須在憑據上呼叫 put_cred()。

注意

__task_cred() 的結果不應直接傳遞給 get_cred(),因為這可能會與 commit_cred() 發生競爭。

有一些便捷函式可以訪問另一個任務憑據的位,從而向呼叫方隱藏 RCU 魔術

uid_t task_uid(task)            Task's real UID
uid_t task_euid(task)           Task's effective UID

如果呼叫方在任何情況下都在此時持有 RCU 讀取鎖,則應改用

__task_cred(task)->uid
__task_cred(task)->euid

同樣,如果需要訪問任務憑據的多個方面,則應使用 RCU 讀取鎖,呼叫 __task_cred(),將結果儲存在臨時指標中,然後在釋放鎖之前從該指標呼叫憑據方面。 這可以防止多次呼叫可能代價高昂的 RCU 魔術。

如果需要訪問另一個任務憑據的某些其他單個方面,則可以使用

task_cred_xxx(task, member)

其中“member”是 cred 結構的非指標成員。 例如

uid_t task_cred_xxx(task, suid);

將從任務中檢索“struct cred::suid”,執行適當的 RCU 魔術。 這不能用於指標成員,因為它們指向的內容可能會在 RCU 讀取鎖釋放的那一刻消失。

更改憑據

如前所述,任務只能更改自己的憑據,而不能更改另一個任務的憑據。 這意味著它不需要使用任何鎖定來更改自己的憑據。

要更改當前程序的憑據,函式應首先透過呼叫以下函式來準備一組新的憑據

struct cred *prepare_creds(void);

這會鎖定 current->cred_replace_mutex,然後分配並構造當前程序憑據的副本,如果成功,則返回並仍然持有互斥鎖。 如果不成功(記憶體不足),則返回 NULL。

互斥鎖可防止 ptrace() 在憑據構造和更改的安全檢查正在進行時更改程序的 ptrace 狀態,因為 ptrace 狀態可能會改變結果,尤其是在 execve() 的情況下。

應適當地更改新的憑據集,並完成任何安全檢查和掛鉤。 當前的憑據集和建議的憑據集都可以用於此目的,因為此時 current_cred() 仍將返回當前的憑據集。

替換組列表時,必須先對新列表進行排序,然後才能將其新增到憑據中,因為會使用二進位制搜尋來測試成員資格。 在實踐中,這意味著在 set_groups() 或 set_current_groups() 之前應呼叫 groups_sort()。 不能在共享的 struct group_list 上呼叫 groups_sort(),因為它可能會在排序過程中置換元素,即使陣列已經排序。

當憑據集準備好時,應透過呼叫以下函式將其提交到當前程序

int commit_creds(struct cred *new);

這將更改憑據和程序的各個方面,使 LSM 有機會也這樣做,然後它將使用 rcu_assign_pointer() 將新憑據實際提交到 current->cred,它將釋放 current->cred_replace_mutex 以允許進行 ptrace(),並且它將通知排程程式和其他更改。

保證此函式返回 0,因此可以在諸如 sys_setresuid() 之類的函式的末尾進行尾呼叫。

請注意,此函式會消耗呼叫方對新憑據的引用。 呼叫方不應_在之後對新憑據呼叫 put_cred()

此外,一旦對一組新的憑據呼叫了此函式,_就不得_進一步更改這些憑據。

如果在呼叫 prepare_creds() 後安全檢查失敗或發生其他錯誤,則應呼叫以下函式

void abort_creds(struct cred *new);

這將釋放 prepare_creds() 獲取的 current->cred_replace_mutex 上的鎖,然後釋放新的憑據。

典型的憑據更改函式如下所示

int alter_suid(uid_t suid)
{
        struct cred *new;
        int ret;

        new = prepare_creds();
        if (!new)
                return -ENOMEM;

        new->suid = suid;
        ret = security_alter_suid(new);
        if (ret < 0) {
                abort_creds(new);
                return ret;
        }

        return commit_creds(new);
}

管理憑據

有一些函式可以幫助管理憑據

  • void put_cred(const struct cred *cred);

    這會釋放對給定憑據集的引用。 如果引用計數達到零,則憑據將被排程為由 RCU 系統銷燬。

  • const struct cred *get_cred(const struct cred *cred);

    這會在即時憑據集上獲取引用,並返回指向該憑據集的指標。

開啟檔案憑據

開啟新檔案時,會獲得對開啟任務的憑據的引用,並將其作為 f_cred 附加到檔案結構中,以代替 f_uidf_gid。 曾經訪問 file->f_uidfile->f_gid 的程式碼現在應訪問 file->f_cred->fsuidfile->f_cred->fsgid

可以安全地訪問 f_cred 而無需使用 RCU 或鎖定,因為該指標在檔案結構的生命週期內不會更改,並且所指向的 cred 結構的內容也不會更改,但上述列出的例外情況除外(請參見任務憑據部分)。

為了避免“困惑的副手”特權提升攻擊,在後續對開啟的檔案執行操作期間的訪問控制檢查應使用這些憑據而不是“current”的憑據,因為該檔案可能已傳遞給特權更高的程序。

覆蓋 VFS 對憑據的使用

在某些情況下,需要覆蓋 VFS 使用的憑據,這可以透過使用不同的憑據呼叫諸如 vfs_mkdir() 之類的函式來完成。 這在以下位置完成

  • sys_faccessat().

  • do_coredump().

  • nfs4recover.c。