1. uid与SUID
1.1. uid
Linux使用uid标识用户,每个用户有一个唯一的uid,root的uid为0,其他可登录用户的uid一般从1000开始,使用id指令可以查看自己的uid
实际上一个进程运行中主要有3个uid,分别是real uid,saved uid和effective uid,real uid用于表示进程的实际调用者,saved uid用于在切换用户时保存uid,effective uid则是真正进行访问控制的uid,所有权限都是基于effective uid的,这才是实际有用的uid
默认情况下三个uid相同,uid为0(root)的用户可以随意更改三个uid,而普通用户则只能把effective uid更改为另外两个uid之一
1.2. SUID
SetUID,SUID是linux中的特殊权限位,用在所有者的执行位,表示为s,如果所有者没有执行权限则表示为S,数字表示为4000,即对应第12个权限位
带有SUID位的二进制可执行文件,在执行时euid会被替换为所有者的euid,即以所有者的身份执行
如以下代码
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1) exit(-1);
if(pid)
waitpid(pid,NULL,0);
else
execlp("id","id",NULL);
return 0;
}
编译之后将拥有者改为root,直接运行,得到
uid=1000(ztsubaki) gid=1000(ztsubaki) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)
当我们为其增加SUID位
sudo chmod u+s ./a.out
其表示为
-rwsr-xr-x 1 root root 16048 Jul 14 20:31 a.out
再次运行程序,输出为
uid=1000(ztsubaki) gid=1000(ztsubaki) euid=0(root) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)
可见后者新增了euid=0(root),表示euid与real uid不同,为root
对于SUID的作用,最常见的还是给予运行者有限的root权限,如passwd命令允许普通用户修改自己的密码,这就必须更改保存密码用的shadow文件,改文件肯定不可能开放所有人的读写权限,实际上该文件权限为-rw-r-----,此时就必须使用SUID以root的身份运行passwd才能修改密码,同时passwd命令对应的可执行程序普通用户无法轻易更改,也确保了无法修改别人的密码
可以查看passwd的权限位
ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 64152 May 30 2024 /usr/bin/passwd
类似地,对于gid,也有egid和SGID,SGID位于第11位,即2000,记为写在组执行权限上的s或S,作用与SUID基本一致
2. SUID与脚本
上文提到,SUID只能作用于二进制可执行文件,无法作用于脚本,这是因为脚本的解释器与脚本本身分离,不受脚本权限的保护,若解释器权限较弱,通过替换解释器就可以执行任意别的内容,如果此时SUID生效就比较危险,故linux不允许脚本使用SUID,为脚本设置的SUID将会失效
如果在知道风险之后依然有使用脚本的需求,我们可能会很自然想到在二进制可执行程序中调用脚本,比如下面的程序
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1) exit(-1);
if(pid)
waitpid(pid,NULL,0);
else
execlp("sh","sh","./t.sh",NULL);
return 0;
}
脚本如下
id
输出如下
uid=1000(ztsubaki) gid=1000(ztsubaki) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)
并没有出现预期结果
经过分析,我们可以在sh的man文件中找到一句话
-p priviliged Do not attempt to reset effective uid if it does not match uid. This is not set by default to help avoid incorrect usage by setuid root programs via system(3) or popen(3).
可见,sh实际上默认会在real uid与effective uid不同时替换掉euid,因此,我们在系统调用中加入-p参数,即
execlp("sh","sh","-p","./t.sh",NULL);
这样就可以得到预期结果
CAUTION无论如何请务必谨慎使用该方法
最后,我们一直通过fork和exec产生子进程测试,如果直接使用system呢
使用如下代码
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
system("id");
pid = fork();
if(pid==-1) exit(-1);
if(pid)
waitpid(pid,NULL,0);
else
execlp("id","id",NULL);
return 0;
}
更改所有者,设置SUID后运行得到
uid=1000(ztsubaki) gid=1000(ztsubaki) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)
uid=1000(ztsubaki) gid=1000(ztsubaki) euid=0(root) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)
发现system调用中SUID还是没有生效,通过strace可以发现
[pid 6020] execve("/bin/sh", ["sh", "-c", "--", "id"], 0x7ffcfa8f9238 /* 34 vars */ <unfinished ...>
system本质上是调用了sh -c来执行命令行,在这个过程中就会替换掉euid,这就导致SUID不会正常生效了
