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?