纠结了很久,决定还是开启这个模块的学习,在这个路口迷茫了很久了,一直没有决定好走向何处。虽然有可能会走错路,可能会学习很多看起来没用的东西,但希望Kernel的学习能够带我刚开始接触PWN时的乐趣。将会主要借助CTF wiki和《从0到1》
内核会通过进程的 task_struct
结构体中的 cred
指针来索引 cred
结构体,然后根据 cred
的内容来判断一个进程拥有的权限,如果 cred
结构体成员中的 uid-fsgid 都为 0,那一般就会认为进程具有 root 权限。
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 */
...
}
因此,很自然就能想到两种思路来进行提权:
cred
结构体的内容task_struct
结构体中的 cred 指针指向一个满足要求的 credcred 结构体的最前面记录了各种 id 信息,对于一个普通的进程而言,uid-fsgid
都是执行进程的用户的身份。因此我们可以通过扫描内存来定位 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 */
...
}
在实际定位的过程中,我们可能会发现很多满足要求的 cred,这主要是因为 cred 结构体可能会被拷贝、释放。一个很直观的想法是在定位的过程中,利用 usage 不为 0 来筛除掉一些 cred,但仍然会发现一些 usage 为 0 的 cred。这是因为 cred 从 usage 为 0, 到释放有一定的时间。此外,cred 是使用 rcu 延迟释放的。
进程的 task_struct
结构体中会存放指向 cred 的指针,因此我们可以
task_struct
结构体的地址cred
指针存储的地址cred
具体的地址comm 用来标记可执行文件的名字,位于进程的 task_struct
结构体中。我们可以发现 comm 其实在 cred 的正下方,所以我们也可以先定位 comm ,然后定位 cred 的地址。
/* Process credentials: */
/* Tracer's credentials at attach: */
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;
#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif
/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];
然而,在进程名字并不特殊的情况下,内核中可能会有多个同样的字符串,这会影响搜索的正确性与效率。因此,我们可以使用 prctl 设置进程的 comm 为一个特殊的字符串,然后再开始定位 comm。
在这种方法下,我们可以直接将 cred 中的 uid-fsgid
都修改为 0。当然修改的方式有很多种,比如说
如果我们在进程初始化时能控制 cred 结构体的位置,并且我们可以在初始化后修改该部分的内容,那么我们就可以很容易地达到提权的目的。这里给出一个典型的例子
非常有意思的是,在这个过程中,我们不需要任何的信息泄露。
我们还可以使用 commit_creds(prepare_kernel_cred(0))
来进行提权,该方式会自动生成一个合法的 cred,并定位当前线程的 task_struct
的位置,然后修改它的 cred 为新的 cred。该方式比较适用于控制程序执行流后使用。