简单了解一下Linux内核这座富丽堂皇的宫殿的又一块砖

前言

在本系列的上一篇博文里,我已经介绍了进程/线程的基本含义以及一些相关数据结构,现在我们来看看Linux中进程的管理。

进程链表

Linux内核定义了一个list_head结构,数据结构定义

struct list_head {
	struct list_head *next;
	struct list_head *prev;
};

字段next 和 prev 分别表示通用双向链表向前和向后的指针元素!list_head字段的指针中存放的是另一个list_head字段的元素,而不是本身的数据结构地址。如图
Linux操作系统--进程/线程(2)_进程
在我们上一篇博客介绍到的进程描述符(task_struct)也有这个结构体,称为进程链表。进程链表是一个双向循环链表,它把所有进程的描述符链接起来。每个task_struct结构都包含一个list_head类型的字段tasks,这个结构的prev和next分别指向前面和后面的task_struct元素。
Linux操作系统--进程/线程(2)_进程_02
这个链表是一个循环的双向链表,开始的时候只有init_task这一个进程,它是内核的第一个进程,它的初始化是通过静态分配内存,"手动"(其它的进程初始化都是通过动态分配内存初始化的)进行的,每新建一个进程,就通过SET_LINKS宏将该进程的task_struct结构加入到这条双向链表中,不过要注意的是如果一个进程新建一个线程(不包括主线程),也就是轻量级进程,它是不会加入该链表的。通过宏for_each_process可以从init_task开始遍历所有的进程。

#define for_each_task(p)
for (p = &init_task ; (p = p->next_task) != &init_task ; )
可运行队列(runqueue)

当内核寻找一个新进程在CPU上运行时,必须只考虑可运行进程(即处在TASK_RUNNING状态的进程)。把可运行状态的进程组成一个双向循环链表,也叫可运行队列(runqueue)。
在task_struct结构中定义了两个指针。

struct task_struct *next_run, *prev_run;

由正在运行或是可以运行的,其进程状态均为TASK_RUNNING的进程所组成的一个双向循环链表,即run_queue就绪队列。该链表的前后向指针用next_run和prev_run,链表的头和尾都是init_task(即0号
进程)。
但是,为了实现在固定的时间内选出“最佳”的可运行程序,内核将可运行进程的优先级划分为0-139,并为此建立了140个可运行进程链表,用以组织处于TASK_RUNNING状态的进程,每个进程优先权对应一个不同的链表
linux内核定义了一个prio_array_t类型的结构体来管理这140个链表。每个可运行的进程都在这140个链表中的一个,通过进程描述符结构中的run_list来实现,它也是一个list_head类型。enqueue_task是把进程描述符插入到某个可运行链表中,dequeue_task则从某个可运行链表中删除该进程描述符。TASK_RUNNING状态的prio_array_t类型的结构体是runqueue结构的arrays[1]成员。
Linux操作系统--进程/线程(2)_线程_03
Linux操作系统--进程/线程(2)_线程_04

pidhash链表

为了通过pid找到进程的描述符,如果直接遍历进程间互联的链表来查找进程id为pid的进程描述符显然是低效的,所以为了更为高效的查找,linux内核使用了4个hash散列表来加快查找,之所以使用4个散列表,是为了能根据不同的pid类型来查找进程描述符,它们分别是进程的pid,线程组领头进程的pid,进程组领头进程的pid,会话领头进程的pid。每个类型的散列表中是通过宏pid_hashfn(x)来进行散列值的计算的。每个进程都可能同时处于这是个散列表中,所以在进程描述符中有一个类型为pid结构的pids成员,通过它可以将进程加入散列表中,pid结构中包含解决散列冲突的pid_chain成员,它是hlist_node类型的,还有一个是将相同pid链起来的pid_list,它是list_head类型。
Linux操作系统--进程/线程(2)_Linux操作系统_05

struct pid_link {
    int nr;  // pid的数值
    struct hlist_node pid_chain;
    struct list_head pid_list;
}

struct task_struct {
    …
    struct pid_link pids[4];
    …
}

Linux操作系统--进程/线程(2)_Linux操作系统_06

Linux 进程安全上下文 struct cred

内核2.6,定义一个新的 struct task_security_struct,然后挂接到task_struct的void *security指针上,但是,内核3.x 在task_struct找不到security成员了,原来是将安全相关的信息剥离到一个叫做 cred 的结构体中,由cred负责保存进程安全上下文

The security context of a task
   95  *
   96  * The parts of the context break down into two categories:
   97  *
   98  *  (1) The objective context of a task.  These parts are used when some other
   99  *      task is attempting to affect this one.
  100  *
  101  *  (2) The subjective context.  These details are used when the task is acting
  102  *      upon another object, be that a file, a task, a key or whatever.
  103  *
  104  * Note that some members of this structure belong to both categories - the
  105  * LSM security pointer for instance.
  106  *
  107  * A task has two security pointers.  task->real_cred points to the objective
  108  * context that defines that task's actual details.  The objective part of this
  109  * context is used whenever that task is acted upon.
  110  *
  111  * task->cred points to the subjective context that defines the details of how
  112  * that task is going to act upon another object.  This may be overridden
  113  * temporarily to point to another security context, but normally points to the
  114  * same context as task->real_cred.
  115  */
  116 struct cred {
  117         atomic_t        usage;
  118 #ifdef CONFIG_DEBUG_CREDENTIALS
  119         atomic_t        subscribers;    /* number of processes subscribed */
  120         void            *put_addr;
  121         unsigned        magic;
  122 #define CRED_MAGIC      0x43736564
  123 #define CRED_MAGIC_DEAD 0x44656144
  124 #endif
  125         uid_t           uid;            /* real UID of the task */
  126         gid_t           gid;            /* real GID of the task */
  127         uid_t           suid;           /* saved UID of the task */
  128         gid_t           sgid;           /* saved GID of the task */
  129         uid_t           euid;           /* effective UID of the task */
  130         gid_t           egid;           /* effective GID of the task */
  131         uid_t           fsuid;          /* UID for VFS ops */
  132         gid_t           fsgid;          /* GID for VFS ops */
  133         unsigned        securebits;     /* SUID-less security management */
  134         kernel_cap_t    cap_inheritable; /* caps our children can inherit */
  135         kernel_cap_t    cap_permitted;  /* caps we're permitted */
  136         kernel_cap_t    cap_effective;  /* caps we can actually use */
  137         kernel_cap_t    cap_bset;       /* capability bounding set */
  138 #ifdef CONFIG_KEYS
  139         unsigned char   jit_keyring;    /* default keyring to attach requested
  140                                          * keys to */
  141         struct key      *thread_keyring; /* keyring private to this thread */
  142         struct key      *request_key_auth; /* assumed request_key authority */
  143         struct thread_group_cred *tgcred; /* thread-group shared credentials */
  144 #endif
  145 #ifdef CONFIG_SECURITY
  146         void            *security;      /* subjective LSM security */
  147 #endif
  148         struct user_struct *user;       /* real user ID subscription */
  149         struct user_namespace *user_ns; /* cached user->user_ns */
  150         struct group_info *group_info;  /* supplementary groups for euid/fsgid */
  151         struct rcu_head rcu;            /* RCU deletion hook */
  152 };

正如uid,euid的关系一样,task_struct也有两种身份cred

struct task_struct{
 ...
 /* process credentials */
 const struct cred __rcu *real_cred; /* objective and real subjective task credentials (COW) */
 const struct cred __rcu *cred;  /* effective (overridable) subjective task credentials (COW) */
 ...
 }

这里详细说明以下这个安全上下文的作用。
linux系统中,一个对象操作另一个对象时通常要做安全性检查。如一个进程操作一个文件,要检查进程是否有权限操作该文件。
linux内核中,credential机制的引入,正是对象间访问所需权限的抽象;主体提供自己权限的证书,客体提供访问自己所需权限的证书,根据主客体提供的证书及操作做安全性检查。
证书管理术语:
客体:指用户空间程序直接可以操作的系统对象,如进程、文件、消息队列、信号量、共享内存等;每个客体都有一组凭证,每种客体有不同的凭证集
客体所有者:客体凭证集有一部分表示客体所有者;如文件中uid表示文件的所有者
主体:操作客体的对象;除进程外大多数系统对象都不是主体,但在特殊环境下某些对象是主体,如文件在设置F_SETOWN后可以发送SIGIO信号到进程,这时文件就是主体,进程就是客体
行为:主体怎样操作客体,如读写执行文件等
客体上下文:客体被访问时所需权限凭证集
主体上下文:主体的权限凭证集
规则:主体操作客体时,用于安全检查
当主体操作客体时,根据主体上下文、客体上下文、操作来做安全计算,查找规则看主体是否有权限操作客体。
进程描述符中cred和real_cred字段分别指向主体与客体的证书

  • usage:表于证书引用管理
  • uid:实际用户id(real UID of the task,进程真正的uid,即为创建该进程的用户的uid)
  • gid:实际用户组id
  • suid:保存的用户uid(saved UID of the task,保留的UID,例如,当一个特权进程需要临时降低其权限时,将其euid更改为非特权的UID,然后将原来的EUID保存到SUID,当需要恢复权限时,将EUID改为SUID中保存的UID即可)
  • sgid;保存的用户组gid
  • euid:真正有效的用户id(effective UID of the task,有效的UID,用于进程访问资源时的访问检查,大多数情况下,EUID是同于UID的,但是也可以不同,或者说动态获取的ID)
  • egid:真正有效的用户组id
  • securebits:安全管理标识;用来控制凭证的操作与继承
  • cap_inheritable:execve时可以继承的权限
  • cap_permitted:可以(通过capset)赋予cap_effective的权限
  • cap_effective:进程实际使用的权限
  • cap_bset:主要用于uid=0或euid=0时,execve可以继承的权限,cap_permitted=cap_inheritable+cap_bset,cap_effective=cap_permitted。可以将cap_bset中的权限通过调用capset赋给cap_inheritable
  • user:主要表示用户信息,如用户进程数、打开文件数等
  • rcu:RCU删除用
struct cred在kernel pwn的利用

注:笔者还没有学习内核pwn的相关知识,所以这里只是简单介绍一下cred这个结构体在内核pwn中提权的作用,没有具体例子说明
可以通过执行commit_creds(prepare_kernel_cred(0))来获得root权限(root的uid、gid均为0)
源码如下:

/* /kernel/cred.c */
/**
 * prepare_kernel_cred - Prepare a set of credentials for a kernel service
 * @daemon: A userspace daemon to be used as a reference
 *
 * Prepare a set of credentials for a kernel service.  This can then be used to
 * override a task's own credentials so that work can be done on behalf of that
 * task that requires a different subjective context.
 *
 * @daemon is used to provide a base for the security record, but can be NULL.
 * If @daemon is supplied, then the security data will be derived from that;
 * otherwise they'll be set to 0 and no groups, full capabilities and no keys.
 *
 * The caller may change these controls afterwards if desired.
 *
 * Returns the new credentials or NULL if out of memory.
 *
 * Does not take, and does not return holding current->cred_replace_mutex.
 */
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
	const struct cred *old;
	struct cred *new;

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;

	kdebug("prepare_kernel_cred() alloc %p", new);

	if (daemon)
		old = get_task_cred(daemon);
	else
		old = get_cred(&init_cred);

	validate_creds(old);

	*new = *old;
	new->non_rcu = 0;
	atomic_set(&new->usage, 1);
	set_cred_subscribers(new, 0);
	get_uid(new->user);
	get_user_ns(new->user_ns);
	get_group_info(new->group_info);

#ifdef CONFIG_KEYS
	new->session_keyring = NULL;
	new->process_keyring = NULL;
	new->thread_keyring = NULL;
	new->request_key_auth = NULL;
	new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING;
#endif

#ifdef CONFIG_SECURITY
	new->security = NULL;
#endif
	if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
		goto error;

	put_cred(old);
	validate_creds(new);
	return new;

error:
	put_cred(new);
	put_cred(old);
	return NULL;
}
EXPORT_SYMBOL(prepare_kernel_cred);

prepare_kernel_cred()
根据源码注释中的描述,这个函数返回一个cred结构体,可以用于代替进程原来的cred以便能够完成需要不同subjective context的任务。如果提供了参数@daemon,那么security data将来源于此,而这个参数也可为空,然后内容字段会被设置成0(uid/gid都是0,就是root权限咯?)

/* /kernel/cred.c */
/**
 * commit_creds - Install new credentials upon the current task
 * @new: The credentials to be assigned
 *
 * Install a new set of credentials to the current task, using RCU to replace
 * the old set.  Both the objective and the subjective credentials pointers are
 * updated.  This function may not be called if the subjective credentials are
 * in an overridden state.
 *
 * This function eats the caller's reference to the new credentials.
 *
 * Always returns 0 thus allowing this function to be tail-called at the end
 * of, say, sys_setgid().
 */
int commit_creds(struct cred *new)
{
	struct task_struct *task = current;
	const struct cred *old = task->real_cred;

	kdebug("commit_creds(%p{%d,%d})", new,
	       atomic_read(&new->usage),
	       read_cred_subscribers(new));

	BUG_ON(task->cred != old);
#ifdef CONFIG_DEBUG_CREDENTIALS
	BUG_ON(read_cred_subscribers(old) < 2);
	validate_creds(old);
	validate_creds(new);
#endif
	BUG_ON(atomic_read(&new->usage) < 1);

	get_cred(new); /* we will require a ref for the subj creds too */

	/* dumpability changes */
	if (!uid_eq(old->euid, new->euid) ||
	    !gid_eq(old->egid, new->egid) ||
	    !uid_eq(old->fsuid, new->fsuid) ||
	    !gid_eq(old->fsgid, new->fsgid) ||
	    !cred_cap_issubset(old, new)) {
		if (task->mm)
			set_dumpable(task->mm, suid_dumpable);
		task->pdeath_signal = 0;
		/*
		 * If a task drops privileges and becomes nondumpable,
		 * the dumpability change must become visible before
		 * the credential change; otherwise, a __ptrace_may_access()
		 * racing with this change may be able to attach to a task it
		 * shouldn't be able to attach to (as if the task had dropped
		 * privileges without becoming nondumpable).
		 * Pairs with a read barrier in __ptrace_may_access().
		 */
		smp_wmb();
	}

	/* alter the thread keyring */
	if (!uid_eq(new->fsuid, old->fsuid))
		key_fsuid_changed(task);
	if (!gid_eq(new->fsgid, old->fsgid))
		key_fsgid_changed(task);

	/* do it
	 * RLIMIT_NPROC limits on user->processes have already been checked
	 * in set_user().
	 */
	alter_cred_subscribers(new, 2);
	if (new->user != old->user)
		atomic_inc(&new->user->processes);
	rcu_assign_pointer(task->real_cred, new);
	rcu_assign_pointer(task->cred, new);
	if (new->user != old->user)
		atomic_dec(&old->user->processes);
	alter_cred_subscribers(old, -2);

	/* send notifications */
	if (!uid_eq(new->uid,   old->uid)  ||
	    !uid_eq(new->euid,  old->euid) ||
	    !uid_eq(new->suid,  old->suid) ||
	    !uid_eq(new->fsuid, old->fsuid))
		proc_id_connector(task, PROC_EVENT_UID);

	if (!gid_eq(new->gid,   old->gid)  ||
	    !gid_eq(new->egid,  old->egid) ||
	    !gid_eq(new->sgid,  old->sgid) ||
	    !gid_eq(new->fsgid, old->fsgid))
		proc_id_connector(task, PROC_EVENT_GID);

	/* release the old obj and subj refs both */
	put_cred(old);
	put_cred(old);
	return 0;
}
EXPORT_SYMBOL(commit_creds);

根据源码注释的描述,这个函数会将当前进程的real_cred和cred都设置成一组新的cred。
综上,通过prepare_kernel_cred(0)获得一个root的cred,然后再用commit_creds()将其安装到当前进程,即commit_creds(prepare_kernel_cred(0)),这样就可以提权啦!