运维开发网
广告位招商联系QQ:123077622
 
广告位招商联系QQ:123077622

为何UNIX/Linux中会有suid程序

运维开发网 https://www.qedev.com 2020-02-24 21:37 出处:51CTO 作者:dog250
linux的单点验证我已经说了不止一次了,linux的整体设计是机制和策略相分离的,单点验证显然是策略方面的东西,因此验证本身并没有内核的介入,那么什么是验证本身呢?其实就是诸如最简单的的密码验证和稍微复杂一点的指纹,声音或者瞳孔验证,不管怎么说这些都是策略,内核不应该介入,因此内核当中你无法知道怎么存储和验证用户的密码是否正确,这些都是用户空间完成的,这个事实似乎会让linux的初学者很惊讶,像

Linux的单点验证我已经说了不止一次了,Linux的整体设计是机制和策略相分离的,单点验证显然是策略方面的东西,因此验证本身并没有内核的介入,那么什么是验证本身呢?其实就是诸如最简单的的密码验证和稍微复杂一点的指纹,声音或者瞳孔验证,不管怎么说这些都是策略,内核不应该介入,因此内核当中你无法知道怎么存储和验证用户的密码是否正确,这些都是用户空间完成的,这个事实似乎会让Linux的初学者很惊讶,像安全验证这么重要的事情怎么会没有内核介入呢?这是因为Linux的机制和策略分离的特性造成的。Linux靠uid,euid以及suid机制来实现这一切,最后可以证明这三者是缺一不可的,三者非常紧凑。以下的分析不考虑组的概念也不怎么考虑seLinux的新策略。

uid是可执行文件的所有者的uid或者是可执行文件调用者的uid,它是一个静态的概念,而euid是有效uid,是一个动态的概念,事实上执行绪的行为判断是通过euid来进行的,uid只是一个参考,正如进程优先级的概念,动态优先级是计算所得的,最终通过动态优先级来判决,而静态优先级仅仅是一个参考。euid就是进程实际有效的uid,作为判决的标准生效;而suid的意义比较特殊,可能可执行文件的uid,euid都是非root,但是该可执行文件需要执行一些特殊的操作而必须拥有root权限,那么该可执行文件会临时拥有root权限,这样的可执行文件拥有suid属性,这种suid属性是必须的,否则普通用户就无法su成root用户了,因为普通用户进程的uid和euid都为非0,按照传统的能力模型,非0的uid/euid是无法改变其uid/euid的,呆会儿我再解释为何uid和euid非0的进程为何改变其euid,先看下面的代码(2.6.24内核):

asmlinkage long sys_setuid(uid_t uid)

{

...//见下面

}

既然uid非0的进程无法改变其uid,那么euid为0就成了普通用户成为root的最后的救命稻草,其实运行中的进程只能改变其euid,uid是其可执行文件的内禀属性,改变它没有意义,因此某种意义上euid才是有意义的,如果没有euid属性,那么普通用户进程基本没有机会升级为root进程,普通进程的子进程还是普通进程,如此反复将永远没有机会,但是suid使得普通进程可以成为root进程,/bin/su就是其中的一例,成为root进程是需要验证的,这就是用户空间的验证逻辑,当普通用户进程调用su -的时候,其实su的uid还是普通进程,但是因为su是具有suid属性的,因此其euid是root,那么接下来只要用户空间的验证通过,exec出来的shell将是root的,这就是单点验证的实质,如果su程序写的不好,比如根本不通过验证,只是exec一个shell,那么很显然这个shell是root的,但是内核不管这些,内核只认识euid,真正的验证以及怎么验证留给用户空间的逻辑,虽然su是suid的,只要它exec一个shell那么该shell就是root的,但是su不会实现的这么傻,它内部实现了密码验证逻辑,只有通过验证才可以exec一个root的shell。

euid表示了一个进程的特权级别归属--root和non-root,而euid为root的进程exec的新进程仍然属于root级别,只有root所有的suid程序才会有上述功能,一个普通用户的程序即使设置了suid位也没有用,其实suid的真实含义就是不从父进程继承euid,而用该可执行文件的所有者的id作为 euid。Linux实现了传统UNIX的基于用户的简单验证和后来的能力模型,此二者其实是相互结合的,只有root用户才会被赋予能力,这其实是为了防止root权力过大而导致系统漏洞而引入的,实际上是位于root上层的又一级的认证体系,首先先从setuid的实现开始阐述:

asmlinkage long sys_setuid(uid_t uid)

{

int old_euid = current->euid;

int old_ruid, old_suid, new_ruid, new_suid;

int retval;

retval = security_task_setuid(uid, (uid_t)-1, (uid_t)-1, LSM_SETID_ID);

if (retval)

return retval;

old_ruid = new_ruid = current->uid;

old_suid = current->suid;

new_suid = old_suid;

if (capable(CAP_SETUID)) { //euid非root级别的进程将没有SETUID的能力

if (uid != old_ruid && set_user(uid, old_euid != uid) < 0) //只有在set_user中可以改变进程的uid

return -EAGAIN;

new_suid = uid;

} else if ((uid != current->uid) && (uid != new_suid)) //euid非root的进程只能控制在自己的名字集内

return -EPERM;

...

current->fsuid = current->euid = uid;

current->suid = new_suid;

...//以下的post_setuid做了收尾工作,其实就是清除掉一些能力

return security_task_post_setuid(old_ruid, old_euid, old_suid, LSM_SETID_ID);

}

static inline void cap_emulate_setxuid (int old_ruid, int old_euid, int old_suid)

{

if ((old_ruid == 0 || old_euid == 0 || old_suid == 0) &&

(current->uid != 0 && current->euid != 0 && current->suid != 0) //注意只要uid,euid,suid有一个为0,就说明该进程还会回到root能力级别,因为进程可以随时更改uid或者别的id来摇身一变成为别的级别的成员

&&!current->keep_capabilities) { //只要不在是root级别的进程了,就清除掉能力集

cap_clear (current->cap_permitted);

cap_clear (current->cap_effective);

}

if (old_euid == 0 && current->euid != 0) { //euid最关键,表示一个进程的所在特权级别,同时在能力模型中cap_effective也是最关键的,指示进程当前的能力,而 cap_permitted指示一个完全集,表示所有可能被赋予的能力

cap_clear (current->cap_effective);

}

if (old_euid != 0 && current->euid == 0) { //如果进程原来不是root级别的,但是现在是了,那么允许所有可以允许的能力,这种情况发生在进程收回放弃的特权之后

current->cap_effective = current->cap_permitted;

}

}

从进程的euid和cap_effective的配对可以看出,只要一个进程的euid不是0了,那么它就没有了任何能力,反过来,一旦一个进程的euid 成为了0,那么它就应该等到所有的能力,同时它自己可以适当的选择当前所需的能力,也就是更改cap_effective但是保持 cap_permitted集合。每当调用setuid之类的函数末了就会调用上述的cap_emulate_setxuid函数进行id判断和必要的能力清除,每当执行一个新的二进制映像的时候就会调用下面的函数进行id判断和能力赋予,这是一个成对的操作

int cap_bprm_set_security (struct Linux_binprm *bprm)

{

cap_clear (bprm->cap_inheritable);

cap_clear (bprm->cap_permitted);

cap_clear (bprm->cap_effective);

if (!issecure (SECURE_NOROOT)) {

if (bprm->e_uid == 0 || current->uid == 0) { //只要euid或者uid一个为0,就给与所有的能力但是不生效

cap_set_full (bprm->cap_inheritable);

cap_set_full (bprm->cap_permitted);

}

if (bprm->e_uid == 0) //只有euid为0才生效,uid为0是静态的,表示一种潜在的能力,而只有euid为0才会真正使能这些能力

cap_set_full (bprm->cap_effective);

}

return 0;

}

看一下上述函数的调用函数:

int prepare_binprm(struct Linux_binprm *bprm)

{

int mode;

struct inode * inode = bprm->file->f_dentry->d_inode;

int retval;

mode = inode->i_mode;

if (bprm->file->f_op == NULL)

return -EACCES;

bprm->e_uid = current->euid; //当前的进程的euid给与要执行的进程

bprm->e_gid = current->egid; //同上的原因

if(!(bprm->file->f_vfsmnt->mnt_flags & MNT_NOSUID)) {

if (mode & S_ISUID) { //是否有suid标志

current->personality &= ~PER_CLEAR_ON_SETID;

bprm->e_uid = inode->i_uid; //如果有suid标志,那么该bprm映像执行后就会以root级别运行,它的孩子当然也会以root级别运行,具体可见fork的写时复制

}

if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {

current->personality &= ~PER_CLEAR_ON_SETID;

bprm->e_gid = inode->i_gid;

}

}

retval = security_bprm_set(bprm); //调用了cap_bprm_set_security

if (retval)

return retval;

memset(bprm->buf,0,BINPRM_BUF_SIZE);

return kernel_read(bprm->file,0,bprm->buf,BINPRM_BUF_SIZE);

}

在加载映像的最后将会调用以下函数:

void cap_bprm_apply_creds (struct Linux_binprm *bprm, int unsafe)

{

kernel_cap_t new_permitted, working;

new_permitted = cap_intersect (bprm->cap_permitted, cap_bset);

working = cap_intersect (bprm->cap_inheritable, current->cap_inheritable);

new_permitted = cap_combine (new_permitted, working);

if (bprm->e_uid != current->uid || bprm->e_gid != current->gid ||

!cap_issubset (new_permitted, current->cap_permitted)) {

current->mm->dumpable = suid_dumpable;

if (unsafe & ~LSM_UNSAFE_PTRACE_CAP) { //如果没有在ptrace那么就会进入下面的逻辑

if (!capable(CAP_SETUID)) { //这个说明当前的euid不为0,即当前进程不是root能力级别的

bprm->e_uid = current->uid; //将当前进程的uid赋予bprm的euid,bprm执行后将不再是root能力级别的了,可能出现uid为0而euid不为0的,此时bprm的 euid仍然为0.

bprm->e_gid = current->gid;

}

if (!capable (CAP_SETPCAP)) {

new_permitted = cap_intersect (new_permitted, current->cap_permitted);

}

}

} //以下两行正式将euid设置给当前进程,此时当前进程已经是新的进程了,这个函数的调用堆栈从load_elf_binary的最后开始

current->suid = current->euid = current->fsuid = bprm->e_uid;

current->sgid = current->egid = current->fsgid = bprm->e_gid;

if (current->pid != 1) {

current->cap_permitted = new_permitted;

current->cap_effective = cap_intersect (new_permitted, bprm->cap_effective);

}

current->keep_capabilities = 0;

}

void compute_creds(struct Linux_binprm *bprm)

{

int unsafe;

if (bprm->e_uid != current->uid)

suid_keys(current);

exec_keys(current);

task_lock(current);

unsafe = unsafe_exec(current);

security_bprm_apply_creds(bprm, unsafe);

task_unlock(current);

security_bprm_post_apply_creds(bprm);

}

Linux的能力模型是一个很复杂的模型,有很多的能力可以设置,并且有很多的配置规则。

上面是传统UNIX的root用户两级机制和能力模型融合的内核实现,在用户空间必须实现验证策略,其实就是密码验证,指纹验证等策略。正是由于suid 的存在,一个普通的用户进程才能su出一个root的shell或者别的用户的shell,如果没有suid属性的存在,那么任何普通用户将无法成为 root用户,因为内核并不进行验证,而只有内核有权更改用户进程的uid,那么内核外必须有进程来代理内核进行验证,该进程必须是绝对可信任的,su是一个suid程序,它代理root运行保持最高权限只有这样才可以保证验证结果的可信,它进行实际的验证工作,通过后才会fork-exec出一个 root用户的shell或者别的用户的shell,因此对su的***将是致命的***,一定要保护好su而不被替换掉,其实只要拿到root特权,你可以自己写一个suid程序,然后它的子进程将是有特权的,以下就是一个例子:

以下是一个suid的程序,编译好之后用chmod 7777 test将之设置为最开放的suid程序:

#include

#include

int main()

{

printf("uid:%d/neuid:%d/negid:%d/n",getuid(),geteuid(),getegid());

setuid(0);

printf("new uid:%d/n",getuid());

execve("/home/zhaoya/temp",NULL,envp);

}

以下程序用zhaoya用户在/home/zhaoya目录下编译为temp:

#include

#include

#include

int main()

{

printf("uid:%d/neuid:%d/n",getuid(),geteuid());

int fd = open("/root/install.log",O_WRONLY);

perror("open");

}

直接运行temp会得出没有权限错误,而用test启动则成功打开root目录下的文件,从输出也可以看出上述内核的行为,直接运行temp的输出是 500,500,而通过test启动的输出则为0,0.如果将test中的setuid(0);注释掉,那么启动temp的结果也是成功打开root目录的文件,但是输出变成了500,0,总之geteuid的结果为0,那么它的权限就是root,而现代Linux的root权限直接对应完整的能力集,而 root用户可以通过能力模型的系统调用接口从cap_effective去除当前不需要的能力,而cap_permitted并不改变,root用户可以通过接口将某些能力置位,其实能力模型主要就是限制root用户的,对于别的用户本身他们就没有什么权限就不用再限制了,对root用户的限制可以提高安全性。

扫码领视频副本.gif

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号