探索Linux的SUID位

在系统安全中,uid扮演了非常重要的角色,这里比较深入的叙述了Linux中的uid机制。

写在前面

本学期学习的信息安全课程中,接触到了Linux的uid方面的,关于访问权限控制的知识。由于从论文上得到的知识,总感觉有点虚幻,就想自己动手探索一下:Linux中uid的特性。以下实验均出自Ubuntu 12.04 64位机器。

ruid、euid、suid

一个程序,准确的说是一个进程,拥有着这三种id。它们分别代表着:

  • ruid : 哪个用户运行这个程序,ruid就是谁
  • euid : 代表着该进程的执行权限,每当进程访问一些资源时,内核就会根据进程的euid来判断该操作是否有权限执行
  • suid : 保存的uid的,通常与euid相同,当进程execve一个进程时,子进程的suid会保存euid字段,变成euid。而fork会把父进程的ruid、euid、suid完全的复制一份。

进程每次打开、创建或删除一个文件时,内核对文件访问权限的测试如下:

  • 若进程的euid为0(root用户),则允许访问
  • 若进程的euid等于文件的所有者id,那么如果文件的所有者适当的访问权限位被设置,则允许访问,否则拒绝访问。适当的访问权限指:若进程为而打开文件,那么用户写位置1,若进程为而打开文件,那么用户读文件位置1.
  • 若进程的有效组id或者进程的附属组id之一等于文件的组id,那么如果组适当的访问权限被设置,则允许访问,反之则拒绝访问。
  • 若其他用户适当的访问权限位被设置,则允许访问,否则拒绝访问。

设置权限

1
2
3
#include <unistd.h>
int setuid( uid_t uid );
int setgid( gid_t gid );

1)若进程具有root权限,即euid为0,那么setuid会将ruid、euid、suid全部设为uid参数。
2)若进程没有root权限,但uid参数等于ruid或者suid,那么setuid只将euid设置为uid参数。
3)如果上面两个条件都不满足,则errno设置为EPERM,并返回-1。

从这些特性中,可以看出,suid的出现,是可以恢复进程依据最小权限原则而临时去掉的权限的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>
int seteuid( uid_t uid );
int setegid( gid_t gid );
```
1)一个root用户,可以将euid设置为任意的uid参数
2)非特权用户,只能将euid设置为ruid或者suid中的一个。

从这些特性中,进程不能通过seteuid操作来恢复之前的权限的。

综述:setuid可用于临时性的改变进程的权限,而seteuid用于永久的取得进程的某些权限。注意**只有root用户执行setuid才改变suid,其他情况下setuid和seteuid均不改变suid.**

## **关于文件的S位**
我们知道文件通常有三种访问权限:r w x,但有时为了达到一些特定目的,给这个文件增加了一个s位。最典型的应用即Linux中,用户修改密码这个操作。保存密码的文件除了root用户,任何人不得访问的,但普通用户可以更改其密码,也就是说对这个密码文件具有可写的权限,这一操作能得以实现,主要归功于文件的s位。

### **s位的打开与关闭**
可以使用chmod更改权限来打开s位,如下:
```sh
# chmod sxxx filename

  • s = 1:代表打开粘着位,关于粘着位放在后面讲。
  • s = 2:代表为群组打开s位
  • s = 4:代表为文件的使用者打开s位。

s 也可以是上述三个值的和,作用是它们功能的并集。

关于文件s位的作用

我的一个同学非常形象的描述这个s位的作用,它就像是一个印章(戳)。如果打开这个文件的s位,也就意味着无论谁执行使用这个文件,它都拥有着文件拥有者的权限。就好像银行行长给你一张支票,上面有他的签名。无论谁拿着这张支票都可以到银行取到钱。因为银行行长的签名具有向银行取钱的权利。

那么对于Linux的shadow文件或者是passwd文件,只有root可以读写。每一个用户可以通过passwd命令来更改自己的密码,为了实现普通用户可以更改仅root才能修改的shadow文件,操作系统为passwd的使用者位打开了s位。在Ubuntu 12.04 64上权限如下:

1
-rwsr-xr-x 1 root root 42824 Sep 13  2012 /usr/bin/passwd

关于文件粘着位(t)的理解

要删除一个文件,你不一定要有这个文件的写权限,但你一定要有这个文件的上级目录的写权限。也就是说,你即使没有一个文件的写权限,但你有这个文件的上级目录的写权限,你也可以把这个文件给删除,而如果没有一个目录的写权限,也就不能在这个目录下创建文件。

如何才能使一个目录既可以让任何用户写入文件,又不让用户删除这个目录下他人的文件,粘着位就是能起到这个作用。粘着位一般只用在目录上,也可作用在普通文件上。如果一个可执行程序文件这一位被设置了,那么当该程序第一次被执行时,在其终止时,程序的正文部分(机器指令)仍被保存在交换区,这使得下次执行该程序时能较快的将其载入内存

如果用户对目录有写权限,则可以删除其中的文件和子目录,即使该用户不是这些文件的所有者,而且也没有读或写许可。粘着位出现执行许可的位置上,用t表示,设置了该位后,其它用户就不可以删除不属于他的文件和目录。但是该目录下的目录不继承该权限,要再设置才可使用。

目录/tmp和/var/tmp是设置粘着位的典型候选者–任何人都可以在这两个目录中创建文件,但用户不能删除或重命名属于其他人的文件,为此在这两个目录的文件模式都设置了粘着位。其权限如下所示:

1
drwxrwxrwt  13 root root  4096 Jan 24 18:47 /tmp

注意:那么原来的执行标志x到哪里去了呢? 系统是这样规定的, 假如本来在该位上有x, 则这些特别标志 (suid, sgid, sticky) 显示为小写字母 (s, s, t). 否则, 显示为大写字母 (S, S, T) 。

测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* set.c 文件 使用root权限执行的
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
uid_t ruid;
uid_t euid;
uid_t suid;
pid_t pid;

setresuid( 800, 900, 100 );
getresuid( &ruid, &euid, &suid );
printf("orignal ruid = %d, euid = %d, suid = %d\n", ruid, euid, suid );
#if 0
if( (pid = fork()) == 0 ) {
execve( "./hello",NULL, NULL );
}
#endif
if((pid = fork()) == 0) {
getresuid( &ruid, &euid, &suid );
printf("fork ruid = %d, euid = %d, suid = %d\n", ruid, euid, suid );
}
#if 0
seteuid(800);
getresuid( &ruid, &euid, &suid );
printf("seteuid(800) ruid = %d, euid = %d, suid = %d\n", ruid, euid, suid );

seteuid(900);
getresuid( &ruid, &euid, &suid );
printf("seteuid(900) ruid = %d, euid = %d, suid = %d\n", ruid, euid, suid );
#endif
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* hello.c 文件
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
uid_t r_uid;
uid_t e_uid;
uid_t s_uid;

getresuid( &r_uid, &e_uid, &s_uid );
printf("hello --- r_uid = %d, e_uid = %d, s_uid = %d\n", r_uid, e_uid, s_uid ) ;
return 0;
}

参考

[1] UNIX环境高级编程第4、8章
[2] 博客园的落崖惊风