linux namespace、cgroup 及 capabilities

2024.04.11补充:一年后再回头来看这篇文章,写的真烂。当初写这篇文章的目的,是为了探索为什么执行 syscall(__NR_fsopen, "cgroup", 0); 时会报错“Operation not permitted”。解决这个问题直接看内核源码不就好了嘛…

fsopen()函数源码ns_capable(current->nsproxy->mnt_ns->user_ns, CAP_SYS_ADMIN) 清清楚楚地说明了该系统调用会检查进程的capability,不具备CAP_SYS_ADMIN的话会返回-EPERM,即 “Operation not permitted”。

做内核漏洞利用的时候,经常需要用到切换namespace的操作。比如普通用户执行syscall(__NR_fsopen, "cgroup", 0);时会报错“Operation not permitted”,而使用unshare创建一个新的namespace后便可以成功执行。

一直不明白它背后的原理,于是抽空了解一下namespace和cgroup。

namespace和cgroup都是linux内核的特性,可以用它们来实现容器,现在最常用的docker就是基于它们的。

cgroup

cgroup(control group)是linux内核的一个特性,它可以用于限制、计算、隔离进程组对计算机资源的使用(如CPU、memory、disk I/O、network等)。

cgroup有如下四个功能:

  1. 资源限制(Resource limits):限制进程组对某一特定资源(CPU,disk,或network)的使用量
  2. 优先级(Prioritization):通过给某个cgroup中的进程分配多一些资源(相比于其他cgroup),从而提高优先级
  3. 审计(Accounting):记录进程/进程组使用的资源量
  4. 控制(Control):进程组控制,如可以使用freezer将进程组挂起或恢复

cgroup是容器(containers)的一个重要组成部分,因为容器中通常会运行多个进程,这些进程通常需要一并控制。

Understanding cgroups以 cpu cgroup为例,展示了如何设置cgroup。总结如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
$ cat /proc/156009/cgroup 			# 查看进程所属cgroup
13:rdma:/
12:pids:/user.slice/user-1000.slice/user@1000.service
11:misc:/
10:freezer:/
9:devices:/user.slice
8:perf_event:/
7:blkio:/user.slice
6:cpuset:/
5:net_cls,net_prio:/
4:cpu,cpuacct:/user.slice
3:hugetlb:/
2:memory:/user.slice/user-1000.slice/user@1000.service
1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-22435931-24c6-4399-b2b0-31b0335ff349.scope
0::/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-22435931-24c6-4399-b2b0-31b0335ff349.scope

$ ls -al /sys/fs/cgroup # 查看cgroup文件系统,目录下每个目录代表一个cgroup类型。每一个cgroup类都遵循层级结构
total 0
drwxr-xr-x 16 root root 400 4月 30 05:48 .
drwxr-xr-x 11 root root 0 4月 30 05:48 ..
dr-xr-xr-x 6 root root 0 4月 30 05:48 blkio # 限制进程的块设备io
lrwxrwxrwx 1 root root 11 4月 30 05:48 cpu -> cpu,cpuacct # 限制进程的cpu使用率
lrwxrwxrwx 1 root root 11 4月 30 05:48 cpuacct -> cpu,cpuacct
dr-xr-xr-x 6 root root 0 4月 30 05:48 cpu,cpuacct
dr-xr-xr-x 3 root root 0 4月 30 05:48 cpuset
dr-xr-xr-x 7 root root 0 4月 30 05:48 devices # 控制进程能够访问某些设备
dr-xr-xr-x 4 root root 0 4月 30 05:48 freezer # 挂起或恢复cgroups中的进程
dr-xr-xr-x 3 root root 0 4月 30 05:48 hugetlb
dr-xr-xr-x 6 root root 0 4月 30 05:48 memory # 限制进程的内存使用量
dr-xr-xr-x 2 root root 0 4月 30 05:48 misc
lrwxrwxrwx 1 root root 16 4月 30 05:48 net_cls -> net_cls,net_prio # 标记cgroups中进程的网络数据包
dr-xr-xr-x 3 root root 0 4月 30 05:48 net_cls,net_prio
lrwxrwxrwx 1 root root 16 4月 30 05:48 net_prio -> net_cls,net_prio
dr-xr-xr-x 3 root root 0 4月 30 05:48 perf_event
dr-xr-xr-x 6 root root 0 4月 30 05:48 pids
dr-xr-xr-x 3 root root 0 4月 30 05:48 rdma
dr-xr-xr-x 6 root root 0 4月 30 05:48 systemd
dr-xr-xr-x 6 root root 0 5月 1 21:31 unified

$ cd cpu
$ ls -al # /sys/fs/cgroup/cpu目录下(相当于根cgroup),存放着进程约束配置文件,及子cgroup(如docker,user.slice等,在子cgroup中还可以继续创建子cgroup。子cgroup能占用的资源不大于父级cgroup)
total 0
dr-xr-xr-x 6 root root 0 4月 30 05:48 .
drwxr-xr-x 16 root root 400 4月 30 05:48 ..
-rw-r--r-- 1 root root 0 5月 21 12:58 cgroup.clone_children
-rw-r--r-- 1 root root 0 4月 30 05:48 cgroup.procs
-r--r--r-- 1 root root 0 5月 21 12:58 cgroup.sane_behavior
-r--r--r-- 1 root root 0 5月 21 12:58 cpuacct.stat
-rw-r--r-- 1 root root 0 5月 21 12:58 cpuacct.usage
-r--r--r-- 1 root root 0 5月 21 12:58 cpuacct.usage_all
-r--r--r-- 1 root root 0 5月 21 12:58 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 5月 21 12:58 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0 5月 21 12:58 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 5月 21 12:58 cpuacct.usage_sys
-r--r--r-- 1 root root 0 5月 21 12:58 cpuacct.usage_user
-rw-r--r-- 1 root root 0 5月 21 12:58 cpu.cfs_burst_us
-rw-r--r-- 1 root root 0 5月 21 12:58 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 4月 30 05:48 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 5月 21 12:58 cpu.idle
-rw-r--r-- 1 root root 0 4月 30 05:48 cpu.shares
-r--r--r-- 1 root root 0 5月 21 12:58 cpu.stat
drwxr-xr-x 3 root root 0 5月 1 21:31 docker
drwxr-xr-x 2 root root 0 5月 4 18:55 init.scope
-rw-r--r-- 1 root root 0 5月 21 12:58 notify_on_release
-rw-r--r-- 1 root root 0 5月 21 12:58 release_agent
drwxr-xr-x 116 root root 0 4月 30 05:48 system.slice
-rw-r--r-- 1 root root 0 5月 21 12:58 tasks
drwxr-xr-x 2 root root 0 4月 30 05:48 user.slice

$ cat tasks # 查看当前层级cgroup中包含的进程号

$ sudo mkdir cgroup_test # 使用mkdir就可以创建一个子级cgroup
$ cd cgroup_test
$ sudo echo 1234 > tasks # 添加1234号进程到新创建的cgroup中
$ cd ../
$ sudo rmdir cgroup_test # 删除创建的cgroup

关于cgroup里的一些概念,可以参考:Cgroup是什么(相关概念、功能、作用、特点、怎么用)

namespace

namespace也是linux内核的一个特性,它将内核资源分隔开,一组进程能看到一些资源,而其他组的进程看到的是不同的资源,组与组之间互不干扰,不知道对方的存在。简单来说,namespace就是内核提供的一种进程间资源隔离技术。

查看进程的namespace信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ls -al /proc/$$/ns        	
total 0
dr-x--x--x 2 bling bling 0 5月 22 21:19 .
dr-xr-xr-x 9 bling bling 0 5月 22 16:22 ..
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 net -> 'net:[4026531840]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 time -> 'time:[4026531834]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 user -> 'user:[4026531837]'
lrwxrwxrwx 1 bling bling 0 5月 22 23:14 uts -> 'uts:[4026531838]'

以上是ubuntu20.04中的一个namespace示例,一共有8类:

namespace名称 使用时的flag 意义 编译选项
IPC CLONE_NEWIPC System V IPC, POSIX message queues信号量,消息队列 CONFIG_IPC_NS
Network CLONE_NEWNET Network devices, stacks, ports, etc.网络设备,协议栈,端口等等 CONFIG_NET_NS
Mount CLONE_NEWNS Mount points挂载点
PID CLONE_NEWPID Process IDs进程号 CONFIG_PID_NS
Time CLONE_NEWTIME 时钟 CONFIG_TIME_NS
User CLONE_NEWUSER 用户和组 ID CONFIG_USER_NS
UTS CLONE_NEWUTS 系统主机名和 NIS(Network Information Service) 主机名(有时称为域名) CONFIG_UTS_NS
Cgroup CLONE_NEWCGROUP Cgroup root directory cgroup 根目录

如何使用?跟namespace相关的系统调用有三个:

  1. clone:创建新的进程并设置namespace

    1
    2
    3
    4
    5
    6
    7
    8
    #include <sched.h>

    int clone(int (*fn)(void *), void *child_stack,
    int flags, void *arg, ...
    /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

    // 使用示例:
    int pid = clone(childFunc, stackTop, CLONE_NEWPID | SIGCHLD, "child");
  2. unshare:让当前进程加入新的namespace

    1
    2
    3
    4
    int unshare(int flags);

    // 使用示例:
    unshare(CLONE_NEWPID);

    linux命令也有一个unshare,可以直接使用如下命令创建一个namespace。新的user,pid,map成root用户,并mount一个新的proc文件系统

    1
    unshare --user --pid --map-root-user --mount-proc --fork bash
  3. setns:让进程加入已经存在 namespace

    1
    2
    3
    4
    5
    int setns(int fd, int nstype);

    // 使用示例
    fd = open("/proc/12425/ns/pid", O_RDONLY);
    setns(fd, CLONE_NEWPID);

参考文章:

搞懂容器技术的基石: namespace (上)

docker 容器基础技术:linux namespace 简介

更深入的理解:

Digging into Linux namespaces - part 1

A deep dive into Linux namespaces

A deep dive into Linux namespaces, part 2

通过namespace判断当前是否在容器中

docker逃逸时,exp可能需要判断当前是否还在容器中,可以通过/proc/1/ns/pid来判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(){
char buffer[0x100];
int re = readlink("/proc/1/ns/pid", buffer, 0x100);
buffer[re] = 0;
printf("buffer:%s\n",buffer);
if (strcmp(buffer, "pid:[4026531836]") != 0) {
printf("we are in docker\n");
return -1;
}
printf("we are in ubuntu\n");
return 0
}

// gcc test.c -o test
// sudo ./test

capabilities

capabilities是linux系统上比”特权/非特权用户”更细粒度的访问控制机制。

可执行程序的capabilities有三个集合,用来保存三类capabilities:

  1. Permitted
  2. Inheritable
  3. Effective
1
2
3
getcap /bin/ping	# 查看可执行程序的capabilities
sudo setcap cap_net_admin,cap_net_raw+ep /bin/ping # 给程序设置capabilities
sudo setcap cap_net_admin,cap_net_raw-ep /bin/ping # 移除程序的capabilities

进程的capabilities有五种集合,:

  1. Permitted:进程能够使用的capabilities的上限
  2. Inheritable:创建子进程时会将Inherited capabilities传递下去
  3. Effective:当前进程执行过程中用到的capabilities,内核检查进程是否可以进行特权操作时,就检查该集合
  4. Bounding:
  5. Ambient:

通过查看/proc/[pid]/status可以获得进程五个capabilities集合的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  ~ echo $$
152301
➜ ~ cat /proc/152301/status
# ......
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff # 无法阅读,可通过capsh --decode=xxx来解析,如下示例
CapAmb: 0000000000000000
# ......
➜ ~ capsh --decode=000001ffffffffff
0x000001ffffffffff=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,38,39,40

# 其他操作命令
capsh --print # 查看当前shell进程的capabilities
getpcaps 1234 # 获取进程号为1234的进程的capabilities

相关系统调用:sys_capget,sys_capset

参考:

Linux Capabilities 简介

An Introduction to Linux Capabilities

Linux Capabilities: Why They Exist and How They Work

Capabiltiy 示例

回到问题

看完cgroup和namespace并未解决我一开始的问题。cgroup是控制cpu和内存等资源的,跟能否通过fsopen打开cgroup应该无关。而namespace无论切换到root还是普通用户都能打开cgroup,那为什么正常shell下的普通用户无法打开cgroup呢?

于是看了下linux的capabilities!

linux中除了通过user(特权进程/非特权进程)限制权限,还有更细粒度的capabilities。执行一个操作时,当user权限未通过,还会检测进程是否具备对应的capabilities。

所以有了一个猜测:通过unshare设置当前进程namespace的时候,它一定有了新的capabilities!

通过以下程序可以证明,执行unshare后,当前进程就有了所有的linux capabilities,所以可以成功执行syscall(__NR_fsopen, "cgroup", 0);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// gcc test.c -lcap -o test
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#undef _POSIX_SOURCE
#include <sys/capability.h>

static void die(const char *fmt, ...) {
va_list params;
va_start(params, fmt);
vfprintf(stderr, fmt, params);
va_end(params);
exit(1);
}

int main(){
if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) {
die("unshare(CLONE_NEWUSER | CLONE_NEWNS): %m");
}

if (unshare(CLONE_NEWNET)) {
die("unshare(CLONE_NEWNET): %m");
}

cap_t caps = cap_get_proc();
ssize_t y = 0;
printf("The process %d was give capabilities %s\n",(int) getpid(), cap_to_text(caps, &y));

struct __user_cap_header_struct cap_header_data;
cap_user_header_t cap_header = &cap_header_data;

struct __user_cap_data_struct cap_data_data;
cap_user_data_t cap_data = &cap_data_data;

cap_header->pid = getpid();
cap_header->version = _LINUX_CAPABILITY_VERSION_1;

if (capget(cap_header, cap_data) < 0) {
perror("Failed capget");
exit(1);
}
printf("Cap data CapEff: 0x%x, CapPrm: 0x%x, CapInh: 0x%x \n",cap_data->effective, cap_data->permitted, cap_data->inheritable);

return 0;
}

/*
执行后输出
➜ ~ ./test
The process 162247 was give capabilities =ep
Cap data CapEff: 0xffffffff, CapPrm: 0xffffffff, CapInh: 0x0
*/

cap_get_proc(3)

capget(2)

参考文章

linux中的容器与沙箱初探

What Are Namespaces and cgroups, and How Do They Work?