Linux® 多年來都使用能力(capability)的概念,但是最近實現了 POSIX 文件能力。POSIX 文件能力將根用戶的權力劃分成更小的特權,比如讀取文件或跟蹤另一個用戶擁有的進程。通過為文件分配能力,可以讓非特權用戶能夠用這些指定的特權執行文件。在本文中,了解程序如何使用能力,以及如何改變系統 setuid root 二進位代碼來使用文件能力。
一些程序需要以非特權用戶的身份執行特權操作。例如,passwd 程序經常對 /etc/passwd 和 /etc/shadow 文件執行寫操作。在 UNIX® 系統上,這種控制是通過設置二進位文件上的 setuid 位實現的。這個位告訴系統,在運行這個程序時,無論執行它的用戶是誰,都應該把它看作屬於擁有這個文件的用戶(通常是根用戶)。因為用戶不能編寫 passwd 程序,而且它對允許用戶執行的操作有嚴格限制,所以這個設置常常是安全的。更複雜的程序使用保存的 uid 在根用戶和非根用戶之間來回切換。
POSIX 能力將根特權劃分成更小的特權,所以可以只用根用戶特權的一個子集來運行任務。文件能力特性可以給一個程序分配這樣的特權,這大大簡化了能力的使用。在 Linux 中已經可以使用 POSIX 能力了。與將用戶切換為根用戶相比,使用能力有幾個好處:
|
本文講解程序如何使用 POSIX 能力,如何確定一個程序需要哪些能力,以及如何為程序分配這些能力。
進程能力
多年以來,POSIX 能力只能分配給進程,而不能分配給文件。因此,程序必須由根用戶啟動(或者程序屬於根用戶並設置了它的 setuid 位),然後才能放棄某些根特權,同時保留其他特權。另外,放棄能力的操作次序也非常嚴格:
進程有三個能力集:允許(permitted,P)、可繼承(inheritable,I) 和有效(effective,E)。在產生進程時,子進程從父進程複製能力集。當一個進程執行一個新程序時,根據公式計算新的能力集(稍後討論這些公式)。
有效集 中的能力是進程當前可以使用的。有效集必須是允許集 的子集。只要有效集不超過允許集的範圍,進程任何時候都可以修改有效集的內容。可繼承集 只用於在執行 exec() 之後計算新的能力集。
清單 1 給出三個公式,它們表示在文件執行之後根據 POSIX 草案計算出的新能力集(參見 參考資料 中 IEEE Std 1003.1-2001 的鏈接)。
pI' = pI pP' = fP | (fI & pI) pE' = pP' & fE |
以 ' 結尾的值表示新計算出的值。以 p 開頭的值表示進程能力。以 f 開頭的值表示文件能力。
可繼承集按原樣從父進程繼承,沒有任何修改,所以進程一旦從可繼承集中刪除一個能力,就應該無法再恢復它(但是請閱讀下面對 SECURE_NOROOT 的討論)。 新的允許集是文件的允許集與文件和進程的可繼承集的交集合併的結果。進程的有效集是新的允許集和文件有效集的交集。從技術上說,在 Linux 中,fE 不是一個集,而是一個布爾值。如果這個值是 true,那麼 pE' 就設置為 pP'。如果是 false,pE' 就是空的。
如果進程要在執行一個文件之後保留任何能力,那麼這些能力必須被包含在文件的允許集或可繼承集中。因為 Linux 在相當長的時期內沒有實現文件能力,所以這是一個難以實施的限制。為了解決這個問題,實現了 “安全模式(secure mode)”。它由兩位組成:
這套規則讓進程可以根據根用戶或者通過運行 setuid root 文件擁有能力。但是,SECURE_NO_SETUID_FIXUP 禁止進程在變成非根之後保留任何能力。但是,如果沒有設置 SECURE_NOROOT,那麼一個已經放棄一些能力的根進程只需執行另一個程序,就能夠恢復它的能力。所以為了能夠使用能力並保證系統安全,根進程必須能夠不可逆轉地將它的 uid 切換到非 0,同時保留一些能力。
通過使用 prctl(3),進程可以請求在下一次調用 setuid(2) 時保留它的能力。這意味著進程可以:
現在,進程可以一直用根特權的一個子集運行。如果攻擊者突破了這個程序,他也只能使用有效集中的能力;即使調用了 cap_set_proc(3),也只能使用允許集中的能力。另外,如果攻擊者迫使這個程序執行另一個文件,那麼所有能力都會撤消,將作為非特權用戶執行這個文件。
清單 2 中的 exec_with_caps() 函數可以縮減代碼的能力,setuid root 程序可以通過它作為指定的 userid 連續執行一個指定的函數,執行時的能力集由一個字元串指定。
#include <sys/prctl.h> #include <sys/capability.h> #include <sys/types.h> #include <stdio.h> int printmycaps(void *d) { cap_t cap = cap_get_proc(); printf("Running with uid %d\n", getuid()); printf("Running with capabilities: %s\n", cap_to_text(cap, NULL)); cap_free(cap); return 0; } int exec_with_caps(int newuid, char *capstr, int (*f)(void *data), void *data) { int ret; cap_t newcaps; ret = prctl(PR_SET_KEEPCAPS, 1); if (ret) { perror("prctl"); return -1; } ret = setresuid(newuid, newuid, newuid); if (ret) { perror("setresuid"); return -1; } newcaps = cap_from_text(capstr); ret = cap_set_proc(newcaps); if (ret) { perror("cap_set_proc"); return -1; } cap_free(newcaps); f(data); } int main(int argc, char *argv[]) { if (argc < 2) { printf("Usage: %s <capability_list>\n", argv[0]); return 1; } return exec_with_caps(1000, argv[1], printmycaps, NULL); } |
為了測試這個函數,將代碼複製到一個文件中並保存為 execwithcaps.c,編譯並作為根用戶運行它:
gcc -o execwithcaps execwithcaps.c -lcap ./execwithcaps cap_sys_admin=eip |
文件能力
文件能力特性當前是在 -mm 內核樹中實現的,有望在 2.6.24 版中被包含在主線內核中。可以利用文件能力特性將能力分配給程序。例如,ping 程序需要 CAP_NET_RAW。因此,它一直是一個 setuid root 程序。有了文件能力特性之後,就可以減少這個程序的特權數量:
chmod u-s /bin/ping setfcaps -c cap_net_admin=p -e /bin/ping |
這需要從 GoogleCode 獲得 libcap 庫和相關程序的最新版本(參見 參考資料 中的鏈接)。以上命令首先從二進位文件上刪除 setuid 位,然後給它分配所需的 CAP_NET_RAW 特權。現在,任何用戶都可以用 CAP_NET_RAW 特權運行 ping,但是如果 ping 程序被突破了,攻擊者也無法掌握其他特權。
問題在於,如何判斷一個非特權用戶在運行某個程序時需要的最小能力集。如果只考慮一個程序的話,那麼可以研究應用程序本身、它的動態鏈接庫和內核源代碼。但是,需要對所有 setuid root 程序都重複這個過程。當然,在允許非特權用戶作為根用戶運行一個應用程序之前,採用這種方法進行檢查並不是個壞主意,但是這種方法不切實際。
如果一個程序提供詳細的錯誤輸出而且表現正常,那麼不使用任何特權來運行這個程序,然後檢查錯誤消息,看看它缺少哪些特權。我們來對 ping 試試這種方法。
chmod u-s /bin/ping setfcaps -r /bin/ping su - myuser ping google.com ping: icmp open socket: Operation not permitted |
如果我們了解 icmp 的實現,這種技巧可以幫助我們判斷問題,但是它確實沒有把問題說清楚。
接下來,我們可以試著在 strace 之下運行這個程序(同樣不設置 suid 位)。strace 會報告這個程序使用的所有系統調用及其返回值,所以可以通過查看 strace 輸出中的返回值來判斷缺少的許可權。
strace -oping.out ping google.com grep EPERM ping.out socket(PF_INET, SOCK_RAW, IPPROTO_ICMP) = -1 EPERM (Operation not permitted) |
我們缺少創建套接字類型 SOCK_RAW 的許可權。查看 /usr/include/linux/capability.h,會看到:
/* Allow use of RAW sockets */ /* Allow use of PACKET sockets */ #define CAP_NET_RAW 13 |
顯然,為了允許非特權用戶使用 ping,需要的能力是 CAP_NET_RAW。但是,有些程序可能會試圖執行它們並不真正需要的操作,-EPERM 會拒絕這些操作。判斷它們真正需要的能力並不這麼容易。
另一種更可行的方法是,在內核中檢查能力的地方插入一個探測。這個探測輸出關於被拒絕的能力的調試信息。
開發人員可以用 kprobes 編寫小的內核模塊,從而在函數的開頭(jprobe)、函數的結尾(kretprobe)或在任何位置(kprobe)運行代碼。可以利用這個功能收集信息,了解內核在運行某些程序時需要哪些能力。(本節的餘下部分假設您的內核啟用了 kprobes 和文件能力。)
清單 3 是一個內核模塊,它插入一個 jprobe 來探測 cap_capable() 函數的開頭。
#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #include <linux/sched.h> static const char *probed_func = "cap_capable"; int cr_capable (struct task_struct *tsk, int cap) { printk(KERN_NOTICE "%s: asking for capability %d for %s\n", __FUNCTION__, cap, tsk->comm); jprobe_return(); return 0; } static struct jprobe jp = { .entry = JPROBE_ENTRY(cr_capable) }; static int __init kprobe_init(void) { int ret; jp.kp.symbol_name = (char *)probed_func; if ((ret = register_jprobe(&jp)) < 0) { printk("%s: register_jprobe failed, returned %d\n", __FUNCTION__, ret); return -1; } return 0; } static void __exit kprobe_exit(void) { unregister_jprobe(&jp); printk("capable kprobes unregistered\n"); } module_init(kprobe_init); module_exit(kprobe_exit); MODULE_LICENSE("GPL"); |
當插入這個內核模塊時,對 cap_capable() 的任何調用都被替換為對 cr_capable() 函數的調用。這個函數輸出需要能力的程序的名稱和被核查的能力。然後,通過調用 jprobe_return() 繼續執行實際的 cap_capable() 調用。
使用清單 4 中的 makefile 編譯這個模塊:
obj-m := capable_probe.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -f *.mod.c *.ko *.o |
然後作為根用戶執行它:
/sbin/insmod capable_probe.ko |
現在在一個窗口中,用以下命令查看系統日誌:
tail -f /var/log/messages |
在另一個窗口中,作為非根用戶執行沒有設置 setuid 位的 ping 二進位程序:
/bin/ping google.com |
系統日誌現在包含關於 ping 的幾條記錄。這些記錄指出這個程序試圖使用的能力。這些能力並非都是必需的。ping 請求的能力是 21、13 和 7,可以檢查 /usr/include/linux/capability.h,將整數轉換為能力名稱:
我們將這個能力授予 ping,看看它是否能夠成功執行。
setfcaps -c cap_net_raw=p -e /bin/ping (become non root user) ping google.com |
不出所料,ping 成功了。
複雜情況
現有的軟體常常編寫得儘可能可靠,在許多 UNIX 變體上很少有改動。發行版有時候會在此之上應用它們自己的補丁,所以在某些情況下不可能用文件能力替代 setuid 位。
這種情況的一個例子是 Fedora 上的 at。at 程序允許用戶將作業安排在以後某個時間執行。例如,可以在下午 2 點提醒用戶打電話:
echo "xterm -display :0.0 -e \ \"echo Call customer 555-5555; echo ^V^G; sleep 10m\" " | \ at 14:00 |
所有 UNIX 系統上都有 at 程序,任何用戶都可以使用它。用戶共享 /var/spool 下面的一個公用作業假離線文件。因此它的安全性極其重要,但是它是跨許多系統工作,所以不能使用系統特有的安全機制(比如能力)。無論如何,它試圖通過使用 setuid(2) 減少特權。在此基礎上,Fedora 通過應用補丁使用 PAM 模塊。
要想查明非根用戶是否可以運行不帶 setuid 位的 at,最快的方法是刪除 setuid 位,然後授予所有能力:
chmod u-s /usr/bin/at setfcaps -c all=p -e /usr/bin/at su - (non root user) /usr/bin/at |
通過指定 -c all=p,我們請求在 /usr/bin/at 上設置包含所有能力的允許能力集。所以,運行這個程序的任何用戶都擁有所有根特權。但是在 Fedora 7 上,運行 /usr/bin/at 會產生以下結果:
You do not have permission to run at. |
如果下載並研究源代碼,就可以找到原因,但是這些細節對本文沒有幫助。肯定可以修改源代碼,讓 at 能夠使用文件能力,但是在 Fedora 上簡單地分配文件能力並不能取代 setuid 位。
文件能力細節
在前面,我們使用一種專用的格式給可執行程序分配能力。我們對 ping 使用了以下命令:
setfcaps -c cap_net_raw=p -e /bin/ping |
setfcaps 程序通過設置一個名為 security.capability 的擴展屬性,設置目標文件的能力。-c 標誌後面是一個格式比較隨意的能力列表:
capability_list=capability_set(s) |
capability_set 可以包含 i 和 p,capability_list 可以包含任何有效能力。能力類型分別代表可繼承集和允許集,可以為每個集指定單獨的能力列表。-e 或 -d 標誌分別表示允許集中的能力在啟動時是否在程序的有效集中。如果能力不在程序的有效集中,那麼程序必須能夠感知能力,必須自己啟用有效集中的位,才能使用能力。
到目前為止,我們已經在允許集中設置了所需的能力,但是還沒有在可繼承集中設置。實際上,我們可以用能力實現更精細更強大的效果。下面回憶一下清單 1:
pI' = pI pP' = fP | (fI & pI) pE' = pP' & fE |
文件可繼承集決定進程的哪些可繼承能力可以放在新的進程允許集中。如果文件可繼承集中只有 cap_dac_override,那麼只能將這個能力繼承到新的進程允許集中。
文件允許集也稱為 “強迫(forced)” 集,其中的能力總是出現在新的進程允許集中,無論這些能力是否在任務的可繼承集中。
最後,文件有效位表示任務的新允許集中的位是否應該在新的有效集中設置;也就是說,程序是否能夠馬上使用這些能力,而不需要用 cap_set_proc(3) 顯式地請求它們。
如果沒有設置 SECURE_NOROOT,系統會對根用戶做一些修改。就是說,系統假設在執行文件時,可繼承集(fI)、允許集(fP)和有效集(fE)包含所有能力。所以二進位文件上的 fI 集只對具有非空能力集的非根進程有作用。對於在變成非根用戶時保留能力的程序,將應用上面的公式,而不會使用上面的假設。SECURE_NOROOT 以後可能會成為每個進程的設置,讓進程樹可以選擇是使用本身的能力,還是使用 root-user-is-privileged 模型。但是到編寫本文時,在任何實際系統上,這還是一個系統範圍的設置,它的默認設置讓根用戶總是擁有所有能力。
為了演示這些集的相互作用,假設管理員用以下命令在 /bin/some_program 上設置了文件能力:
setfcaps -c cap_sys_admin=i,cap_dac_read_search=p -e \ /bin/some_program |
如果一個非根用戶在擁有所有能力的情況下運行這個程序,首先計算它的可繼承集(pI)和 fI 的交集,所以縮減到只包含 cap_sys_admin。接下來,計算 fP 和這個集的並集,所以結果是 cap_sys_admin+cap_dac_read_search。這個集成為新的任務允許集。
最後,因為設置了有效位,新的任務有效集將包含新允許集中的兩個能力。
另一方面,如果一個完全沒有特權的用戶運行同一個程序,他的可繼承集是空的,這個集與 fI 求交集,會產生一個空集。這個空集與 fP 求並集,產生 cap_dac_read_search。這個集成為新的任務允許集。最後,因為設置了有效位,新的有效集複製新的允許集,同樣只包含 cap_dac_read_search。
在這兩種情況下,如果沒有設置有效位,那麼任務需要使用 cap_set_proc(3) 將它所需的位從允許集複製到有效集。
總結和練習
下面總結一下:
為了演示前面討論的內容,我們編寫了清單 5 和清單 6 中的程序。在清單 5 中,print_caps 僅僅輸出當前的能力集。在清單 6 中,嘗試作為根用戶執行 exec_as_nonroot_priv。它請求在下一次調用 setuid(2) 時保留它的能力,變成第一個命令行參數指定的非根用戶,將它的能力集設置為第二個命令行參數指定的集,然後執行第三個命令行參數指定的程序。
#include <stdio.h> #include <stdlib.h> #include <sys/capability.h> int main(int argc, char *argv[]) { cap_t cap = cap_get_proc(); if (!cap) { perror("cap_get_proc"); exit(1); } printf("%s: running with caps %s\n", argv[0], cap_to_text(cap, NULL)); cap_free(cap); return 0; } |
#include <sys/prctl.h> #include <sys/capability.h> #include <sys/types.h> #include <unistd.h> #include <stdio.h> void printmycaps(void) { cap_t cap = cap_get_proc(); if (!cap) { perror("cap_get_proc"); return; } printf("%s\n", cap_to_text(cap, NULL)); cap_free(cap); } int main(int argc, char *argv[]) { cap_t cur; int ret; int newuid; if (argc<4) { printf("Usage: %s <uid> <capset>" "<program_to_run>\n", argv[0]); exit(1); } ret = prctl(PR_SET_KEEPCAPS, 1); if (ret) { perror("prctl"); return 1; } newuid = atoi(argv[1]); printf("Capabilities before setuid: "); printmycaps(); ret = setresuid(newuid, newuid, newuid); if (ret) { perror("setresuid"); return 1; } printf("Capabilities after setuid, before capset: "); printmycaps(); cur = cap_from_text(argv[2]); ret = cap_set_proc(cur); if (ret) { perror("cap_set_proc"); return 1; } printf("Capabilities after capset: "); cap_free(cur); printmycaps(); ret = execl(argv[3], argv[3], NULL); if (ret) perror("exec"); } |
我們用這些程序檢驗一下可繼承集和允許集的效果。在 print_caps 上設置文件能力,然後用 exec_as_nonroot_priv 仔細設置初始進程能力集並執行 print_caps。首先,只在 print_caps 的允許集中設置一些能力:
gcc -o print_caps print_caps.c -lcap setfcaps -c cap_dac_override=p -d print_caps |
現在,作為非根用戶執行 print_caps:
su - (username) ./print_caps |
接下來,作為根用戶通過 exec_as_nonroot_priv 執行 print_caps:
./exec_as_nonroot_priv 1000 cap_dac_override=eip ./print_caps |
在這兩種情況下,print_caps 運行時的能力集都是 cap_dac_override=p。注意,有效位是空的。這意味著 print_caps 必須先調用 cap_set_proc(3),然後才能使用 cap_dac_override 能力。要想改變這種情況,可以在 setflags 命令中使用 -e 標誌設置有效位。
setfcaps -c cap_dac_override=p -e print_caps |
print_caps 的 fI 是空的,所以進程的 pI 中的能力都不能繼承到 pP' 中。pP' 只包含來自文件強迫集(fP)中的一位。
另一個有意思的測試檢驗可繼承文件能力的效果,同樣作為非根用戶和通過 exec_as_nonroot_priv 程序兩種方式運行 print_caps:
setfcaps -c cap_dac_override=i -e print_caps su - (nonroot_user) ./print_caps exit ./exec_as_nonroot_priv 1000 cap_dac_override=eip ./print_caps |
這一次,非根用戶的能力集是空的,作為根用戶啟動的進程的允許集和有效集中包含 cap_dac_override。
再次運行 print_caps,這一次直接作為根用戶運行,而不通過 exec_as_nonroot_priv。注意,能力集是空的。無論文件能力如何設置,根用戶在執行程序之後總是獲得完整的能力集。exec_as_nonroot_priv 並不作為根用戶運行 print_caps。相反,它使用根用戶的特權為非根進程設置一些可繼承能力。
(責任編輯:A6)
[火星人 ] POSIX 文件能力:分配根用戶的能力已經有786次圍觀