简介
这是名称空间的漏洞,文章先介绍user namespaces的简单只是,然后从补丁入手,分析源码,找到漏洞出现的原因。因为对这块的源码不是那么熟悉,所以着重描述源码分析的部分,其他可以参考末尾的链接
本文出现的代码都基于linux-4.15.4
namespace
linux中有实现名称空间,用来隔离不同的资源,实现原理就是将原本是全局的变量放到各个namespaces之中去。
user namespaces
linux中user namespaces的man说明:overview of Linux user namespaces
user namespaces是linux中用来隔离与安全相关的标志符和属性的名称空间,主要包括UID、GID、根目录、秘钥和capacity。在名称空间中,user namespaces可以实现进程和名称空间中有不同的uid和gid,比如名称空间中可以有root权限而在真实系统中没有。
在上面的main说明中可以看到两个proc文件: /proc/<pid>/uid_map 和 /proc/<pid>/gid_map。向这个文件写入值可以用来将系统中的uid或gid映射到namespaces中去。其中:
- 第一个字段ID-inside-ns表示在容器显示的UID或GID,
- 第二个字段ID-outside-ns表示容器外映射的真实的UID或GID。
- 第三个字段表示映射的范围,一般填1,表示一一对应。
比如,把真实的uid=1000映射成容器内的uid=0
$
cat
/proc/2465/uid_map
0 1000 1
- 写这两个文件的进程需要这个namespace中的CAP_SETUID (CAP_SETGID)权限(可参看Capabilities)
- 写入的进程必须是此user namespace的父或子的user namespace进程。
- 另外需要满如下条件之一:1)父进程将effective uid/gid映射到子进程的user namespace中,2)父进程如果有CAP_SETUID/CAP_SETGID权限,那么它将可以映射到父进程中的任一uid/gid。
补丁分析
这个漏洞的修补在这里,问题出在kernel/user_namespace.c中的map_write之中:
diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c index e5222b5..923414a 100644 --- a/kernel/user_namespace.c +++ b/kernel/user_namespace.c @@ -974,10 +974,6 @@ static ssize_t map_write(struct file *file, const char __user *buf, if (!new_idmap_permitted(file, ns, cap_setid, &new_map)) goto out; - ret = sort_idmaps(&new_map); - if (ret < 0) - goto out; - ret = -EPERM; /* Map the lower ids from the parent user namespace to the * kernel global id space. @@ -1004,6 +1000,14 @@ static ssize_t map_write(struct file *file, const char __user *buf, e->lower_first = lower_first; } + /* + * If we want to use binary search for lookup, this clones the extent + * array and sorts both copies. + */ + ret = sort_idmaps(&new_map); + if (ret < 0) + goto out; + /* Install the map */ if (new_map.nr_extents <= UID_GID_MAP_MAX_BASE_EXTENTS) { memcpy(map->extent, new_map.extent,
只是调换了几行代码的位置,先不着急,分析一下这个函数。
在understand中,找出这个函数的调用流程图:
然后去看看调用map_write的函数proc_uid_map_write,函数原型:
ssize_t proc_uid_map_write(struct file *file, const char __user *buf, size_t size, loff_t *ppos)
参数很像文件描述符的写操作函数,在寻找源码中和该函数相关的操作,发现在fs/proc/base.c之中有这样一个结构用到了proc_uid_map_write:
static const struct file_operations proc_uid_map_operations = { .open = proc_uid_map_open, .write = proc_uid_map_write, .read = seq_read, .llseek = seq_lseek, .release = proc_id_map_release, };
确认是文件的操作,接着在这个文件中,还有下面的代码
REG("uid_map", S_IRUGO|S_IWUSR, proc_uid_map_operations)
所以,推测这就是 /proc/<pid>/uid_map文件写操作的实现
源代码分析
接着回到漏洞源代码,开始分析,先从proc_uid_map_write函数开始,也就是文件写操作的第一个函数
ssize_t proc_uid_map_write(struct file *file, const char __user *buf, size_t size, loff_t *ppos) { struct seq_file *seq = file->private_data; struct user_namespace *ns = seq->private; struct user_namespace *seq_ns = seq_user_ns(seq); if (!ns->parent) return -EPERM; if ((seq_ns != ns) && (seq_ns != ns->parent)) return -EPERM; return map_write(file, buf, size, ppos, CAP_SETUID, &ns->uid_map, &ns->parent->uid_map); }
看到只是做了两个检查,然后调用了map_write函数,而map_write函数的后两个参数分别为名称空间的uid_map和父名称空间的uid_map(由名称空间的知识可以知道,名称空间的新建是需要clone处新进程,传入特定参数来创建新的名称空间)
看看这些个map的定义,看到uid_gid_extent的定义正好是符合 /proc/<pid>/uid_map等的文件格式,而且在user_naspace的man手册中写道,这些文件一次能写入多个值,在Linux中4.14之前,这个极限被(任意地)设为5行。从Linux 4.15,限制是340行。这样下面这两个结构就不难理解了,当数据行数在5之内的时候,直接写在extent里面,当大于5的时候,放在forward指向的位置:
#define UID_GID_MAP_MAX_BASE_EXTENTS 5
#define UID_GID_MAP_MAX_EXTENTS 340
struct uid_gid_extent { u32 first; u32 lower_first; u32 count; }; struct uid_gid_map { /* 64 bytes -- 1 cache line */ u32 nr_extents; union { struct uid_gid_extent extent[UID_GID_MAP_MAX_BASE_EXTENTS]; struct { struct uid_gid_extent *forward; struct uid_gid_extent *reverse; }; }; };
看map_write的源码的第一部分,比较好理解了,capacity相关的含义对照man手册中的解释,除去几个参数判断的位置,比较重要的就是kbuf这块内存,调用了memdup_user_nul函数先在内核中分配了一块内存,然后将用户态写入的数据复制到内核之中,最后这块内存由kbuf指向
struct seq_file *seq = file->private_data; struct user_namespace *ns = seq->private; struct uid_gid_map new_map; unsigned idx; struct uid_gid_extent extent; char *kbuf = NULL, *pos, *next_line; ssize_t ret = -EINVAL; memset(&new_map, 0, sizeof(struct uid_gid_map)); ret = -EPERM; /* Only allow one successful write to the map */ if (map->nr_extents != 0) goto out; /* * Adjusting namespace settings requires capabilities on the target. */ if (cap_valid(cap_setid) && !file_ns_capable(file, ns, CAP_SYS_ADMIN)) goto out; /* Only allow < page size writes at the beginning of the file */ ret = -EINVAL; if ((*ppos != 0) || (count >= PAGE_SIZE)) goto out; /* Slurp in the user data */ //从用户空间复制写入的数据到kbuf kbuf = memdup_user_nul(buf, count); if (IS_ERR(kbuf)) { ret = PTR_ERR(kbuf); kbuf = NULL; goto out; } /* Parse the user data */ ret = -EINVAL; pos = kbuf;
接着看,有一个大循环,不断的按行解析出用户输入数据,存放进extent中,然后调用了两个比较关键的函数,mappings_overlap和insert_extent,mappings_overlap用来检测uid_gid_extent和uid_gid_map有没有重叠的部分,有返回true,insert_extent用来向uid_gid_map中插入一个uid_gid_extent。
for (; pos; pos = next_line) { /* Find the end of line and ensure I don't look past it */ next_line = strchr(pos, ' '); if (next_line) { *next_line = '