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
不会正常生效了