linux和docker的capabilities介紹


驗證環境:centos7 x86/64 內核版本4.19.9

在linux 2.2版本之前,當內核對進程進行權限驗證的時候,可以將進程划分為兩類:privileged(UID=0)和unprivilege(UID!=0)。其中privileged的進程擁有所有內核權限,而unprivileged則根據如可執行文件的權限(effective UID, effective GID,supplementary group等)進行判斷。

基於文件訪問的進程權限控制

此時進程執行主要涉及6個id:Real uid/gid,Effective uid/gid/supplementary group,Saved set-user-ID/saved set-group-ID。下面以不同的user id為例進行講解,group id也是類似的。

  • supplementary group為user的增補組,例如在添加一個名為usetTest1的user時候,-g執行該user的primary group,-G指定該usetTest1的supplementary groups。使用id命令可以看到“gid=”后面對應usetTest1的primary group,“groups=”后面對應usetTest1的supplementary groups。supplementary groups可以用與DAC驗證
[root@localhost ~]# groupadd newGrp1
[root@localhost ~]# groupadd newGrp2
[root@localhost ~]# useradd -u 10000 -g root -G newGrp1,newGrp2 userTest1
[root@localhost ~]# su userTest1
[userTest@localhost root]$ id
uid=10000(userTest) gid=0(root) groups=0(root),1001(newGrp1),1002(newGrp2) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
  • 進程的RUID為執行該進程的用戶ID,該值通常不需要變更;當判斷一個進程是否對某個可執行文件有權限時,需要驗證EUID,在沒有開啟SUID功能時,EUID的值等於RUID;SUID主要用於設置EUID。

在上一步中創建了一個usetTest1用戶,可以在/etc/passwd查看該用戶的home目錄,為/home/userTest1

userTest1:x:10000:0::/home/userTest1:/bin/bash

為驗證SUID的功能,su切換到userTest1,並在/home/userTest1下創建一個空文件,可以看到wr.log僅對用戶userTest1開放寫權限

[userTest1@localhost ~]# touch wr.log
[userTest1@localhost ~]# ll-rw-r--r--. 1 userTest1 root 10 Dec 13 18:50 wr.log

在/home/userTest1下編譯一個小程序,用於查看當前進程的RUID,EUID和SUID,並寫入wr.log。可以看到getIds對所有用戶開發了可執行權限

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    uid_t ruid;
    uid_t euid;
    uid_t suid;
    getresuid(&ruid, &euid, &suid);
    printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d\n",ruid,euid,suid);

    uid_t rgid;
    uid_t egid;
    uid_t sgid;

    getresgid(&rgid, &egid, &sgid);
    printf("real_group_id=%d, effictive_group_id=%d, saved_group_id=%d\n",rgid,egid,sgid);

    FILE *stream;
    stream = fopen( "wr.log", "a+" );
    fprintf( stream, "%s", "hello" );
    fclose( stream );

    return 0;
}
total 20
-rwxr-xr-x. 1 userTest1 root 8712 Dec 13 18:50 getIds
-rw-r--r--. 1 userTest1 root  554 Dec 13 18:50 getIds.c
-rw-r--r--. 1 userTest1 root   10 Dec 13 18:50 wr.log

在userTest1用戶下執行getIds,有如下內容,可以看到其UID為10000,跟創建該用戶時設置的值是一樣的,RUID=EUID;拉起該進程用戶所在的group以及該文件所屬的group都是root,所以group的數值顯示均為0

[userTest1@localhost ~]$ ./getIds
real_user_id=10000, effictive_user_id=10000, saved_user_id=10000
real_group_id=0, effictive_group_id=0, saved_group_id=0

在同一個host上創建不同組的用戶userTest2。

[root@localhost ~]# groupadd -g 20001 newGrp3
[root@localhost home]# useradd -u 10001 -g newGrp3 userTest2

切換到用戶userTest2,並進入/home/userTest1(可能需要為該目錄添加other的rx權限)下執行getIds,但因為wr.log的用戶和組是userTest1:root,而當前用戶是userTest2:newGrp3,因此會因為無法打開wr.log出現段錯誤。同時也可以看到當前進程的RUDI=EUID=10001,即創建userTest2時的UID;RGID=EGID=20001,為創建newGrp3時的GID

[userTest2@localhost userTest1]$ ./getIds
real_user_id=10001, effictive_user_id=10001, saved_user_id=10001
real_group_id=20001, effictive_group_id=20001, saved_group_id=20001 Segmentation fault (core dumped)

SUID的作用就是使可執行文件在不同用戶下能以文件擁有者的權限去執行。在userTest1用戶下為getIds添加SUID,此時getIds文件的權限中user對應的x變為了s

[userTest1@localhost ~]$ chmod 4755 getIds
[userTest1@localhost ~]$ ll
-rwsr-xr-x. 1 userTest1 root 8712 Dec 13 18:50 getIds
-rw-r--r--. 1 userTest1 root  554 Dec 13 18:50 getIds.c
-rw-r--r--. 1 userTest1 root   15 Dec 13 19:02 wr.log

切換到userTest2,執行getIds,此時可以執行成功,RUID沒有變,但EUID和SUID變為了userTest1的值,此時EUID被SUID值為了10000。即當前程序使用userTest1的權限(EUID)去寫入wr.log,因此不會出錯。但使用SUID是有安全風險的,本例中的程序並沒有能力影響除了wr.log之外的系統環境,但如果是一個包含很多功能的命令(如mount ip等),對該命令授予使用某個用戶的完整權限,很大程度上有權限泄露的風險,因此對文件設置SUID時需要謹慎。

[userTest2@localhost userTest1]$ ./getIds
real_user_id=10001, effictive_user_id=10000, saved_user_id=10000
real_group_id=20001, effictive_group_id=20001, saved_group_id=20001

更多關於RUID EUID和SUID的內容參見深刻理解——real user id, effective user id, saved user id in Linux

 

使用capabilities解決上述問題

在linux內核2.2版本之后將基於用戶的權限進行了划分,稱為capabilities,capabilities是線程相關的,使用時需要在線程上進程設置(完整的capabilities介紹參見capabilities)。那么如何以capabilities解決上述的問題呢?一個簡單的辦法是改變wr.log的用戶和組,這樣就不會出現權限問題

對getIds.c做一個小改動增加一行修改wr.log的用戶和用戶組的操作,其中10001為usetTest2對應的UID,20001為userTest2對應的GID

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    uid_t ruid;
    uid_t euid;
    uid_t suid;
    getresuid(&ruid, &euid, &suid);
    printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d\n",ruid,euid,suid);

    uid_t rgid;
    uid_t egid;
    uid_t sgid;

    getresgid(&rgid, &egid, &sgid);
    printf("real_group_id=%d, effictive_group_id=%d, saved_group_id=%d\n",rgid,egid,sgid);

    chown("/home/userTest1/wr.log", 10001, 20001);
    FILE *stream;
    stream = fopen( "wr.log", "a+" );
    fprintf( stream, "%s", "hello" );
    fclose( stream );

    return 0;
}

編譯上述文件,並使用root用戶在userTest1目錄下設置getIds1擁有修改文件用戶和組的權限CAP_CHOWN,+ep代表將該權限添加到capabilities的Effective和Permitted集合中(下面介紹),

[root@localhost userTest1]# setcap cap_chown+ep getIds1

在userTest2下執行getIds可以看到可以執行成功,注意到wr.log的用戶和組也被修改為了userTest2的用戶和組

[userTest2@localhost userTest1]$ ./getIds1
real_user_id=10001, effictive_user_id=10001, saved_user_id=10001
real_group_id=20001, effictive_group_id=20001, saved_group_id=20001
[userTest2@localhost userTest1]$ ll -rwxrwxrwx. 1 userTest1 root 8712 Dec 13 18:50 getIds -rwxr-xr-x. 1 root root 8760 Dec 13 20:08 getIds1-rw-r--r--. 1 userTest2 newGrp3 30 Dec 13 20:09 wr.log

查看getIds的capabilities,可以看到與設置的一樣。最終程序能夠運行的原理其實是一樣的,即程序的EUID和文件的EUID是一樣的。

[userTest2@localhost userTest1]$ getcap getIds1
getIds1 = cap_chown+ep

更簡單的辦法是給chown設置capabilities,這樣進程執行的時候會獲取chown上的capabilities,這樣就可以擁有權限去執行。在host上執行下面命令。切換到userTest2時就可以使用chown命令直接修改用戶和組。此處不能通過給bash設置cap_chow capabilities來操作,因此此時是非root用戶,bash進程在執行chown命令的時候會丟掉所有capabilities,導致缺少capabilities而無法運行

# setcap cap_chown=eip /bin/chown

 

    capabilities介紹

  •  capabilities可以分為線程capabilities和文件capabilities。
  • 線程capabilities包含以下4個capabilities集合:
  1. Effective:內核進行線程capabilities檢查時實際使用到的集合
  2. Inheritable:當程序對應的可執行文件設置了inheritable bit位時,調用execve執行該程序會繼承調用者的Inheritable集合,並將其加入到permitted集合。但在非root用戶下執行execve時,通常不會保留inheritable 集合,可以考慮使用ambient 集合,當一個程序drop掉一個capabilities時,只能通過execve執行SUID置位的程序或者程序的文件帶有該capabilities的方式來獲得該capabilities
  3. permitted:effective集合和inheritable集合的超集,限制了它們的范圍,因此如果一個capabilities不存在permitted中,是不可以通過cap_set_proc來獲取的。當一個線程從permitted集合中丟棄一個capabilities時,只能通過獲取程序可執行文件的capabilities或execve一個set-user-ID-root(以root用戶權限運行的)程序來獲得
  4. ambient :是在內核4.3之后引入的,用於補充Inheritable使用上的缺陷,ambien集合可以使用函數prctl修改。當程序由於SUID(SGID)bit位而轉變UID(GID),或執行帶有文件capabilities的程序時會導致該集合被清空

線程可以使用3種方式修改capabilities:

  1. fork:子進程使用fork后會繼承父進程的capabilities
  2. cap_set_proc:直接調用系統函數修改,但需要CAP_SETPCAP capabilities權限。在內核2.6.33版本之后,禁止程序直接修改非本進程的capabilities,只允許修改調用者自身進程的capabilities(參見capset)。
  3. execve:使用該函數后的capabilities計算方式如下:
P'(ambient)     = (file is privileged) ? 0 : P(ambient)
P'(permitted)   = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset) | P'(ambient)
P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable)

P:執行前的線程capabilities
P':執行后的線程capabilities
F:文件的capabilities
privileged file指設置了capabilities或設置了SUID或SGID的文件,如果SUID或SGID被忽略,則上述轉換將不會發生

cap_bset為bounding set,主要用來限制擁有CAP_SETPCAP權限的線程通過execve獲取文件的permitted capabilities(不影響inheritable集合),可以看到如果cap_bset為空,它是無法獲取到文件的permitted集合,即(F(permitted) & cap_bset)=0。
該特性在內核2.6.25版本前后是不一樣的。2.6.25版本之前該特性時系統范圍內設置的(通過/proc/sys/kernal/cap-bound),2.6.25之后是線程范圍內設置的,限制了程序可以獲得的文件的capabilities。bounding集合可以通
過繼承父進程獲得,init進程在內核啟動后可以獲得所有的bounding集合,可以使用prctl來減少bounding集合中的capabilities,但無法添加新的bounding capabilities
F(effective):當一個程序以set-user-ID-root運行或者進程的EUID為0,這類程序被稱為capability-dumb binary,此時程序運行的文件的effective bit會被內核設置為enable。內核在程序運行時會檢查該程序是否獲得了
capability-dumb binary文件的所有permitted集合,如果沒有,返回EPREM錯誤(通常是因為文件的permitted集合被bound集合過濾導致程序無法獲取文件的所有permitted集合)
注:根據公式,線程的permitted集合是可以通過獲取文件capabilities擴展的
  • 文件capabilities和線程capabilities共同決定了執行execve之后線程的capabilities。設置文件capabilities需要有CAP_SETFCAP 權限。文件capabilities有如下3種:
  1. Effective:為一個標記位,非capabilities集合。如果設置該標記位,執行execve后的新permitted集合中的capabilities都會添加到effective集合中;反之不會添加(參見上述公式中的:P'(effective) = F(effective) ? P'(permitted) : P'(ambient))。
  2. Inheritable:該集合主要是配合線程capabilities集合使用,具體使用方式參見上述公式
  3. Permitted:同上

文件的capabilities使用linux 擴展屬性來實現(extended attribute,以下簡稱EA),EA使用命名空間管理,實現方式比較簡單,即key-value方式。文件的capabilities保存在EA的security.capability中,security就是一個命名空間。使用setcap給/usr/bin/的ls目錄添加一個capabilities,加入ES,IS和PS中。

# setcap cap_net_raw=eip ls

使用getfattr可以導出該文件對應的EA,"-m -"用於導出所有EA,"-e hex"以16進制方式導出EA

# getfattr -d -m - -e hex ls
# file: ls
security.capability=0x0100000200200000002000000000000000000000
security.selinux=0x73797374656d5f753a6f626a6563745f723a62696e5f743a733000

可以看到有2個EA,security.capability對應文件的capabilities,另外一個“security.selinux”主要被linux的安全模塊調用,實現強制訪問控制(MAC),當然我們可以是使用text方式解碼此命名空間的內容,跟使用ls -Z查看的結果是一樣的

# getfattr -d -m - -e text ls
# file: ls
security.capability="\000\000\000 \000\000\000 \000\000\000\000\000\000\000\000\000"
security.selinux="system_u:object_r:bin_t:s0"
# ls -Z ls
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       ls

上面使用hex解碼出的security.capability值為0x0100000200200000002000000000000000000000,含5個32位的長度,即0x01000002 00200000 00200000 00000000 00000000,對應以下的結構體,注意其為小端序,轉化為大端序為0x0200001 00002000 0002000 00000000 00000000

struct vfs_cap_data {
    __le32 magic_etc;        /* Little endian */
    struct {
        __le32 permitted;    /* Little endian */
        __le32 inheritable;  /* Little endian */
    } data[VFS_CAP_U32];
};

第一個32位數值0x0200001的最后一個bit位為1,該bit位就是文件effective的bit位,linux/capability.h中有如下定義(參見代碼)

VFS_CAP_FLAGS_EFFECTIVE 0x000001

在設置文件的capabilities時,內核會根據capabilities的版本(版本的介紹參見capabilities)進行不同處理,同時也會將capabilities 版本號和effective bit位進行位或以及小端序處理(即0x02000000&0x000001=0x0200001),這樣就得到了上面的0x0200001。內核代碼會根據長度優先處理VFS_CAP_REVISION_2的情況,內核代碼參見L501。第三個和第四個32位數據時是一樣的,因為添加capabilities時指定了=eip,表示permitted和inheritable,同樣也是小端序,2000代表的就是cap_net_raw

#define VFS_CAP_REVISION_2 0x02000000
  • 當執行一個capability-dumb binaries(即設置了文件capabilities,但程序本身並沒有調用libcap庫管理這些capabilities)時,程序會嘗試獲取所有的文件capabilities,如果獲取失敗,則返回錯誤。這主要是為了防止程序缺少某些capabilities而無法運行

寫一個小程序驗證上述功能,僅用於輸出一句話,編譯該文件,輸出文件名為hello

#include<stdio.h>
int main()
{
    printf("hello world\n");
    return 0;
}

為hello添加容器中不存在的capabilities ,將該hello使用如下命令拷貝到容器中執行,會出現“sh: ./hello: Operation not permitted”的錯誤,因此hello程序會嘗試獲取文件的cap_mac_admin,但因為容器的bounding集合中不存在該capabilities而獲取失敗。

# setcap cap_mac_admin=eip hello
# docker cp hello c03a:/home
  • 當使用execve執行set-user-ID-root程序時有如下規則:
    • 當執行一個set-user-ID-root程序,或程序的RUD或EUID為0時,則該文件的inheritable集合和permitted集合設置為全1
    • 當執行一個set-user-ID-root程序,或程序的EUID為0時,則該文件的effective bit設置為1
  •  當線程在不同用戶之間進行轉換時,有如下規則:
    • 如果線程的RUID,EUID以及SUID的一個或多個為0,當這些UID的值都轉變為非0時,permitted, effective, ambient集合都會被清空
    • 當線程的EUID從0轉變為非0,則程序的effective集合會被清空
    • 當線程的EUID從非0轉變為0,則permitted集合會被拷貝到effective集合中如果系統用戶的UID從從非0轉變為0(setfsuid),則如下capabilities會從effective集合中清除;當系統的EUID從非0轉變為0,則permitted集合會被拷貝到effective集合中
    • 如果系統UID 從0轉變為非0,則如下capabilities會被清空
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_FOWNER, CAP_FSETID, CAP_LINUX_IMMUTABLE (since Linux 2.6.30), CAP_MAC_OVERRIDE, CAP_MKNOD
  • 當調用capset或cap_set_proc設置程序的capabilities時,必須遵守如下規則:
    • 如果調用者沒有CAP_SETPCAP ,則新的inheritable必須是現有inheritable和permitted的合集的子集
    • (Since Linux 2.6.25)新的inheritable必須是現有inheritable和bounding的合集的子集
    • 新的permitted必須是現有permitted的子集
    • 新的effective集合必須是現有permitted集合的子集

這樣也看出如果現有進程中的permitted中不存在某個capabilities,那么即使該進程有CAP_SETPCAP權限,也不能設置該capabilities。

使用如下程序驗證上述部分功能,test.c用於設置當前線程(進程)的capabilities,調用execve執行test1,中間使用getchar()來中斷程序,用於手動查看當前程序的capabilities;test1僅查看execv執行之后的capabilities,編譯時需要加上-lcap選項,鏈接cap庫

test.c //調用者
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#undef _POSIX_SOURCE
#include <sys/capability.h>

extern int errno;

void listCaps(pid_t pid)
{
    uid_t ruid;
    uid_t euid;
    uid_t suid;
    getresuid(&ruid, &euid, &suid);
    printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d\n",ruid,euid,suid);

    struct __user_cap_header_struct head={_LINUX_CAPABILITY_VERSION_1, pid};
    struct __user_cap_data_struct data={};

    capget(&head, &data);
    printf("Cap data ES=0x%x, PS=0x%x, IS=0x%x\n", data.effective,data.permitted, data.inheritable);
}

int main(int argc, char **argv)
{
       int error;
        int stat;
        pid_t parentPid = getpid();
        cap_t caps = cap_init();

        cap_value_t capList[] ={ CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_SETPCAP,CAP_SETFCAP } ;
        unsigned num_caps = 4;
        cap_set_flag(caps, CAP_EFFECTIVE, num_caps, capList, CAP_SET);
        cap_set_flag(caps, CAP_INHERITABLE, num_caps, capList, CAP_SET);
        cap_set_flag(caps, CAP_PERMITTED, num_caps, capList, CAP_SET);

        if (cap_set_proc(caps)) {
                perror("capset()");

                return EXIT_FAILURE;
        }
        listCaps(parentPid);
        getchar();
        char *argv2[]={"test1",NULL};
        char *envp[]={0,NULL};
        error=execve("test1",argv2,envp);
        if(0 != error){
            printf("error=%d\n",error);
        }
        parentPid = getpid();

        listCaps(parentPid);

        cap_free(caps);
        return 0;
}
test1.c //被調用者
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#undef _POSIX_SOURCE
#include <sys/capability.h>

extern int errno;

void listCaps(pid_t pid)
{
    uid_t ruid;
    uid_t euid;
    uid_t suid;
    getresuid(&ruid, &euid, &suid);
    printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d\n",ruid,euid,suid);

    struct __user_cap_header_struct head={_LINUX_CAPABILITY_VERSION_1, pid};
    struct __user_cap_data_struct data={};

    capget(&head, &data);
    printf("Cap data ES=0x%x, PS=0x%x, IS=0x%x\n", data.effective,data.permitted, data.inheritable);
}

int main(int argc, char **argv)
{
    int stat;
    pid_t parentPid = getpid();
    listCaps(parentPid);
    return 0;
}

  上述代碼編譯方式如下:

首先下載libcap源碼

wget https://git.kernel.org/pub/scm/libs/libcap/libcap.git/snapshot/libcap-2.26.tar.gz
為驗證方便,將上述文件放在在libcap-2.26/libcap下編譯
在libcap.h中引用了一個名為cap_names.h的頭文件,但libcap目錄下不存在,可以直接從這拷貝

使用docker namespace中user namespace中的方式創建一個unprivileged類型的docker容器(有獨立的user namespace)

docker run -itd centos:latest /bin/sh

在host上找到對應該進程的PID,可以在/proc/$PID/task/TID/status中查看進程的capabilities信息,截圖如下

CapInh: 00000000a80625fb
CapPrm: 00000000a80625fb
CapEff: 00000000a80625fb
CapBnd: 00000000a80625fb

可以使用capsh來解析上述值

# capsh --decode=00000000a80625fb
0x00000000a80625fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_rawio,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap

當前也可以通過容器啟動進程/bin/sh映射到host上的pid查看對應的capabilities

# getpcaps 5318
Capabilities for `5318': = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_rawio,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip

將編譯好的test和test1拷貝到創建的容器中,當前用戶ID如下

sh-4.2# id
uid=0(root) gid=0(root) groups=0(root)

執行./test可以看到如下信息,當前進程(執行execev之前)的capabilities如下,由於進程此時由於getchar()中斷,使用上述方式查看當前進程的信息如下(雖然該進程的所有uid為0,但其並不是一個真正系統級別的UID,其UID經過了user namespace的映射,映射到/etc/subuid)

sh-4.2# ./test
real_user_id=0, effictive_user_id=0, saved_user_id=0
Cap data ES=0x80000103, PS=0x80000103, IS=0x80000103
CapInh: 0000000080000103
CapPrm: 0000000080000103
CapEff: 0000000080000103
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000

回車,看到完整的信息如下,雖然test1沒有設置文件capabilities,但由於test1的uid都是0,仍然會部分遵守上述提到的"當執行一個set-user-ID-root程序,或程序進程的RUD或EUID為0時,則該文件的inheritable集合和permitted集合設置為全1"的規則(沒有全部繼承是受bounding集合的限制)

sh-4.2# ./test
real_user_id=0, effictive_user_id=0, saved_user_id=0
Cap data ES=0x80000103, PS=0x80000103, IS=0x80000103

real_user_id=0, effictive_user_id=0, saved_user_id=0
Cap data ES=0xa80425fb, PS=0xa80425fb, IS=0x80000103
  • 當執行一個set-user-ID-root程序,或程序進程的RUD或EUID為0時,則該文件的inheritable集合和permitted集合設置為全1
  • 當執行一個set-user-ID-root程序,或程序進程的EUID為0時,則該文件的effective bit設置為1

在容器中創建一個新的用戶,其用戶和組都是captest,並修改test和test1的用戶和組

useradd captest
chown captest:captest test*
chmod 777 test*

切換到captest用戶下執行./test會返回”capset(): Operation not permitted“的錯誤,查看該用戶的進程

[captest@78d4c79e2648 home]$ ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD captest  204    203  0 22:37 pts/3    00:00:00 bash

查看上述bash進程的capabilities信息如下,可以看到effective集合被清空了,這樣就導致bash下的子進程的effective集合也是空,沒有權限執行。

CapInh: 00000000a80425fb
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000

bash進程的父進程為“su captest”,其EUID為root,即0,該進程會執行bash命令,即EUID從0 轉變為非0。

root      203    186  0 21:53 pts/1    00:00:00 su captest
captest   204    203  0 21:53 pts/1    00:00:00 bash

上述現象符合如下規則

如果程序的RUID,EUID以及SUID的一個或多個為0,當這些UID的值都轉變為非0時,permitted, effective, ambient集合都會被清空
當程序的EUID從0轉變為非0,則程序的effective集合會被清空

 在captest用戶下執行./test1,同樣也可以看到,由於udi全部非0,ES和PS都會清空,但IS沒有變

[captest@78d4c79e2648 home]$ ./test1
real_user_id=1001, effictive_user_id=1001, saved_user_id=1001
Cap data ES=0x0, PS=0x0, IS=0xa80425fb

在內核4.14版本的時候引入了一個Namespaced file capabilities的概念,主要用於解決VFS_CAP_REVISION_2下file capabilities無法在不同user namespace下隔離的問題。在VFS_CAP_REVISION_3版本之前,一個CAP_SETFCAP進程可以在獲取file capabilities權限的時候,並不關心該進程所在的user namespace,如果一個進程擁有CAP_SETFCAP的權限,那它可以通過設置並執行多個file capabilities來獲取原來沒有的capabilities權限,這樣就導致進程權限完全不受user namespace的限制且毫無意義(既然自己可以設置capabilities並通過exec獲得,那文件自身擁有的capabilities就沒用了)。因此在4.14版本新增了一個rootid的變量,rootid是指新創建的命名空間中的UID 0對應初始user namespace的UID的值,即映射到host主機上的user ID的值(/etc/subuid)

struct vfs_ns_cap_data {
    __le32 magic_etc;
    struct {
        __le32 permitted;    /* Little endian */ __le32 inheritable; /* Little endian */ } data[VFS_CAP_U32]; __le32 rootid; };

 

    TIPS 

  • capabilities只是給了線程執行某項功能的能力,但線程是否能調用另外的程序,需要看當前線程的EUID是否與文件的EUID對應,且能獲取該文件的所有capabilities(如果文件設置了capabilities)
  • execve與fork不一樣,它並不會創建一個子進程,execve執行程序的時候會對capabilities進行重新計算,如果此時的程序uid不為0或文件capabilities為空,則該程序會失去所有的capabilities。
  • 不要輕易給一個線程或文件設置CAP_SYS_ADMIN權限,CAP_SYS_ADMIN的權限類似root
  • 在容器中給文件設置capabilities時可能會出現“Operation not permitted”的錯誤,可以查看host的系統日志,centos上一般時SElinux功能沒有關閉導致的,執行setenforce 0即可
  •  capabilities中定義了各個capabilities的值,但要注意這些值代表的是2的冪次方,如“#define CAP_SETPCAP 8”,則CAP_SETPCAP 的值為2^8=256
  • docker可以在run的時候使用--cap-add為容器的初始進程添加capabilities,--cap-drop移除capabilities
  • 使用CAP_SETFCAP可以為文件設置任意capabilities,使用CAP_SETPCAP則需要按照一定的規定添加(見上文)
  • 一個線程的capabilities集合與自身設置和文件capabilities設置相關,同時也受uid的限制。
  • 切換系統用戶其實也是執行一個進程(sh或bash)

參考:

http://man7.org/linux/man-pages/man7/capabilities.7.html

https://raesene.github.io/blog/2017/08/27/Linux-capabilities-and-when-to-drop-all/

https://review.lineageos.org/c/LineageOS/android_kernel_samsung_apq8084/+/192902/6

https://github.com/riyazdf/dockercon-workshop/tree/master/capabilities

https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities

https://s3hh.wordpress.com/2017/09/20/namespaced-file-capabilities/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM