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搜索动态库的顺序如下:

  1. DT_PATH指定的库文件(deprecated)
  2. LD_LIBRARY_PATH指定的库文件
  3. DT_RUNPATH指定的库文件
  4. /etc/ld.so.cache这个二进制文件指定的库文件,该文件通过ldconfig命令生成
  5. 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]中同样有描述:

  1. The LD_PRELOAD environment variable.
  2. The --preload command-line option when invoking the dynamic linker directly.
  3. 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

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

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
#include <stdio.h>
#include <stdlib.h>

int main ()
{
const char *preload = getenv("LD_PRELOAD");
printf("LD_PRELOAD = %s\n", preload);
return 0;
}

// lib.c
#include <stdio.h>

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
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)

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