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有如下四个功能:
资源限制(Resource limits):限制进程组对某一特定资源(CPU,disk,或network)的使用量
优先级(Prioritization):通过给某个cgroup中的进程分配多一些资源(相比于其他cgroup),从而提高优先级
审计(Accounting):记录进程/进程组使用的资源量
控制(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 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 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 lrwxrwxrwx 1 root root 11 4月 30 05:48 cpu -> cpu,cpuacct 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 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 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 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 $ sudo mkdir cgroup_test $ cd cgroup_test $ sudo echo 1234 > tasks $ cd ../ $ sudo rmdir cgroup_test
关于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相关的系统调用有三个:
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, ... ) ;int pid = clone(childFunc, stackTop, CLONE_NEWPID | SIGCHLD, "child" );
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
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 }
capabilities
capabilities是linux系统上比”特权/非特权用户”更细粒度的访问控制机制。
可执行程序的capabilities有三个集合,用来保存三类capabilities:
Permitted
Inheritable
Effective
1 2 3 getcap /bin/ping sudo setcap cap_net_admin,cap_net_raw+ep /bin/ping sudo setcap cap_net_admin,cap_net_raw-ep /bin/ping
进程的capabilities有五种集合,:
Permitted:进程能够使用的capabilities的上限
Inheritable:创建子进程时会将Inherited capabilities传递下去
Effective:当前进程执行过程中用到的capabilities,内核检查进程是否可以进行特权操作时,就检查该集合
Bounding:
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 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 getpcaps 1234
相关系统调用: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 #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 ; }
cap_get_proc(3)
capget(2)
参考文章
linux中的容器与沙箱初探
What Are Namespaces and cgroups, and How Do They Work?