1. 程式人生 > 其它 >[apue] linux 檔案訪問許可權那些事兒

[apue] linux 檔案訪問許可權那些事兒

前言

說到 linux 上的檔案許可權,其實我們在說兩個實體,一是檔案,二是程序。一個程序能不能訪問一個檔案,其實由三部分內容決定:

  1. 檔案的所有者、所在的組;
  2. 檔案對所有者、組使用者、其它使用者設定的許可權訪問位;
  3. 啟動程序的使用者、所在的組、有效使用者、有效使用者組。

下面先簡單說明一下這些基本概念,最後再說明它們是如何相互作用並影響程序訪問檔案的。

使用者與組

使用者 ID 唯一標識一個登入使用者,記錄在口令檔案 (/etc/passwd) 中。ID 為 0 的使用者為超級使用者或根使用者 (root),具有繞過檔案許可權檢查的特權。

組 ID 用於將一類使用者組織在一起,記錄在組檔案 (/etc/group) 中。下面這段 shell 指令碼用來演示如何建立使用者並將它們新增到組中:

 1 #! /bin/bash
 2 useradd lippman
 3 useradd steven
 4 useradd caveman
 5 useradd paperman
 6 echo "create user ok"
 7 
 8 groupadd men
 9 groupadd share
10 echo "create group ok"
11 
12 usermod -a -G share lippman
13 usermod -a -G share steven
14 usermod -a -G men lippman
15 usermod -a -G men caveman
16 usermod -a -G men paperman 17 echo "add user to group ok" 18 19 groups lippman steven caveman paperman 20 echo "show user and their group ok" 21 22 groupdel men 23 groupdel share 24 echo "delete group ok" 25 26 groups lippman steven caveman paperman 27 echo "show user and their group ok" 28 29
userdel lippman 30 userdel steven 31 userdel caveman 32 userdel paperman 33 echo "delete user ok" 34 35 rm -rf /home/lippman 36 rm -rf /home/steven 37 rm -rf /home/caveman 38 rm -rf /home/paperman 39 echo "remve user home dir ok"

這段指令碼需要有管理員許可權,請確保當前使用者為 root 使用者或屬於 sudoer 使用者組並使用 sudo 執行。下面是指令碼的輸出:

$ sudo ./user_init.sh
create user ok
create group ok
add user to group ok
lippman : lippman men share
steven : steven share
caveman : caveman men
paperman : paperman men
show user and their group ok
delete group ok
lippman : lippman
steven : steven
caveman : caveman
paperman : paperman
show user and their group ok
delete user ok
remve user home dir ok

在你的機器上執行這段指令碼的時候要特別小心,確保不會有同名的使用者或組已經存在,否則可能會將資料誤刪除。特別是刪除使用者時,使用者的工作目錄是不會一併刪除的,為了防止下次執行指令碼時報警 (工作目錄已存在),這裡同時刪除使用者的工作目錄 (line 35-38)。groups 命令為引數列表中的每個使用者羅列它們所在的組,一個使用者可以屬於多個組,它建立時所在的組稱為初始組,其它組稱為附加組,一個使用者最多可以新增的附加組數量上限可以通過 sysconf (_SC_NGROUPS_MAX) api 獲取 (或通過 getconf NGROUPS_MAX 命令獲取),在我的機器上這個值是 65536。關於系統限制值,可以參考我之前寫的這篇文章:《[apue] 一個快速確定新系統上各類限制值的工具 》。

從上面兩組高亮的輸出可以看出,附加組是可以先於使用者刪除的,刪除之後使用者就不在組中了。useradd 命令建立的使用者初始組名稱默認同使用者名稱,也可以通過 -g 引數指定一個已存在的組作為初始組,及通過 -G 引數指定一個或多個附加組,這與 usermod 命令的使用方式是相似的:

 1 #! /bin/bash
 2 groupadd men
 3 groupadd share
 4 echo "create group ok"
 5 
 6 useradd lippman -G share,men
 7 useradd -g share steven
 8 useradd -g men caveman
 9 useradd -g men paperman
10 echo "create user ok"
11 
12 groups lippman steven caveman paperman
13 echo "show user and their group ok"
14 
15 groupdel men
16 groupdel share
17 echo "delete group ok"
18 
19 groups lippman steven caveman paperman
20 echo "show user and their group ok"
21 
22 userdel lippman
23 userdel steven
24 userdel caveman
25 userdel paperman
26 echo "delete user ok"
27 
28 rm -rf /home/lippman
29 rm -rf /home/steven
30 rm -rf /home/caveman
31 rm -rf /home/paperman
32 echo "remve user home dir ok"

與之前不同的是,為了在建立使用者時指定初始組,組的建立被放在了前面。這段指令碼的輸出如下:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
groupdel: cannot remove the primary group of user 'caveman'
groupdel: cannot remove the primary group of user 'steven'
delete group ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
delete user ok
remve user home dir ok

可以看到通過 -G 新增組時,還是建立了預設的初始組 (lippman),而組在作為使用者的初始組存在的情況下是無法被刪除的 (line 15-16,附加組可以),所以最好調整一下刪除使用者和組的順序:

 1 #! /bin/bash
 2 groupadd men
 3 groupadd share
 4 echo "create group ok"
 5 
 6 useradd lippman -G share,men
 7 useradd -g share steven
 8 useradd -g men caveman
 9 useradd -g men paperman
10 echo "create user ok"
11 
12 groups lippman steven caveman paperman
13 echo "show user and their group ok"
14 
15 userdel lippman
16 userdel steven
17 userdel caveman
18 userdel paperman
19 echo "delete user ok"
20 
21 rm -rf /home/lippman
22 rm -rf /home/steven
23 rm -rf /home/caveman
24 rm -rf /home/paperman
25 echo "remve user home dir ok"
26 
27 groupdel men
28 groupdel share
29 echo "delete group ok"

這樣再跑就沒問題了:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
delete user ok
remve user home dir ok
delete group ok

還有個有意思的點可以關注一下:

  • 刪除使用者時,使用者的初始組也會被一起刪除,但僅限該初始組沒有被其它使用者共享的情況下;
  • 單獨建立的附加組即使沒有包含任何使用者,也不會隨著最後使用者的刪除被自動刪除。

為了簡化後面的描述,將使用以下術語表示上面的概念:

  • 超級使用者: root
  • 使用者 ID:uid (user id)
  • 使用者組 ID:gid (group id)
  • 使用者初始組 (登入組):initgrp (initial group)
  • 使用者附加組:supgrp (supplementary group)

與使用者和組相關的一些命令羅列如下:

  • 使用者:useradd / usermod / userdel / users
  • 使用者密碼:passwd / useradd | usermod -p
  • 使用者組:groupadd / groupmod / groupdel
  • 使用者組密碼:gpasswd / groupadd | groupmod -p (你沒看錯,使用者組也可以有密碼)
  • 使用者與組的關係:id / groups / groupmems / usermod | useradd -g | G / gpasswd -a | -d
  • 使用者登入:su / sudo / who / whoami / last / ac

這裡需要強調的是通過 usermod 修改使用者組時,有三種方式:

  • usermod -a group1,group2... user:將 group[1-n] 新增到 user 的附加組中,原附加組保持不變;
  • usermod -G group1,group2... user:將 user 的附加組設定為 group[1-n],原附加組被清除;
  • usermod -g group user:將 user 的初始組設定為 group。

另外,像上面例子那樣,刪除使用者時需要同時刪除使用者 home 目錄的時候,只需要給 userdel 新增一個 -r 引數即可。有時除了 home 目錄,系統還會為新使用者建立郵件目錄,如果刪除使用者不清理這些目錄的話,再次建立的同名使用者就會告警,這裡都可以通過 -r 引數一併刪除,避免後顧之憂。

後面我們會用這裡建立的使用者及組來做一些驗證,具體就是在 line 14 插入一些測試指令碼,用於驗證一些在多使用者場景下的許可權問題。使用 su 命令可在多使用者之間切換,如果使用者設定了密碼,則在切換時要求輸入密碼,這裡為了測試的便利性,都沒有給使用者帳號新增密碼,在實際場景中應避免這樣使用。

檔案的使用者與組

檔案本身有很多型別:

  • 普通檔案
  • 目錄檔案
  • 符號連結
  • 塊裝置
  • 字元裝置
  • FIFO
  • 套接字

所有檔案都有建立者的 uid 和 gid,也有對應的檔案許可權位。針對普通檔案,還可以再做一細分:

  • 可執行檔案
  • 一般檔案

可執行檔案一般符合某種固定格式 (例如 elf),是程序的載體。針對這種檔案,可以多設定兩種標誌位:

  • 設定使用者 ID
  • 設定組 ID

它們決定了以該檔案作為程序啟動時,新程序所使用的 uid 和 gid。對於 Solaris 系統,設定組 ID 也可以給普通的一般檔案設定,不過含義也大為不同:表示啟用強制性檔案記錄鎖,這是一種非標準擴充套件,不在本文的討論範圍,這裡就不再展開說明了。

針對目錄檔案,也可以多設定兩種標誌位:

  • 設定組 ID
  • 粘住位 (sticky bit / svtx)

設定組 ID 與檔案中的標誌位相同,但是作用於目錄時,意義又不一樣了:表示該組下建立的檔案的使用者組 ID 將追隨自己,而不是建立程序的組 ID,關於程序的組 ID 詳見下一節,關於目錄設定組 ID 位後新建檔案的所有權,詳見“新建檔案的許可權”這一節;目錄加入粘住位時,會改變目錄預設的刪除檔案、修改檔名的規則,具體見“程序訪問檔案時核心許可權檢查過程”這一節。

為了簡化後面的描述,將使用以下術語表示上面的概念:

  • 檔案建立者(擁有者)使用者 ID:ouid (owner uid)
  • 檔案建立者(擁有者)使用者組 ID:ogid (owner gid)
  • 檔案許可權位:perm (permission)
  • 檔案設定使用者 ID:setuid
  • 檔案設定組 ID:setgid
  • 檔案粘住位:svtx (saved text bit)

需要注意的是檔案沒有附加組的概念,它屬於上一節"使用者與組"的範疇,檔案只能屬於一個使用者組,在建立時確定,不隨使用者的變更而變更,本節末尾有一個測試用例來驗證這一點。以上與檔案相關的概念術語對應到檔案的 stat 結構體的關係如下:

  • ouid:st_uid
  • ogid:st_gid
  • 檔案型別:type = st_mode & S_IFMT
    • 普通檔案:type & S_IFREG
    • 目錄檔案:type & S_IFDIR
    • 符號連結:type & S_IFLNK
    • 塊裝置:type & S_IFBLK
    • 字元裝置:type & S_IFCHR
    • FIFO:type & S_IFFIFO
    • 套接字:type & S_IFSOCK
  • setuid:st_mode & S_ISUID
  • setgid:st_mode & S_ISGID
  • svtx:st_mode & S_ISVTX

這個結構體使用 stat / fstat / lstat 等 api 獲取,如果要獲取符號連結本身的屬性,需要使用 lstat,否則獲取的是符號連結指向的目標屬性。此外,還可以通過在 find 命令中指定引數來查詢特定型別的檔案,以上內容與 find 引數之間的對應關係如下:

  • 普通檔案:-type f
  • 目錄檔案:-type d
  • 符號連結:-type l
  • 塊裝置:-type b
  • 字元裝置:-type c
  • FIFO:-type p
  • 套接字:-type s

這些符號簡寫其實與 ls 輸出的檔案型別是一致的 (每行第一個字元),關於 ls 的輸出例子,請參考後面和 find 結合查詢檔案的例子。三個額外的標誌位通過 chmod 修改時,使用的關鍵字元如下:

  • setuid:chmod u+/-s
  • setgid:chmod g+/-s
  • svtx:chmod o+/-t

注意額外標誌位是與固定的 u/g/o 許可權組搭配的,關於許可權組請參考“檔案訪問許可權位”一節。如果進行了錯誤的搭配,雖然不會報錯,但是也不會生效。由於這三個標誌位是與執行許可權放在一起的,所以最終顯示什麼字元還與之前有沒有設定可執行 (x) 許可權有關:

  • setuid+x:rws --- ---
  • setuid-x:rwS --- ---
  • setgid+x:--- rws ---
  • setgid-x:--- rwS ---
  • svtx+x : --- --- rwt
  • svtx-x :--- --- rwT

即小寫字母表示有執行許可權,大寫表示沒有。也可以使用 find 搜尋帶有特定標誌位的檔案,上面的內容與 find 搜尋引數的對應關係為:

  • setuid:-perm -u+s
  • setgid:-perm -g+s
  • svtx:-perm -o+t

格式與 chmod 非常類似。其它的 rwx 許可權位也都是可以搜尋的,這裡就不贅述了。下面我們用這個命令在測試機上搜索一些“特殊”的檔案,首先看下 setuid 標誌位:

$ find / -perm -u+s 2>/dev/null | xargs ls -ldh
-rwsr-xr-x 1 root root     52K Oct 31  2018 /usr/bin/at
-rwsr-xr-x 1 root root     73K Aug  9  2019 /usr/bin/chage
-rws--x--x 1 root root     24K Feb  3 00:31 /usr/bin/chfn
-rws--x--x 1 root root     24K Feb  3 00:31 /usr/bin/chsh
-rwsr-xr-x 1 root root     57K Aug  9  2019 /usr/bin/crontab
-rwsr-xr-x 1 root root     32K Oct 31  2018 /usr/bin/fusermount
-rwsr-xr-x 1 root root     77K Aug  9  2019 /usr/bin/gpasswd
-rwsr-xr-x 1 root root     44K Feb  3 00:31 /usr/bin/mount
-rwsr-xr-x 1 root root     41K Aug  9  2019 /usr/bin/newgrp
-rwsr-xr-x 1 root root     28K Apr  1  2020 /usr/bin/passwd
-rwsr-xr-x 1 root root     24K Apr  1  2020 /usr/bin/pkexec
---s--x--- 1 root stapusr 208K Oct 14  2020 /usr/bin/staprun
-rwsr-xr-x 1 root root     32K Feb  3 00:31 /usr/bin/su
---s--x--x 1 root root    144K Jan 27 05:56 /usr/bin/sudo
-rwsr-xr-x 1 root root     32K Feb  3 00:31 /usr/bin/umount
-rwsr-sr-x 1 abrt abrt     15K Oct  2  2020 /usr/libexec/abrt-action-install-debuginfo-to-abrt-cache
-rwsr-x--- 1 root dbus     57K Sep 30  2020 /usr/libexec/dbus-1/dbus-daemon-launch-helper
-rwsr-xr-x 1 root root     16K Apr  1  2020 /usr/lib/polkit-1/polkit-agent-helper-1
-rwsr-xr-x 1 root root     11K Apr  1  2020 /usr/sbin/pam_timestamp_check
-rwsr-xr-x 1 root root     36K Apr  1  2020 /usr/sbin/unix_chkpwd
-rws--x--x 1 root root     40K Aug  9  2019 /usr/sbin/userhelper
-rwsr-xr-x 1 root root     12K Nov 17  2020 /usr/sbin/usernetctl

搜尋到的全是普通檔案,且是可執行檔案,大部分位於 /usr/bin 下面,一般是超級使用者開放給普通使用者使用的命令。再看下 setgid 針對普通檔案的情況:

$ find / -type f -perm -g+s 2>/dev/null | xargs ls -lh
-r-x--s--x 1 root slocate   40K Apr 11  2018 /usr/bin/locate
---x--s--x 1 root nobody   374K Aug  9  2019 /usr/bin/ssh-agent
-r-xr-sr-x 1 root tty       15K Jun 10  2014 /usr/bin/wall
-rwxr-sr-x 1 root tty       20K Feb  3 00:31 /usr/bin/write
-rwsr-sr-x 1 abrt abrt      15K Oct  2  2020 /usr/libexec/abrt-action-install-debuginfo-to-abrt-cache
---x--s--x 1 root ssh_keys 455K Aug  9  2019 /usr/libexec/openssh/ssh-keysign
-r-x--s--x 1 root utmp      11K Jun 10  2014 /usr/libexec/utempter/utempter
-rwxr-sr-x 1 root root      11K Nov 17  2020 /usr/sbin/netreport
-rwxr-sr-x 1 root postdrop 214K Apr  1  2020 /usr/sbin/postdrop
-rwxr-sr-x 1 root postdrop 258K Apr  1  2020 /usr/sbin/postqueue

情況和 setuid 檔案類似,一般是一些特殊的使用者組 (slocate / nobody / tty / postdrop …) 開放給普通使用者使用的命令。setgid 針對目錄時含義完全不同,所以這裡限定了查詢型別是普通檔案,關於 setgid 目錄的例子,留到後面再說明。最後順便從上面的 ls 輸出看一下各列含義:

  • 第一列主要是 perm,其中也附帶顯示一些其它資訊:
    • 第一個字母是型別,- 表示普通檔案;
    • 之後分別是三組許可權位,特殊標誌也顯示在這裡
  • 第二列為硬連結數;
  • 第三列為 ouid;
  • 第四列為 ogid;
  • 第五列為 size,對於目錄只表示目錄檔案本身佔用的空間,不代表目錄內檔案佔用總空間,想要顯示目錄佔用總空間,需要使用 du 命令;
  • 第六列為最後修改日期;
  • 第七列為檔名。

使用 -h 選項使用 human readable 方式顯示檔案大小——新增合適的單位 (K/M/G) 來讓人更易讀,否則直接顯示位元組數;使用 -d 選項來列印目錄檔案本身而不是列出目錄下的檔案;ls 選項非常多,有興趣的同學可以自行 man 頁檢視。

case:file_group_unchanged.sh

這個用例用來驗證檔案的 ogid 在建立時確定,不隨使用者所屬使用者組的變更而變更。它由兩部分指令碼組成,第一部分指令碼中使用者將基於現在的組建立檔案,在使用者切換所屬組後,第二部分指令碼中將基於新切換的組再建立檔案,並分別列出兩個檔案的詳情,通過觀察它們的 ogid 來證明原檔案的組不變。先來看第一部分指令碼:

1 #! /bin/sh
2 echo "switch to user $(whoami)"
3 # ensure new user can create file
4 cd /tmp
5 
6 touch this_is_a_test_file
7 ls -lh this_is_a_test_file

再來看第二部分指令碼:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 groups $(whoami) 
 7 echo "show user and their group ok"
 8 
 9 touch this_is_a_demo_file
10 ls -lh this_is_a*
11 
12 rm this_is_a*
13 echo "remove testing file ok"

最後來看框架指令碼中新增的部分:

1     # case: file group unchanged
2     cp ./file_group_unchanged_1.sh /tmp/
3     cp ./file_group_unchanged_2.sh /tmp/
4     su - lippman -s /tmp/file_group_unchanged_1.sh
5     # change current owner's group
6     usermod -g share lippman
7     su - lippman -s /tmp/file_group_unchanged_2.sh
8     # change group back, otherwise we will got error on delete group
9     usermod -g lippman lippman

其中:

  • cp 命令將指令碼放置在所有使用者可訪問的目錄 (預設位置為使用者私有工作目錄,其它使用者一般不能訪問),以便下一步做測試;
  • su 命令將以新使用者的身份執行指令碼,這裡使用了 lippman 使用者,當然也可以選用其它任何使用者,usermod -g 在 root 許可權下執行時,可將任意使用者的 initgrp 設定為任意已存在的使用者組。
  • 夾在兩部分之間的指令碼 (line 6) 用於切換使用者所屬的組。

這個用例需要使用兩個分開指令碼的原因可以羅列如下:

  • 在指令碼中呼叫 usermod 總是報錯,提示沒有許可權 (即使只是將 initgrp 修改為supgrp 中的一個也是如此);
  • 如果使用 sudo usermod,則需要將 lippman 加入 sudoer 檔案才能起作用,但是那樣就感覺測試用例的可移植性差一些了;
  • usermod 命令修改使用者組之後,使用者需要重新登入才能生效,這裡每次 su 就相當於一次使用者登入。

上面指令碼的執行結果如下:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
-rw-r--r-- 1 lippman lippman 0 May 30 21:13 this_is_a_test_file
switch to user lippman
lippman : share men
show user and their group ok
-rw-r--r-- 1 lippman lippman 0 May 30 21:13 this_is_a_test_file
-rw-r--r-- 1 lippman share   0 May 30 21:13 this_is_a_demo_file
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

重點觀察新舊檔案的 ogid 項,發現使用者切換組後,原檔案的 ogid 不受影響,和預期的一致。

最後需要補充的一點是,su -s 選項用來基於新使用者身份執行一段指令碼,而不能直接輸入 su username,否則會在指令碼中執行過程中彈出互動式子 shell 從而導致執行被中斷。

程序的使用者與組

程序有比較多的使用者和組屬性:

  • 實際使用者 ID
  • 實際組 ID
  • 有效使用者 ID
  • 有效組 ID
  • 附加組 ID
  • 儲存的設定使用者 ID
  • 儲存的設定組 ID

讓我從程序的建立開始一一梳理:使用者的初始程序是由登入 (login) 程式啟動的,它讀取 passwd 配置檔案中該使用者對應的 uid 和 gid,作為使用者根程序的實際使用者 ID 和實際組 ID,用於標識程序是誰,一般在整個登入會話過程中不會改變,當然超級使用者可以改變它們,這個話題超出了文章的範圍,放在以後說明。程序的有效使用者 ID 和有效組 ID 預設情況下與實際使用者 ID 和實際組 ID 一致,只有當出現以下情況時它們才不一致:  

  • 新程序的可執行檔案有 setuid 標誌且 ouid 與當前使用者不同;
  • 新程序的可執行檔案有 setgid 標誌且 ogid 與當前使用者不同。

場景一,程序的有效使用者 ID 被設定為可執行檔案的 ouid;場景二,程序的有效組 ID 被設定為可執行檔案的 ogid;兩個標誌可以同時存在,亦可以同時生效 (網上有說法只有一個能生效是不對的,請看本節末尾的驗證用例)。

有效使用者 ID 與有效組 ID 是程序訪問檔案時核心許可權檢查的主要依據,具體的檢查過程請參考“程序訪問檔案時核心許可權檢查過程”這節。

程序的附加組 ID 即啟動程序使用者的附加使用者組 (supgrp),這個作為有效組 ID 的補充手段用於許可權校驗,附加組 ID 中每個組都與有效組 ID 的作用等價 (即只要有一個附加使用者組匹配了檔案 ogid,那麼對應的許可權就會生效)。沒有"設定附加組 ID" 這類的東西,所以附加組都是“原汁原味”不會改變的,這一點請看本節最後的驗證用例。

以我們耳熟能詳的 access 函式為例,它使用的是實際使用者 ID 與實際組 ID 進行訪問許可權檢查,而不是有效使用者 ID 和有效組 ID,也就是說 access 返回失敗的檔案,程序並不一定就不能訪問,這一點需要注意 (雖然沒什麼用,因為你也不能確定它可以訪問)。書上有一個很好的例子,本節就不再畫蛇添足了,在“程序訪問檔案時核心許可權檢查過程”這節中你可以看到一個 shell 版本的 demo,演示了相同的功能。

為了簡化後面的描述,將使用以下術語表示上面的概念:

  • 程序實際使用者 ID:ruid (real uid)
  • 程序實際組 ID:rgid (real gid)
  • 程序有效使用者 ID:euid (effective uid)
  • 程序有效組 ID:egid (effective gid)
  • 程序附加組 ID:supgid (supplementary gid)
  • 程序儲存的設定使用者 ID:save setuid
  • 程序儲存的設定組 ID:save setgid

與程序 ID 相關 api 羅列如下:

  • ruid:getuid / setuid / setreuid / getresuid / setresuid
  • rgid:getgid / setgid / setregid / getresuid / setresuid
  • euid:geteuid / seteuid / setreuid / getresgid / setresgid
  • egid:getegid / setegid / setregid / getresgid / setresgid
  • supgid:getgroups / setgroups
  • save setuid:getresuid / setresuid
  • save setgid:getresgid / setresgid

set 部分一般需要嚴格的許可權檢查,留在以後介紹程序關係時說明。save setuid / save setgid 和程序執行過程中執行 exec 相關,也不在這裡展開。先來看 get 部分,有些 api 可以一次性獲取多個 id,所以在一個 ID 後面會跟多種獲取途徑。一般通過 ps 命令來顯示程序的各種 ID:

$ ps -axo pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 1031    81    81    81    81    81    81 -                    /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
 1037   998   997   998   997   998   997 997                  /usr/bin/lsmd -d
 5971   999   998   999   998   999   998 998                  /usr/lib/polkit-1/polkitd --no-debug
12357  1002  1003  1002  1003  1002  1003 1003                 sshd: yunh@pts/0
12465  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
14457    89    89    89    89    89    89 12,89                pickup -l -t unix -u
28301    89    89    89    89    89    89 12,89                qmgr -l -t unix -u
28632     0     0     0     0     0     0 -                    /usr/sbin/rsyslogd -n
28675     0     0     0     0     0     0 -                    /usr/sbin/crond -n
28720   997   995   997   995   997   995 -                    /usr/sbin/chronyd
28839    32    32    32    32    32    32 -                    /sbin/rpcbind -w

上面我們講的各種 ID 和 ps 命令的 format 引數 (-o) 及標題之間關係如下:

  • ruid:-o ruid / RUID
  • rgid:-o rgid / RGID
  • euid:-o euid / EUID
  • egid:-o egid / EGID
  • supgid:-o supgid / SUPGID
  • save setuid:-o suid / SUID
  • save setgid:-o sgid / SGID

ps 還可以展示許多其它的 ID,和本文關係不大,就不一一羅列了。

case:setuid_setgid_order.sh

這個用例用於驗證 setuid 和 setgid 可以同時作用於一個可執行檔案,並且最終影響啟動的程序。這個例子由三段指令碼組成,需要在框架指令碼中新增如下程式碼:

1     # case: setuid setgid order
2     cp ./setugid /tmp/
3     cp ./setuid_setgid_order_1.sh /tmp/
4     cp ./setuid_setgid_order_2.sh /tmp/
5     cp ./setuid_setgid_order_3.sh /tmp/
6     su - lippman -s /tmp/setuid_setgid_order_1.sh
7     su - caveman -s /tmp/setuid_setgid_order_2.sh
8     su - lippman -s /tmp/setuid_setgid_order_3.sh

其中 setugid 是一個可執行檔案,啟動後 sleep 10 秒然後退出,主要是用來驗證啟動程序的一些屬性,比較簡單就不放原始碼了; line 3-5 將三段指令碼複製到公共目錄,原因同上;line 6-8 分別啟動三個使用者去執行指令碼。第一個指令碼用來準備 setuid / setgid 程式:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 # create setuid/setgid/setuid & setgid program
10 cp setugid setuid_demo 
11 chmod u+s,ugo+wx /tmp/setuid_demo
12 ls -lh setuid_demo
13 
14 cp setugid setgid_demo
15 chmod g+s,ugo+wx setgid_demo
16 ls -lh setgid_demo
17 
18 cp setugid setuid_setgid_demo
19 chmod ug+s,ugo+wx setuid_setgid_demo
20 ls -lh setuid_setgid_demo
21 
22 echo "create testing setuid/setgid file ok"

就是將 setugid 這個程式複製了三份,並分別設定了它們的 setuid / setgid / setuid & setgid 標誌位。注意這裡使用 id 列印了當前登入使用者的各種 ID 值,這個在後面會用到。第二段指令碼分別啟動三個程序,並列印它們的 ID 值:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 ./setuid_demo &
10 ./setgid_demo &
11 ./setuid_setgid_demo &
12 
13 echo "start setuid/setgid program ok"
14 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
15 
16 echo "waiting them to exit..."
17 wait

重點就是 line 9-11 了,使用子程序的方式啟動,這樣可以同步列印 ps 的輸出結果 (line 14),在退出這段指令碼前使用 wait 等待所有子程序結束。看到這裡似乎就足夠了,那第三段指令碼是用來做什麼的呢?答案是清理剛才的可執行檔案:

1 #! /bin/sh
2 echo "switch to user $(whoami)"
3 # ensure new user can create file
4 cd /tmp
5 
6 rm setuid_demo
7 rm setgid_demo
8 rm setuid_setgid_demo
9 echo "remove testing file ok"

至於為什麼要一個單獨的指令碼來清理,稍後再說,這裡先上指令碼的輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman),1004(men),1005(share)
-rwsrwxrwx 1 lippman lippman 9.4K May 31 20:51 setuid_demo
-rwxrwsrwx 1 lippman lippman 9.4K May 31 20:51 setgid_demo
-rwsrwsrwx 1 lippman lippman 9.4K May 31 20:51 setuid_setgid_demo
create testing setuid/setgid file ok
switch to user caveman
uid=1005(caveman) gid=1004(men) groups=1004(men)
start setuid/setgid program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
16113     0     0     0     0     0     0 0                    sudo ./user_init.sh
16124     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
16261     0     0     0     0     0     0 1004                 su - caveman -s /tmp/setuid_setgid_order_2.sh
16273  1005  1004  1005  1004  1005  1004 1004                 /bin/sh /tmp/setuid_setgid_order_2.sh
16275  1005  1004  1003  1004  1003  1004 1004                 ./setuid_demo
16276  1005  1004  1005  1006  1005  1006 1004                 ./setgid_demo
16277  1005  1004  1003  1006  1003  1006 1004                 ./setuid_setgid_demo
16278  1005  1004  1005  1004  1005  1004 1004                 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
waiting them to exit...
16275 exit
16277 exit
16276 exit
Last login: Mon May 31 20:51:09 CST 2021 on pts/0
switch to user lippman
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

首先各個檔案的 setuid / setgid 位被正確設定了;其次 ps 的輸出可以看到,都是使用 uid / gid 的形式,這裡就體現到了 id 命令的重要性,它已經提前將兩個使用者的 id 打印出來了,可以對號入座了:

  • setuid_demo:euid 1003 為 lippman,egid 1004 為 men;
  • setgid_demo:euid 1005 為 caveman,egid 1006 為 lippman;
  • setuid_setgid_demo:euid 1003 為 lippman,egid 1006 為 lippman;

全使用數字輸出可能有點亂,推薦將一個程式的 euid / egid 與它的 ruid / rgid 對比著來看,就能看出區別來:每個標誌位都能單獨起作用,不存在誰生效了另外一個就不生效的問題。

需要注意的一點就是,不能使用 shell 指令碼來充當 demo 程序,為 shell 指令碼檔案設定 setud / setgid 不會起作用,其實這個細想一下也可以想通——真正啟動的程序是 sh / bash 這類實體,shell 指令碼檔案只是它們解釋執行的資料檔案。

最後來說為什麼要將清理指令碼單獨列出來,這是因為 caveman 沒有刪除檔案的許可權,如果合併到指令碼二的話,會導致刪除失敗,所以有必要切回到建立檔案的使用者再去刪除檔案,關於刪除檔案需要的許可權,請參考“檔案訪問許可權位”一節; 關於 svtx 位設定後 (/tmp 目錄) 的刪除檔案許可權,請參考“程序訪問檔案時核心許可權檢查過程”一節。

case:process_supgid_unchanged.sh

這個用例主要用來驗證程序啟動後 supgid 不隨使用者 supgrp 改變而改變。這個例子由一段指令碼組成,被使用者執行兩次,使用者在執行期間 supgid 發生了改變。需要在框架指令碼中新增如下程式碼:

1     # case: process groups unchanged
2     cp ./setugid /tmp/
3     cp ./process_supgid_unchanged.sh /tmp/
4     rm /tmp/should_wait 2>/dev/null
5     su - lippman -s /tmp/process_supgid_unchanged.sh
6     # change current owner's supplementary group
7     usermod -G lippman lippman
8     touch /tmp/should_wait
9     su - lippman -s /tmp/process_supgid_unchanged.sh

主要分為三步,先以當前附加使用者組啟動程序 (line 5),然後改變使用者的附加程序組 (line 7),最後以新的附加使用者組啟動程序 (line 9)。通過對比兩次啟動的程序 supgid 來觀察它們的差異。這裡以使用者身份啟動一個指令碼的方法與之前相同,不同的是設定了一個標誌位檔案 /tmp/should_wait 來標識是否需要等待啟動的程序,這也是研究了很多方法之後找到的一個解決方案,之前嘗試過使用環境變數、使用者配置檔案 (~/.bash_profile),都達不到期望的效果。下面來看測試指令碼的內容:

 1 #! /bin/bash
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 ./setugid &
10 echo "start program ok"
11 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
12 
13 if [ -f "/tmp/should_wait" ]; then 
14     echo "waiting them to exit..."
15     wait
16 fi

主要就是啟動程序 (line 9),列印程序資訊 (line 11),這個程序還是複用的上個用例中的 setugid 程式,主要是利用它啟動後 sleep 10 秒的時機通過 ps 來觀察一些程序的屬性。和之前修改使用者組一樣,修改了使用者的附加組資訊後,需要使用者重新登入才能生效,所以這裡需要同樣的使用者執行兩次指令碼。在第二次執行時,如果也不等待 demo 子程序結束就退出,會導致刪除使用者時報錯:

userdel: user lippman is currently used by process 4911

而第一次執行時又不能等待子程序 (需要保證舊的程序還執行時修改使用者附加組資訊),所以這裡使用了事先配置好的標誌檔案 (/tmp/should_wait) 來決定是否等待。下面看下指令碼的輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman),1004(men),1005(share)
start program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
 4677     0     0     0     0     0     0 0                    sudo ./user_init.sh
 4688     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
 4892     0     0     0     0     0     0 1004,1005,1006       su - lippman -s /tmp/process_supgid_unchanged
 4909  1003  1006  1003  1006  1003  1006 1004,1005,1006       /bin/bash /tmp/process_supgid_unchanged.sh
 4911  1003  1006  1003  1006  1003  1006 1004,1005,1006       ./setugid
 4912  1003  1006  1003  1006  1003  1006 1004,1005,1006       ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
27258  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
28544  1002  1003  1002  1003  1002  1003 1003                 vim user_init.sh
Last login: Tue Jun  1 11:34:10 CST 2021 on pts/1
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman)
start program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
 4677     0     0     0     0     0     0 0                    sudo ./user_init.sh
 4688     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
 4911  1003  1006  1003  1006  1003  1006 1004,1005,1006       ./setugid
 4923     0     0     0     0     0     0 1006                 su - lippman -s /tmp/process_supgid_unchanged
 4934  1003  1006  1003  1006  1003  1006 1006                 /bin/bash /tmp/process_supgid_unchanged.sh
 4936  1003  1006  1003  1006  1003  1006 1006                 ./setugid
 4937  1003  1006  1003  1006  1003  1006 1006                 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
27258  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
28544  1002  1003  1002  1003  1002  1003 1003                 vim user_init.sh
waiting them to exit...
4911 exit
4936 exit
delete user ok
remve user home dir ok
delete group ok

重點看一下 setugid 程序的 SUPGID 資訊,第一次啟動時,lippman 擁有三個附加組:1006 / 1004 / 1005 (通過 id 命令),啟動程序的 SUPGID 項和使用者 supgrp 內容一致; 第二次啟動前,通過 -G lippman 為使用者指定了唯一的一個附加組 1006,使用者的 supgrp 和新程序的 supgid 果然都變成了 1006,而舊程序的 supgid 仍保持三個不變 (新舊程序可以通過程序號區分)。

這個結論和使用者 gid 與檔案 ogid 的關係非常類似,都是設定後不隨使用者的改變而改變了。最後解釋一下為什麼沒有“設定使用者附加組 ID”這種東西,根源在於檔案 inode 中沒有預留空間儲存 supgid,附加組都是程序啟動時從 /etc/group 中獲取的;另一方面,即使能儲存,這個使用者組許可權匹配過程也會變得複雜,由 ogid -> egid + supgid 一對多的關係,變為多對多的關係,這麼一整就亂套了。

檔案訪問許可權位

所有型別的檔案都有訪問許可權位,包括目錄,不過目錄的許可權位與普通檔案的許可權位意義稍有不同,下面會詳細說明。許可權位共有 9 個,按針對的使用者範圍分為三類:

  • 檔案建立者
  • 檔案建立者所在的使用者組 (該組的所有使用者,使用者組為該使用者的附加組也算)
  • 不在上面範圍的其它使用者

針對每類使用者,又有三類訪問許可權:

  • 讀 (r)
  • 寫 (w)
  • 執行 (x)

它們規定了每類使用者具有的許可權,如果申請的許可權超過了給定的許可權,訪問就會被拒絕。常用的一些操作及它們申請的許可權羅列如下:

  • 開啟檔案
    • 路徑中的每個目錄:x
    • 路徑中的每個符號連結:- (無許可權要求,主要看跳轉過程中所涉及到目錄和檔案的許可權)
    • 檔案本身:
      • O_RDONLY:r
      • O_WRONLY:w
      • O_RDWR:rw
      • O_TRUNC:w
  • 建立檔案
    • 路徑中的每個目錄:x
    • 路徑中的每個符號連結:- (無許可權要求,主要看跳轉過程中所涉及到目錄和檔案的許可權)
    • 直屬目錄:wx
  • 刪除和重新命名檔案
    • 路徑中的每個目錄:x
    • 路徑中的每個符號連結:- (無許可權要求,主要看跳轉過程中所涉及到目錄和檔案的許可權)
    • 直屬目錄:wx
    • 待刪除和重新命名檔案:- (無許可權要求)
  • 執行檔案 (通過 exec 函式族啟動程序):
    • 路徑中的每個目錄:x
    • 路徑中的每個符號連結:- (無許可權要求,主要看跳轉過程中所涉及到目錄和檔案的許可權)
    • 檔案本身:x
  • 列出目錄
    • 路徑中的每個目錄:x
    • 路徑中的每個符號連結:- (無許可權要求,主要看跳轉過程中所涉及到目錄和檔案的許可權)
    • 目錄本身:r

需要對上面的目錄許可權位做一些單獨說明:

  • 目錄執行許可權位 (x) 也稱為搜尋位,當一個目錄位於路徑的一部分時,如果使用者沒有目錄的執行位許可權,則不能通過該目錄找到下一級檔案或目錄,許可權校驗直接失敗;
  • 對某個檔案進行操作時,至少需要通過一個目錄,如果是絕對路徑,就是根目錄;如果是相對路徑,就是當前目錄,沒有指定 ./ 也會隱含通過當前目錄;
  • 對同一個檔案,使用不同的路徑效果也會不同,例如 /usr/include/stdio.h,使用絕對路徑是一種方式,如果當前目錄位於 /usr/include/path/to/current/dir,則使用 ../../../../stdio.h 也是一樣的,但是如果 path/to/current/dir 中有任意一個目錄沒有搜尋位 (x),則檔案訪問就會失敗;反之亦然。這裡主要是想強調一下“路徑中的每個目錄”的重要性,例子本身舉的比較牽強,畢竟那些目錄沒有搜尋位的話,當前目錄也是不可能切 (cd) 過去的;

為了簡化後面的描述,將使用以下術語表示上面的概念:

  • 許可權分組建立者:uperm (owner perm)
  • 許可權分組建立者組:gperm (owner group perm)
  • 許可權分組其它使用者:operm (other perm)
  • 讀許可權:r (read)
  • 寫許可權:w (write)
  • 執行/搜尋許可權:x (execute)

以上許可權對應到 stat 中 st_mode 欄位的關係如下:

  • uperm
    • r:S_IRUSR
    • w:S_IWUSR
    • x:S_IXUSR
    • rwx:S_IRWXU
  • gperm
    • r:S_IRGRP
    • w:S_IWGRP
    • x:S_IXGRP
    • rwx:S_IRWXG
  • operm
    • r:S_IROTH
    • w:S_IWOTH
    • x:S_IXOTH
    • rwx:S_IRWXO

使用 access 進行許可權訪問時, mode 引數指定的標誌位與許可權對應關係如下:

  • R_OK:r
  • W_OK:w
  • X_OK:x
  • F_OK:- (只看檔案是否存在,不檢查許可權位)

四個標誌位是可以組合使用的。shell 也有內建命令 (或者說選項) 來檢查檔案的型別和訪問許可權,其中檔案型別使用的關鍵字和 ls、find 相同 (-f / -d / -l ...),許可權方面則和 access 類似:

  • -r: r
  • -w: w
  • -x: x
  • -e: exist (不檢查型別,只檢查是否存在)

下節的測試用例演示了 shell 的內建許可權檢查。

程序訪問檔案時核心許可權檢查過程

有了上面的基礎,再談程序訪問檔案時的許可權檢查過程就簡單多了:

  • euid == root,允許訪問
  • euid == ouid:
    • 申請的 perm <= uperm,允許訪問
    • 否則拒絕訪問
  • egid == ogid 或 supgid 包含 ogid 時:
    • 申請的 perm <= gperm,允許訪問
    • 否則拒絕訪問
  • 否則 (歸類於 other):
    • 申請的 perm <= operm,允許訪問
    • 否則拒絕訪問

這裡需要注意幾點:

  • 檢查過程是“熔斷”的,即一個程序被歸類為檔案的某個許可權分組後,當該分組許可權不滿足時,即使更低級別的許可權分組允許,也不再向後嘗試,而是直接拒絕。舉個例子,如果某個檔案許可權位為 --- --- rwx,則只要程序屬於使用者的 owner 或 owner group,那麼一定不允許訪問,反而是不在上面範圍內的其它使用者,可以獲得訪問許可權,具體可參考本節末尾給出的測試用例;
  • 這個檢查過程和上一節中提到的檔案路徑中每個目錄需要執行許可權是不矛盾的,也就是說,完整的過程是對路徑中每個節點,依次執行本節說的檢查過程,不過呢,對於路徑中通過的目錄節點,申請的 perm 固定為 x 而已。舉個例子,想要讀取 /usr/include/stdio.h 檔案,需要分四步(每步都需要執行上面完整的過程):
    • 檢查當前程序與 / 目錄的 x 許可權;
    • 檢查當前程序與 /usr 目錄的 x 許可權;
    • 檢查當前程序與 /usr/include 目錄的 x 許可權;
    • 檢查當前程序與 /usr/include/stdio.h 檔案的 r 許可權。
  • 上一個例子擴充套件一下,如果是想執行 /usr/local/bin/sed 程式,目錄部分完全相同,只是最後一步有一些區別:
    • 檢查當前程序與 /usr/local/bin/sed 檔案的 x 許可權。
  • 如果想要在 /usr/local/bin 下建立新的可執行檔案,則整個過程是這樣的:
    • 檢查當前程序與 / 目錄的 x 許可權;
    • 檢查當前程序與 /usr 目錄的 x 許可權;
    • 檢查當前程序與 /usr/local 目錄的 x 許可權;
    • 檢查當前程序與 /usr/local/bin 目錄的 wx 許可權;  
    • 新建立檔案的許可權與當前程序有關,這方面內容請參考“新建檔案的許可權”一節。
  • 如果要刪除或更名檔案,大部分檢查過程和建立完全一樣,就像之前說過的,刪除檔案和檔案本身許可權設定其實沒什麼關係。

不過如果直屬目錄設定了 svtx 粘住位,則刪除、重新命名檔案的許可權檢查過程稍有不同:

  • 有直屬目錄的 wx 許可權,這個基本條件不變;
  • 還需要滿足以下條件:
    • euid == root
    • 或 euid == 檔案 ouid
    • 或 euid == 直屬目錄 ouid

也就是說刪除、重新命名 svtx 目錄下的檔案時,要求非超級使用者程序必需擁有該檔案或檔案所在的直屬目錄,換個通俗的說法就是——你只能動你自己建立的檔案,不能動其它人建立的的檔案。我們熟知的 /tmp 目錄允許任何人在其中建立檔案,而它就設定了 svtx 位。如果你要建立多個帳戶之間的共享目錄 (如 /share),使用 svtx 位是一個好習慣,具體可參考本節末尾的測試用例。

使用之前介紹過的 find 命令搜尋帶有特殊標誌位檔案的辦法,來查詢一下測試機上的 svtx 目錄,得到如下輸出:

$ find / -perm -o+t 2>/dev/null | xargs ls -ldh
drwxrwxrwt  2 root root   60 Mar 31 10:58 /dev/mqueue
drwxrwxrwt  2 root root   80 Apr 25 10:59 /dev/shm
drwxrwxrwt  2 root root 4.0K Mar 31 10:42 /matrix/matrix-bios/run
drwxr-xr-t  3 root root 4.0K May 22 16:36 /matrix/matrix-bios/var/state
drwxrwxrwt 12 root root 4.0K May 22 16:45 /tmp
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.font-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.ICE-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.Test-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.X11-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.XIM-unix
drwxrwxrwt  2 root root 4.0K Oct 31  2018 /var/cache/coolkey
drwxrwxrwt  6 root root 4.0K May 22 13:00 /var/tmp

可以看到臨時目錄赫然在列。

case:perm_group_fuse.sh

這個指令碼單純驗證以下許可權組熔斷規則:uperm > gperm > operm,當訪問被歸類到某一級別的許可權組後,就不再向低級別許可權組探查。先來看探查許可權的指令碼:

 1 #! /bin/sh
 2 if [ $# -lt 1 ]; then 
 3     echo "Usage: probe_file_perm.sh file_to_test"
 4     exit 1
 5 fi
 6 
 7 filename=$1
 8 if [ -e "$filename" ]; then 
 9     echo "exist"
10 else
11     echo "not exist"
12     exit 1
13 fi
14 
15 if [ -r "$filename" ]; then 
16     echo "can read"
17 else
18     echo "can NOT read"
19 fi
20 
21 if [ -w "$filename" ]; then 
22     echo "can write"
23 else
24     echo "can NOT write"
25 fi
26 
27 if [ -x "$filename" ]; then 
28     echo "can execute"
29 else
30     echo "can NOT exeucte"
31 fi
32 
33 exit 0

其實就是 shell 版的 access,通過 strace 來觀察這段指令碼的執行,發現其底層呼叫的 api 和 access 是一致的,所以這裡所有的結論也適用於 access。下面來看呼叫點:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 echo "checking this_is_a_test_file"
 7 ./probe_file_perm.sh "this_is_a_test_file"
 8 echo ""
 9 
10 echo "checking this_is_a_demo_file"
11 ./probe_file_perm.sh "this_is_a_demo_file"
12 echo ""

對兩個 demo 檔案分別進行許可權測試,這兩個檔案的建立請看這段指令碼:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 if [ ! -e this_is_a_test_file ]; then 
 7     touch this_is_a_test_file
 8     # '--- r-x -w-'
 9     chmod 0052 this_is_a_test_file
10     ls -lh this_is_a_test_file
11 fi
12 
13 if [ ! -e this_is_a_demo_file ]; then 
14     touch this_is_a_demo_file
15     # '--- r-x -w-'
16     chmod 0052 this_is_a_demo_file
17     ls -lh this_is_a_demo_file
18 fi
19 
20 echo "prepare testing file ok"

當檔案不存在時,建立對應的檔案並設定許可權。注意這裡許可權比較特殊,0052 對應的許可權位是 "--- r-x -w-",即 uperm 沒有任何許可權、gperm 可讀可執行、operm 可寫,奇葩是夠奇葩的,不過這樣能很容易的根據最終訪問許可權來確定命中了哪組許可權位。最後上框架指令碼中的驅動程式碼:

 1     # case: permission group fusing
 2     cp ./probe_file_perm.sh /tmp/
 3     cp ./perm_group_fuse_1.sh /tmp/
 4     cp ./perm_group_fuse_2.sh /tmp/
 5     rm /tmp/this_is_a_test_file 2>/dev/null
 6     su - steven -s /tmp/perm_group_fuse_1.sh
 7     rm /tmp/this_is_a_demo_file 2>/dev/null
 8     su - caveman -s /tmp/perm_group_fuse_1.sh
 9     # start access test
10     su - steven -s /tmp/perm_group_fuse_2.sh
11     su - caveman -s /tmp/perm_group_fuse_2.sh
12     su - paperman -s /tmp/perm_group_fuse_2.sh
13     su - lippman -s /tmp/perm_group_fuse_2.sh

下面做個簡單說明:

  • line 2-4:準備測試指令碼;
  • line 5-8:生成測試檔案,注意這裡為了讓兩個不同使用者使用同樣的指令碼生成不同的檔案,通過檔案是否存在來控制檔案的生成,最終 steven 使用者生成的是 this_is_a_test_file; caveman 使用者生成的是 this_is_a_demo_file;
  • line 10-13:讓各種使用者去測試這兩個檔案的訪問許可權。

ok,我們知道框架指令碼中對使用者組的設定是這樣的:

  • men:caveman / paperman / lippman
  • share:steven / lippman
  • lippman:lippman

所以很容易就可以弄明白以下關係:

  • line 10:通過 steven 測試 test 檔案的 uperm 與 demo 檔案的 operm;
  • line 11:通過 caveman 測試 test 檔案的 operm 與 demo 檔案的 uperm;
  • line 12:通過 paperman 測試 test 檔案的 operm 與 demo 檔案的 gperm;
  • line 13:通過 lippman 測試 test 檔案的 gperm 與 demo 檔案的 gperm;

所有許可權組基本上是都覆蓋到了,由於檔案許可權位的獨特性,可以得到 uperm 無許可權;gperm 有讀和執行許可權;operm 有寫許可權,下面通過輸出來驗證一下:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user steven
----r-x-w- 1 steven share 0 Jun  1 19:58 this_is_a_test_file
prepare testing file ok
switch to user caveman
----r-x-w- 1 caveman men 0 Jun  1 19:58 this_is_a_demo_file
prepare testing file ok
Last login: Tue Jun  1 19:58:01 CST 2021 on pts/1
switch to user steven
checking this_is_a_test_file
exist
can NOT read
can NOT write
can NOT exeucte

checking this_is_a_demo_file
exist
can NOT read
can write
can NOT exeucte

Last login: Tue Jun  1 19:58:01 CST 2021 on pts/1
switch to user caveman
checking this_is_a_test_file
exist
can NOT read
can write
can NOT exeucte

checking this_is_a_demo_file
exist
can NOT read
can NOT write
can NOT exeucte

switch to user paperman
checking this_is_a_test_file
exist
can NOT read
can write
can NOT exeucte

checking this_is_a_demo_file
exist
can read
can NOT write
can execute

switch to user lippman
checking this_is_a_test_file
exist
can read
can NOT write
can execute

checking this_is_a_demo_file
exist
can read
can NOT write
can execute

delete user ok
remve user home dir ok
delete group ok

上面的資料可以整理成表格:

this_is_a_test_file (steven) this_is_a_demo_file (caveman)
steven --- (uperm) -w- (operm)
caveman -w- (operm) --- (uperm)
paperman -w- (operm) r-x (gperm)
lippman r-x (gperm) r-x (gperm)

表格中每行表示一個使用者,每列表示一個檔案,行列交叉處表示程序對檔案的訪問許可權。可見,每個程序的最終許可權與之前清單中列出的預期是一致的,說明確實是熔斷了。例如以 steven 使用者程序訪問 test 檔案為例,如果沒有發生熔斷,當 uperm 判定無許可權後,那是不是應該退而求其次使用 gperm 判斷了?如此一來最終的訪問許可權就變成了 r-x 而不是 ---。

case:share_with_svtx.sh

這個測試用例用來驗證 svtx 目錄中,使用者不能刪除、重新命名不屬於自己的檔案,除非使用者擁有檔案或直屬目錄。這個用例分為兩段指令碼,第一段用來建立一些測試目錄和檔案,第二段用來進行測試。首先看第一段指令碼:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 if [ ! -d /tmp/share ]; then 
 7     mkdir /tmp/share
 8     # allow every to create file 
 9     chmod ugo+rwx,o+t /tmp/share
10     ls -lhd /tmp/share
11 fi
12 
13 touch "/tmp/share/$(whoami)"
14 # 'rwx rw- r--'
15 chmod 0731 "/tmp/share/$(whoami)"
16 ls -lh "/tmp/share/$(whoami)"
17 
18 echo "prepare testing file ok"

這個指令碼會被每個使用者執行一遍,因此在建立共享目錄 /tmp/share 之前,需要做個檢測 (line 6)。為了保證每個使用者都可以在共享目錄下建立檔案,目錄的許可權被設定為了 'rwx rwx rwt',其中 t 是為了驗證 svtx 位作用於目錄的效果 (line 9)。之後每個使用者在共享目錄下建立以自己命名的檔案,檔案許可權設定為 'rwx rw- r--',以區分不同的許可權分組 (line 14-16)。下面看下第二段指令碼:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # $1: file to move
 7 try_move_file ()
 8 {
 9     local file="$1"
10     local file_new="$1.new"
11     mv "$file" "$file_new" 2>/dev/null
12     if [ $? -eq 0 ]; then 
13         echo "can move"
14         # move back
15         mv "$file_new" "$file"
16     else
17         echo "can NOT move"
18     fi
19 }
20 
21 echo "checking /tmp/share/steven"
22 ./probe_file_perm.sh "/tmp/share/steven"
23 try_move_file "/tmp/share/steven"
24 echo ""
25 
26 echo "checking /tmp/share/caveman"
27 ./probe_file_perm.sh "/tmp/share/caveman"
28 try_move_file "/tmp/share/caveman"
29 echo ""
30 
31 echo "checking /tmp/share/paperman"
32 ./probe_file_perm.sh "/tmp/share/paperman"
33 try_move_file "/tmp/share/paperman"
34 echo ""
35 
36 echo "checking /tmp/share/lippman"
37 ./probe_file_perm.sh "/tmp/share/lippman"
38 try_move_file "/tmp/share/lippman"
39 echo ""

這個指令碼也會被每個使用者分別執行,它對每個使用者建立在共享目錄下的檔案挨個進行訪問許可權檢查 (line 21-39),許可權檢查是複用之前的 probe_file_perm.sh 指令碼進行的;重新命名檢查是通過 shell 函式 try_move_file 進行的;這裡沒有對刪除進行測試,主要是一旦刪除成功,還需要重新建立檔案以便下個使用者檢測,比較費事。重新命名成功後,也需要將重新命名後的檔案再重新命名回來,防止下個使用者找不到要檢測的檔案,這個體現在 try_move_file 中了 (line 15)。最後將它們整合在框架指令碼中:

 1     # case: share with svtx
 2     cp ./probe_file_perm.sh /tmp/
 3     cp ./share_with_svtx_1.sh /tmp/
 4     cp ./share_with_svtx_2.sh /tmp/
 5     su - steven -s /tmp/share_with_svtx_1.sh
 6     su - caveman -s /tmp/share_with_svtx_1.sh
 7     su - lippman -s /tmp/share_with_svtx_1.sh
 8     su - paperman -s /tmp/share_with_svtx_1.sh
 9     # start access test
10     su - paperman -s /tmp/share_with_svtx_2.sh
11     su - lippman -s /tmp/share_with_svtx_2.sh
12     su - caveman -s /tmp/share_with_svtx_2.sh
13     su - steven -s /tmp/share_with_svtx_2.sh
14     rm -rf /tmp/share

總體分為三步:複製指令碼檔案 (line 2-4);準備測試檔案 (line 5-8);進行測試 (line 10-13);最後通過刪除共享目錄來清理測試檔案。ok,看下指令碼執行效果:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user steven
drwxrwxrwt 2 steven share 4.0K Jun  2 15:00 /tmp/share
-rwx-wx--x 1 steven share 0 Jun  2 15:00 /tmp/share/steven
prepare testing file ok
switch to user caveman
-rwx-wx--x 1 caveman men 0 Jun  2 15:00 /tmp/share/caveman
prepare testing file ok
switch to user lippman
-rwx-wx--x 1 lippman lippman 0 Jun  2 15:00 /tmp/share/lippman
prepare testing file ok
switch to user paperman
-rwx-wx--x 1 paperman men 0 Jun  2 15:00 /tmp/share/paperman
prepare testing file ok
Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user paperman
checking /tmp/share/steven
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/caveman
exist
can NOT read
can write
can execute
can NOT move

checking /tmp/share/lippman
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/paperman
exist
can read
can write
can execute
can move

Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user lippman
checking /tmp/share/steven
exist
can NOT read
can write
can execute
can NOT move

checking /tmp/share/caveman
exist
can NOT read
can write
can execute
can NOT move

checking /tmp/share/lippman
exist
can read
can write
can execute
can move

checking /tmp/share/paperman
exist
can NOT read
can write
can execute
can NOT move

Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user caveman
checking /tmp/share/steven
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/caveman
exist
can read
can write
can execute
can move

checking /tmp/share/lippman
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/paperman
exist
can NOT read
can write
can execute
can NOT move

Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user steven
checking /tmp/share/steven
exist
can read
can write
can execute
can move

checking /tmp/share/caveman
exist
can NOT read
can NOT write
can execute
can move

checking /tmp/share/lippman
exist
can NOT read
can NOT write
can execute
can move

checking /tmp/share/paperman
exist
can NOT read
can NOT write
can execute
can move

delete user ok
remve user home dir ok
delete group ok

建立的測試檔案許可權符合預期,第一個建立測試檔案的使用者為 steven,它順便建立了共享目錄,因而目錄所有者就是 steven,而其它使用者只是自己檔案的擁有者,注意這點區分,會對接下來的分析產生影響。進行檔案許可權檢查的輸出比較多,這裡通過表格來展示:

steven caveman lippman paperman
paperman --x (operm) -wx (gperm) --x (operm) rwx (uperm)
lippman -wx (gperm) -wx (gperm) rwx (uperm) -wx (gperm)
caveman --x (operm) rwx (uperm) --x (operm) -wx (gperm)
steven rwx (uperm) --x (operm) --x (operm) --x (operm)

這個表和之前用例中的表大同小異——每列表示一個檔案,每行表示一個使用者,行列交叉處表示使用者對檔案的訪問許可權。由於檔名直觀的顯示了是由哪個使用者建立的,所以列標題得以簡化。與之前用例同理,根據最終訪問許可權可以倒推出命中的是什麼許可權分組,而這也正好映射了兩個使用者之間的關係,即使用者訪問自己的檔案,是 uperm;訪問同組使用者的檔案,是 gperm;否則是 operm。

上表中得到的結論和實際一致嗎?大體是差不多的,但是可能有一些地方需要推敲一下,例如為何 paperman / caveman 訪問 lippman 的檔案是 operm,而 lippman 訪問 paperman / caveman 的檔案卻是 gperm,難道不應該都是 gperm 嗎?畢竟它們都屬於 men 組呀。這裡再看一下它們建立的檔案詳情:

-rwx-wx--x 1 lippman lippman 0 Jun  2 14:34 /tmp/share/lippman
-rwx-wx--x 1 caveman men 0 Jun  2 14:34 /tmp/share/caveman
-rwx-wx--x 1 paperman men 0 Jun  2 14:34 /tmp/share/paperman

不看不知道,一看嚇一跳,lippman 檔案的 ogid 是 lippman;caveman / paperman 檔案的 ogid 是 men。於是 lippman 使用者訪問 caveman 和 paperman 檔案時,通過 supgid 中的 men 進行了匹配,所以是 gperm 許可權組;反之,caveman / paperman 使用者訪問 lippman 檔案時,沒有使用者組可以匹配 lippman 使用者組,於是只能命中 operm,這是導致訪問許可權差異的根源。通過之前的章節我們知道,gid 是可以傳遞給檔案並記錄下來的 (ogid),而 supgrp 是無法傳遞並記錄在檔案中的 (只能記錄在程序的 supgid),所以,雖然使用者都在一個組,但是它們產生的檔案可能並不在一個組。同理,lippman 訪問 steven 檔案和 steven 訪問 lippman 檔案的差異,也是由此而來。

為了清晰起見,下面單獨列出使用者是否可以重新命名檔案:

steven caveman lippman paperman
paperman no no no yes
lippman no no yes no
caveman no yes no no
steven yes yes yes yes

只看前三行的話,是比較明確的——使用者只能重新命名自己的檔案,同組的使用者也不能相互重新命名。再看第四行使用者 steven,它可以對所有人的檔案進行重新命名,這是為什麼呢?如果大家還記得 steven 是目錄的建立者的話,就不會覺得驚訝了,這一條直接讓它符合了這條許可權檢查規則:euid == 直屬目錄 ouid。另外可以在框架指令碼中一行 rm -rf /tmp/share 刪掉所有檔案包括共享目錄本身,也和另一條規則相關:euid == root。無意間將所有規則都演示了一遍,妙哇~

新建檔案的許可權

檔案不會自己產生,它們都是由程序建立的。不論是通過命令還是 api,新建檔案的許可權與建立程序的屬性密切相關,具體影響規則如下:

  • ouid
    • <= euid
  • ogid
    • mac / freebsd <= 直屬目錄 ogid
    • Solaris:
      • 直屬目錄有 setgid 標誌位 <= 直屬目錄 ogid
      • 否則 <= egid
    • linux:
      • 檔案系統 mount 時指定了 grpid 或 bsdgroups 引數:
        • 直屬目錄有 setgid 標誌位 <= 直屬目錄 ogid
        • 直屬目錄沒有 setgid 標誌位 <= egid
      • 否則 (未指定或明確指定了 nogrpid 或 sysvgroups 引數) <= egid
  • perm
    • open 或 creat 時指定的 mode 引數
    • umask 指定的遮蔽字 (在最終的 perm 中關閉對應的位)

對於新檔案的所有權,ouid 是比較明確的,就是繼承建立程序的 euid;ogid 稍微複雜一些,不過總的來說就兩個途徑:要麼繼承程序的 egid、要麼繼承直屬目錄的 ogid,具體需要按系統分情況來看。當然了,以上內容都源自 apue,作者成書較早,當時的系統版本都比較老,例如針對的 linux 平臺還是 2.4 的核心,我用 CentOS 7.5 (核心 3.10) 驗證的時候發現有些出入——檔案系統掛載 (mount) 時並未使用 grpid 或 bsdgroups 引數,但是也遵循上面的規則。即:

  • 直屬目錄有 setgid:ogid <= 直屬目錄 ogid
  • 否則:ogid <= 建立程序 egid

本節末尾的測試用例驗證了這一點,其它平臺限於本文討論的範圍沒做驗證。可以把目錄的 setgid 理解成是強制繼承目錄的組許可權,從而保證以該目錄為根節點的路徑樹對一組使用者有一致的訪問許可權 (通過 ogid),而不會出現這樣的情況——雖然某個使用者程序屬於目錄 ogid 所在的組,但它是通過 supgid 加入此組的,而它自己的 egid 卻不在這個組,這樣它雖然可以在這個目錄下建立檔案,但這個組的其它使用者卻不能訪問這些檔案,從而導致這個目錄變得“支離破碎”。關於同組使用者卻不能建立同組檔案的例項,請參考 “share_with_svtx” 用例。下面使用第一節介紹的 find 引數來查詢一下測試機上的 setgid 目錄:

# find / -type d -perm -g+s 2>/dev/null | xargs ls -lhd
drwxr-sr-x  5 root systemd-journal 100 May 23 12:57 /run/log/journal
drwxr-sr-x  2 root systemd-journal  40 May 23 12:56 /run/log/journal/def
drwxr-s---+ 2 root systemd-journal 120 May 31 19:05 /run/log/journal/efe3d136ddc241e382a960b78ccc4718

得到的結果非常少,改變一下 ls 的選項,讓它遞迴列出目錄內容,看看這裡面都有些什麼:

# find / -type d -perm -g+s 2>/dev/null | xargs ls -lhR
/run/log/journal:
total 0
drwxr-x---  2 root root             60 Mar 31 10:41 86bac26592284276a583f8df03ff9a47
drwxr-sr-x  2 root systemd-journal  40 May 23 12:56 def
drwxr-s---+ 2 root systemd-journal 120 May 31 19:05 efe3d136ddc241e382a960b78ccc4718

/run/log/journal/86bac26592284276a583f8df03ff9a47:
total 8.0M
-rw-r----- 1 root root 8.0M Mar 31 10:41 system.journal

/run/log/journal/def:
total 0

/run/log/journal/efe3d136ddc241e382a960b78ccc4718:
total 168M
-rwxr-x---+ 1 root systemd-journal 56M Apr 16 14:40 system@6678dc3c22194c5190a67760a8fcc447-0000000000000001-0005becc0ba81976.journal
-rw-r-----+ 1 root systemd-journal 48M May  9 09:25 system@6678dc3c22194c5190a67760a8fcc447-000000000000d1f4-0005c0113fd51d85.journal
-rw-r-----+ 1 root systemd-journal 48M May 31 19:02 system@6678dc3c22194c5190a67760a8fcc447-0000000000017e6d-0005c1db87b877ab.journal
-rw-r-----+ 1 root systemd-journal 16M Jun  3 18:09 system.journal

/run/log/journal/def:
total 0

/run/log/journal/efe3d136ddc241e382a960b78ccc4718:
total 168M
-rwxr-x---+ 1 root systemd-journal 56M Apr 16 14:40 system@6678dc3c22194c5190a67760a8fcc447-0000000000000001-0005becc0ba81976.journal
-rw-r-----+ 1 root systemd-journal 48M May  9 09:25 system@6678dc3c22194c5190a67760a8fcc447-000000000000d1f4-0005c0113fd51d85.journal
-rw-r-----+ 1 root systemd-journal 48M May 31 19:02 system@6678dc3c22194c5190a67760a8fcc447-0000000000017e6d-0005c1db87b877ab.journal
-rw-r-----+ 1 root systemd-journal 16M Jun  3 18:09 system.journal

看起來絕大部分檔案的確繼承了根目錄的 ogid,如果一個使用者只是通過 supgid 加入了 systemd-journal 組,那麼它建立的 journal 檔案也將可以被同組的其它使用者訪問到。

關於 setgid 最後再補充一點,新建檔案型別為目錄時,遵循完全相同的規則,與普通檔案唯一的不同是,當組 ID 繼承直屬目錄的 ogid 時,同時也會繼承它的 setgid 標誌位,具體細節請參考節末用例。

最後再來說明一下 umask,在終端有一個同名的命令,可以列印當前 umask 值:

$ umask
0002

也可以用更直觀的方式列印:

$ umask -S
u=rwx,g=rwx,o=rx

這種情況下顯示的是最終保留的位。子程序一般會繼承父程序的 umask 值,不過子程序修改 umask 值不會影響父程序。

case:setgid_parent_dir.sh

這個用例用來驗證 setgid 作用於目錄時,目錄下的檔案 ogid 將繼承目錄的 ogid 而不是建立者的 egid。這個用例只有一段指令碼:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 if [ ! -d /tmp/share ]; then 
 7     mkdir /tmp/share
 8     # allow everyone to create file 
 9     chmod ugo+rwx,g+s /tmp/share
10     ls -lhd /tmp/share
11 fi
12 
13 mkdir "/tmp/share/$(whoami)_dir"
14 touch "/tmp/share/$(whoami)_file"
15 ls -lhd /tmp/share/$(whoami)_*
16 
17 echo "prepare testing file ok"

這段指令碼將以使用者身份執行,第一個進入的使用者負責建立 setgid 共享目錄 (line 6-11),目錄的 ogid 將追隨使用者的 egid;然後每個使用者在這個目錄下面建立一個目錄、一個普通檔案,命名規則是“使用者名稱_file | dir”,然後列出它們以驗證 ogid 繼承了父目錄。注意 ls 的引數 (line 15) 不能用引號包圍,否則會報找不到檔案錯誤,原因是 shell 萬用字元只在不被引號包圍的情況下才能生效。在框架指令碼中加入啟動程式碼:

1     # case: setgid parent dir
2     cp ./setgid_parent_dir.sh /tmp/
3     su - lippman -s /tmp/setgid_parent_dir.sh
4     su - caveman -s /tmp/setgid_parent_dir.sh
5     su - paperman -s /tmp/setgid_parent_dir.sh
6     su - steven -s /tmp/setgid_parent_dir.sh
7     #ls -lh "/tmp/share/"
8     rm -rf /tmp/share

挨個使用者執行該指令碼。最終輸出如下:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
drwxrwsrwx 2 lippman lippman 4.0K Jun  3 21:13 /tmp/share
drwxr-sr-x 2 lippman lippman 4.0K Jun  3 21:13 /tmp/share/lippman_dir
-rw-r--r-- 1 lippman lippman 0 Jun  3 21:13 /tmp/share/lippman_file
prepare testing file ok
switch to user caveman
drwxr-sr-x 2 caveman lippman 4.0K Jun  3 21:13 /tmp/share/caveman_dir
-rw-r--r-- 1 caveman lippman 0 Jun  3 21:13 /tmp/share/caveman_file
prepare testing file ok
switch to user paperman
drwxr-sr-x 2 paperman lippman 4.0K Jun  3 21:13 /tmp/share/paperman_dir
-rw-r--r-- 1 paperman lippman 0 Jun  3 21:13 /tmp/share/paperman_file
prepare testing file ok
switch to user steven
drwxr-sr-x 2 steven lippman 4.0K Jun  3 21:13 /tmp/share/steven_dir
-rw-r--r-- 1 steven lippman 0 Jun  3 21:13 /tmp/share/steven_file
prepare testing file ok
delete user ok
remve user home dir ok
delete group ok

可以看到以下現象:

  • 共享目錄確實設定了 setgid 位 (rws) 且其 ogid 為建立者 egid (lippman);
  • 每個新建立的檔案,不論普通檔案還是目錄,ogid 繼承了共享目錄的 ogid (lippman) 而非建立使用者的 egid (men / share);
  • 每個新目錄也自動繼承了直屬目錄的 setgid 位 (r-s)。

一個用例驗證了兩個結論。現在回過頭來看目錄 setgid 的作用,它保證了這個目錄下的所有檔案 (一般情況而言) 都有一致的 ogid,那這個有什麼用處呢,下面分幾個方面來考察一下。

所有能在這個目錄下建立檔案的使用者都能刪除、重新命名別人的檔案嗎?

是的。能建立檔案說明程序有目錄 wx 許可權,就能刪除和重新命名檔案,進一步的,如果有目錄 r 許可權,還可以列出所有檔案。

所有能在這個目錄下建立檔案的使用者都能訪問目錄下的檔案嗎?

不一定,檔案雖然都有一樣的 ogid,但是還要看檔案建立者對 gperm 的設定,例如上面預設的設定是 r-- (目錄 r-x),那麼同組的人就只能讀、不能寫,建立者可以設定任意的 gperm 來允許或阻止同組人對自己檔案的訪問,所以結論是:在 setgid 目錄下能建立檔案的人,只能讀取、寫入或執行其它使用者願意讓你讀取、寫入或執行的檔案。一般情況下是會允許同組使用者讀和執行的,不然也沒必要在共享目錄中建立檔案了。

考察了 setgid 目錄的特性,再回到這個標誌位的用處上來,前面其實已經簡單說過,但是這個問題書上沒有講,網上也很少有人涉及,還比較費思量,多廢一些口舌是值得的。如果僅僅是實現上面所說的功能,將需要訪問目錄的使用者加到同一個使用者組 (group id) 不就行了嗎?特別是有附加使用者組 (supgrp) 這種好東西,幾乎可以將一個使用者指派到無限的使用者組;然後將目錄 ogid 設定為這個組,這樣當用戶建立檔案時,新檔案的 ouid 跟隨程序 euid、ogid 跟隨程序的 egid 也就是目錄 ogid,不也能達到一樣的效果嗎?哎,等等,這裡好像有什麼地方不對,新檔案的 ogid 是使用者程序的 egid 沒錯,但是不一定就是目錄 ogid 啊,為什麼呢?因為使用者屬於這個組可能是通過附加使用者組 (supgrp),而不是初始使用者組 (initgrp),這樣一來,它建立的檔案 ogid 將和父目錄的 ogid 不同。

這段話說的有點繞,舉個例子來說明,還以框架指令碼中的使用者為例,如果父目錄 ogid 為 men,steven 不在對應的組,沒有訪問許可權;caveman / paperman / lippman 在這個組中,都能建立檔案;其中 caveman / paperman 在目錄中建立檔案的 ogid 跟隨自己的 egid 也將為 men;而 lippman 建立的檔案 ogid 將為 lippman 而非 men,從而在這個目錄下面製造了一個“另類”,這個檔案另類到其它同組使用者可能根本沒有訪問許可權——即使設定了 gperm 的適當許可權也不行,除非放開 operm 許可權,但是那樣又會導致檔案被任意不相干的人訪問,這不是我們期望的。

所以刨根究底,目錄 setgid 其實是為了填使用者附加組 (supgrp) 挖的坑,讓幾乎不設限制的把一個使用者新增到附加組這種行為,最終能得到期望的執行效果……

那麼,最終要如何設定某個組的共享目錄才是合理的合理的呢?以下幾個是需要注意的點:

  • 目錄 ogid 必需為要共享的組的 gid;
  • 目錄最好設定 setgid;
  • 有額外要求的話 (禁止刪除、重新命名非自己建立的檔案),可以設定 svtx 位。

之後,如果有哪個使用者需要加入共享組,直接將這個組新增到他的附加組並重新登入即可,反之,將使用者從組中刪除即可。

更改檔案許可權

檔案許可權的變更主要分為兩部分,一是檔案所有權 (ouid / ogid) 的變更; 一是檔案訪問許可權的變更 (perm)。下面分別說明。

變更檔案所有權

檔案所有權可通過 chown / chgrp 命令或基於 chown / fchown / lchown api 變更,最終能否設定新的所有權由以下規則決定:

  • euid == root
  • 或 euid == ouid
    • new owner == -1 或 new owner == ouid
    • 且 new group == -1 或 new group == ogid 或 supgid 包含 new group

即超級使用者可以將任意檔案更改到任意使用者;而檔案 owner (或 setuid 後相當於檔案 owner) 程序只可以將屬於自己的檔案 ogid 更新到本人所屬的其它組 (egid 和 supgid 組成的範圍)。其中 -1 在 api 介面中表示對應欄位不變更,相當於忽略對應的欄位。當然並不是所有平臺都是這樣,sysv 系統和 freebsd 稍有不同:

  • freebsd / mac 都遵循和 linux 一樣的規則:只允許 root 使用者修改檔案到任意使用者;
  • Solaris 預設配置和 linux 一樣,但是也可以通過修改配置來遵循 sysv 的規則:允許任意使用者修改自己擁有的檔案到其它使用者。

不允許非 root 使用者修改檔案所有權的目的,據書上的說法是為了防止使用者擺脫磁碟空間限額,這裡限於文章範圍,沒有做驗證。非超級使用者更改檔案所有權後,普通檔案的特殊標誌位也會跟著變化:

  • setuid 標誌位:清除
  • setgid 標誌位:清除

為什麼在更新檔案所有權後要清除這兩個位呢?書上沒有細說,感覺主要是 chown 後 setgid 代表的使用者組發生了變更,防止誤用。關於這一點可以參考本節末尾的用例。

下面簡單瞭解一下 chown 與 chgrp 的用法:

chown user:group file
chown user file
chown :group file
chgrp group file

chown 可同時變更檔案的 ouid 與 ogid,使用冒號分隔使用者名稱與組名。如果只修改其中一個,另一個可以留空,特別當用戶組為空時,冒號可以省略;chgrp 相對“單純”一些,只能修改檔案的 ogid。詳細的選項可以參考 man 手冊頁。

變更檔案訪問許可權

檔案訪問許可權可通過 chmod 命令或基於 chmod / fchmod api 變更,最終能否設定新的許可權由以下規則決定:

  • euid == root
  • 或 euid == ouid

即只有超級使用者、檔案 owner (或 setuid 後相當於檔案 owner) 程序可以更改檔案的許可權。新的檔案許可權由 mode 引數指定,這裡的 mode 引數和 open / creat 一致。注意沒有 lchmod,也就是說符號連結本身的許可權基本是被忽略的,沒有修改的必要,對此有疑問的同學可參考 man 手冊頁中的這段描述:

       chmod  never  changes  the permissions of symbolic links; the chmod system call cannot
       change their permissions.  This is not a problem since  the  permissions  of  symbolic
       links  are  never  used.   However, for each symbolic link listed on the command line,
       chmod changes the permissions of the pointed-to file.  In contrast, chmod ignores sym‐
       bolic links encountered during recursive directory traversals.

在 mac 上符號連結的許可權是可以被修改的 (通過 -h 選項),不過即使將符號連結的許可權都關閉,仍可以通過它找到目標檔案,僅是 readlink 出錯不能讀取符號連結內容而已,所以將符號連結的許可權當成空氣就好了。

除了可以設定許可權位以外,還可以設定特殊標誌位。特殊標誌位除了遵循上面通用的規則,還有自己特定的規則,下面分別說明:

svtx 位

對於非超級使用者設定普通檔案的 svtx 標誌位,不同系統有不同的策略:

  • freebsd / mac / Solaris:忽略 (只允許 root 設定)
  • linux:允許 (其實設定了也沒什麼效果)

限於本文範圍,只在 linux 上做了驗證,可參考本節末尾的用例。這個 svtx 位 (作用於普通檔案) 在很早之前是為了減少頻繁啟動程式 (例如 vi / gcc) 的記憶體交換而設計的一種機制 (程式退出後仍保留在記憶體中以便下次快速載入),現在隨著作業系統記憶體管理天翻地覆的改變,早已不再使用了,如果不是後來擴展出了作用於目錄的用法,這個標誌都不可能保留到現在。對於還支援這個標誌位的系統 (例如 Solaris),它的底層機制也完全不同了,可能使用了某種快取記憶體機制;不過普通檔案如果沒有可執行位,那麼系統也不會快取記憶體它們,因為這個標誌位只針對普通檔案中的可執行檔案生效。

目錄 setgid 位

在 setgid 目錄中建立檔案時,新檔案的 ogid 會追隨直屬目錄而非建立程序 (見“新建檔案的許可權”一節),上一節末尾的用例中提到當檔案型別為目錄時,新目錄會繼承父目錄的 setgid 位。而在以下條件成立時,新目錄的 setgid 繼承會被自動關閉:

  • euid != root
  • 且新檔案 ogid != egid
  • 且新檔案 ogid 不包含於 supgid

不論新目錄有沒有 setgid 位,它的 ogid 必定是直屬目錄的 ogid 沒錯了,所以並不是直屬目錄的 setgid 沒起作用,而是不再向下傳遞了而已。那為什麼要設計這麼一條規則呢? 書上的說法是——“防止了使用者建立一個設定組 ID 檔案,而該檔案是由並非使用者組所屬的組擁有的”,聽的雲裡霧裡的,不過就規則本身來說可以這樣理解:有一個 setgid 的共享目錄,可以訪問它的人可分為三種,第一類是 root 或 owner,一般擁有最高許可權;第二類是組成員,使用者的 egid 或 supgid 中的一個必定可以匹配目錄的 ogid;第三類是其它使用者,就是不在上面範圍的使用者。從規則的描述上來看,第一類人基本不受權限制約,被排除,由第二三條規則可知也不是第二類人,所以規則其實指的就是第三類人,即目錄 operm 所描述的使用者集合。此時如果 operm 指定的許可權允許他們建立目錄,則新建立的目錄是不會自動繼承 setgid 標誌位的。這個還是比較容易驗證的,以 setgid_parent_dir.sh 為例,再溫習一下它的輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
drwxrwsrwx 2 lippman lippman 4.0K Jun  7 20:28 /tmp/share
drwxr-sr-x 2 lippman lippman 4.0K Jun  7 20:28 /tmp/share/lippman_dir
-rw-r--r-- 1 lippman lippman    0 Jun  7 20:28 /tmp/share/lippman_file
prepare testing file ok
switch to user caveman
drwxr-sr-x 2 caveman lippman 4.0K Jun  7 20:28 /tmp/share/caveman_dir
-rw-r--r-- 1 caveman lippman    0 Jun  7 20:28 /tmp/share/caveman_file
prepare testing file ok
switch to user paperman
drwxr-sr-x 2 paperman lippman 4.0K Jun  7 20:28 /tmp/share/paperman_dir
-rw-r--r-- 1 paperman lippman    0 Jun  7 20:28 /tmp/share/paperman_file
prepare testing file ok
switch to user steven
drwxr-sr-x 2 steven lippman 4.0K Jun  7 20:28 /tmp/share/steven_dir
-rw-r--r-- 1 steven lippman    0 Jun  7 20:28 /tmp/share/steven_file
prepare testing file ok
delete user ok
remve user home dir ok
delete group ok

共享目錄 share 是由 lippman 建立的,所以它的 owner 是 lippman,組也是 lippman,而其它使用者無論 ogid 還是 supgid,都不屬於 lippman 組,因此符合上面所說第三類人的條件,但是輸出顯示它們建立的目錄卻都繼承了 setgid 位,可見書中這裡描述有誤。為了驗證這一點,修改 setgid_parent_dir.sh 中一行設定程式碼:

chmod ug+rwx,g+s /tmp/share

去掉了 o 來關閉對其它使用者的寫入許可權,再執行一下指令碼,得到如下輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
drwxrwsr-x 2 lippman lippman 4.0K Jun  7 20:31 /tmp/share
drwxr-sr-x 2 lippman lippman 4.0K Jun  7 20:31 /tmp/share/lippman_dir
-rw-r--r-- 1 lippman lippman    0 Jun  7 20:31 /tmp/share/lippman_file
prepare testing file ok
switch to user caveman
mkdir: cannot create directory '/tmp/share/caveman_dir': Permission denied
touch: cannot touch '/tmp/share/caveman_file': Permission denied
ls: cannot access /tmp/share/caveman_*: No such file or directory
prepare testing file ok
switch to user paperman
mkdir: cannot create directory '/tmp/share/paperman_dir': Permission denied
touch: cannot touch '/tmp/share/paperman_file': Permission denied
ls: cannot access /tmp/share/paperman_*: No such file or directory
prepare testing file ok
switch to user steven
mkdir: cannot create directory '/tmp/share/steven_dir': Permission denied
touch: cannot touch '/tmp/share/steven_file': Permission denied
ls: cannot access /tmp/share/steven_*: No such file or directory
prepare testing file ok
delete user ok
remve user home dir ok
delete group ok

其它使用者果然都無法建立任何檔案了,可見之前的推論是沒問題的。

檔案 setuid / setgid 位

對於非超級使用者程序寫入一個檔案時,以下特殊標誌位會自動關閉:

  • setuid 標誌位:清除
  • setgid 標誌位:清除

這一策略主要是為了防止一些黑客篡改帶有 setuid / setgid 位的可執行檔案,讓它們以普通使用者身份獲得超級使用者許可權去執行一些惡意程式碼來幹壞事。本節末尾有一個用例演示了這一點。

如果使用 truncate / ftruncate 代替 write 來“寫”檔案時,得到的結果相同。

case:chgrp_clear_setgid.sh

這個用例主要用來驗證變更檔案 ouid 或 ogid 後,檔案的 setuid 與 setgid 標誌位會被清除。它由一段指令碼組成:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 touch "/tmp/this_is_a_demo_file"
 7 chmod ug+s,o+t "/tmp/this_is_a_demo_file"
 8 ls -lh "/tmp/this_is_a_demo_file"
 9 
10 mkdir "/tmp/this_is_a_demo_dir"
11 chmod ug+s,o+t "/tmp/this_is_a_demo_dir"
12 ls -lhd "/tmp/this_is_a_demo_dir"
13 
14 #chown lippman:men "/tmp/this_is_a_demo_file"
15 #chown :men "/tmp/this_is_a_demo_file"
16 chgrp men "/tmp/this_is_a_demo_file"
17 #chgrp share "/tmp/this_is_a_demo_file"
18 #chgrp lippman "/tmp/this_is_a_demo_file"
19 ls -lh "/tmp/this_is_a_demo_file"
20 
21 chgrp share "/tmp/this_is_a_demo_dir"
22 ls -lhd "/tmp/this_is_a_demo_dir"
23 
24 echo "chgrpp clear setgid over"

主要就是建立普通檔案 (line 6-8)、建立目錄 (line 10-12)、變更檔案所有權 (line 16,21)、檢查標誌位 (line 19,22),這裡同時使用普通檔案和目錄檔案作為對比。修改檔案所有權的方式有很多,這裡列舉了四種方法 (line 14-16),選擇了最直觀的 chgrp 方式。此處只可將檔案 ogid 設定為程序 egid + supgid 範圍內的使用者組 (line 16-18 men / share / lippman),否則報錯:

chgrp: changing group of '/tmp/this_is_a_demo_file': Operation not permitted

在框架指令碼中合適的位置插入程式碼來啟動這個用例:

 1     # case: change ogid clear setgid
 2     rm /tmp/this_is_a_demo_file 2>/dev/null
 3     rm -rf /tmp/this_is_a_demo_dir 2>/dev/null
 4     cp ./chgrp_clear_setgid.sh /tmp/
 5     su - lippman -s /tmp/chgrp_clear_setgid.sh
 6     chmod ug+s,o+t "/tmp/this_is_a_demo_file"
 7     chown caveman "/tmp/this_is_a_demo_file"
 8     ls -lh "/tmp/this_is_a_demo_file"
 9     chmod ug+s,o+t "/tmp/this_is_a_demo_dir"
10     chown steven "/tmp/this_is_a_demo_dir"
11     ls -lhd "/tmp/this_is_a_demo_dir"

這裡使用 lippman 使用者執行上面的用例,因為他同時屬於三個使用者組,可以在這之間做無縫切換,便於驗證。

上面那個指令碼只能驗證變更檔案 ogid,從本節前面的內容可以得知,想要變更檔案 ouid,必需使用 root 許可權,剛好框架指令碼具有 root 許可權,於是在後半部分順便驗證了檔案 ouid 的變更對特殊標誌位的影響 (line 6-11)。上面指令碼的輸出如下:

 1 $ sudo ./user_init.sh 
 2 create group ok
 3 create user ok
 4 lippman : lippman men share
 5 steven : share
 6 caveman : men
 7 paperman : men
 8 show user and their group ok
 9 switch to user lippman
10 -rwSr-Sr-T 1 lippman lippman 0 Jun  7 19:12 /tmp/this_is_a_demo_file
11 drwsr-sr-t 2 lippman lippman 4.0K Jun  7 19:12 /tmp/this_is_a_demo_dir
12 -rw-r-Sr-T 1 lippman men 0 Jun  7 19:12 /tmp/this_is_a_demo_file
13 drwsr-sr-t 2 lippman share 4.0K Jun  7 19:12 /tmp/this_is_a_demo_dir
14 chgrpp clear setgid over
15 -rw-r-Sr-T 1 caveman men 0 Jun  7 19:12 /tmp/this_is_a_demo_file
16 drwsr-sr-t 2 steven share 4.0K Jun  7 19:12 /tmp/this_is_a_demo_dir
17 delete user ok
18 remve user home dir ok
19 delete group ok

由於建立的是普通檔案,所以初始許可權為 "rw- r-- r--",經過 chmod 處理後變為 "rwS r-S r-T",從前面章節可以知道,這裡 S 分別表示 setuid 與 setgid 沒有 x 許可權位的組合,T 是 svtx 沒有 x 許可權的表示;相對的,由於目錄的初始許可權為 "rwx r-x r-x",經過 chmod 處理後就變為 “rws r-s r-t”,上面的輸出符合預期。第一次變更普通檔案 ogid,從 lippman 變更為了 men,再觀察檔案許可權位,發現 setuid 確實被清除了,setgid 卻仍然保留;第二次是變更普通檔案使用者 ouid,從 lippman 變更為 caveman,結果與前面相同。難道 x 許可權位的缺失導致了 setgid 沒有正確清除?抱著試一試的態度,將程式碼中 chmod 的引數修改如下:

chmod ug+xs,o+xt "/tmp/this_is_a_demo_file"
chmod ug+xs "/tmp/this_is_a_demo_dir"

為檔案的所有 perm 分組加入了 x 許可權,此時指令碼輸出如下:

create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
-rwsr-sr-t 1 lippman lippman 0 Jun  7 19:25 /tmp/this_is_a_demo_file
drwsr-sr-t 2 lippman lippman 4.0K Jun  7 19:25 /tmp/this_is_a_demo_dir
-rwxr-xr-t 1 lippman men 0 Jun  7 19:25 /tmp/this_is_a_demo_file
drwsr-sr-t 2 lippman share 4.0K Jun  7 19:25 /tmp/this_is_a_demo_dir
chgrpp clear setgid over
-rwxr-xr-t 1 caveman men 0 Jun  7 19:25 /tmp/this_is_a_demo_file
drwsr-sr-t 2 steven share 4.0K Jun  7 19:25 /tmp/this_is_a_demo_dir
delete user ok
remve user home dir ok
delete group ok

所有 S 變成了 s,且在變更檔案 ogid / ouid 後,都能正確的將 setuid 與 setgid 位清除了。從上面的輸出還可以得到以下結論:

  • 作用於普通檔案的 svtx (沒毛用) 不受影響;
  • 作用於目錄的 svtx 不受影響;
  • 作用於目錄的 setuid (沒毛用) 不受影響;
  • 作用於目錄的 setgid 不受影響;

case:write_clear_setuid.sh

這個用例是用來驗證 write 或 truncate 後,可執行檔案的 setuid 和 setgid 標誌位將被清除。它由一段指令碼組成:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # create setuid & setgid program
 7 cp setugid setugid_demo
 8 chmod ug+s,ugo+wx /tmp/setugid_demo
 9 ls -lh setugid_demo
10 
11 echo "create testing setuid/setgid file ok"
12 echo "1" >> setugid_demo
13 
14 echo "write 1 bytes into executable file"
15 ls -lh setugid_demo
16 
17 chmod ug+s,ugo+wx /tmp/setugid_demo
18 truncate -s 8K setugid_demo
19 
20 echo "truncate executable file to 8K"
21 ls -lh setugid_demo
22 
23 rm setugid_demo
24 echo "remove testing file ok"

主要分以下幾步:建立帶 setuid / setgid 標誌的可執行檔案 (line 7-9)、在檔案末尾寫入一位元組觀察結果 (line 11-15)、重新設定標誌位並截斷檔案再觀察結果 (line 17-21)。在框架指令碼中加入以下啟動程式碼:

1     # case: write clear setuid 
2     cp ./setugid /tmp/
3     cp ./write_clear_setuid.sh /tmp/
4     su - lippman -s /tmp/write_clear_setuid.sh

用 lippman 身份執行該指令碼。下面是指令碼輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
-rwsrwsrwx 1 lippman lippman 9.5K Jun  8 09:48 setugid_demo
create testing setuid/setgid file ok
write 1 bytes into executable file
-rwxrwxrwx 1 lippman lippman 9.5K Jun  8 09:48 setugid_demo
truncate executable file to 8K
-rwxrwxrwx 1 lippman lippman 8.0K Jun  8 09:48 setugid_demo
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

初始時測試檔案是帶著兩個標誌位的 (rws rws rwx),經過末尾寫入一 byte 後兩個標誌位消失了 (rwx rwx rwx),重新設定標誌位並截斷檔案後 (從 9.5 K 截短到 8K),兩個標誌位也能消失,驗證了上面的結論。

後記

寫了這麼多,是否已經把 linux 檔案許可權說盡了?非也非也。這都是幾十年前的東西了,現代 linux 也推出了更靈活的基於 ACL (Access Control List) 的訪問許可權設定,可以針對某個使用者做單獨的設定,讓他可以或不能訪問某個特定目錄或檔案,這比把使用者加入一個組並獲得該組所有目錄的訪問許可權要安全的多。下面是與 ACL 相關的命令:

  • setfacl
  • getfacl

當設定了 acl 後,ls 的輸出也會有所不同,通常是在許可權位末尾多一些特殊字元用以標記。此外還可以像 windows 一樣設定檔案的屬性:

  • chattr
  • lsattr

選項的不同,對檔案產生的影響也不同,這裡羅列一些比較常用的選項:

  • i:只讀屬性
    • 普通檔案:不允許對檔案進行刪除、重新命名、新增資料;
    • 目錄:不能建立、刪除、重新命名目錄下的檔案;
  • a:追加屬性
    • 普通檔案:只能向檔案中追加資料,不能刪除或編輯檔案;
    • 目錄:只能在目錄中建立或修改檔案,不能刪除檔案;
  • ……

可以實現對檔案、目錄行為更精細的控制。具體細節沒有做深入研究,這裡只是作為一個引子,推薦各位讀者繼續探索。不過再高深的許可權控制,也是以基礎知識作為根底的,例如設定 acl 時指定的許可權位 rwx 就和我們在前面說明的完全一致。

行文至此,我主要想寫一個關於目錄許可權的“突發奇想”——如果我只放開目錄的 wx 許可權,不放開 r 許可權,那麼其它使用者能在這個目錄下做什麼呢?根據前面的知識,我們知道 ’-wx‘ 許可權下使用者可以建立、刪除、重新命名檔案,也可以通過目錄訪問其中的檔案,唯獨不能列出目錄內容。那麼這個目錄對於使用者就像是一個“黑暗森林”,誰也看不到別人,甚至看不到自己,呃……好像還是蠻有用的,因為好多安全問題就是你的檔案暴露在了陌生人面前,如果他都看不到的話,你的檔案是不是就更安全了呢?

如果再加入 svtx 位,一個使用者不能刪除、重新命名另一個使用者的檔案,這樣就更有意思了:檔案的建立完全憑運氣,先到先佔用,建立的檔案失敗了,說明已經有人佔用了這個名稱,你只能換個別的名稱再試。好在有子目錄,如果將所有工作都放在子目錄中進行,衝突的概率應該會大大降低。唯一不方便的是時間長了可能忘記自己建立過哪些檔案,所以可能需要將建立過的檔案記錄在一個清單中……

對於 root 或 owner 這個目錄則是一覽無餘,擁有上帝視角,很好奇這樣一個目錄時間長了會發展成什麼樣子……哈哈,以上只是一些不著邊際的想法,供大家一樂。

下載

本文相關的指令碼都上傳在了 git 上,可以通過以下路徑訪問:

https://github.com/goodpaperman/apue/tree/master/04.chapter/permission

或者直接復刻整個庫到本地,再切換到對應目錄即可:

git clone [email protected]:goodpaperman/apue.git
cd apue/04.chapter/permission

每個用例對應一個或多個指令碼,多個指令碼時以數字字尾區分。在框架指令碼 user_init.sh 中可以通過將條件語句修改為 true 來開啟對應的用例,例如:

1 if true; then 
2     # case: write clear setuid 
3     cp ./setugid /tmp/
4     cp ./write_clear_setuid.sh /tmp/
5     su - lippman -s /tmp/write_clear_setuid.sh
6 fi

你可以開啟所有的用例開關,不過那樣輸出就會混在一起,閱讀起來不是特別方便。

參考

[1]. Linux檢視使用者所屬使用者組

[2]. 一個使用者最多能加入多少個組?

[3]. Linux的chmod與symbolic link

[4]. 檔案的uid、gid 程序的euid 、egid 、附加組ID(如果支援) 總結

[5]. Linux SetGID(SGID)檔案特殊許可權用法詳解

[6]. Linux下檢視某個使用者組下的所有使用者

[7]. Linux修改使用者所屬組的方法

[8]. shell不能執行su 後的指令碼

[9]. shell指令碼中使用其他使用者執行指令碼

[10]. Linux, sudo with no password (免密碼sudo)

[11]. Linux命令:修改檔案許可權命令chmod、chgrp、chown詳解

[12]. 關於 Linux系統使用者、組和許可權管理

[13]. Linux使用者(user)與使用者組(group)管理(超詳細解釋)

[14]. 配置 Linux 的訪問控制列表(ACL)