# 简介
LD_PRELOAD
是个环境变量,用于动态库的加载,其动态库加载的优先级最高。一般情况下,其加载优先级从高到低依次为
LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib>/usr/lib
# 自定义函数替换外部库函数
程序经常要调用一些外部库的函数,例如 rand
。如果我们自定义一个 rand
函数,将其编译成动态库后,通过 LD_PRELOAD
加载。当程序调用 rand
函数时,调用的其实是我们自定义的函数。举个栗子如下。
# 实验环境
以下 Dockerfile 配置的容器镜像
FROM alpine:3.12.3
RUN apk update && apk add build-base
2
3
# 链接系统的 rand()
可行执行程序源码如下
// test_fakerand.c #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand(time(NULL)); int i = 10; while (i--) { printf("%d\n", rand() % 100); } return 0; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18执行
gcc -g -o test_fakerand test_fakerand.cxx ./test_fakerand # 输出 22 54 34 15 95
1
2
3
4
5
6
7
8
9
10
# 链接自定义的 rand()
自定义
rand()
函数如下// fakerand.c int rand(){ return 42; // 不能再假的随机数 }
1
2
3
4
5编成动态库
# 注意:用 C 的编译风格确保函数不会被重命名,C++ 的话需要添加 `extern "C"` 包裹链接的函数 gcc -shared -fPIC fakerand.c -o libfakerand.so
1
2运行
LD_PRELOAD=$PWD/libfakerand.so ./test_fakerand # 或以下两条命令 # export LD_PRELOAD=$PWD/libfakerand.so # ./test_fakerand # 输出 42 42 42 42 42
1
2
3
4
5
6
7
8
9
10
11
可见,我们已经成功将 rand
函数替换为自定义版。
使用 ldd
工具可以查看两种运行方式各自加载的动态库。
直接运行时,由于没有加载 libfakerand.so,使用系统的
rand
函数ldd test_fakerand # 输出 /lib/ld-musl-x86_64.so.1 (0x7f4516990000) libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f4516990000)
1
2
3
4
5指定
LD_PRELOAD=$PWD/libfakerand.so
后,使用ldd
查看所加载的so
列表中有自定义的 libfakerand.so。由于LD_PRELOAD
加载顺序最高,因此会优先使用libfakerand.so
的rand
函数LD_PRELOAD=$PWD/libfakerand.so ldd test_fakerand # 输出 /lib/ld-musl-x86_64.so.1 (0x7f884894b000) /workspace/examples/libfakerand.so => /workspace/examples/libfakerand.so (0x7f8848941000) libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f884894b000)
1
2
3
4
5
6
使用 nm -D
命令可以查看动态库 libfakerand.so
的符号。
nm -D libfakerand.so
# 输出
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w __cxa_finalize
w __deregister_frame_info
w __register_frame_info
000000000000116c T _fini
0000000000001000 T _init
000000000000113f T rand
2
3
4
5
6
7
8
9
10
11
# 自定义函数替换外部同名库函数
下面的例子我们想封装一个 open
函数,其内部调用 libc
的 open
函数。
int open(const char *pathname, int flags){
// ...
// 一些恶意注入的代码
// ...
// 调用 "真正的" open 函数,在此有 libc.so 提供
return open(pathname, flags);
}
2
3
4
5
6
7
8
这种写法会导致递归调用。
那如何在自定义库中调用真正的 open
函数呢?一种姿势如下
自定义
open
函数# fakeopen.c #include <dlfcn.h> #include <stdio.h> typedef int (*orig_open_func_type)(const char *pathname, int flags); int open(const char *pathname, int flags, ...) { // ... // 一些恶意注入的代码 // ... printf("The victim used open(...) to access '%s'!!!\n", pathname); // 别忘了包含 stdio.h! orig_open_func_type orig_open = (orig_open_func_type) dlsym(RTLD_NEXT, "open"); return orig_open(pathname, flags); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19编译生成动态库
libfake_open.so
gcc -shared -fPIC -o libfake_open.so fakeopen.c -ldl
1
温馨提示
RTLD_NEXT
的 man 手册解释如下:
There are two special pseudo-handles, RTLD_DEFAULT and RTLD_NEXT. The former will find the first occurrence of the desired symbol using the default library search order. The latter will find the next occurrence of a function in the search order after the current library. This allows one to provide a wrapper around a function in another shared library.
换句话说,RTLD_DEFAULT
基于默认加载顺序查找第一个满足要求的函数,而 RTLD_NEXT
则是在当前库之后查找第一次出现的函数。
编写测试程序
// test_fakeopen.c #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <errno.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd; if(2 != argc) { printf("Usage : \n"); return 1; } errno = 0; fd = open(argv[1],O_RDONLY|O_CREAT, S_IRWXU); if(-1 == fd) { printf("open() failed with error [%s]\n", strerror(errno)); return 1; } else { printf("open() Successful.\n"); } return 0; }
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编译并运行测试程序可执行文件
gcc -g -o test_fakeopen test_fakeopen.c # 不替换 open 函数 ./test_fakeopen fakerand.c ## 输出 open() Successful. # 替换 open 函数 LD_PRELOAD=/workspace/examples/libfakeopen.so ./test_fakeopen fakerand.c ## 输出 The victim used open(...) to access 'fakerand.c'!!! open() Successful.
1
2
3
4
5
6
7
8
9
10
11
12
13
14