注:本文绝大部分内容来自 Linux Namespaces实践part 5,原文系列文章详细描述了Linux Namespace相关内容,英语过关的建议阅读原文,本文内容主要用来记录学习内容,如有不当之处还请评论区指正。
User Namespace
隔离了安全相关的属性,即用户ID、组ID、根目录、keyrings以及capabilities。一个进程的用户ID和组ID在User Namespace
内外可以是不同的。特别地,一个进程可以在User Namespace
外有非零的用户ID(普通用户),而在User Namespace
内部有为零的用户ID(root用户)。
在阐述内容之前,先列举几个作者遇到的问题:
-
无
sys/capability.h
头文件,cap_t
,cap_to_text()
等Ubuntu上需要通过
sudo apt-get install libcap-dev
安装库
连接时需加上-lcap
选项:g++ -o a.sh test.cpp -lcap
-
userns_child_exec.c
遇到write: operation not permitted
问题系列文章写于2013年,之后Linux 3.19更新了
gid_map
文件的访问规则,可以参考User Namespace手册,文中与样例Example的代码注释都阐述了相关问题及解决方案
创建User Namespace
通过传递CLONE_NEWUSER
标志给clone()
或unshare()
函数可以创建一个User Namespace
。Linux 3.8之后,创建User Namespace
不在需要root权限。值得一提的是,Win10的wsl目前不支持CLONE_NEWUSER
标志,即无法创建User Namespace
,在即将到来的wsl2(目前预览版可以尝鲜)将内置完整的Linux内核,可以提供该功能。
一个栗子
本例通过demo_userns.c展示了没有特权的进程在新User Namespace
中可以有特权。代码如下:
int childFunc(void *arg) {
cap_t caps;
for (;;) {
printf("eUID = %ld; eGID = %ld; \n", (long)geteuid(), (long)getegid());
caps = cap_get_proc();
printf("capabilityies: %s\n", cap_to_text(caps, NULL));
if (arg == NULL)
break;
sleep(5);
}
return 0;
}
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
int main(int argc, char *argv[]) {
pid_t pid;
pid = clone(childFunc, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, argv[1]);
if (pid == -1)
errExit("clone");
if (waitpid(pid, NULL, 0) == -1)
errExit("waitpid");
return 0;
}
通过传递CLONE_NEWUSER
标志给clone()
函数创建了新User Namespace
,之后子进程childFunc
在新创建的User Namespace
中打印出自己的用户ID、组ID以及capabilities
。有关capability的内容参考Linux 手册 Capability。运行上述程序得到如下结果:
eric@eric_ubuntu_server:~/ns_in_opt/part_5$ id -u
1000
eric@eric_ubuntu_server:~/ns_in_opt/part_5$ id -g
1000
eric@eric_ubuntu_server:~/ns_in_opt/part_5$ ./demo_userns
eUID = 65534; eGID = 65534;
capabilityies: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep
上述输出结果展示了一些有趣的信息:
capabilities: = ...
表明子进程拥有所有的capability
创建一个新
User Namespace
时,该Namespace
中的第一个进程被授予完整的权限capability
。- 进程的用户和组ID在
Namespace
内外不同需要将
User Namespace
内的用户与组ID映射到Namespace
外部,对于没有映射的Namespace
,getuid()
与getgid()
会返回/proc/sys/kernel/overflowuid
或/proc/sys/kernel/overflowgid
的内容,即65534
另外,User Namespace
还是可以嵌套的,即除了初始的User Namespace
,每一个User Namespace
都有一个父Namespace
,零个或多个字Namespace
。
映射用户与组ID
注:如前所述,Linux 3.19对gid_map
做了些更改,对于3.19及以上版本,需要先向/proc/PID/setgroups
文件中写入"deny"才能对gid_map
文件进行更改。
创建User Namespace
之后,映射用户ID与组ID是必不可少的一步,这些映射可以通过向/proc/PID/uid_map
与/proc/PID/gid_map
文件中写入映射信息实现。映射信息的格式如下:
ID-inside-ns ID-outside-ns length
上述信息将Namespace
内从ID-inside-ns
到ID-inside-ns + length
映射到了ID-outside-ns
同样长度。不妨假设ID为/proc/PID/uid_map
中PID的进程为A,正在向/proc/PID/uid_map
文件写信息的进程为B。ID-outside-ns
取决于进程A和进程B是否在同一个User Namespace
:
- 若在同一个
Namespace
,则ID-outside-ns
即为其父Namespace
中的值 - 若不在同一个
Namespace
,则ID-outside-ns
的值为进程B所在的Namespace
中的us用户或组ID。
又一个栗子
通过给demo_userns
程序传递任意一个参数可让其每隔几秒输出进程的用户、组ID以及cap。
-
demon_userns
程序传递任意参数执行,如下为输出(前两个)eric@eric_ubuntu_server:~/ns_in_opt/part_5$ ./demo_userns x eUID = 65534; eGID = 65534; capabilityies: = cap_chown,... eUID = 65534; eGID = 65534; capabilityies: = cap_chown,...
可以看到,eUID与eGID(有效用户、组ID)都为65534
-
在另一个终端中执行
echo '0 1000 1' > /proc/PID/uid_map
命令eric@eric_ubuntu_server:~$ ps -C demo_userns -o 'pid uid comm' PID UID COMMAND 4738 1000 demo_userns 4739 1000 demo_userns eric@eric_ubuntu_server:~$ echo '0 1000 1' > /proc/4739/uid_map
即查看当前子进程的PID,之后向
/proc/PID/uid_map
文件中写如映射信息。回到另一个终端我们看到:
eUID = 0; eGID = 65534; capabilityies: = cap_chown,... eUID = 0; eGID = 65534; capabilityies: = cap_chown,...
子进程在新创建的
User Namespace
中建立了用户的映射,不再是默认65534 -
在上述相同的终端中执行如下命令
eric@eric_ubuntu_server:~$ echo 'deny' > /proc/4739/setgroups eric@eric_ubuntu_server:~$ echo '0 1000 1' > /proc/4739/gid_map
首先向
/proc/PID/setgroups
文件中写入字符串’deny’,之后将映射信息写入gid_map
中回到另一个终端,看到:
eUID = 0; eGID = 0; capabilityies: = cap_chown,... capabilityies: = cap_chown,...
即将子进程的
User Namespace
中ID为0的用户/组映射到了父Namespace
中ID为1000的用户/组。
映射规则以及深入探索映射
关于uid_map
与gid_map
的映射规则不在少数,且涉及更深层次的系统细节,本文不详细讨论这些内容,感兴趣的可以参考Linux user_namespace(7),建议运行其后的示例程序以获得更直观的感受。
另外,通过shell命令或其它方式查看用户与组ID的映射时,得到的结果与当前进程所在的User Namespace
有关,详细信息参考上述Linux man手册的示例程序,此处不再详细描述。
总结
Linux中User Namespace
为非特权用户提供了获得特权的途径,由于其相关细节非常之多,本文仅概括性地描述了一些常见的操作及可能遇到的问题。针对·User Namespace·的运用还要结合其它类型的·Namespace·才可彰显,作者也会在接下来的学习过程中进行记录。