注:本文絕大部分內容來自Linux Namespaces實踐part 5,原文系列文章詳細描述了Linux Namespace相關內容,英語過關的建議閱讀原文,本文內容主要用來記錄學習內容,如有不當之處還請評論區指正。
User Namespace
隔離了安全相關的屬性,即用戶ID、組ID、根目錄、keyrings以及capabilities。一個進程的用戶ID和組ID在User Namespace
內外可以是不同的。特別地,一個進程可以在User Namespace
外有非零的用戶ID(普通用戶),而在User Namespace
內部有爲零的用戶ID(root用戶)。
在闡述內容之前,先列舉幾個作者遇到的問題:
-
無
sys/capability.h
頭文件,cap_t
,cap_to_text()
等Ubuntu上需要通過
sudo apt-get install libcap-dev
安裝庫
連接時需加上-lcap
選項:g++ -o a.sh test.cpp -lcap
-
userns_child_exec.c
遇到write: operation not permitted
問題系列文章寫於2013年,之後Linux 3.19更新了
gid_map
文件的訪問規則,可以參考User Namespace手冊,文中與樣例Example的代碼註釋都闡述了相關問題及解決方案
創建User Namespace
通過傳遞CLONE_NEWUSER
標誌給clone()
或unshare()
函數可以創建一個User Namespace
。Linux 3.8之後,創建User Namespace
不在需要root權限。值得一提的是,Win10的wsl目前不支持CLONE_NEWUSER
標誌,即無法創建User Namespace
,在即將到來的wsl2(目前預覽版可以嚐鮮)將內置完整的Linux內核,可以提供該功能。
一個栗子
本例通過demo_userns.c展示了沒有特權的進程在新User Namespace
中可以有特權。代碼如下:
int childFunc(void *arg) {
cap_t caps;
for (;;) {
printf("eUID = %ld; eGID = %ld; \n", (long)geteuid(), (long)getegid());
caps = cap_get_proc();
printf("capabilityies: %s\n", cap_to_text(caps, NULL));
if (arg == NULL)
break;
sleep(5);
}
return 0;
}
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
int main(int argc, char *argv[]) {
pid_t pid;
pid = clone(childFunc, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, argv[1]);
if (pid == -1)
errExit("clone");
if (waitpid(pid, NULL, 0) == -1)
errExit("waitpid");
return 0;
}
通過傳遞CLONE_NEWUSER
標誌給clone()
函數創建了新User Namespace
,之後子進程childFunc
在新創建的User Namespace
中打印出自己的用戶ID、組ID以及capabilities
。有關capability的內容參考Linux 手冊 Capability。運行上述程序得到如下結果:
eric@eric_ubuntu_server:~/ns_in_opt/part_5$ id -u
1000
eric@eric_ubuntu_server:~/ns_in_opt/part_5$ id -g
1000
eric@eric_ubuntu_server:~/ns_in_opt/part_5$ ./demo_userns
eUID = 65534; eGID = 65534;
capabilityies: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep
上述輸出結果展示了一些有趣的信息:
capabilities: = ...
表明子進程擁有所有的capability
創建一個新
User Namespace
時,該Namespace
中的第一個進程被授予完整的權限capability
。- 進程的用戶和組ID在
Namespace
內外不同需要將
User Namespace
內的用戶與組ID映射到Namespace
外部,對於沒有映射的Namespace
,getuid()
與getgid()
會返回/proc/sys/kernel/overflowuid
或/proc/sys/kernel/overflowgid
的內容,即65534
另外,User Namespace
還是可以嵌套的,即除了初始的User Namespace
,每一個User Namespace
都有一個父Namespace
,零個或多個字Namespace
。
映射用戶與組ID
注:如前所述,Linux 3.19對gid_map
做了些更改,對於3.19及以上版本,需要先向/proc/PID/setgroups
文件中寫入"deny"才能對gid_map
文件進行更改。
創建User Namespace
之後,映射用戶ID與組ID是必不可少的一步,這些映射可以通過向/proc/PID/uid_map
與/proc/PID/gid_map
文件中寫入映射信息實現。映射信息的格式如下:
ID-inside-ns ID-outside-ns length
上述信息將Namespace
內從ID-inside-ns
到ID-inside-ns + length
映射到了ID-outside-ns
同樣長度。不妨假設ID爲/proc/PID/uid_map
中PID的進程爲A,正在向/proc/PID/uid_map
文件寫信息的進程爲B。ID-outside-ns
取決於進程A和進程B是否在同一個User Namespace
:
- 若在同一個
Namespace
,則ID-outside-ns
即爲其父Namespace
中的值 - 若不在同一個
Namespace
,則ID-outside-ns
的值爲進程B所在的Namespace
中的us用戶或組ID。
又一個栗子
通過給demo_userns
程序傳遞任意一個參數可讓其每隔幾秒輸出進程的用戶、組ID以及cap。
-
demon_userns
程序傳遞任意參數執行,如下爲輸出(前兩個)eric@eric_ubuntu_server:~/ns_in_opt/part_5$ ./demo_userns x eUID = 65534; eGID = 65534; capabilityies: = cap_chown,... eUID = 65534; eGID = 65534; capabilityies: = cap_chown,...
可以看到,eUID與eGID(有效用戶、組ID)都爲65534
-
在另一個終端中執行
echo '0 1000 1' > /proc/PID/uid_map
命令eric@eric_ubuntu_server:~$ ps -C demo_userns -o 'pid uid comm' PID UID COMMAND 4738 1000 demo_userns 4739 1000 demo_userns eric@eric_ubuntu_server:~$ echo '0 1000 1' > /proc/4739/uid_map
即查看當前子進程的PID,之後向
/proc/PID/uid_map
文件中寫如映射信息。回到另一個終端我們看到:
eUID = 0; eGID = 65534; capabilityies: = cap_chown,... eUID = 0; eGID = 65534; capabilityies: = cap_chown,...
子進程在新創建的
User Namespace
中建立了用戶的映射,不再是默認65534 -
在上述相同的終端中執行如下命令
eric@eric_ubuntu_server:~$ echo 'deny' > /proc/4739/setgroups eric@eric_ubuntu_server:~$ echo '0 1000 1' > /proc/4739/gid_map
首先向
/proc/PID/setgroups
文件中寫入字符串’deny’,之後將映射信息寫入gid_map
中回到另一個終端,看到:
eUID = 0; eGID = 0; capabilityies: = cap_chown,... capabilityies: = cap_chown,...
即將子進程的
User Namespace
中ID爲0的用戶/組映射到了父Namespace
中ID爲1000的用戶/組。
映射規則以及深入探索映射
關於uid_map
與gid_map
的映射規則不在少數,且涉及更深層次的系統細節,本文不詳細討論這些內容,感興趣的可以參考Linux user_namespace(7),建議運行其後的示例程序以獲得更直觀的感受。
另外,通過shell命令或其它方式查看用戶與組ID的映射時,得到的結果與當前進程所在的User Namespace
有關,詳細信息參考上述Linux man手冊的示例程序,此處不再詳細描述。
總結
Linux中User Namespace
爲非特權用戶提供了獲得特權的途徑,由於其相關細節非常之多,本文僅概括性地描述了一些常見的操作及可能遇到的問題。針對·User Namespace·的運用還要結合其它類型的·Namespace·纔可彰顯,作者也會在接下來的學習過程中進行記錄。