Linux中的SetUid和capability权能机制

中国科学院大学操作系统安全课小实验。

1. 前置知识

以下内容来自课件。

1.1 用户划分和访问控制

用户划分

访问控制 r读 w写 x执行

访问控制

实现举例

实现举例

1.2 特殊权限的访问控制setuid/setgid

必须有一种方法,允许用户执行那些只有超级用户或者管理者才有读写权限的程序或命令。

概念讲解

  • 设置UID

passwd的拥有者(也就是root)原本对passwd有执行权限,所以passwd拥有者对它的访问控制从rwx变成了rws。

  • 设置GID

  • 设置粘滞位STICKY

小测试

增加问题:

  • 当进程P显示England时,对文件filex有什么访问权限?
  • 当进程P显示Australia时,对文件filex有什么访问权限?

1.3 权能体系capabilites

权能体系提出背景

权能体系目的

完整的权能列表请见第二章实验内容。

权能列表

2. 实验内容

实验环境:Ubuntu 18.04 64位

第一题

Q:解释“passwd”,“sudo”, “ping”等命令为什么需要setuid位,去掉s位试运行,添加权能试运行。

A:

① 为什么需要setuid位:

首先明确setuid的作用是:执行该设置后,文件执行时将以文件拥有者的身份执行,而非普通用户的身份。

以普通用户的身份登录操作系统,通过whoami命令查看当前用户:

通过which命令查看passwd、sudo、ping文件所在位置:

通过ls -all查看这些文件的详细信息:

从上图可以看出passwd、sudo和ping的文件拥有者是root,即超级用户,所属用户组是root,并且都设置了SUID。

当其他的普通用户执行passwd、sudo和ping时,由于不是root,无法执行这些文件。而setuid之后,普通用户便可以临时以文件拥有者root的身份执行这些文件。所以passwd、sudo和ping等命令需要setuid。

② 去掉uid位

通过chmod命令移除passwd、sudo和ping文件的setuid:

移除setuid后再以普通用户的身份执行这些命令,发现这些命令全部都无法执行:

③ 恢复uid位

由于恢复passwd和ping的setuid时需要使用sudo,故应先恢复sudo。

在重启系统时按下ESC键,进入GNU GRUB界面,选择Advanced options for Ubuntu:

再选择一个recovery mode:

选择root,进入最高级管理员的shell界面,输入chmod u+s /usr/bin/sudo恢复sudo的setuid:

继续在shell界面输入exit退回上一个界面,选择resume,回车重启系统。

通过sudo+chmod恢复passwd和ping的setuid:

重新添加权能后上述三个命令又可以正常运行了:

第二题

Q:指出每个权能对应的系统调用,简要解释功能。

A:在linux内核2.2之后引入了capabilities机制,来对root权限进行更加细粒度的划分。如果进程不是特权进程,而且也没有root的有效id,系统就会去检查进程的capabilities,来确认该进程是否有执行特权操作的的权限。

通过find命令查找capability.h所在的位置:

查看该文件内容,整理得下表:(此处直接粘贴了原markdown内容,貌似知乎不支持markdown表格QAQ)

| 权能 | 编号 | 描述 |

| ---- | ---- | ---- |

|CAP_CHOWN | 0(chown) | 允许改变文件的所有权|

|CAP_DAC_OVERRIDE | 1 | 忽略对文件的所有DAC访问限制|

|CAP_DAC_READ_SEARCH |2 | 忽略所有对读、搜索操作的限制|

|CAP_FOWNER |3 | 如果文件属于进程的UID,就取消对文件的限制|

|CAP_FSETID |4 |允许设置setuid位|

|CAP_KILL |5(kill) |允许对不属于自己的进程发送信号|

|CAP_SETGID |6(setgid) |允许改变组ID|

|CAP_SETUID |7(setuid) |允许改变用户ID|

|CAP_SETPCAP |8(capset) |允许向其它进程转移能力以及删除其它进程的任意能力|

|CAP_LINUX_IMMUTABLE |9(chattr) |允许修改文件的不可修改(IMMUTABLE)和只添加(APPEND-ONLY)属性|

|CAP_NET_BIND_SERVICE |10 |允许绑定到小于1024的端口|

|CAP_NET_BROADCAST |11 |允许网络广播和多播访问|

|CAP_NET_ADMIN |12 |允许执行网络管理任务:接口、防火墙和路由等,详情请参考/usr/src/linux/include/linux/capability.h文件|

|CAP_NET_RAW |13(socket) |允许使用原始(raw)套接字|

|CAP_IPC_LOCK |14(mlock) |允许锁定共享内存片段|

|CAP_IPC_OWNER |15 |忽略IPC所有权检查|

|CAP_SYS_MODULE |16(init_module) |插入和删除内核模块|

|CAP_SYS_RAWIO |17 |允许对ioperm/iopl的访问 |

|CAP_SYS_CHROOT |18(chroot) |允许使用chroot()系统调用|

|CAP_SYS_PTRACE |19(ptrace)| 允许跟踪任何进程|

|CAP_SYS_PACCT |20(acct) |允许配置进程记帐(process accounting)|

|CAP_SYS_ADMIN |21 |允许执行系统管理任务:加载/卸载文件系统、设置磁盘配额、开/关交换设备和文件等。详情请参考/usr/src/linux/include/linux/capability.h文件。|

|CAP_SYS_BOOT |22(reboot) |允许重新启动系统|

|CAP_SYS_NICE |23(nice) |允许提升优先级,设置其它进程的优先级|

|CAP_SYS_RESOURCE |24(setrlimit) |忽略资源限制|

|CAP_SYS_TIME |25(stime) |允许改变系统时钟|

|CAP_SYS_TTY_CONFIG |26(vhangup) |允许配置TTY设备|

|CAP_MKNOD |27(mknod) |允许使用mknod()系统调用|

|CAP_LEASE |28(fcntl) |为任意文件建立租约|

|CAP_AUDIT_WRITE |29|允许向内核审计日志写记录|

|CAP_AUDIT_CONTROL|30|启用或禁用内核审计,修改审计过滤器规则|

|CAP_SETFCAP|31|设置文件权能|

|CAP_MAC_OVERRIDE|32|允许MAC配置或状态改变,为smack LSM实现|

|CAP_MAC_ADMIN|33|覆盖强制访问控制|

|CAP_SYSLOG|34(syslog)|执行特权syslog(2)操作|

|CAP_WAKE_ALARM|35|触发将唤醒系统的东西|

|CAP_BLOCK_SUSPEND|36(epoll)|可以阻塞系统挂起的特性|

|CAP_AUDIT_READ|37|允许通过一个多播socket读取审计日志|

第三题

Q:查找你Linux发行版系统(Ubuntu/centos等)中所有设置了setuid位的程序,指出其应该有的权能。

A:

使用uname -a命令查看操作系统相关信息:

使用sudo find / -perm /u=s命令查找所有设置了setuid位的程序,结果如下:

/snap/core18/2284/bin/mount /snap/core18/2284/bin/ping /snap/core18/2284/bin/su /snap/core18/2284/bin/umount /snap/core18/2284/usr/bin/chfn /snap/core18/2284/usr/bin/chsh /snap/core18/2284/usr/bin/gpasswd /snap/core18/2284/usr/bin/newgrp /snap/core18/2284/usr/bin/passwd /snap/core18/2284/usr/bin/sudo /snap/core18/2284/usr/lib/dbus-1.0/dbus-daemon-launch-helper /snap/core18/2284/usr/lib/openssh/ssh-keysign /snap/core18/2128/bin/mount /snap/core18/2128/bin/ping /snap/core18/2128/bin/su /snap/core18/2128/bin/umount /snap/core18/2128/usr/bin/chfn /snap/core18/2128/usr/bin/chsh /snap/core18/2128/usr/bin/gpasswd /snap/core18/2128/usr/bin/newgrp /snap/core18/2128/usr/bin/passwd /snap/core18/2128/usr/bin/sudo /snap/core18/2128/usr/lib/dbus-1.0/dbus-daemon-launch-helper /snap/core18/2128/usr/lib/openssh/ssh-keysign /snap/core20/1081/usr/bin/chfn /snap/core20/1081/usr/bin/chsh /snap/core20/1081/usr/bin/gpasswd /snap/core20/1081/usr/bin/mount /snap/core20/1081/usr/bin/newgrp /snap/core20/1081/usr/bin/passwd /snap/core20/1081/usr/bin/su /snap/core20/1081/usr/bin/sudo /snap/core20/1081/usr/bin/umount /snap/core20/1081/usr/lib/dbus-1.0/dbus-daemon-launch-helper /snap/core20/1081/usr/lib/openssh/ssh-keysign /snap/core20/1376/usr/bin/chfn /snap/core20/1376/usr/bin/chsh /snap/core20/1376/usr/bin/gpasswd /snap/core20/1376/usr/bin/mount /snap/core20/1376/usr/bin/newgrp /snap/core20/1376/usr/bin/passwd /snap/core20/1376/usr/bin/su /snap/core20/1376/usr/bin/sudo /snap/core20/1376/usr/bin/umount /snap/core20/1376/usr/lib/dbus-1.0/dbus-daemon-launch-helper /snap/core20/1376/usr/lib/openssh/ssh-keysign /snap/snapd/14978/usr/lib/snapd/snap-confine /usr/bin/newgrp /usr/bin/gpasswd /usr/bin/pkexec /usr/bin/traceroute6.iputils /usr/bin/vmware-user-suid-wrapper /usr/bin/chfn /usr/bin/arping /usr/bin/sudo /usr/bin/passwd /usr/bin/chsh /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/lib/openssh/ssh-keysign /usr/lib/xorg/Xorg.wrap /usr/lib/policykit-1/polkit-agent-helper-1 /usr/lib/eject/dmcrypt-get-device /usr/lib/snapd/snap-confine /usr/sbin/pppd find: ‘/proc/2865/task/2865/fd/6’: 没有那个文件或目录 find: ‘/proc/2865/task/2865/fdinfo/6’: 没有那个文件或目录 find: ‘/proc/2865/fd/5’: 没有那个文件或目录 find: ‘/proc/2865/fdinfo/5’: 没有那个文件或目录 /bin/su /bin/umount /bin/fusermount /bin/ping /bin/mount find: ‘/run/user/1000/gvfs’: 权限不够

部分程序及其对应的权能如下:

第四题

Q:实现一个程序其满足以下的功能:

(1)能够永久的删除其子进程的某个权能。

(2)能暂时性的删除其子进程的某个权能。

(3)能让上面被暂时性删除的权能重新获得。

A:

(1)分析

① 如何在C++中执行shell命令?
需要用到unistd.h头文件中的exec系列函数。本程序中使用的是execlp()函数,其底层通过execve实现系统调用。
此外,setuid()和setgid()也在unistd.h中。

② 如何实现永久删除/暂时删除/恢复子进程的某个权能?
C++程序中,执行shell命令时,需要fork()一个子进程,那么子进程(简单理解成ping http://baidu.com)就拥有了父进程的所有权能。
当子进程的某项权能eip标志都置位时:

  • 清除该权能的ei标志可暂时删除该权能。
  • 设置该权能的ei标志又可恢复该权能。
  • 当将该权能的eip标志都删除时,则永久删除了该权能。

③ 需要用到哪些权能相关的函数,这些函数分别是什么意思?
权能手册:https://man7.org/linux/man-pages/man7/capabilities.7.html
libcap手册:https://man7.org/linux/man-pages/man3/libcap.3.html

    • <sys/capability.h>相关代码

<sys/capability.h>完整代码:http://www.castaglia.org/proftpd/doc/devel-guide/src/lib/libcap/include/sys/capability.h.html

 typedef int cap_value_t;
 typedef struct _cap_struct *cap_t;
 /*_cap_struct的内容在libcap/libcap.h
 struct _cap_struct {
   struct __user_cap_header_struct head;
   struct __user_cap_data_struct set;
 };
 */
 typedef enum {
   CAP_EFFECTIVE=0,                        /* Specifies the effective flag */
   CAP_PERMITTED=1,                        /* Specifies the permitted flag */
   CAP_INHERITABLE=2   /* Specifies the inheritable flag */
 } cap_flag_t;
 typedef enum {
   CAP_CLEAR=0,                            /* The flag is cleared/disabled */
   CAP_SET=1  /* The flag is set/enabled */
 } cap_flag_value_t;
    
 int cap_from_name(const char *name, cap_value_t *cap_p); // 与lcap链接使用,详情:https://linux.die.net/man/3/cap_to_text
 ​
    • libcap库中相关代码
函数名所在位置详细信息
cap_init()libcap/cap_alloc.chttps://man7.org/linux/man-pages/man3/cap_init.3.html
cap_set_flag()libcap/cap_flag.chttps://linux.die.net/man/3/cap_set_flag
cap_set_proc()libcap/cap_proc.chttps://linux.die.net/man/3/cap_set_proc
 // no_values是array_values数组的大小
 int cap_set_flag(cap_t cap_d, cap_flag_t set,
  int no_values, cap_value_t *array_values,
  cap_flag_value_t raise)
   
 int cap_set_proc(cap_t cap_d)
 int cap_from_name(const char * name , cap_value_t * cap_p );
int prctl(int option, unsigned long arg2, unsigned long arg3,
    unsigned long arg4, unsigned long arg5);
 ​
 PR_SET_KEEPCAPS(自 Linux 2.2.18 起)
   设置调用线程的“保持能力”状态
   旗帜。该标志的作用在
   能力(7) .  arg2必须为 0(清除标志)
   或 1(设置标志)。“保持能力”的价值将
   在后续调用execve(2)时重置为 0 。

④ 为什么执行程序时需要加sudo
经过测试,若不加sudo执行程序,作为普通用户执行cap_init()和cap_set_proc()后,进程的权能为空,可能和cap_init()等cap设置操作需要root权限执行有关,故需要使用sudo执行程序。使用sudo执行程序后,在由root用户切换成普通用户时,需要使用prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0),用以在切换用户时保留能力,让普通用户能继承root用户的权能。

(2)代码与运行

安装libcap库、g++(gcc)等相关依赖和环境:

 sudo apt update
 sudo apt install build-essential
 sudo apt-get install libcap-dev

执行代码前需要先执行如下命令:

 sudo setcap cap_net_raw+i /bin/ping
 getcap /bin/ping
 g++ os_cap.cpp -o os_cap -lcap
 sudo ./os_cap
 # 注意:运行完程序后,需要恢复正常的ping权能:sudo setcap cap_net_raw+ip /bin/ping。
 # 下次运行os_cap程序前依旧需要执行上面几条命令。

完整源代码:

 #include <iostream>
 #include <cstring>
 #include <unistd.h>
 #include <sys/wait.h>
 #include <sys/types.h>
 #include <sys/capability.h>
 #include <sys/prctl.h>
 using namespace std;
 ​
 const int MAX_CAP_NAME_LEN = 19;
 ​
 string choices = "\
 1:永久的删除其子进程的某个权能。\n\
 2:暂时性的删除其子进程的某个权能。\n\
 3:能让上面被暂时性删除的权能重新获得。\n\
 4:退出本程序。\n\
 请输入选项对应的数字并回车(以ping baidu.com命令为例):\n";
 ​
 void printMenu(){
 cout << choices;
 }
 ​
 // 打印进程uid gid信息
 void whoami() {
     printf("当前进程(uid=%i,euid=%i,gid=%i)\n", getuid(), geteuid(), getgid());
 }
 ​
 // 打印进程pid和权能信息
 void listCaps(){
     // 获取当前进程的权能,用以打印当前进程的权能状态
     cap_t caps = cap_get_proc();
     ssize_t y = 0;
     printf("当前进程pid=%d,权能%s\n",(int) getpid(), cap_to_text(caps, &y));
     fflush(0);
     // 及时释放空间
     cap_free(caps);
 }
 ​
 void doPing(){
     if(!fork()){
         printf("执行shell命令的子进程(uid=%i,euid=%i,gid=%i)\n", getuid(), geteuid(), getgid());
         
         cap_t caps = cap_get_proc();
         ssize_t y = 0;
         printf("执行shell命令的子进程pid=%d,权能%s\n",(int) getpid(), cap_to_text(caps, &y));
         fflush(0);
         cap_free(caps);
         
         printf("进行ping baidu.com测试:\n");
         execlp("ping", "ping", "-c", "1", "baidu.com", NULL);
    }
 }
 ​
 void init(){
     if(getuid()!=0){
         printf("请使用sudo执行本程序\n");
         exit(1);
    }
 ​
   //CAP_SETPCAP 8(capset) 允许向其它进程转移能力以及删除其它进程的任意能力
     cap_value_t cap_values[] = {CAP_SETUID, CAP_SETGID, CAP_SETPCAP, CAP_NET_RAW};  // 设置权能列表
     cap_t caps = cap_init();    // 初始化权能状态,返回结构体指针
 ​
     // 将caps_values数组中的权能的标记(EFFECTIVE/PERMITTED/INHERITABLE)置位
     cap_set_flag(caps, CAP_PERMITTED, 4, cap_values, CAP_SET);
     cap_set_flag(caps, CAP_EFFECTIVE, 4, cap_values, CAP_SET);
     
     cap_set_proc(caps); // 由caps设定当前进程的权能状态
     prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0); // 切换用户时保留能力,让普通用户能继承root用户的特权
     cap_free(caps); //释放权能状态
 ​
     printf("root用户下的进程和特权信息:\n");
     whoami();
     listCaps();
 ​
     //切换成普通用户
     setuid(1000);
     setgid(1000);
     
     caps = cap_get_proc();  //获取当前进程的权能状态
     cap_set_flag(caps, CAP_EFFECTIVE, 4, cap_values, CAP_SET);
     cap_set_flag(caps, CAP_INHERITABLE, 4, cap_values, CAP_SET);
     cap_set_proc(caps);
     cap_free(caps);
 ​
     printf("普通用户下的进程特权信息:\n");
     whoami();
     listCaps();
 }
 ​
 int main(){
     string s;
     init();
     printMenu();
     while(getline(cin, s)){    
         if(s.length()!=1){
         printf("【warning】:请输入正确的数字!\n");
        }else{
             char choice = s[0]; 
             // 永久或暂时删除子进程的某个权能
         if(choice=='1' || choice=='2' || choice=='3'){
                 char cap_name[20];
                 string line;
                 printf("请输入想要操作的权能名称,如cap_net_raw(不区分大小写):\n");
                 getline(cin, line);
                 if(line.length()>MAX_CAP_NAME_LEN) {
                     printf("【warning】:请输入正确的权能名称!\n");
                }else{
                     strcpy(cap_name, line.c_str());
                     cap_value_t cap2num;
                     if(cap_from_name(cap_name, &cap2num)<0){
                         printf("【warning】:请输入正确的权能名称!\n");
                    }else{
                         cap_t caps = cap_get_proc();    // 获取当前进程的权能状态
                         
                         // 能让上面被暂时性删除的权能重新获得
                         if(choice=='3'){
                             cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap2num, CAP_SET);
                             cap_set_flag(caps, CAP_INHERITABLE, 1, &cap2num, CAP_SET);
                             if(cap_set_proc(caps)){
                                 printf("【error】:设置当前进程的权能状态失败!\n");
                            }else{
                                 printf("【info】:成功恢复进程被暂时删除的权能\n");
                            }
                        } else {
                             // 2:暂时删除子进程的某个权能
                             cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap2num, CAP_CLEAR);
                             cap_set_flag(caps, CAP_INHERITABLE, 1, &cap2num, CAP_CLEAR);
                             // 1:永久删除子进程的某个权能
                             if(choice=='1'){
                                 cap_set_flag(caps, CAP_PERMITTED, 1, &cap2num, CAP_CLEAR);
                            }
                 
                             // 设置当前进程的权能状态
                             if(cap_set_proc(caps)){
                                 printf("【error】:设置当前进程的权能状态失败!\n");
                            }else{
                                 printf("【info】:成功%s删除进程的%s权能\n", choice=='1'?"永久":"暂时", cap_name);
                            }
                          }
             
                         cap_free(caps); // 记得释放资源!!
                         doPing();
                    }
                }
          }else if(choice=='4'){
          break;
          }else{
                 printf("【warning】:请输入正确的数字!\n");
              }
        }
         int status = 0;
         wait(&status); //等待子进程(ping baidu.com)结束
         if(!WIFEXITED(status)){
             printf("【error】:子进程ping baidu.com非正常结束\n");
        }
         printMenu(); // 子进程结束后再显示
    }
     return 0;
 }

(3)常规功能测试

  • 暂时删除cap_net_raw权能后ping失败,恢复cap_net_raw权能后ping成功。

  • 永久删除cap_net_raw权能后ping失败,想要恢复该权能但失败了,常规测试完成,退出程序。

(4)异常功能测试

  • 输入选项错误

  • 权能名称错误

  • 未使用sudo执行程序

参考资料

Linux Capabilites 机制详细介绍

Linux的capability深入分析(1) - 投河自尽的鱼 - 博客园

https://xsyin.github.io/2018/05/01/Linux-权能机制/

Linux Capability探索实验

Linux setuid与权能介绍

【问题收录】Ubuntu(14.04)那些我遇到的各种事_君的名字的博客-CSDN博客

exec系列函数(execl,execlp,execle,execv,execvp)使用_炸鸡叔的博客-CSDN博客_execle函数

文件访问权限:更改用户ID - simple_life - 博客园

权能代码(in linux):

  • /include/uapi/linux/capability.h

https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/capability.h

  • /security/capability.c

https://elixir.bootlin.com/linux/latest/source/security/security.c

  • /security/commoncap.c

https://elixir.bootlin.com/linu