setcap vs. LD_PRELOAD
在Linux中,一个进程拉起另一个进程的流程大致如下:
graph LR; F[parent process] --> A[start] A --fork--> B[child] A --> C[wait] B --exec--> D[new process] D --> E[end] C --> E
最常见的就是通过shell终端执行命令。此场景下,/bin/bash就是这个parent process,而要执行的那个命令就是new process。
Linux有一些特性,可以使得创建出的进程比拉起的进程权限高。例如可执行文件配置了set-user-ID位,则拉起的进程就是root权限,而其父进程有可能是普通用户权限。如果可执行文件配置了file capability,则创建出的进程就具备了某些capability,如果父进程没有这些capability,则这也是一种权限放大的场景。
当发生这种权限放大的场景时,Linux的安全特性要求,此时子进程中的某些敏感环境变量会被清空,例如:LD_PRELOAD,LD_LIBRARY_PATH。由于这些环境变量都是从父进程继承过来的,如果不清空,则表明会使用高权限级别执行这些环境变量指定的可执行代码。
LD_LIBRARY_PATH
参考文献[1],ld.so搜索动态库的顺序如下:
- DT_PATH指定的库文件(deprecated)
- LD_LIBRARY_PATH指定的库文件
- DT_RUNPATH指定的库文件
- /etc/ld.so.cache这个二进制文件指定的库文件,该文件通过ldconfig命令生成
- In the default path /lib, and then /usr/lib. (On some 64-bit architectures, the default paths for 64-bit shared objects are /lib64, and then /usr/lib64.) If the binary was linked with the -z nodeflib linker option, this step is skipped.
所以针对LD_LIBRARY_PATH,除了第二条的方法失效,其他的都可以用。
LD_PRELOAD
那针对LD_PRELOAD,是不是就没法用呢?其实也不是。
在没有setcap以及set-user-ID的情况下,如果ld.so需要预加载一个库文件,指定方法在文献[1]中同样有描述:
- The
LD_PRELOAD
environment variable. - The
--preload
command-line option when invoking the dynamic linker directly. - The
/etc/ld.so.preload
file.
在secure-execution模式下,方法2和方法3均不受影响。方法1也仍然可以使用。但是需要一些特殊的设置,在[1]中也有描述。
In secure-execution mode, preload pathnames containing slashes are ignored. Furthermore, shared objects are preloaded only from the standard search directories and only if they have set-user-ID mode bit enabled (which is not typical).
综上,需要3点配置:
- LD_PRELOAD环境变量指定的库文件不能包含斜线’/‘
- 库文件只会从标准路径下加载。这里标准路径可以参考LD_LIBRARY_PATH中的描述。注意,此时ld.so只会搜索标准路径,不会搜索通过其他手段配置的路径(如上一节描述的)。
- 库文件必须使能了set-user-id位
示例代码
代码目录树:1
2
3
4
5
6
7
8[ben@localhost test]$ tree .
.
├── lib.c
├── libtest.so
├── main
├── main.c
├── test
└── test.c
main.c生成main可执行程序,test.c生成test可执行程序,lib.c生成libtest.so。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47// main.c
int main()
{
pid_t pid = fork();
if (pid == 0) {
char *envp[] = {
"LD_PRELOAD=libtest.so",
// "LD_PRELOAD=./libtest.so",
NULL
};
char *argv[] = {
"test",
NULL
};
int err = execve("./test", argv, envp);
}
else {
int status;
wait(&status);
}
return 0;
}
// test.c
int main ()
{
const char *preload = getenv("LD_PRELOAD");
printf("LD_PRELOAD = %s\n", preload);
return 0;
}
// lib.c
static void func(void) __attribute__((constructor));
void func(void)
{
printf("I'm libtest.so loaded\n");
}
在test可执行程序是普通的二进制时,输出为1
2
3[ben@localhost test]$ ./main
I'm libtest.so loaded
LD_PRELOAD = ./libtest.so
当test配置了capability以后:1
2
3[ben@localhost test]$ sudo setcap cap_net_admin,cap_net_raw=eip ./test
[ben@localhost test]$ ./main
LD_PRELOAD = (null)
可见LD_PRELOAD指定libtest.so未被加载,且LD_PRELOAD环境变量被清空了。
LD_PRELOAD不含斜线
1 | [ben@localhost test]$ ./main |
LD_PRELOAD仍然被清空了,但ld.so似乎尝试去加载libtest.so了,但是没找着。
将libtest.so放入标准路径
如果没有配置set-user-id位:1
2
3[ben@localhost test]$ ./main
ERROR: ld.so: object 'libtest.so' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
LD_PRELOAD = (null)
仍然提示找不到。如果设置了set-user-id位:1
2
3
4[ben@localhost test]$ sudo chmod a+s /usr/lib64/libtest.so
[ben@localhost test]$ ./main
I'm libtest.so loaded
LD_PRELOAD = (null)
在满足上一节提到的3个条件时,libteso.so就可以正常加载了。
看看如果放到/usr/lib下面会怎么样?1
2
3
4
5[ben@localhost test]$ ls /usr/lib/libtest.so -l
-rwsr-sr-x. 1 root root 8208 1月 24 19:48 /usr/lib/libtest.so
[ben@localhost test]$ ./main
ERROR: ld.so: object 'libtest.so' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
LD_PRELOAD = (null)
看看还是一样找不到。可见在x64平台上,/usr/lib并非标准路径,而/usr/lib64以及/lib64才是。
参考文献
[1] ld.so(8) — Linux manual page
[2] Stackoverflow - Does using linux capabilities disables LD_PRELOAD