如何利用小游戏调试工具提升小程序开发效率,提升开发效率与合规性
581
2022-09-24
为何UNIX/Linux中会有suid程序
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用户的限制可以提高安全性。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~