namespaces之 User Namespace机制

来源:互联网 发布:淘宝盗用别人3c认证 编辑:程序博客网 时间:2024/06/05 18:46

USER Namespaces

主要是对用户和用户组进行隔离。它是从Linux 3.8内核才慢慢支持的,现在有些linux发行版还不支持user namespaces(主要是因为还不完全成熟,处于对安全的担心,在编译内核时并未开启USER Namespaces,Centos 7就不支持,后边的测试基于Ubuntu 14.04). 
User namespaces允许每个命名空间中User ID和 group ID的映射。在容器上下文环境中,这就意味着User和User Group 在容器中的操作可以拥有特权,在容器外边没有。换句话说,一个进程在user namespace中操作的权限设置不同于在宿主系统中的权限设置。user namespace一个特定的目标就是允许一个进程在容器中的操作拥有root特权,与此同时,在托管容器的宿主机上是一个无特权的正常进程。 
为了支持这种行为,每个进程的UID实际上有两种值:容器中的是一种,容器外是另一种。Group ID和进程UID相似。这种对偶性通过维护每个 User namespace用户ID的映射完成的:每个用户命令空间有一张表,这张表中记录着宿主机中用户ID对应namespace 的用户ID。这种映射是通过读写/proc/PID/uid_map(/proc/PID/gid_map gid映射文件)伪文件来设置,其中PID是user namespace这个进程的进程ID。例如,宿主机中UID为1000的用户被映射到namespace中可能会为0,用户ID为1000的进程在宿主机中是一个常态的用户(普通用户),但是它在namespace中将拥有root特权。如果在宿主机系统中没有为特定用户ID提供映射,在user namespace中,这个user ID会映射到/proc/sys/kernel/overflowuid(gid文件为overflowgid)文件提供的值(这个文件中默认的值是65534)。

测试使,只需要在clone函数中添加CLONE_NEWUSER标志即可:

clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, NULL);

编译后,普通用户(ty)就可以执行,先查看该用户ID:

ty@ubuntu:~$ id
uid=1000(ty) gid=1000(ty) groups=1000(ty)

使用clone函数创建进程,激活user namespace:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
 
#define STACK_SIZE (1024 * 1024)
 
static char child_stack[STACK_SIZE];
 
int child_main(void* arg) {
char path[256];
printf("Child inside Namespace\n");
printf("euid=%d, egid=%d, pid=%d\n",geteuid(),getegid(), getpid());
execl("/bin/bash", "bash", NULL);
printf("execv error\n");
return -1;
}
 
int main()
{
printf("Parent outside Namespace \n");
int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, NULL);
if(child_pid == -1)
perror("clone");
waitpid(child_pid, NULL, 0);
printf("child exit...\n");
 
exit(EXIT_SUCCESS);
}

再执行namespace_user文件 :

ty@ubuntu:~/program/git/Linux-LXC/namespace$ gcc namespace-user.c -o namespace-user
ty@ubuntu:~/program/git/Linux-LXC/namespace$ ./namespace-user
Parent outside Namespace
Child inside Namespace
nobody@ubuntu:~/program/git/Linux-LXC/namespace$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

可以发现ty用户映射到user namespace中为nobody,ID为65534,使用/proc/sys/kernel/overflowuid中默认值;下边测试是否拥有特权: 
Inside namespace:

nobody@ubuntu:~/program/git/Linux-LXC/namespace$ ls -l /etc/passwd
-rw-r--r-- 1 nobody nogroup 1979 4 24 22:00 /etc/passwd

outside namespace:

ty@ubuntu:~$ ls -l /etc/passwd
-rw-r--r-- 1 root root 1979 4 24 23:21 /etc/passwd

如上显示,在namespace中/etc/passwd用户拥有者和用户组都已成当前登录用户nobody,但是仍不能对齐进行修改。 
因为当前只是隔离了用户和用户组,系统的其他资源仍是和宿主机共用,所以不能修改。在User Namespace中映射的用户和用户组只能对被隔离的资源有特权,没有被隔离的资源虽然发现该用户已经成为其拥有者和组,但操作仍是依据于宿主机中的用户权限。

uid-gid映射

上边使用的是系统默认的映射的UID和GID值,即就是/proc/sys/kernel/overflowuid(overflowgid)文件中的默认值。接下来,自己设置UID和GID的映射。 
通过上边的学习,知道/proc/PID/uid_map和/proc/PID/gid_map文件中是关于映射设置的信息,所以通过修改这两个文件达到映射工作,这两个文件是通过三组数字实现映射设置: 
first-ns-id first-target-id count 
first-ns-id:是给定进程的namespace中第一个合法的ID,常被设置为 0 ,即就是root的ID,这个ID在user namespace中是合法的; 
first-target-id:是父进程(宿主机)命令空间中的真实ID,first-ns-id会被映射到宿主机中的first-target-id; 
count:表示映射的范围,为 1 表示只映射一个,大于1表示按顺序映射; 
docker容器中一般如下,表示把namespace内部从0 开始的 ID 映射到外部从0开始的 ID,最大范围为4294967295 
0 0 4294967295

测试CAP_SETUID权限

对于UID和GID的映射,需要特权操作,这个执行的进程必须要有CAP_SETUID权限(属于linux内核能力的概念,实现访问控制。这块只是单纯的使用了,后边详细学习下)可用。 
引用头文件sys/capability.h(没有该头文件,需要安装libcap-dev包),使用如下命令获取并输出权限查看子进程是否拥有cap_setuid和cap_setgid权限:

caps = cap_get_proc();
printf("capabilities:%s\n",cap_to_text(caps,NULL));

输出如下即就是拥有该权限:

capabilities:= cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,...37+ep

添加UID、GID映射

上一步已经测试子进程拥有cap+和cap_setgid权限,直接在子进程中向文件中添加映射信息,代码如下:

void set_uid_map(pid_t pid, int first_ns_id, int first_target_id, int count){ //设置UID映射
char path[256];
sprintf(path,"/proc/%d/uid_map", pid);
FILE *uid_map = fopen(path, "w");
fprintf(uid_map, "%d %d %d",first_ns_id, first_target_id, count);
fclose(uid_map);
}
 
void set_gid_map(pid_t pid, int first_ns_id, int first_target_id, int count){ //设置GID映射
char path[256];
int n;
 
sprintf(path,"/proc/%d/gid_map", pid);
FILE *gid_map = fopen(path, "w");
n= fprintf(gid_map, "%d %d %d",first_ns_id, first_target_id, count);
printf("fprintf return = %d\n",n);
fclose(gid_map);
}
 
int child_main(void* arg) {
char path[256];
cap_t caps;
int fd;
char buf[256];
 
caps = cap_get_proc();
printf("capabilities:%s\n",cap_to_text(caps,NULL));
 
set_uid_map(getpid(), 0, 1000, 1);
set_gid_map(getpid(), 0, 1000, 1);
 
printf("euid=%d, egid=%d, pid=%d\n",geteuid(),getegid(), getpid());
execl("/bin/bash", "bash", NULL);
printf("execv error\n");
return -1;
}

运行如下:

ty@ubuntu:~namespace$ gcc namespace-user.c -o namespace-user -lcap
ty@ubuntu:~namespace$ ./namespace-user
capabilities:= cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,...37+ep
Child first_ns Namespace
fprintf return = 8
euid=0, egid=65534, pid=13910
root@ubuntu:~namespace# id
uid=0(root) gid=65534(nogroup) groups=0(root),65534(nogroup)

根据输出发现,user namespace中的用户已经映射为root(UID=0),但是gid还是默认值,没有成功。 
问题:测试发现set_gid_map函数中fprintf语句确实写入了8个字符(0 1000 1),但是打开gid_map仍未空,还不知道为什么,正在调试,有知道了原因的可以提点我下,谢谢


0 0