使用 LD_PRELOAD 拦截库函数调用
LD_PRELOAD
是 Linux 下一个很有意思的环境变量,通过这个变量,可以在运行程序时强制加载某个动态库,而且是最先加载。通过 LD_PRELOAD
可以非常方便地拦截库函数的调用,而且不需要修改可执行程序。
例子:拦截 strlen
下面通过一个例子来演示使用 LD_PRELOAD
拦截 strlen
的方法。首先是一段测试代码:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char s[] = "hello, world!";
printf("length is %d\n", strlen(s));
return 0;
}
编译生成可执行文件 test
,运行,输出 length is 13
,没有问题。
下面创建一个 hook.c
文件,里面包含另一个 strlen
实现:
#include <string.h>
size_t strlen(const char *str) {
return 4;
}
这个版本的 strlen
不管输入什么都会返回 4。用下面的命令编译 hook.c
:
$ gcc -shared -fPIC hook.c -o hook.so
这样,就生成了 hook.so
,其中包含着一个盗版的 strlen
函数,接下来通过下面的命令运行刚才的程序:
$ LD_PRELOAD=$PWD/hook.so ./test
这一次运行程序,输出结果变成了 length is 4
,这说明 strlen
函数确实被替换了。
实现机制
首先我们分析一下正常的情况。程序中使用了 strlen
函数,而这个函数是 C 语言标准库中提供的,通过 ldd test
,我们可以看到 test
文件执行所需的动态库:
linux-vdso.so.1 (0x00007fffda2b4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1da8a15000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1da8dbe000)
其中 libc.so.6
就是 Linux 下的标准 C 库。一个可执行文件运行的时候,系统会预先加载所需的动态库文件,当可执行文件 test
的镜像加载时,系统会进行动态绑定(也叫重定位),将 test
中对 strlen
的调用与 libc.so.6
中的符号 strlen
关联起来,库函数的调用从而可以正常执行。
环境变量 LD_PRELOAD
的作用就是在加载其他的动态库之前,如果这些库中也定义了某些程序用到的函数,它们就会优先与可执行文件匹配。
因此,hook.so
中定义了 strlen
,优先于 libc.so.6
与 test
中的调用匹配,因此后一次运行的时候输出了错误的字符串长度。
实现功能透明
通常进行库函数拦截是为了进行监控和统计,功能上要保持一致。因此,在假的库函数中,应该调用真的库函数完成相应功能,例如:
#include <string.h>
size_t strlen(const char *str) {
return real_strlen(str);
}
现在的问题变成了如何确定 real_strlen
,方法就是,通过 Linux 下的动态链接器。
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
typedef size_t (*strlen_t)(const char *str);
size_t strlen(const char *str) {
strlen_t real_strlen = (strlen_t) dlsym(RTLD_NEXT, "strlen");
printf("calling strlen.\n");
return real_strlen(str);
}
在这个版本中,首先使用 dlsym
函数找到 strlen
函数的地址,由于指定了 RTLD_NEXT
参数,因此找到的地址一定是真正的 strlen
函数。另外要注意的是,引用 dlfcn.h
头文件之前需要定义宏 _GNU_SOURCE
。
找到真正的 strlen
函数之后,使用 printf
打印一条记录,然后返回真正的 strlen
的结果。
由于这次用到了 Linux 下的动态连接器,需要在编译的时候指定 -ldl
选项:
$ gcc -shared -fPIC -ldl hook.c -o hook.so
再次运行 test
程序,结果如下:
$ LD_PRELOAD=$PWD/hook.so ./test
calling strlen.
length is 13
优化
上面的版本仍然有一个不足之处,那就是每次调用 strlen
的时候,都需要定位真正的 strlen
函数,实际上每次查找到的函数地址都是一样的。最适合的是动态库被加载的时候查找真正 strlen
函数的地址并存储下来,之后每次直接调用即可。
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
typedef size_t (*strlen_t)(const char *str);
strlen_t real_strlen;
size_t strlen(const char *str) {
printf("calling strlen.\n");
return real_strlen(str);
}
void lib_init() {
real_strlen = (strlen_t) dlsym(RTLD_NEXT, "strlen");
}
Linux 下的动态库文件是 ELF 格式,其中 init
section 中的函数会在库文件加载的时候自动调用,而要让一个函数放在 init
section 需要用 ld 的 -init
参数。
对于 GCC,可以通过 -Wl
参数指定连接器参数,编译的命令如下:
$ gcc -shared -fPIC -ldl -Wl,-init,lib_init -o hook.so hook.c