验证环境:centos7 x86/64 内核版本4.19.9
在linux 2.2版本之前,当内核对进程进行权限验证的时候,可以将进程划分为两类:privileged(UID=0)和unprivilege(UID!=0)。其中privileged的进程拥有所有内核权限,而unprivileged则根据如可执行文件的权限(effective UID, effective GID,supplementary group等)进行判断。
基于文件访问的进程权限控制
此时进程执行主要涉及6个id:Real uid/gid,Effective uid/gid/supplementary group,Saved set-user-ID/saved set-group-ID。下面以不同的user id为例进行讲解,group id也是类似的。
- supplementary group为user的增补组,例如在添加一个名为usetTest1的user时候,-g执行该user的primary group,-G指定该usetTest1的supplementary groups。使用id命令可以看到“gid=”后面对应usetTest1的primary group,“groups=”后面对应usetTest1的supplementary groups。supplementary groups可以用与DAC验证
[root@localhost ~]# groupadd newGrp1 [root@localhost ~]# groupadd newGrp2 [root@localhost ~]# useradd -u 10000 -g root -G newGrp1,newGrp2 userTest1 [root@localhost ~]# su userTest1 [userTest@localhost root]$ id uid=10000(userTest) gid=0(root) groups=0(root),1001(newGrp1),1002(newGrp2) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
- 进程的RUID为执行该进程的用户ID,该值通常不需要变更;当判断一个进程是否对某个可执行文件有权限时,需要验证EUID,在没有开启SUID功能时,EUID的值等于RUID;SUID主要用于设置EUID。
在上一步中创建了一个usetTest1用户,可以在/etc/passwd查看该用户的home目录,为/home/userTest1
userTest1:x:10000:0::/home/userTest1:/bin/bash
为验证SUID的功能,su切换到userTest1,并在/home/userTest1下创建一个空文件,可以看到wr.log仅对用户userTest1开放写权限
[userTest1@localhost ~]# touch wr.log
[userTest1@localhost ~]# ll-rw-r--r--. 1 userTest1 root 10 Dec 13 18:50 wr.log
在/home/userTest1下编译一个小程序,用于查看当前进程的RUID,EUID和SUID,并写入wr.log。可以看到getIds对所有用户开发了可执行权限
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { uid_t ruid; uid_t euid; uid_t suid; getresuid(&ruid, &euid, &suid); printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d ",ruid,euid,suid); uid_t rgid; uid_t egid; uid_t sgid; getresgid(&rgid, &egid, &sgid); printf("real_group_id=%d, effictive_group_id=%d, saved_group_id=%d ",rgid,egid,sgid); FILE *stream; stream = fopen( "wr.log", "a+" ); fprintf( stream, "%s", "hello" ); fclose( stream ); return 0; }
total 20 -rwxr-xr-x. 1 userTest1 root 8712 Dec 13 18:50 getIds -rw-r--r--. 1 userTest1 root 554 Dec 13 18:50 getIds.c -rw-r--r--. 1 userTest1 root 10 Dec 13 18:50 wr.log
在userTest1用户下执行getIds,有如下内容,可以看到其UID为10000,跟创建该用户时设置的值是一样的,RUID=EUID;拉起该进程用户所在的group以及该文件所属的group都是root,所以group的数值显示均为0
[userTest1@localhost ~]$ ./getIds real_user_id=10000, effictive_user_id=10000, saved_user_id=10000 real_group_id=0, effictive_group_id=0, saved_group_id=0
在同一个host上创建不同组的用户userTest2。
[root@localhost ~]# groupadd -g 20001 newGrp3 [root@localhost home]# useradd -u 10001 -g newGrp3 userTest2
切换到用户userTest2,并进入/home/userTest1(可能需要为该目录添加other的rx权限)下执行getIds,但因为wr.log的用户和组是userTest1:root,而当前用户是userTest2:newGrp3,因此会因为无法打开wr.log出现段错误。同时也可以看到当前进程的RUDI=EUID=10001,即创建userTest2时的UID;RGID=EGID=20001,为创建newGrp3时的GID
[userTest2@localhost userTest1]$ ./getIds real_user_id=10001, effictive_user_id=10001, saved_user_id=10001 real_group_id=20001, effictive_group_id=20001, saved_group_id=20001 Segmentation fault (core dumped)
SUID的作用就是使可执行文件在不同用户下能以文件拥有者的权限去执行。在userTest1用户下为getIds添加SUID,此时getIds文件的权限中user对应的x变为了s
[userTest1@localhost ~]$ chmod 4755 getIds [userTest1@localhost ~]$ ll -rwsr-xr-x. 1 userTest1 root 8712 Dec 13 18:50 getIds -rw-r--r--. 1 userTest1 root 554 Dec 13 18:50 getIds.c -rw-r--r--. 1 userTest1 root 15 Dec 13 19:02 wr.log
切换到userTest2,执行getIds,此时可以执行成功,RUID没有变,但EUID和SUID变为了userTest1的值,此时EUID被SUID值为了10000。即当前程序使用userTest1的权限(EUID)去写入wr.log,因此不会出错。但使用SUID是有安全风险的,本例中的程序并没有能力影响除了wr.log之外的系统环境,但如果是一个包含很多功能的命令(如mount ip等),对该命令授予使用某个用户的完整权限,很大程度上有权限泄露的风险,因此对文件设置SUID时需要谨慎。
[userTest2@localhost userTest1]$ ./getIds real_user_id=10001, effictive_user_id=10000, saved_user_id=10000 real_group_id=20001, effictive_group_id=20001, saved_group_id=20001
更多关于RUID EUID和SUID的内容参见深刻理解——real user id, effective user id, saved user id in Linux
使用capabilities解决上述问题
在linux内核2.2版本之后将基于用户的权限进行了划分,称为capabilities,capabilities是线程相关的,使用时需要在线程上进程设置(完整的capabilities介绍参见capabilities)。那么如何以capabilities解决上述的问题呢?一个简单的办法是改变wr.log的用户和组,这样就不会出现权限问题
对getIds.c做一个小改动增加一行修改wr.log的用户和用户组的操作,其中10001为usetTest2对应的UID,20001为userTest2对应的GID
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { uid_t ruid; uid_t euid; uid_t suid; getresuid(&ruid, &euid, &suid); printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d ",ruid,euid,suid); uid_t rgid; uid_t egid; uid_t sgid; getresgid(&rgid, &egid, &sgid); printf("real_group_id=%d, effictive_group_id=%d, saved_group_id=%d ",rgid,egid,sgid); chown("/home/userTest1/wr.log", 10001, 20001); FILE *stream; stream = fopen( "wr.log", "a+" ); fprintf( stream, "%s", "hello" ); fclose( stream ); return 0; }
编译上述文件,并使用root用户在userTest1目录下设置getIds1拥有修改文件用户和组的权限CAP_CHOWN,+ep代表将该权限添加到capabilities的Effective和Permitted集合中(下面介绍),
[root@localhost userTest1]# setcap cap_chown+ep getIds1
在userTest2下执行getIds可以看到可以执行成功,注意到wr.log的用户和组也被修改为了userTest2的用户和组
[userTest2@localhost userTest1]$ ./getIds1 real_user_id=10001, effictive_user_id=10001, saved_user_id=10001 real_group_id=20001, effictive_group_id=20001, saved_group_id=20001
[userTest2@localhost userTest1]$ ll -rwxrwxrwx. 1 userTest1 root 8712 Dec 13 18:50 getIds -rwxr-xr-x. 1 root root 8760 Dec 13 20:08 getIds1-rw-r--r--. 1 userTest2 newGrp3 30 Dec 13 20:09 wr.log
查看getIds的capabilities,可以看到与设置的一样。最终程序能够运行的原理其实是一样的,即程序的EUID和文件的EUID是一样的。
[userTest2@localhost userTest1]$ getcap getIds1
getIds1 = cap_chown+ep
更简单的办法是给chown设置capabilities,这样进程执行的时候会获取chown上的capabilities,这样就可以拥有权限去执行。在host上执行下面命令。切换到userTest2时就可以使用chown命令直接修改用户和组。此处不能通过给bash设置cap_chow capabilities来操作,因此此时是非root用户,bash进程在执行chown命令的时候会丢掉所有capabilities,导致缺少capabilities而无法运行
# setcap cap_chown=eip /bin/chown
capabilities介绍
- capabilities可以分为线程capabilities和文件capabilities。
- 线程capabilities包含以下4个capabilities集合:
- Effective:内核进行线程capabilities检查时实际使用到的集合
- Inheritable:当程序对应的可执行文件设置了inheritable bit位时,调用execve执行该程序会继承调用者的Inheritable集合,并将其加入到permitted集合。但在非root用户下执行execve时,通常不会保留inheritable 集合,可以考虑使用ambient 集合,当一个程序drop掉一个capabilities时,只能通过execve执行SUID置位的程序或者程序的文件带有该capabilities的方式来获得该capabilities
- permitted:effective集合和inheritable集合的超集,限制了它们的范围,因此如果一个capabilities不存在permitted中,是不可以通过cap_set_proc来获取的。当一个线程从permitted集合中丢弃一个capabilities时,只能通过获取程序可执行文件的capabilities或execve一个set-user-ID-root(以root用户权限运行的)程序来获得
- ambient :是在内核4.3之后引入的,用于补充Inheritable使用上的缺陷,ambien集合可以使用函数prctl修改。当程序由于SUID(SGID)bit位而转变UID(GID),或执行带有文件capabilities的程序时会导致该集合被清空
线程可以使用3种方式修改capabilities:
- fork:子进程使用fork后会继承父进程的capabilities
- cap_set_proc:直接调用系统函数修改,但需要CAP_SETPCAP capabilities权限。在内核2.6.33版本之后,禁止程序直接修改非本进程的capabilities,只允许修改调用者自身进程的capabilities(参见capset)。
- execve:使用该函数后的capabilities计算方式如下:
P'(ambient) = (file is privileged) ? 0 : P(ambient) P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset) | P'(ambient) P'(effective) = F(effective) ? P'(permitted) : P'(ambient) P'(inheritable) = P(inheritable)
P:执行前的线程capabilities
P':执行后的线程capabilities
F:文件的capabilities
privileged file指设置了capabilities或设置了SUID或SGID的文件,如果SUID或SGID被忽略,则上述转换将不会发生
cap_bset为bounding set,主要用来限制拥有CAP_SETPCAP权限的线程通过execve获取文件的permitted capabilities(不影响inheritable集合),可以看到如果cap_bset为空,它是无法获取到文件的permitted集合,即(F(permitted) & cap_bset)=0。
该特性在内核2.6.25版本前后是不一样的。2.6.25版本之前该特性时系统范围内设置的(通过/proc/sys/kernal/cap-bound),2.6.25之后是线程范围内设置的,限制了程序可以获得的文件的capabilities。bounding集合可以通
过继承父进程获得,init进程在内核启动后可以获得所有的bounding集合,可以使用prctl来减少bounding集合中的capabilities,但无法添加新的bounding capabilities
F(effective):当一个程序以set-user-ID-root运行或者进程的EUID为0,这类程序被称为capability-dumb binary,此时程序运行的文件的effective bit会被内核设置为enable。内核在程序运行时会检查该程序是否获得了
capability-dumb binary文件的所有permitted集合,如果没有,返回EPREM错误(通常是因为文件的permitted集合被bound集合过滤导致程序无法获取文件的所有permitted集合)
注:根据公式,线程的permitted集合是可以通过获取文件capabilities扩展的
- 文件capabilities和线程capabilities共同决定了执行execve之后线程的capabilities。设置文件capabilities需要有CAP_SETFCAP 权限。文件capabilities有如下3种:
- Effective:为一个标记位,非capabilities集合。如果设置该标记位,执行execve后的新permitted集合中的capabilities都会添加到effective集合中;反之不会添加(参见上述公式中的:P'(effective) = F(effective) ? P'(permitted) : P'(ambient))。
- Inheritable:该集合主要是配合线程capabilities集合使用,具体使用方式参见上述公式
- Permitted:同上
文件的capabilities使用linux 扩展属性来实现(extended attribute,以下简称EA),EA使用命名空间管理,实现方式比较简单,即key-value方式。文件的capabilities保存在EA的security.capability中,security就是一个命名空间。使用setcap给/usr/bin/的ls目录添加一个capabilities,加入ES,IS和PS中。
# setcap cap_net_raw=eip ls
使用getfattr可以导出该文件对应的EA,"-m -"用于导出所有EA,"-e hex"以16进制方式导出EA
# getfattr -d -m - -e hex ls # file: ls security.capability=0x0100000200200000002000000000000000000000 security.selinux=0x73797374656d5f753a6f626a6563745f723a62696e5f743a733000
可以看到有2个EA,security.capability对应文件的capabilities,另外一个“security.selinux”主要被linux的安全模块调用,实现强制访问控制(MAC),当然我们可以是使用text方式解码此命名空间的内容,跟使用ls -Z查看的结果是一样的
# getfattr -d -m - -e text ls # file: ls security.capability="