LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

一般情况下,其加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。几米夜空转载的文章《LD_PRELOAD作用》程序调用流图和代码例子值得一看。

1. 简单举例

我们以Rafał Cieślak的一篇文章的例子为例说明它的用法,翻译自Rafał Cieślak的博客,译者qhwdw

random_num.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;
}

我不使用任何参数来编译它,如下所示:

gcc random_num.c -o random_num

我希望它输出的结果是明确的:从 0-99 中选择的十个随机数字,希望每次你运行这个程序时它的输出都不相同。

使用LD_PRELOAD注入程序

现在,让我们假装真的不知道这个可执行程序的出处。甚至将它的源文件删除,或者把它移动到别的地方 —— 我们已不再需要它了。我们将对这个程序的行为进行重大的修改,而你并不需要接触到它的源代码,也不需要重新编译它。

因此,让我们来创建另外一个简单的 C 文件:

unrandom.c:

int rand() {
    // the most random number in the universe
    return 42;
}

我们将编译它进入一个共享库中:

gcc -shared -fPIC unrandom.c -o unrandom.so

因此,现在我们已经有了一个可以输出一些随机数的应用程序,和一个定制的库,它使用一个常数值 42 实现了一个 rand() 函数。现在……就像运行 random_num 一样,然后再观察结果:

LD_PRELOAD=$PWD/unrandom.so ./random_num

如果你想偷懒或者不想自动亲自动手(或者不知什么原因猜不出发生了什么),我来告诉你 —— 它输出了十次常数 42。

如果先这样执行:

export LD_PRELOAD=$PWD/unrandom.so

然后再以正常方式运行这个程序,这个结果也许会更让你吃惊:一个未被改变过的应用程序在一个正常的运行方式中,看上去受到了我们做的一个极小的库的影响……

使用LD_PRELOAD注入程序

当我们的程序启动后,为程序提供所需要的函数的某些库被加载。我们可以使用 ldd 去学习它是怎么工作的:

ldd random_num

linux-vdso.so.1 => (0x00007fff4bdfe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f48c03ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f48c07e3000)

它列出了被程序 random_num 所需要的库的列表。这个列表是构建进可执行程序中的,并且它是在编译时决定的。在你的机器上的具体的输出可能与示例有所不同,但是,一个 libc.so 肯定是有的 —— 这个文件提供了核心的 C 函数。它包含了 “真正的” rand()。

我使用下列的命令可以得到一个全部的函数列表,我们看一看 libc 提供了哪些函数:

nm -D /lib/libc.so.6

这个 nm 命令列出了在一个二进制文件中找到的符号。-D 标志告诉它去查找动态符号,因为 libc.so.6 是一个动态库。这个输出是很长的,但它确实在列出的很多标准函数中包括了 rand()。

现在,在我们设置了环境变量 LD_PRELOAD 后发生了什么?这个变量为一个程序强制加载一些库。在我们的案例中,它为 random_num 加载了 unrandom.so,尽管程序本身并没有这样去要求它。下列的命令可以看得出来:

LD_PRELOAD=$PWD/unrandom.so ldd random_nums

linux-vdso.so.1 =>  (0x00007fff369dc000)
/some/path/to/unrandom.so (0x00007f262b439000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f262b044000)
/lib64/ld-linux-x86-64.so.2 (0x00007f262b63d000)

注意,它列出了我们当前的库。实际上这就是代码为什么得以运行的原因:random_num 调用了 rand(),但是,如果 unrandom.so 被加载,它调用的是我们所提供的实现了 rand() 的库。

2. 打开文件显示路径

编写inspect_open.c:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

typedef int (*orig_open_f_type) (const char *pathname, int flags);

int open(const char *pathname, int flags, ...)
{
    // remember to include stdio.h!
    printf("open():%s\n", pathname);
    /* Some evil injected code goes here. */
    orig_open_f_type orig_open;
    orig_open = (orig_open_f_type) dlsym(RTLD_NEXT, "open");
    return orig_open(pathname, flags);
}

FILE *fopen(const char *path, const char *mode) {
    printf("fopen():%s\n", path);
    FILE* (*original_fopen) (const char*, const char*);
    original_fopen = dlsym(RTLD_NEXT, "fopen");
    return (*original_fopen)(path, mode);
}

编译为动态库:

gcc -shared -fPIC inspect_open.c -o inspect_open.so -ldl

编写读取文件的例子test.c:

#include<stdlib.h>
#include<stdio.h>

int main()
{
    FILE*fp;
    fp=fopen("file01.txt","r");
    if(fp==NULL)
    {
        printf("Can not openthe file!\n");
        exit(0);
    }
    fclose(fp);
    return 0;
}

编译:

gcc test.c -o test

加载库运行:

LD_PRELOAD=$PWD/inspect_open.so ./test

结果如下,成功注入了函数,打印文件路径:

使用LD_PRELOAD注入程序

inspect_open.c中还重写了open,不过这个实现是不完美的,使用会导致异常。有一些参考文章可以学习:

https://catonmat.net/simple-ld-preload-tutorial-part-two

https://stackoverflow.com/questions/35771395/why-doesnt-ld-preload-trick-catch-open-when-called-by-fopen

文章目录