Linux C 中有一个很不错的特性,可以在不改变程序的前提下,修改动态库所调用的函数,也就是 Preload 功能。
这里简单介绍其使用方法。
简介
在 *NIX
系统中,LD_PRELOAD
是一个环境变量,可以影响程序在运行时的动态链接 (Runtime Linker),它允许在程序运行前优先加载的动态链接库,也可以在 /etc/ld.so.preload
文件中添加。
其实现的功能和 Windows 下通过修改 import table
来 hook API
很类似,只是更简单一些。
最常见的使用场景是不修改程序,而直接修改动态库中函数的实现,例如重新实现 malloc()
和 free()
函数。
示例
#include <stdio.h>
int main(void)
{
FILE *fd;
printf("Calling the fopen() function...\n");
fd = fopen("test.txt","r");
if (fd == NULL) {
printf("fopen() returned NULL\n");
return 1;
}
printf("fopen() succeeded\n");
return 0;
}
然后可以通过如下方式编译、执行。
$ gcc foobar.c -o foobar
$ ./foobar
Calling the fopen() function...
fopen() succeeded
接着我们生成自己定义 fopen()
函数。
#include <stdio.h>
FILE *fopen(const char *path, const char *mode)
{
printf("Always failing fopen\n");
return NULL;
}
然后,编译生成动态库。
$ gcc -Wall -fPIC -shared -o libawrap.so awrap.c
$ LD_PRELOAD=./libawrap.so ./foobar
Calling the fopen() function...
Always failing fopen
fopen() returned NULL
也可以通过如下命令查看符号的查找过程。
LD_DEBUG=symbols ./foobar
以及其真正依赖的库。
$ ldd ./foobar
linux-vdso.so.1 => (0x00007fffaffe7000)
libc.so.6 => /lib64/libc.so.6 (0x00007f0c22128000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0c224f5000)
$ LD_PRELOAD=./libawrap.so ldd ./foobar
linux-vdso.so.1 => (0x00007fff023fe000)
./libawrap.so (0x00007fbfa3e08000)
libc.so.6 => /lib64/libc.so.6 (0x00007fbfa3a3b000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbfa400a000)
高级用法
假设我们仍然需要调用系统提供的函数,可以使用如下的方法。
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
FILE *fopen(const char *path, const char *mode)
{
FILE *(*oopen)(const char *, const char*);
printf("A wrapped fopen\n");
oopen = dlsym(RTLD_NEXT, "fopen");
if (oopen == NULL) {
fprintf(stderr, "Failed to find fopen\n");
return NULL;
}
return oopen(path, mode);
}
也就是通过 dlsym()
查找下个 fopen()
符号。
任务白名单
如上所述,execXXX
系列函数底层都是通过 execve()
系统调用实现,可以通过 preload 替换动态库中的 execXXX
函数。
exec.c 用来覆盖execXXX
main.c 主函数
testwrap.sh 测试主脚本
test.sh 测试脚本,被testwrap.sh调用
实际上,如 execle()
的实现可以参考 glibc 代码中 posix/execle.c
的实现。
- Bash脚本支持。大部分脚本在执行时都会调用
execXXX
接口,其中部分 (如echo) 直接调用的是系统函数。 - Python、Perl、Java 等程序,由于这里的程序可以直接调用系统接口,那么很难直接进行限制。
对于调用的接口可以通过 strace
或者 ltrace
命令查看,关于 ltrace
可以查看 github.com 。
简单来说,就是需要设置一个环境变量,可以在执行命令之前设置,或者在程序中添加。两者的区别在于,前者会检查本程序内执行的命令,而后者只影响到后续的命令执行,例如 Bash 脚本中的。
$ LD_PRELOAD=./libawrap.so ./test
$ LD_PRELOAD=./libawrap.so strace -ff ./test
其它
一个程序调用了那些函数,可以通过 nm -u binary
查看符号表,或者通过 strace/ltrace
查看。
安全相关
这一方式对于静态编译无效,因为不需要在执行时链接动态库里的函数;如果文件设置了 SUID
SGID
,出于安全考虑,在加载时会忽略 LD_PRELOAD
变量。
注意 设置的 SUID/SGID
对应的用户应该与真正运行的用户不同,例如文件用户为 monitor ,如果进程以 monitor 用户运行那么实际上还是有效的。
也可以替换掉默认的 ld 加载器,直接忽略 LD_PRELOAD
变量。
Permission denied
在脚本执行时,可能会遇到上述的报错,通常来说需要确保路径到执行文件有访问权限。
如果是通过 ./script.sh
执行,还需要确保文件有执行权限,或者通过 /bin/bash script.sh
执行,此时只需要有读取权限就可以了。
使用链接参数
其中 GUN 的连接器提供了 ld --wrap=symbol
选项,解释如下 man 1 ld
。
–wrap=symbol Use a wrapper function for symbol. Any undefined reference to symbol will be resolved to __wrap_symbol. Any undefined reference to __real_symbol will be resolved to symbol.
也即是说,GUN 链接器会将未定义的符号解析成 __wrap_symbol
,然后真正调用的函数封装成 __real_symbol
,那么可以通过这一特性将系统调用包裹起来,不过只适用于该程序中。
其中示例如下。
#include <stdio.h>
ssize_t __real_write(int fd, const void *buf, size_t count);
ssize_t __wrap_write (int fd, const void *buf, size_t count)
{
printf("<<< write >>> %lu\n", count);
return __real_write(fd, buf, count);
}
#include <stdio.h>
#include <unistd.h>
int main(void)
{
write(0, "Hello, Kernel!\n", 15);
return 0;
}
$ gcc -Wl,-wrap,write write.c main.c -o test
参考
详细可以参考 Dynamic linker tricks: Using LD_PRELOAD to cheat, inject features and investigate programs 或者 本地文档 。