1. 程式人生 > 其它 >Linux DAC 許可權管理詳解

Linux DAC 許可權管理詳解

技術標籤:Linux Kernel解析SecurityDAClinux許可權管理setresuidselinux

文章目錄

1. 背景簡介

linux下有多種許可權控制的機制,常見的如:DAC(Discretionary Access Control)自主式許可權控制和MAC(Mandatory Access Control)強制訪問控制。

其實本質上的模型都是差不多的,參與的物件有3種:主體(subject)、客體(object)、規則(policy)。
在這裡插入圖片描述

許可權判定過程大概如下:

  • 1、主體擁有自己的憑證來標識自己的身份。在DAC中,主體通常是程序,而憑證是程序對應用的euid和egid。
  • 2、客體擁有屬性來標識自己的身份。在DAC中,客體通常是檔案,而許可權相關屬性是檔案對應的uid和gid。
  • 3、主體對客體的操作可以稱之為行為。DAC的特點就是行為比較簡單,行為僅包括R(讀)、W(寫)、X(執行)這三種。
  • 4、針對"主體對客體發起的行為",查詢規則表來進行許可權判定。DAC的UGO規則非常簡單,把主體分為User、Group、Other三種類型,每種型別擁有自己的RWX mask。

DAC的許可權控制策略是非常簡潔,行為是簡單的RWX三種,主體也很簡單的就能被分為UGO三類。這種簡潔造也就了DAC檢查的開銷非常小。

但是凡事都有好有壞,DAC的簡潔造就了它的高效,但是過於簡潔也讓它的許可權劃分粒度過大,一旦獲得了root許可權,幾乎就是無所不能。在CPU日益高漲的今天,效能開銷已經不是問題了,許可權的細粒度管理更加重要,所以誕生了MAC。MAC在DAC的基礎上,把行為

規則判定結果進一步細分。所以它的許可權管理粒度更細,但是開銷也稍大。

DAC是Linux許可權管理的基礎機制,我們本文的重點也是DAC。

2. 主體(subject)

2.1 使用者

我們在許可權管理的時候,通常做的第一件事就是建立群組(groupadd)、建立使用者(useradd)。這些資訊儲存在以下的三個檔案當中,其中最重要的資訊就是UIDGID密碼

  • /etc/passwd

每一行都表示的是一個使用者的資訊;一行有7個段位;每個段位用:號分割,例如:

beinan:x:500:500:beinan sun:/home/beinan:/bin/bash
linuxsir:x:501:502::/home/linuxsir:/bin/bash

第一欄位:使用者名稱(也被稱為登入名);在上面的例子中,我們看到這兩個使用者的使用者名稱分別是 beinan 和linuxsir;
第二欄位:口令;在例子中我們看到的是一個x,其實密碼已被對映到/etc/shadow 檔案中;
第三欄位:UID ;請參看本文的UID的解說;
第四欄位:GID;請參看本文的GID的解說;
第五欄位:使用者名稱全稱,這是可選的,可以不設定,在beinan這個使用者中,使用者的全稱是beinan sun ;而linuxsir 這個使用者是沒有設定全稱;
第六欄位:使用者的家目錄所在位置;beinan 這個使用者是/home/beinan ,而linuxsir 這個使用者是/home/linuxsir ;
第七欄位:使用者所用SHELL 的型別,beinan和linuxsir 都用的是 bash ;所以設定為/bin/bash ;

useradd 會把新使用者的主組設定為 /etc/default/useradd 中 GROUP 變數指定的值,再或者預設是 100。

  • /etc/group

/etc/group 的內容包括使用者組(Group)、使用者組口令、GID及該使用者組所包含的使用者(User),每個使用者組一條記錄;格式如下:

group_name:passwd:GID:user_list

在/etc/group 中的每條記錄分四個欄位:
第一欄位:使用者組名稱;
第二欄位:使用者組密碼;
第三欄位:GID
第四欄位:使用者列表,每個使用者之間用,號分割;本欄位可以為空;如果欄位為空表示使用者組為GID的使用者名稱;
我們舉個例子:

root:x:0:root,linuxsir 

注:使用者組root,x是密碼段,表示沒有設定密碼,GID是0,root使用者組下包括root、linuxsir以及GID為0的其它使用者(可以通過/etc/passwd檢視);;
  • /etc/shadow

/etc/shadow 檔案的內容包括9個段位,每個段位之間用:號分割;我們以如下的例子說明:

beinan:$1$VE.Mq2Xf$2c9Qi7EQ9JP8GKF8gH7PB1:13072:0:99999:7:::
linuxsir:$1$IPDvUhXP$8R6J/VtPXvLyXxhLWPrnt/:13072:0:99999:7::13108:

第一欄位:使用者名稱(也被稱為登入名),在/etc/shadow中,使用者名稱和/etc/passwd 是相同的,這樣就把passwd 和shadow中用的使用者記錄聯絡在一起;這個欄位是非空的;
第二欄位:密碼(已被加密),如果是有些使用者在這段是x,表示這個使用者不能登入到系統;這個欄位是非空的;
第三欄位:上次修改口令的時間;這個時間是從1970年01月01日算起到最近一次修改口令的時間間隔(天數),您可以通過passwd 來修改使用者的密碼,然後檢視/etc/shadow中此欄位的變化;
第四欄位:兩次修改口令間隔最少的天數;如果設定為0,則禁用此功能;也就是說使用者必須經過多少天才能修改其口令;此項功能用處不是太大;預設值是通過/etc/login.defs檔案定義中獲取,PASS_MIN_DAYS 中有定義;
第五欄位:兩次修改口令間隔最多的天數;這個能增強管理員管理使用者口令的時效性,應該說在增強了系統的安全性;如果是系統預設值,是在新增使用者時由/etc/login.defs檔案定義中獲取,在PASS_MAX_DAYS 中定義;
第六欄位:提前多少天警告使用者口令將過期;當用戶登入系統後,系統登入程式提醒使用者口令將要作廢;如果是系統預設值,是在新增使用者時由/etc/login.defs檔案定義中獲取,在PASS_WARN_AGE 中定義;
第七欄位:在口令過期之後多少天禁用此使用者;此欄位表示使用者口令作廢多少天后,系統會禁用此使用者,也就是說系統會不能再讓此使用者登入,也不會提示使用者過期,是完全禁用;
第八欄位:使用者過期日期;此欄位指定了使用者作廢的天數(從1970年的1月1日開始的天數),如果這個欄位的值為空,帳號永久可用;
第九欄位:保留欄位,目前為空,以備將來Linux發展之用;

2.2 程序

我們在講Linux許可權管理的時候經常講到使用者,但是核心中卻沒有使用者這個資料結構。核心中只會識別UID,至於使用者名稱是通過在/etc/passwd檔案中查詢對應關係得到的。

以下是獲取uid,並且根據uid查詢到對應檔名的例項:

// test.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
int main()
{
    uid_t userid;
    struct passwd* pwd;
    userid=getuid();		// 使用系統呼叫獲取到當前程序的uid
    printf("userid is %d\n",userid);
    pwd=getpwuid(userid);	// 根據uid查詢`/etc/passwd`檔案,得到對應的使用者名稱和使用者目錄
    printf("username is %s\nuserdir is %s\n",pwd->pw_name,pwd->pw_dir);
}

// 對應輸出:
$ ./test 
userid is 1000
username is ipu
userdir is /home/ipu

$ cat /etc/passwd
...
ipu:x:1000:1000:ipu:/home/ipu:/bin/bash

許可權管理時真正代表使用者的是程序,操作檔案的也是程序,也就是說使用者所擁有的檔案訪問許可權是通過程序來體現的。

2.2.1 憑證(credentials)

使用者擁有的許可權,是通過程序的credentials成員來描述的。

task_stuct結構體包含credentials的定義:

struct task_struct {
    ...
	/* Process credentials: */

	/* Tracer's credentials at attach: */
	/* ptrace時attach的tracer的證書 */
	const struct cred __rcu		*ptracer_cred;

	/* Objective and real subjective task credentials (COW): */
	/* `客體`和`實際主體`的程序證書 */
	const struct cred __rcu		*real_cred;

	/* Effective (overridable) subjective task credentials (COW): */
	/* `有效的主體`(可覆蓋)的程序證書 */
	const struct cred __rcu		*cred;

    ...
}

對DAC來說,最重要的就是struct cred中的uid、gid定義:

/*
 * The security context of a task
 *
 * The parts of the context break down into two categories:
 *
 *  (1) The objective context of a task.  These parts are used when some other
 *	task is attempting to affect this one.
 *
 *  (2) The subjective context.  These details are used when the task is acting
 *	upon another object, be that a file, a task, a key or whatever.
 *
 * Note that some members of this structure belong to both categories - the
 * LSM security pointer for instance.
 *
 * A task has two security pointers.  task->real_cred points to the objective
 * context that defines that task's actual details.  The objective part of this
 * context is used whenever that task is acted upon.
 *
 * task->cred points to the subjective context that defines the details of how
 * that task is going to act upon another object.  This may be overridden
 * temporarily to point to another security context, but normally points to the
 * same context as task->real_cred.
 */
/ *
 * 程序的安全上下文
 *
 * 上下文的內部分為兩類:
 * (1)程序的`客體`上下文。當某些其他程序試圖影響本程序時,將使用這些部分。
 * (2)`主體`上下文。當程序作用於另一個物件(檔案、程序、鍵或其他物件)時,將使用這些詳細資訊。
 *
 * 請注意,此結構體的某些成員同時屬於這兩個類別 - 例如LSM安全指標。
 *
 * 一個程序有兩個安全指標:
 * 1、`task->real_cred`指向`客體`上下文,它定義了程序的實際細節。每當該程序被其他人作用時,都會使用此上下文的客觀部分。
 * 2、`task->cred`指向`主體`上下文,該上下文定義了該任務將如何作用於另一個物件的詳細資訊。可能會暫時覆蓋它以指向另一個安全上下文,但通常指向與task->real_cred相同的上下文。
 * /

struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */
};

// objective :本譯客觀,這裡可翻譯成客體。在本程序作被其他程序作用時,稱為客體。  
// subjective :本譯主觀,這裡可翻譯成主體。本程序作為主體時,作用於其他客體(如 檔案、程序、鍵或其他物件)。

2.2.2 uid/suid/euid/fsuid

仔細看cred結構,其中最令人疑惑的是uid/gid有四種表達方式,這其中的區別在哪裡呢?

namemeaningdescript
uidreal UID程序原本的uid
suidsaved UID一個uid快取
在SUID機制設定euid時,suid同時被設定成euid
在setuid()設定uid時,suid同時被設定成uid
euideffective UID有效uid,許可權判斷時看的就是euid。
初始狀態時,uid和euid相同,做一些許可權切換時euid可能改變不等於uid了。
fsuidUID for VFS opslinux系統中特有的檔案操作uid,通常情況下和euid相等。
除非呼叫setfsuid()設定成不一樣

2.2.3 初始uid (fork())

uid的初始狀態是在程序建立時,複製父程序的安全憑證:

_do_fork() → copy_process() → copy_creds():

int copy_creds(struct task_struct *p, unsigned long clone_flags)
{

	/* (1) 分配新的cred,並複製當前程序cred的內容 */
	new = prepare_creds();

	/* (2) 賦值給新建程序 */
	p->cred = p->real_cred = get_cred(new);

}

普通程序在初始狀態時,uid/suid/euid/fsuid都是相同的。

2.2.4 uid許可權升級 (SUID execve())

在linux日常使用時,一般我們使用普通使用者來操作,bash程序是普通使用者的uid,那麼各種操作創建出來的新程序也是普通使用者uid。

某些情況下,需要切換到root操作,使用su或者sudo命令輸入對應密碼就能切換到root使用者許可權。這種許可權升級的操作是什麼原理呢?

許可權的升級依賴於SUID(set-user-id)機制,在檔案的UGO策略中除了記錄三組使用者的rwx mask位,還針對x許可權定義了一個補充的s位,對應UGO使用者分別為SUID/SGID/SBIT。如果檔案設定了SUID,那麼它在執行的時候,會把程序的許可權(euid)設定成檔案屬主的uid。我們檢視sudo/su檔案就是SUID被設定:

[[email protected] uid]$ ll /bin/sudo
---s--x--x. 1 root root 147320 Aug  9  2019 /bin/sudo
[[email protected] uid]$ ll /bin/su
-rwsr-xr-x. 1 root root 32128 Aug  9  2019 /bin/su

這個SUID機制就是專門為提升/切換使用者許可權而設計的,切換使用者也必須先提升到root使用者才能切換到其他使用者。在這類檔案被執行後,不需要驗證密碼,程序的euid被設定成檔案屬主的uid,如果檔案屬主是root使用者當前程序就有了root許可權,同時這時程序的uid和euid也不相等了。

具體的execve()程式碼解析如下:

do_execve() → do_execveat_common():

step 1、分配一份新的安全憑證:
→ prepare_bprm_creds() → prepare_exec_creds()

step 2.1、根據被執行檔案是否有suid/sgid被職位,來使用檔案屬主uid/gid替代程序原來的uid/gid
 → prepare_binprm() → bprm_fill_uid()
static void bprm_fill_uid(struct linux_binprm *bprm)
{
	struct inode *inode;
	unsigned int mode;
	kuid_t uid;
	kgid_t gid;

	/*
	 * Since this can be called multiple times (via prepare_binprm),
	 * we must clear any previous work done when setting set[ug]id
	 * bits from any earlier bprm->file uses (for example when run
	 * first for a setuid script then again for its interpreter).
	 */
	bprm->cred->euid = current_euid();
	bprm->cred->egid = current_egid();

	/* (2.1.1) 如果path不支援suid則返回 */
	if (path_nosuid(&bprm->file->f_path))
		return;

	if (task_no_new_privs(current))
		return;

	inode = bprm->file->f_path.dentry->d_inode;
	mode = READ_ONCE(inode->i_mode);
	if (!(mode & (S_ISUID|S_ISGID)))
		return;

	/* Be careful if suid/sgid is set */
	inode_lock(inode);

	/* reload atomically mode/uid/gid now that lock held */
	/* (2.1.2) 從檔案inode讀出對應的mode、uid、gid */
	mode = inode->i_mode;
	uid = inode->i_uid;
	gid = inode->i_gid;
	inode_unlock(inode);

	/* We ignore suid/sgid if there are no mappings for them in the ns */
	if (!kuid_has_mapping(bprm->cred->user_ns, uid) ||
		 !kgid_has_mapping(bprm->cred->user_ns, gid))
		return;

	/* (2.1.3) 如果suid標誌被設定,使用檔案屬主uid替代程序原來的euid */
	if (mode & S_ISUID) {
		bprm->per_clear |= PER_CLEAR_ON_SETID;
		bprm->cred->euid = uid;
	}

	/* (2.1.4) 如果sgid和group的x屬性被設定,使用檔案屬主gid替代程序原來的egid */
	if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
		bprm->per_clear |= PER_CLEAR_ON_SETID;
		bprm->cred->egid = gid;
	}
}

step 2.2、
 → prepare_bprm_creds() → security_bprm_set_creds() → cap_bprm_set_creds()

int cap_bprm_set_creds(struct linux_binprm *bprm)
{

	/* (2.2.1) 把suid和fsuid同步成euid的值 */
	new->suid = new->fsuid = new->euid;
	new->sgid = new->fsgid = new->egid;

}

step 3、更新程序為新的安全憑證:
→ load_elf_binary() → install_exec_creds() → commit_creds()

注意:可以看到SUID的許可權是非常大的,如果檔案屬主是root,不需要驗證密碼程序許可權被提升為root許可權。這類檔案如果有漏洞的話,就會被攻擊獲取到root許可權,需要特別小心。

2.2.5 uid許可權降級 (setreuid()/setuid()/setresuid()/setfsuid())

在使用sudo/su進行操作時,並不希望程序一直處於root許可權當中,它最終會根據配置切換到適當的許可權。

例如sudo -u test cat /etc/shadow的命令執行過程:

  • 1、首先因為sudo的SUID被設定且檔案屬主為root,execve()執行sudo,程序許可權被切換成root許可權。
  • 2、接下來sudo程序執行在root許可權狀態,來驗證當前使用者密碼。
  • 3、如果密碼驗證成功,則讀取/etc/sudoers配置檔案,其中規定了當前使用者使用sudo命令時的許可權。/etc/sudoers檔案中的配置格式如下%wheel ALL=(ALL) ALL,規定了指定使用者能切換到哪些使用者,能執行哪些命令。
  • 4、如果當前使用者允許切換到test使用者,則執行sudo命令的-u test引數,呼叫setuid()系統呼叫把當前程序的euid切換成test使用者。
  • 5、切換到test使用者以後,如果/etc/sudoers允許執行cat命令,則繼續執行cat /etc/shadow命令。

從上述的過程可以看到許可權切換的關鍵途徑,一般通過SUID機制來無密碼的把許可權升級到root,然後在root狀態下驗證使用者密碼,再根據配置通過setreuid()/setuid()/setresuid()系統呼叫到許可權降級到合適使用者。

setreuid()/setuid()/setresuid()系統呼叫可以降級許可權,它沒有升級的能力。它的幾個基本準則是:

這幾個系統呼叫的詳細解析過程:

SYSCALL_DEFINE1(setuid, uid_t, uid)
{
	struct user_namespace *ns = current_user_ns();
	const struct cred *old;
	struct cred *new;
	int retval;
	kuid_t kuid;

	/* (1) 在當前namespace下,把uid轉換成kuid */
	kuid = make_kuid(ns, uid);
	if (!uid_valid(kuid))
		return -EINVAL;

	/* (2) 分配新的程序許可權憑證,預設拷貝了舊的憑證值 */
	new = prepare_creds();
	if (!new)
		return -ENOMEM;
	old = current_cred();

	retval = -EPERM;
	/* (3.1) 如果當前程序有CAP_SETUID許可權的能力 (一般這裡指root許可權,在通過SUID切換到root許可權時,會賦予使用者所有能力)
			設定新cred的suid和uid為傳入引數uid
	 */
	if (ns_capable(old->user_ns, CAP_SETUID)) {
		new->suid = new->uid = kuid;
		if (!uid_eq(kuid, old->uid)) {
			retval = set_user(new);
			if (retval < 0)
				goto error;
		}
	/* (3.2) 如果當前程序沒有CAP_SETUID(不是root使用者),設定的uid也不等於原cred的uid或suid中的一個
			出錯返回
	 */
	} else if (!uid_eq(kuid, old->uid) && !uid_eq(kuid, new->suid)) {
		goto error;
	}

	/* (3.3) 綜合上述邏輯分為幾種情況:
			1、有CAP_SETUID許可權(root使用者),把新cred的uid、suid、euid、fsuid全都設定成新的uid
			2、沒有CAP_SETUID許可權(非root使用者),只能把新cred的euid、fsuid設定成原cred的uid或suid中的一個。例如原來使用SUID機制切換了許可權,這裡也可以切換回去。
			3、不符合以上兩種條件的都是非法操作。
	 */
	new->fsuid = new->euid = kuid;

	/* (4) lsm check點 */
	retval = security_task_fix_setuid(new, old, LSM_SETID_ID);
	if (retval < 0)
		goto error;

	/* (5) 更新為新憑證 */
	return commit_creds(new);

error:
	abort_creds(new);
	return retval;
}

SYSCALL_DEFINE2(setreuid, uid_t, ruid, uid_t, euid)
{
	struct user_namespace *ns = current_user_ns();
	const struct cred *old;
	struct cred *new;
	int retval;
	kuid_t kruid, keuid;

	kruid = make_kuid(ns, ruid);
	keuid = make_kuid(ns, euid);

	if ((ruid != (uid_t) -1) && !uid_valid(kruid))
		return -EINVAL;
	if ((euid != (uid_t) -1) && !uid_valid(keuid))
		return -EINVAL;

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

	retval = -EPERM;
	/* (1.1) 如果傳入ruid != -1,則更新新cred的uid值
			否則保持新cred的uid值不變
	 */
	if (ruid != (uid_t) -1) {
		new->uid = kruid;
		/* (1.2) 如果ruid != -1,還需要滿足以下附加條件,才能更新新cred的uid值:
				1、有CAP_SETUID許可權(root使用者),可以把uid設定成任意值。
				2、沒有CAP_SETUID許可權(非root使用者),只能把uid設定成原cred的uid或euid中的任一個。
		 */
		if (!uid_eq(old->uid, kruid) &&
		    !uid_eq(old->euid, kruid) &&
		    !ns_capable(old->user_ns, CAP_SETUID))
			goto error;
	}

	/* (2.1) 如果傳入euid != -1,則更新新cred的euid值
			否則保持新cred的euid值不變
	 */
	if (euid != (uid_t) -1) {
		new->euid = keuid;
		/* (2.2) 如果euid != -1,還需要滿足以下附加條件,才能更新新cred的euid值:
				1、有CAP_SETUID許可權(root使用者),可以把euid設定成任意值。
				2、沒有CAP_SETUID許可權(非root使用者),只能把euid設定成原cred的uid、euid或suid中的任一個。
		 */
		if (!uid_eq(old->uid, keuid) &&
		    !uid_eq(old->euid, keuid) &&
		    !uid_eq(old->suid, keuid) &&
		    !ns_capable(old->user_ns, CAP_SETUID))
			goto error;
	}

	if (!uid_eq(new->uid, old->uid)) {
		retval = set_user(new);
		if (retval < 0)
			goto error;
	}

	/* (3) 如果ruid被成功更新,
			或者euid被成功更新,且euid不等於原uid的值
			更新suid的值為euid
	 */
	if (ruid != (uid_t) -1 ||
	    (euid != (uid_t) -1 && !uid_eq(keuid, old->uid)))
		new->suid = new->euid;

	/* (4) 同步更新fsuid為euid (linux下,大部分情況下fsuid就等於euid) */	
	new->fsuid = new->euid;

	retval = security_task_fix_setuid(new, old, LSM_SETID_RE);
	if (retval < 0)
		goto error;

	return commit_creds(new);

error:
	abort_creds(new);
	return retval;
}

SYSCALL_DEFINE3(setresuid, uid_t, ruid, uid_t, euid, uid_t, suid)
{
	struct user_namespace *ns = current_user_ns();
	const struct cred *old;
	struct cred *new;
	int retval;
	kuid_t kruid, keuid, ksuid;

	kruid = make_kuid(ns, ruid);
	keuid = make_kuid(ns, euid);
	ksuid = make_kuid(ns, suid);

	if ((ruid != (uid_t) -1) && !uid_valid(kruid))
		return -EINVAL;

	if ((euid != (uid_t) -1) && !uid_valid(keuid))
		return -EINVAL;

	if ((suid != (uid_t) -1) && !uid_valid(ksuid))
		return -EINVAL;

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

	old = current_cred();

	retval = -EPERM;
	/* (1.1) 有CAP_SETUID許可權(root使用者),可以把uid/euid/suid設定成任意值。 */
	if (!ns_capable(old->user_ns, CAP_SETUID)) {
		/* (1.2) 如果傳入ruid != -1,沒有CAP_SETUID許可權(非root使用者),只能把uid設定成原cred的uid、euid或suid中的任一個。 */
		if (ruid != (uid_t) -1        && !uid_eq(kruid, old->uid) &&
		    !uid_eq(kruid, old->euid) && !uid_eq(kruid, old->suid))
			goto error;
		/* (1.3) 如果傳入euid != -1,沒有CAP_SETUID許可權(非root使用者),只能把euid設定成原cred的uid、euid或suid中的任一個。 */
		if (euid != (uid_t) -1        && !uid_eq(keuid, old->uid) &&
		    !uid_eq(keuid, old->euid) && !uid_eq(keuid, old->suid))
			goto error;
		/* (1.4) 如果傳入suid != -1,沒有CAP_SETUID許可權(非root使用者),只能把suid設定成原cred的uid、euid或suid中的任一個。 */
		if (suid != (uid_t) -1        && !uid_eq(ksuid, old->uid) &&
		    !uid_eq(ksuid, old->euid) && !uid_eq(ksuid, old->suid))
			goto error;
	}

	/* (2.1) 更新uid的值為傳入的ruid */
	if (ruid != (uid_t) -1) {
		new->uid = kruid;
		if (!uid_eq(kruid, old->uid)) {
			retval = set_user(new);
			if (retval < 0)
				goto error;
		}
	}
	/* (2.2) 更新euid的值為傳入的euid */
	if (euid != (uid_t) -1)
		new->euid = keuid;
	/* (2.3) 更新suid的值為傳入的suid */
	if (suid != (uid_t) -1)
		new->suid = ksuid;
	/* (3) 同步更新fsuid為euid (linux下,大部分情況下fsuid就等於euid) */	
	new->fsuid = new->euid;

	retval = security_task_fix_setuid(new, old, LSM_SETID_RES);
	if (retval < 0)
		goto error;

	return commit_creds(new);

error:
	abort_creds(new);
	return retval;
}

SYSCALL_DEFINE1(setfsuid, uid_t, uid)
{
	const struct cred *old;
	struct cred *new;
	uid_t old_fsuid;
	kuid_t kuid;

	old = current_cred();
	old_fsuid = from_kuid_munged(old->user_ns, old->fsuid);

	kuid = make_kuid(old->user_ns, uid);
	if (!uid_valid(kuid))
		return old_fsuid;

	new = prepare_creds();
	if (!new)
		return old_fsuid;

	/* (1) 符合以下條件,把當前程序fsuid設定成傳入的uid
			情況1、有CAP_SETUID許可權(root使用者)。
			情況2、沒有CAP_SETUID許可權(非root使用者),且傳入的uid等於原uid/suid/euid中的任意一員。
	 */
	if (uid_eq(kuid, old->uid)  || uid_eq(kuid, old->euid)  ||
	    uid_eq(kuid, old->suid) || uid_eq(kuid, old->fsuid) ||
	    ns_capable(old->user_ns, CAP_SETUID)) {
		if (!uid_eq(kuid, old->fsuid)) {
			new->fsuid = kuid;
			if (security_task_fix_setuid(new, old, LSM_SETID_FS) == 0)
				goto change_okay;
		}
	}

	abort_creds(new);
	return old_fsuid;

change_okay:
	commit_creds(new);
	return old_fsuid;
}

3. 客體(object)

對DAC模式來說,客體通常是檔案。但是程序也是可以作為客體的,還記得程序有兩個cred成員,task->cred是程序作為主體時的許可權憑證,而task->real_cred是程序作為客體時的許可權憑證。

對客體檔案來說,最重要的屬性是檔案的屬主uid和gid:

inode->i_uid
inode->i_gid

對應命令檢視時的:

在這裡插入圖片描述

需要注意的是程序主體(subject)也是和檔案耦合在一起,一是程序的執行程式碼是從檔案中載入的,而是SUID機制能把程序的執行許可權修改成檔案屬主。所以在DAC模型理解時,注意相互之間的概念,避免繞暈。

4. 規則(policy)

對DAC模式來說,規則通常比較簡單,一般就儲存在客體檔案inode相關屬性中。而MAC模式,規則比較複雜,需要獨立的檔案來儲存。

4.1 UGO(User、Ggroup、Other)規則

UGO是最通用的規則了,客體檔案inode->i_mode中儲存了UGO 3組mask,每組mask由rwx三個行為組成。
在這裡插入圖片描述

UGO把操作當前檔案的程序分為3種類型:

  • User使用者。檔案的屬主,即主體程序的euid等於客體檔案的uid。
  • Group同組使用者。即主體程序的egid等於客體檔案的gid。
  • Other使用者。不滿足上述兩種條件的其他使用者。

每組使用者對當前檔案的行為,又分為3種許可權:

  • r(Read,讀取):
    對檔案而言,具有讀取檔案內容的許可權;
    對目錄來說,具有瀏覽目錄的許可權。
  • w(Write,寫入):
    對檔案而言,具有新增,修改,刪除檔案內容的許可權;
    對目錄來說,具有新建,刪除,修改,移動目錄內檔案的許可權。
  • x(eXecute,執行):
    對檔案而言,具有執行檔案的許可權;
    對目錄了來說該使用者具有進入目錄的許可權。

目錄許可權的總結:

  • 1、目錄的只讀訪問不允許使用cd進入目錄,必須要有執行的許可權才能進入。
  • 2、只有執行許可權只能進入目錄,不能看到目錄下的內容,要想看到目錄下的檔名和目錄名,需要可讀許可權。
  • 3、一個檔案能不能被刪除,主要看該檔案所在的目錄對使用者是否具有寫許可權,如果目錄對使用者沒有寫許可權,則該目錄下的所有檔案都不能被刪除,檔案所有者除外
  • 4、目錄的w位不設定,即使你擁有目錄中某檔案的w許可權也不能寫該檔案

檔案預設許可權:umask值用於設定使用者在建立檔案時的預設許可權,當我們在系統中建立目錄或檔案時,目錄或檔案所具有的預設許可權就是由umask值決定的。對於root使用者,系統預設的umask值是0022;對於普通使用者,系統預設的umask值是0002。執行umask命令可以檢視當前使用者的umask值。
umask值一共有4組數字,其中第1組數字用於定義特殊許可權,我們一般不予考慮,與一般許可權有關的是後3組數字。
預設情況下,對於目錄,使用者所能擁有的最大許可權是777;對於檔案,使用者所能擁有的最大許可權是目錄的最大許可權去掉執行許可權,即666。因為x執行許可權對於目錄是必須的,沒有執行許可權就無法進入目錄,而對於檔案則不必預設賦予x執行許可權。
對於root使用者,他的umask值是022。當root使用者建立目錄時,預設的許可權就是用最大許可權777去掉相應位置的umask值許可權,即對於所有者不必去掉任何許可權,對於所屬組要去掉w許可權,對於其他使用者也要去掉w許可權,所以目錄的預設許可權就是755;當root使用者建立檔案時,預設的許可權則是用最大許可權666去掉相應位置的umask值,即檔案的預設許可權是644

有了UGO規則,主體程序在RWX客體檔案時會做對應的許可權規則檢查。例如,open一個檔案時,幾個許可權校驗的關鍵節點:

  • 第一步許可權檢查: 最開始對檔案所在路徑上每個目錄項對應的inode進行執行許可權檢查.對應程式碼:path_openat->link_path_walk->may_lookup->inode_permission;
  • 第二步許可權檢查:如果是新建檔案,對檔案所在目錄項的inode做寫和可執行許可權檢查。對應程式碼:path_openat->do_last->lookup_open->vfs_create->may_create->inode_permission.
  • 第三步許可權檢查:真正開啟檔案之前,對檔案所對應的inode做讀寫許可權檢查。對應程式碼:path_openat->do_last->may_open->inode_permission.

inode_permission()最後會呼叫acl_permission_check()來做UGO規則檢查:

inode_permission() → __inode_permission() → do_inode_permission() → generic_permission() → acl_permission_check()

static int acl_permission_check(struct inode *inode, int mask)
{
	/* (1) 從i_mode中取出UGO規則 */
	unsigned int mode = inode->i_mode;

	/* (2) User使用者取最高3bit規則,主體程序的euid等於客體檔案的uid */
	if (likely(uid_eq(current_fsuid(), inode->i_uid)))
		mode >>= 6;
	else {
		/* (3) User使用者匹配失敗首先去匹配ACL規則 */
		if (IS_POSIXACL(inode) && (mode & S_IRWXG)) {
			int error = check_acl(inode, mask);
			if (error != -EAGAIN)
				return error;
		}

		/* (4) ACL匹配失敗則嘗試匹配Group使用者規則,
				Group使用者取中間3bit規則,主體程序的egid等於客體檔案的gid 
		*/
		if (in_group_p(inode->i_gid))
			mode >>= 3;
	}
	/* (5) 如果以上條件都未匹配成功,則為Other使用者,取最低3bit規則 */

	/*
	 * If the DACs are ok we don't need any capability check.
	 */
	/* (6) 使用規則允許的3bit和當前操作進行匹配,決定放行還是拒絕 */
	if ((mask & ~mode & (MAY_READ | MAY_WRITE | MAY_EXEC)) == 0)
		return 0;
	return -EACCES;
}

4.2 ACL(Access Control List)規則

UGO的規則非常簡潔和實用,但是在使用的過程中人們發現這個分組粒度實在是太粗了,僅僅3種分組UGO。如果一個使用者需要在UGO之外分配一個獨有的許可權,該怎麼操作呢?

在普通許可權中,使用者對檔案只有三種身份,就是屬主、屬組和其他人;每種使用者身份擁有讀(read)、寫(write)和執行(execute)三種許可權。但是在實際工作中,這三種身份實在是不夠用。ACL 許可權就是為了解決這個問題的。在使用 ACL 許可權給使用者 st 陚予許可權時,st 既不是 /project 目錄的屬主,也不是屬組,僅僅賦予使用者 st 針對此目錄的 r-x 許可權。這有些類似於 Windows 系統中分配許可權的方式,單獨指定使用者並單獨分配許可權,這樣就解決了使用者身份不足的問題。ACL是Access Control List(訪問控制列表)的縮寫,不過在Linux系統中,ACL用於設定使用者針對檔案的許可權,而不是在交換路由器中用來控制資料訪問的功能(類似於防火牆)。

ACL主要是以下兩條命令:

# getfacl 檔名			// 檢視ACL許可權
# setfacl 選項 檔名		// 設定ACL許可權
		選項:
		-m:設定 ACL 許可權。如果是給予使用者 ACL 許可權,則使用"u:使用者名稱:許可權"格式賦予;如果是給予組 ACL 許可權,則使用"g:組名:許可權" 格式賦予;
		-x:刪除指定的 ACL 許可權;
		-b:刪除所有的 ACL 許可權;
		-d:設定預設 ACL 許可權。只對目錄生效,指目錄中新建立的檔案擁有此預設許可權;
		-k:刪除預設 ACL 許可權;
		-R:遞迴設定 ACL 許可權。指設定的 ACL 許可權會對目錄下的所有子檔案生效;

例如:我們要求root/project目錄的屬主,許可權是rwxtgroup是此目錄的屬組,tgroup組中擁有班級學員zhangsanlisi,許可權是rwx;其他人的許可權是0。這時試聽學員st來了,她的許可權是r-x。我們來看具體的分配命令。

[[email protected] ~]# useradd zhangsan
[[email protected] ~]# useradd lisi
[[email protected] ~]# useradd st
[[email protected] ~]# groupadd tgroup
// 新增需要試驗的使用者和使用者組,省略設定密碼的過程
[[email protected] ~]# mkdir /project #建立需要分配許可權的目錄
[[email protected] ~]# chown root:tgroup /project/
// 改變/project目錄的屬主和屬組
[[email protected] ~]# chmod 770 /project/
// 指定/project目錄的許可權
[[email protected] ~]# ll -d /project/
drwxrwx--- 2 root tgroup 4096 1月19 04:21 /project/
// 檢視一下許可權,已經符合要求了
// 這時st學員來試聽了,如何給她分配許可權
[[email protected] ~]# setfacl -m u:st:rx /project/
// 給使用者st賦予r-x許可權,使用"u:使用者名稱:許可權" 格式
[[email protected] /]# cd /
[[email protected] /]# ll -d project/
drwxrwx---+ 3 root tgroup 4096 1月19 05:20 project/
// 使用ls-l査詢時會發現,在許可權位後面多了一個"+",表示此目錄擁有ACL許可權
[[email protected] /]# getfacl project
// 檢視/prpject目錄的ACL許可權
#file: project <-檔名
#owner: root <-檔案的屬主
#group: tgroup <-檔案的屬組
user::rwx <-使用者名稱欄是空的,說明是屬主的許可權
user:st:r-x <-使用者st的許可權
group::rwx <-組名欄是空的,說明是屬組的許可權
mask::rwx <-mask許可權
other::--- <-其他人的許可權

// 大家可以看到,st 使用者既不是 /prpject 目錄的屬主、屬組,也不是其他人,我們單獨給 st 使用者分配了 r-x 許可權。這樣分配許可權太方便了,完全不用先辛苦地規劃使用者身份了。

ACL規則是UGO規則的補充,相當於擴充了UGO使用者組,但是操作的行為RWX還是不能擴充。

ACL規則資訊儲存在inode當中。例如:Ext4 擴充套件屬性(xattrs)通常儲存在磁碟上的一個單獨的資料塊中,通過inode.i_file_acl*引用。擴充套件屬性的第一應用是儲存檔案的ACL以及其他安全資料(selinux)。

在上一節acl_permission_check()的分析中我們就已經看到,在User使用者匹配失敗後就去匹配ACL規則,在ACL規則匹配失敗以後才去匹配Group使用者。我們看看具體的過程:

inode_permission() → __inode_permission() → do_inode_permission() → generic_permission() → acl_permission_check() → check_acl():

static int check_acl(struct inode *inode, int mask)
{
#ifdef CONFIG_FS_POSIX_ACL
	struct posix_acl *acl;

	if (mask & MAY_NOT_BLOCK) {
		acl = get_cached_acl_rcu(inode, ACL_TYPE_ACCESS);
	        if (!acl)
	                return -EAGAIN;
		/* no ->get_acl() calls in RCU mode... */
		if (is_uncached_acl(acl))
			return -ECHILD;
	        return posix_acl_permission(inode, acl, mask & ~MAY_NOT_BLOCK);
	}

	/* (1) 獲取到acl資訊 */
	acl = get_acl(inode, ACL_TYPE_ACCESS);
	if (IS_ERR(acl))
		return PTR_ERR(acl);
	if (acl) {
			/* (2) 查詢主體程序在ACL規則中的許可權 */
	        int error = posix_acl_permission(inode, acl, mask);
	        posix_acl_release(acl);
	        return error;
	}
#endif

	return -EAGAIN;
}

4.3 Capability規則

在UGO的使用過程中,人們還發現UGO的另一個弊端,就是行為的劃分也是粗粒度的,僅僅三種行為RWX。這樣會造成許可權過量的情況,如果使用者只需要某項行為的許可權,但是你只能給他root的rwx許可權,那他就擁有了所有其他行為的root rwx許可權。這種擁有所有root許可權的程式是攻擊的首選目標,如果它有漏洞就會造成系統門戶大開。

Linux引入了capabilities機制對root行為進行細粒度的控制,實現按需授權,從而減小系統的安全攻擊面。

例如,把ping的許可權從SUID的root許可權,降為普通使用者+cap_net_admin,cap_net_raw能力,功能上完全一致,但是安全風險大大降低。

  • step 1、因為 ping 命令在執行時需要訪問網路,這就需要獲得 root 許可權,常規的做法是通過 SUID 實現的(和 passwd 命令相同)。紅框中的 s 說明應用程式檔案被設定了 SUID,這樣普通使用者就可以執行這些命令了。
    在這裡插入圖片描述

  • step 2、移除 ping 命令檔案上的 SUID 許可權。在移除 SUID 許可權後,普通使用者在執行 ping 命令時碰到了 “ping: socket: Operation not permitted” 錯誤。
    在這裡插入圖片描述

  • step 3、為 ping 命令檔案新增 capabilities。通過 setcap 命令可以新增執行 ping 命令所需的 capabilities 為 cap_net_admin 和 cap_net_raw,被賦予合適的 capabilities 後,ping 命令又可以正常工作了,相比 SUID 它只具有必要的特權,在最大程度上減小了系統的安全攻擊面。
    在這裡插入圖片描述

Linux 將傳統上與超級使用者 root 關聯的特權劃分為不同的單元,稱為 capabilites。Capabilites 作為執行緒(Linux 並不真正區分程序和執行緒)的屬性存在,每個單元可以獨立啟用和禁用。如此一來,許可權檢查的過程就變成了:在執行特權操作時,如果程序的有效身份不是 root,就去檢查是否具有該特權操作所對應的 capabilites,並以此決定是否可以進行該特權操作。比如要向程序傳送訊號(kill()),就得具有 capability CAP_KILL;如果設定系統時間,就得具有 capability CAP_SYS_TIME。

capabilities能力的全集可以參考capabilities man page

在主體程序的credentials結構體中,定義了本程序的capability能力:

struct cred {
	...
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
	...
}

和SUID機制一樣,在檔案執行的時候,把檔案的capability和當前程序的capability進行綜合:

do_execve() → do_execveat_common()→ prepare_bprm_creds() → security_bprm_set_creds() → cap_bprm_set_creds():

int cap_bprm_set_creds(struct linux_binprm *bprm)
{
	const struct cred *old = current_cred();
	struct cred *new = bprm->cred;
	bool effective = false, has_fcap = false, is_setid;
	int ret;
	kuid_t root_uid;

	if (WARN_ON(!cap_ambient_invariant_ok(old)))
		return -EPERM;

	ret = get_file_caps(bprm, &effective, &has_fcap);
	if (ret < 0)
		return ret;

	/* (1) root uid等於0 */
	root_uid = make_kuid(new->user_ns, 0);

	/* (2) 判斷當前程序是不是root使用者,針對root許可權的capability能力賦值 */
	handle_privileged_root(bprm, has_fcap, &effective, root_uid);

	/* (3) 普通使用者的capability綜合過程,就沒有仔細分析了 */
	/* if we have fs caps, clear dangerous personality flags */
	if (__cap_gained(permitted, new, old))
		bprm->per_clear |= PER_CLEAR_ON_SETID;

	/* Don't let someone trace a set[ug]id/setpcap binary with the revised
	 * credentials unless they have the appropriate permit.
	 *
	 * In addition, if NO_NEW_PRIVS, then ensure we get no new privs.
	 */
	is_setid = __is_setuid(new, old) || __is_setgid(new, old);

	if ((is_setid || __cap_gained(permitted, new, old)) &&
	    ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) ||
	     !ptracer_capable(current, new->user_ns))) {
		/* downgrade; they get no more than they had, and maybe less */
		if (!ns_capable(new->user_ns, CAP_SETUID) ||
		    (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
			new->euid = new->uid;
			new->egid = new->gid;
		}
		new->cap_permitted = cap_intersect(new->cap_permitted,
						   old->cap_permitted);
	}

	new->suid = new->fsuid = new->euid;
	new->sgid = new->fsgid = new->egid;

	/* File caps or setid cancels ambient. */
	if (has_fcap || is_setid)
		cap_clear(new->cap_ambient);

	/*
	 * Now that we've computed pA', update pP' to give:
	 *   pP' = (X & fP) | (pI & fI) | pA'
	 */
	new->cap_permitted = cap_combine(new->cap_permitted, new->cap_ambient);

	/*
	 * Set pE' = (fE ? pP' : pA').  Because pA' is zero if fE is set,
	 * this is the same as pE' = (fE ? pP' : 0) | pA'.
	 */
	if (effective)
		new->cap_effective = new->cap_permitted;
	else
		new->cap_effective = new->cap_ambient;

	if (WARN_ON(!cap_ambient_invariant_ok(new)))
		return -EPERM;

	if (nonroot_raised_pE(new, old, root_uid, has_fcap)) {
		ret = audit_log_bprm_fcaps(bprm, new, old);
		if (ret < 0)
			return ret;
	}

	new->securebits &= ~issecure_mask(SECURE_KEEP_CAPS);

	if (WARN_ON(!cap_ambient_invariant_ok(new)))
		return -EPERM;

	/* Check for privilege-elevated exec. */
	bprm->cap_elevated = 0;
	if (is_setid ||
	    (!__is_real(root_uid, new) &&
	     (effective ||
	      __cap_grew(permitted, ambient, new))))
		bprm->cap_elevated = 1;

	return 0;
}

↓

static void handle_privileged_root(struct linux_binprm *bprm, bool has_fcap,
				   bool *effective, kuid_t root_uid)
{
	const struct cred *old = current_cred();
	struct cred *new = bprm->cred;

	if (!root_privileged())
		return;
	/*
	 * If the legacy file capability is set, then don't set privs
	 * for a setuid root binary run by a non-root user.  Do set it
	 * for a root user just to cause least surprise to an admin.
	 */
	if (has_fcap && __is_suid(root_uid, new)) {
		warn_setuid_and_fcaps_mixed(bprm->filename);
		return;
	}
	/*
	 * To support inheritance of root-permissions and suid-root
	 * executables under compatibility mode, we override the
	 * capability sets for the file.
	 */
	/* (2.1) 如果當前程序的euid或者uid等於root使用者0
			則給程序賦值所有capability (cap_bset成員的值包含了capability全集,所有bit都為1)
	 */
	if (__is_eff(root_uid, new) || __is_real(root_uid, new)) {
		/* pP' = (cap_bset & ~0) | (pI & ~0) */
		new->cap_permitted = cap_combine(old->cap_bset,
						 old->cap_inheritable);
	}
	/*
	 * If only the real uid is 0, we do not set the effective bit.
	 */
	if (__is_eff(root_uid, new))
		*effective = true;
}

在許可權判斷時,如果UGO許可權匹配拒絕,繼續嘗試進行capability的匹配:

inode_permission() → __inode_permission() → do_inode_permission() → generic_permission():

int generic_permission(struct inode *inode, int mask)
{
	int ret;

	/*
	 * Do the basic permission checks.
	 */
	/* (1) 首先進行UGO許可權匹配,如果失敗則嘗試進行capability能力匹配 */
	ret = acl_permission_check(inode, mask);
	if (ret != -EACCES)
		return ret;

	if (S_ISDIR(inode->i_mode)) {
		/* DACs are overridable for directories */
		if (!(mask & MAY_WRITE))
			/* (2.1) 嘗試進行CAP_DAC_READ_SEARCH能力匹配 */
			if (capable_wrt_inode_uidgid(inode,
						     CAP_DAC_READ_SEARCH))
				return 0;
		/* (2.2) 嘗試進行CAP_DAC_OVERRIDE能力匹配 */
		if (capable_wrt_inode_uidgid(inode, CAP_DAC_OVERRIDE))
			return 0;
		return -EACCES;
	}

	/*
	 * Searching includes executable on directories, else just read.
	 */
	mask &= MAY_READ | MAY_WRITE | MAY_EXEC;
	if (mask == MAY_READ)
		/* (2.3) 嘗試進行CAP_DAC_READ_SEARCH能力匹配 */
		if (capable_wrt_inode_uidgid(inode, CAP_DAC_READ_SEARCH))
			return 0;
	/*
	 * Read/write DACs are always overridable.
	 * Executable DACs are overridable when there is
	 * at least one exec bit set.
	 */
	if (!(mask & MAY_EXEC) || (inode->i_mode & S_IXUGO))
		/* (2.4) 嘗試進行CAP_DAC_OVERRIDE能力匹配 */
		if (capable_wrt_inode_uidgid(inode, CAP_DAC_OVERRIDE))
			return 0;

	return -EACCES;
}

4.4 selinux規則

綜合上述的規則來說,在UGO使用的過程中,大家越來越發現了UGO的弊端就是許可權劃分的粒度過粗。為了解決這個問題,ACL和Capability從不同的角度嘗試解決這個問題:

  • ACL嘗試擴充UGO的使用者組。把3組擴充成自定義多組。
  • Capability嘗試擴充RWX行為。把3組行為擴充成多組行為。

在這之後,selinux綜合了ACL和Capability的擴充套件思路,推出了一套完成的擴充使用者組和行為的細粒度許可權管理方案。

後續我們再來詳細分析selinux的實現。

5. 提權漏洞及防護

5.1 核心漏洞提權

利用核心的漏洞,直接修改執行程序的euid的值,用來獲取root許可權。例如:CVE-2017-16995、CVE-2018-1000001、CVE-2016-5195。

這種漏洞的防護分為幾步:

  • step 1、在程式execve()執行時記錄程序的uid/suid/euid/fsuid初始值。在LSM鉤子security_bprm_committed_creds()上記錄:
do_execve() → do_execveat_common() → exec_binprm() → load_elf_binary() → install_exec_creds() → security_bprm_committed_creds()
  • step 2、在更改uid的合法途徑,這幾個系統呼叫(setreuid()/setuid()/setresuid()/setfsuid())中判斷uid有沒有被非法修改,合法則記錄改動。在LSM鉤子security_task_fix_setuid()上記錄:
setreuid()/setuid()/setresuid()/setfsuid() → security_task_fix_setuid()
  • step 3、在各個關鍵路徑上,比較當前的uid值和初始值,如果合法途徑不可能做到,則為非法提權:
有兩條簡單的規則來進行判斷:
1、如果execve()初始的uid或者euid的值為0則表明程序有root許可權,則當前uid/suid/euid/fsuid為任意值都是合法的,因為root使用者有這種能力。
2、如果execve()初始的uid或者euid的值不為0則表明程序沒有root許可權,則當前的uid/suid/euid/fsuid值只能為初始值uid或euid的其中一種。因為合法途徑setreuid()/setuid()/setresuid()/setfsuid()只能在有限範圍內改動。

5.2 sudo漏洞提權

CVE-2019-14287,對於sudoer中配置的非root許可權使用者,使用"sudo #-1"漏洞獲得了root許可權。

該漏洞的防護,可以在setresuid()系統呼叫中做pre鉤子,如果普通使用者傳入-1引數則進行攔截。

參考資料:

1、Linux 使用者身份與程序許可權
2、Linux 特殊許可權 SUID,SGID,SBIT
3、linux核心open過程的許可權管理
4、Linux Capabilities 簡介
5、linux許可權檢查機制
6、inode許可權檢查
7、linux核心setuid分析
8、setuid seteuid setreuid 三個函式講解
9、Linux Capabilities 入門:讓普通程序獲得 root 的洪荒之力
10、UID, EUID, SUID, FSUID