为何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.h>

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.h>

#include.h>

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用户的限制可以提高安全性。



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273344

相关文章
|
2月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
165 6
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
607 2
|
3月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
64 2
|
26天前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
32 1
|
29天前
|
Unix 物联网 大数据
操作系统的演化与比较:从Unix到Linux
本文将探讨操作系统的历史发展,重点关注Unix和Linux两个主要的操作系统分支。通过分析它们的起源、设计哲学、技术特点以及在现代计算中的影响,我们可以更好地理解操作系统在计算机科学中的核心地位及其未来发展趋势。
|
2月前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
39 5
|
1月前
|
Linux 应用服务中间件 nginx
Linux下权限设置之suid、sgid、sticky
Linux下权限设置之suid、sgid、sticky
|
3月前
|
NoSQL Linux C语言
嵌入式GDB调试Linux C程序或交叉编译(开发板)
【8月更文挑战第24天】本文档介绍了如何在嵌入式环境下使用GDB调试Linux C程序及进行交叉编译。调试步骤包括:编译程序时加入`-g`选项以生成调试信息;启动GDB并加载程序;设置断点;运行程序至断点;单步执行代码;查看变量值;继续执行或退出GDB。对于交叉编译,需安装对应架构的交叉编译工具链,配置编译环境,使用工具链编译程序,并将程序传输到开发板进行调试。过程中可能遇到工具链不匹配等问题,需针对性解决。
|
3月前
|
安全 Linux
在Linux中,suid、sgid和sticky bit这几个术语意思?
在Linux中,suid、sgid和sticky bit这几个术语意思?
|
3月前
|
Ubuntu 安全 Unix
在Linux中,有哪几种linux/unix发行版本?
在Linux中,有哪几种linux/unix发行版本?
下一篇
无影云桌面