Linux下文件設置用戶編碼setuid詳解

    咱們知道,Linux每一個文件的詳細屬性都存儲在一個stat結構中,能夠用stat命令查看文件的詳細屬性,例如文件大小,修改時間,所屬用戶,文件權限等。 node

struct stat {
    dev_t     st_dev;     /* ID of device containing file */
    ino_t     st_ino;     /* inode number */
    mode_t    st_mode;    /* protection */
    nlink_t   st_nlink;   /* number of hard links */
    uid_t     st_uid;     /* user ID of owner */
    gid_t     st_gid;     /* group ID of owner */
    dev_t     st_rdev;    /* device ID (if special file) */
    off_t     st_size;    /* total size, in bytes */
    blksize_t st_blksize; /* blocksize for filesystem I/O */
    blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
    time_t    st_atime;   /* time of last access */
    time_t    st_mtime;   /* time of last modification */
    time_t    st_ctime;   /* time of last status change */
};


在stat結構中,st_mode字段表示了文件的模式相關屬性。下圖是關於該字段的詳細描述,能夠發現st_mode經過一個8進制數字存儲文件模式: shell

  • 後三位分佈表示了文件讀、寫、執行的權限以及文件的掩碼mask信息
  • 第4位表示文件的粘着位,設置用戶組編號,設置用戶編號信息
  • 其餘位表示了文件的類型信息(管道,設備,文件夾,普通文件等)
S_IFMT     0170000   bit mask for the file type bit fields
S_IFSOCK   0140000   socket
S_IFLNK    0120000   symbolic link
S_IFREG    0100000   regular file
S_IFBLK    0060000   block device
S_IFDIR    0040000   directory
S_IFCHR    0020000   character device
S_IFIFO    0010000   FIFO
S_ISUID    0004000   set-user-ID bit
S_ISGID    0002000   set-group-ID bit (see below)
S_ISVTX    0001000   sticky bit (see below)
S_IRWXU      00700   mask for file owner permissions
S_IRUSR      00400   owner has read permission
S_IWUSR      00200   owner has write permission
S_IXUSR      00100   owner has execute permission
S_IRWXG      00070   mask for group permissions
S_IRGRP      00040   group has read permission
S_IWGRP      00020   group has write permission
S_IXGRP      00010   group has execute permission
S_IRWXO      00007   mask for permissions for others (not in group)
S_IROTH      00004   others have read permission
S_IWOTH      00002   others have write permission
S_IXOTH      00001   others have execute permission


下面一段代碼將對setuid的使用做出簡單說明。父進程經過fork建立一個子進程,而後父進程和子進程將分別打印設置用戶ID,最後父進程將會嘗試建立一個新的文件。 安全

#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
    int fd;
    pid_t pid;

    if ((pid = fork()) < 0) {
        fprintf(stderr, "fork error\n");
        exit(1);
    } else if (pid == 0) {
        printf("child real user id: %d\n", getuid());
        printf("child effective user id: %d\n", geteuid());
    } else {
        sleep(1);
        printf("parrent real user id: %d\n", getuid());
        printf("parrent effective user id: %d\n", geteuid());
        exit(0);
    }
    if ((fd = open(argv[1], O_CREAT | O_RDWR)) < 0) {
        fprintf(stderr, "open error\n");
        fprintf(stderr, "error: %s\n", strerror(errno));
    }

    close(fd);

    return 0;
}

編譯並執行程序,結果以下: bash

roo@roose:~$ ls -l a.out
-rwxrwxr-x 1 roo roo 9174 Jan 29 17:17 a.out*
roo@roose:~$ ./a.out /usr/local/testfile
child real user id: 1000
child effective user id: 1000
parrent real user id: 1000
parrent effective user id: 1000
open error
error: Permission denied

根據打印的信息,能夠發現 app

  • 父進程和子進程的實際用戶ID和有效用戶ID都是1000,即爲普通用戶
  • 嘗試建立文件/usr/local/testfile時,程序提示權限錯誤,說明執行用戶權限爲普通用戶


下面修改a.out的屬性,設置文件擁有者爲root用戶,添加setuid socket

roo@roose:$ sudo chown root:root a.out
roo@roose:$ sudo chmod 4775 a.out
roo@roose:$ ls -l a.out
-rwsrwxr-x 1 root root 9174 Jan 29 17:17 a.out

而後從新執行程序 ide

roo@roose:~$ ./a.out /usr/local/testfile
child real user id: 1000
child effective user id: 0
parrent real user id: 1000
parrent effective user id: 0

根據打印的信息,能夠發現: ui

  • 進程的實際用戶編號(uid)爲執行用戶的用戶編號1000,即爲普通用戶
  • 進程的有效用戶編號(euid)是文件所屬用戶的用戶編號0,即爲root用戶
  • 子進程也集成了父進程的實際用戶編號和有效用戶編號
  • 父進程成功建立了/usr/local/testfile文件,說明程序是以root權限執行的

嘗試執行設置了setuid的shell腳本,建立腳本文件test.sh內容以下: this

#!/bin/bash
sleep 10
touch /usr/local/testfile
roo@roose:~$ sudo chown root:root test.sh
roo@roose:~$ sudo chmod 4775 test.sh
roo@roose:~$ ./test.sh &
[1] 54280
roo@roose:~$ ps -eo uid,euid,command | grep test.sh
 1000  1000 /bin/bash ./test.sh
 1000  1000 grep --color=auto test.sh
roo@roose:~$ touch: cannot touch ‘/usr/local/testfile’: Permission denied

一樣對test.sh賦予相關權限並執行,結果卻和上面C生成的可執行文件不太同樣: spa

  • 程序的實際用戶編號(uid)和有效用戶編號(euid)都是1000,即爲普通用戶;
  • 程序也沒有權限建立/usr/local/testfile,即添加的設置用戶編號沒有效果。

爲何已經設置了setuid卻沒有效果呢?經過查找相關資料,發現出現該問題的緣由是:

    出於安全問題,Linux會忽略任何編譯器文件的設置用戶位。

詳細說明以下(博主太懶,有空再翻譯)

http://unix.stackexchange.com/questions/364/allow-setuid-on-shell-scripts

Linux ignores the setuid¹ bit on all interpreted executables (i.e. executables starting with a #! line). The comp.unix.questions FAQ explains the security problems with setuid shell scripts. These problems are of two kinds: shebang-related and shell-related; I go into more details below.

If you don't care about security and want to allow setuid scripts, under Linux, you'll need to patch the kernel. As of 3.x kernels, I think you need to add a call to install_exec_creds in the load_script function, before the call to open_exec, but I haven't tested.
Setuid shebang

There is a race condition inherent to the way shebang (#!) is typically implemented:
    The kernel opens the executable, and finds that it starts with #!.
    The kernel closes the executable and opens the interpreter instead.
    The kernel inserts the path to the script to the argument list (as argv[1]), and executes the interpreter.

If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, most unices ignore the setuid bit when they detect a shebang.

One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don't think anyone does it this way.

A few unix systems (mainly OpenBSD, NetBSD and Mac OS X, all of which require a kernel setting to be enabled) implement secure setuid shebang using an additional feature: the path /dev/fd/N refers to the file already opened on file descriptor N (so opening /dev/fd/N is roughly equivalent to dup(N)). Many unix systems (including Linux) have /dev/fd but not setuid scripts.

    The kernel opens the executable, and finds that it starts with #!. Let's say the file descriptor for the executable is 3.
    The kernel opens the interpreter.
    The kernel inserts /dev/fd/3 the argument list (as argv[1]), and executes the interpreter.

Sven Mascheck's shebang page has a lot of information on shebang across unices, including setuid support.


Setuid interpreters

Let's assume you've managed to make your program run as root, either because your OS supports setuid shebang or because you've used a native binary wrapper (such as sudo). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.     Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g. /lib/ld.so), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD). The invoker of the program can execute arbitrary code in that program's context by placing a specially-crafted libc.so in $LD_LIBRARY_PATH (amongst other tactics). All sane systems ignore the LD_* variables in setuid executables.     In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as PATH, IFS, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts's context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don't know that there is any particular implementation that I would trust.     Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don't do anything fancier than dynamic linking (which does take precautions).     Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed #!/usr/bin/suidperl -wT instead of #!/usr/bin/perl -wT, but on most modern systems, #!/usr/bin/perl -wT is sufficient. Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability. A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it's set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset option is turned on, that setenv is off, and that env_file and env_keep only contain innocuous variables. TL,DR:     Setuid shebang is insecure but usually ignored.     If you run a program with privileges (either through sudo or setuid), write native code or perl, or start the program with a wrapper that sanitizes the environment (such as sudo with the env_reset option).
相關文章
相關標籤/搜索