Linux 环境变量LD_PRELOAD

文章目录

  • 前言
  • 一、简介
  • 二、例程1
    • 2.1 示例1
      • 2.1.1 演示
      • 2.1.2 编译选项
    • 2.2 示例2
      • 2.2.1 代码示例
      • 2.2.2 代码介绍
      • 2.2.3 export 简介
  • 三、例程2

前言

LD_PRELOAD环境变量是一个非常有用的Linux环境变量,它可以在程序启动时强制动态链接器(ld.so)加载指定的共享库文件,从而覆盖原有的库文件或者添加新的功能。

当程序启动时,动态链接器会首先搜索LD_PRELOAD环境变量指定的共享库文件,如果找到了,则会先加载该共享库文件,然后再加载程序所依赖的其它库文件。这样就可以在程序执行过程中,优先使用LD_PRELOAD环境变量指定的共享库文件,而不是系统默认的库文件。

使用LD_PRELOAD环境变量可以达到以下几个目的:

(1)功能扩展:通过加载自定义的共享库文件,可以为程序添加新的功能或者修改程序行为。

(2)调试程序:通过加载调试库文件,可以在程序执行时获取更多的调试信息,方便程序调试和错误排查。

(3)性能分析:通过加载性能分析库文件,可以在程序执行时获取更多的性能信息,方便优化程序性能。

在使用LD_PRELOAD环境变量时,必须确保共享库文件与程序的ABI(Application Binary Interface)兼容,否则可能会导致程序崩溃或者出现其他异常情况。

一、简介

当程序加载动态链接库时,Linux 动态链接器将搜索所有默认路径中列出的动态链接库,以及任何由 LD_PRELOAD 环境变量指定的动态链接库。如果 LD_PRELOAD 环境变量设置了一个或多个动态链接库的名称,Linux 动态链接器将先加载这些动态链接库,然后再加载默认路径中列出的其他动态链接库。

攻击者可以使用 LD_PRELOAD 环境变量来注入恶意动态链接库来修改程序的行为或获取敏感信息。例如,攻击者可以设置 LD_PRELOAD 环境变量来指定一个恶意动态链接库,该库中的代码将在程序加载时被执行。如果程序调用了某些系统函数,例如 open() 或 getenv(),恶意动态链接库中的代码将被执行,从而使攻击者能够获取程序的敏感信息或修改程序的行为。

LD_PRELOAD 环境变量对于程序员来说也是一个有用的调试工具。通过设置 LD_PRELOAD 环境变量,程序员可以覆盖默认的系统函数实现,以便在程序中使用自己的实现。例如,程序员可以编写一个自定义的 malloc() 实现来跟踪内存分配和释放的情况,以帮助调试内存泄漏等问题。

总之,LD_PRELOAD 环境变量是一个在 Linux 中常用的环境变量,可用于指定程序加载动态链接库时要先加载的库。然而,由于其潜在的安全风险,应该注意谨慎使用 LD_PRELOAD 环境变量,并确保只加载可信的动态链接库。

二、例程1

LD_PRELOAD 环境变量是一个非常有用的调试工具,可以帮助程序员在调试程序时重写某些系统库函数。例如,程序员可以编写一个自定义的 malloc() 函数来跟踪内存的分配和释放,从而帮助调试内存泄漏问题。

2.1 示例1

2.1.1 演示

首先我们来给一个最简单的一个例子用来替换掉malloc() 函数和free函数。

//mymalloc.c
#include 
#include 

void *malloc(size_t size) {
    printf("call my malloc\n");
    return NULL;
}

void free(void *ptr) {
    printf("call my free\n");
}
//malloc.c
#include 
#include 

int main() 
{
    int *ptr;
    int n = 5;

    ptr = (int*) malloc(n * sizeof(int));

    free(ptr); 

    return 0;
}
# gcc -shared -fPIC -o mymalloc.so mymalloc.c 
# gcc malloc.c 
# LD_PRELOAD=./mymalloc.so ./a.out 
call my malloc
call my free

LD_PRELOAD 环境变量将导致你的 malloc() 和 free() 函数实现被预先加载并用于代替标准函数。

这样实际就是把malloc函数替换成我们自己的malloc函数了。但这个例子没啥用,我们要hook掉malloc函数后,调用我们的hook函数后,然后返回原来的libc中的malloc函数。

其中:

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

这将创建一个共享库文件 “mymalloc.so”。

2.1.2 编译选项

(1)-shared
-shared 是一个 gcc 编译器选项,用于指定生成一个共享库(shared library)。

在 Linux 系统中,共享库是一种可重用的代码库,可以被多个进程共享,从而节省系统资源。共享库通常包含一组函数,可以被程序动态加载并使用。与静态库不同,静态库的代码被复制到程序中,因此每个使用该库的程序都会包含该库的一份拷贝,浪费了系统资源。

使用 -shared 选项编译代码时,gcc 编译器将生成一个共享库文件,其中包含指定的目标文件的可重定位代码和符号表信息。共享库文件的扩展名通常是 “.so”(shared object)。

在上面的例子中,我们通过使用 -shared 选项将 mymalloc.c 编译为一个共享库文件 mymalloc.so。这个共享库包含了我们自己实现的 malloc() 和 free() 函数,可以被 LD_PRELOAD 环境变量预先加载并替换系统默认的函数实现,从而实现跟踪内存分配和释放的调试目的。

总之,-shared 是一个 gcc 编译器选项,用于指定生成一个共享库文件,它可以被多个进程共享,从而节省系统资源。

(2)-fPIC
-fPIC 是 gcc 编译器选项之一,用于生成位置无关代码(Position Independent Code,简称 PIC)。位置无关代码是一种可被动态链接的代码,可以被加载到任何内存地址而不需要进行重新定位。这使得它们非常适合用于共享库等需要在多个进程之间共享的代码中。

在 Linux 系统中,共享库是一种可重用的代码库,可以被多个进程共享。共享库通常包含一组函数,可以动态加载并使用。为了使共享库能够在多个进程之间共享,它的代码必须是位置无关的。否则,每个进程都需要将共享库的代码复制到自己的地址空间中,并进行重新定位,这将浪费系统资源。

使用 -fPIC 选项编译代码时,gcc 编译器将生成位置无关代码,其中包含使用相对地址引用的指令,而不是绝对地址引用。这使得共享库可以加载到任何内存地址而不需要进行重新定位。

在上面的例子中,我们通过使用 -fPIC 选项编译 mymalloc.c 文件,以生成一个位置无关的共享库文件 mymalloc.so。这个共享库可以被多个进程共享,从而实现节省系统资源的目的。

总之,-fPIC 是一个 gcc 编译器选项,用于生成位置无关代码,这种代码可以被动态链接,并且可以被加载到任何内存地址而不需要进行重新定位。在共享库等需要在多个进程之间共享的代码中,使用 -fPIC 选项可以提高系统性能并节省系统资源。

2.2 示例2

2.2.1 代码示例

接下来我们调整上面的代码,C语言程序中使用内存分配函数 malloc() 和释放函数 free() 。怀疑程序中存在内存泄漏问题,但不确定在哪里。你可以使用 LD_PRELOAD 来预先加载你自己的 malloc() 和 free() 函数实现,以跟踪内存分配和释放,并打印有用的调试信息。
具体步骤如下:
(1)编写一个自己的 malloc() 和 free() 函数实现,用于跟踪内存分配和释放。例如,你可以使用以下代码:

#define _GNU_SOURCE
#include 
#include 
#include 

void* malloc(size_t size)
{
    void* (*original_malloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
    if (original_malloc == NULL) {
        fprintf(stderr, "Error: cannot find original malloc function.\n");
        return NULL;
    }
    void* ptr = original_malloc(size);

    //使用 fprintf 函数来代替 printf 函数
    //printf("malloc(%zu) = %p\n", size, ptr);
    fprintf(stderr, "malloc(%zu) = %p\n", size, ptr);
 
    return ptr;
}

void free(void *ptr)
{
    void (*original_free)(void*) = dlsym(RTLD_NEXT, "free");
    if (original_free == NULL) {
        fprintf(stderr, "Error: cannot find original free function.\n");
        return;
    }
    original_free(ptr);
    printf("free(%p)\n", ptr);
}

malloc 中调用了printf会出现段错误,在自定义的 “malloc” 函数中调用了 “printf” 函数导致的。由于 “printf” 函数本身使用了内存分配(例如,使用变长参数),因此在自定义的 “malloc” 函数中调用 “printf” 函数可能会导致无限递归,最终导致栈溢出和段错误。

这里在自定义的 malloc 函数中使用 fprintf 函数来代替 printf 函数。

(2)将 LD_PRELOAD 环境变量设置为共享库的完整路径。例如,如果共享库位于 “/home/user/lib/mymalloc.so”,可以使用以下命令:

export LD_PRELOAD=/home/user/lib/mymalloc.so
./a.out

或者

LD_PRELOAD=/home/user/lib/mymalloc.so  ./a.out

其中a.out就是我们编写的一个使用了malloc() 和 free() 函数的可执行程序。

像平常一样运行你的程序。LD_PRELOAD 环境变量将导致你的 malloc() 和 free() 函数实现被预先加载并用于代替标准函数。调试信息将被打印到控制台。通过使用 LD_PRELOAD 进行调试,你可以轻松跟踪程序中的内存分配和释放,并识别内存泄漏问题。

2.2.2 代码介绍

(1) _GNU_SOURCE
在 C 语言中,预处理器指令 “#define” 用于定义宏。在这个示例代码中,它定义了 “_GNU_SOURCE” 宏,这个宏是一个特殊的宏,它激活了 GNU C 库提供的一些非标准函数和特性。这些函数和特性通常是为了提高程序的可移植性、安全性或性能而提供的。

在这个示例中, “_GNU_SOURCE” 宏的定义会影响到一些函数的定义和行为。例如,它会启用 “dlsym” 函数,这个函数是一个在 GNU C 库中提供的非标准函数,用于在运行时动态地获取共享库中的符号(例如函数和变量)。如果没有定义 “_GNU_SOURCE”,则在编译时 “dlsym” 函数将会被视为未定义的符号。

总的来说, “_GNU_SOURCE” 宏是一个特殊的宏,用于启用 GNU C 库中提供的一些非标准函数和特性。在需要使用这些函数和特性时,可以使用 “#define _GNU_SOURCE” 来启用它们。

(2)dlsym
“dlsym” 函数是一个在动态链接库中查找符号(例如函数、变量)的函数。它位于 “dlfcn.h” 头文件中,通常用于加载共享库并获取其中的符号地址。

在这个示例中,“dlsym” 函数用于获取原始的 “malloc” 和 “free” 函数的地址,以便在自定义的 “malloc” 和 “free” 函数中调用它们。具体来说,它通过使用 “RTLD_NEXT” 参数在当前动态链接库的符号表中查找下一个符号,即在当前库之后出现的符号。由于系统默认会将标准 C 库(libc)和其他库预先加载到进程中,所以使用 “RTLD_NEXT” 参数可以获取到系统中已经加载的标准 C 库的 “malloc” 和 “free” 函数的地址。

如果 “dlsym” 函数成功地查找到了符号,则返回符号的地址,否则返回 NULL。在这个示例中,如果无法找到原始的 “malloc” 或 “free” 函数,程序将输出错误信息并返回 NULL。

2.2.3 export 简介

export 是一个 shell 命令,用于将一个变量或函数导出到环境中,从而使其在子进程中也可用。在上面的例子中,我们使用 export 命令将 LD_PRELOAD 变量导出到环境中,以便在运行程序时,子进程也能够使用这个变量。

简单来说,export 命令将一个变量从当前 shell 进程中导出到环境中,使得其他子进程也可以访问该变量。如果不使用 export 命令,变量只会在当前 shell 进程中可用,而在子进程中不可用。

在上面的例子中,我们导出了 LD_PRELOAD 变量,这个变量被设置为我们自己实现的 malloc() 和 free() 函数所在的共享库的路径。当我们运行程序时,子进程会继承这个环境变量,并使用我们的实现来替换系统默认的 malloc() 和 free() 函数实现,从而实现内存分配和释放的跟踪和调试。

export 命令是将一个变量或函数从当前 shell 进程导出到环境中,从而使得其他子进程也可以访问该变量或函数。因此,导出的作用域是当前 shell 进程及其所有子进程。

具体来说,如果在一个 shell 进程中使用 export 命令导出了一个变量或函数,那么在当前 shell 进程中,以及在该进程启动的所有子进程中,这个变量或函数都是可用的。但是,在其他的 shell 进程中,这个变量或函数是不可见的,除非在这些进程中也使用 export 命令将其导出到环境中。

在上面的例子中,我们使用 export 命令将 LD_PRELOAD 变量导出到环境中,以便在运行程序时,子进程也能够使用这个变量。这意味着,在当前 shell 进程中,以及在该进程启动的所有子进程中,LD_PRELOAD 变量都是可用的,并且被设置为我们自己实现的 malloc() 和 free() 函数所在的共享库的路径。如果在其他的 shell 进程中运行程序,则需要在这些进程中重新设置 LD_PRELOAD 变量才能使用我们自己的 malloc() 和 free() 函数实现。

三、例程2

#define _GNU_SOURCE
#include 
#include 

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

这段代码定义了一个名为fopen的函数,它和C标准库中的fopen函数具有相同的参数和返回值。当程序调用fopen函数时,实际上会调用这个自定义的fopen函数,而不是C标准库中的fopen函数。

在这个自定义的fopen函数中,我们首先使用dlsym函数获取C标准库中的fopen函数的地址,然后打印出要打开的文件的路径,最后调用原始的fopen函数来打开文件,并返回打开的文件指针。

在编译共享库时使用-l dl选项来链接libdl库:

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

这里编译动态库时加了 -ldl 选项,dlsym函数是在libdl库中定义的,它是动态链接器的一部分,用于在运行时查找和获取共享库中的符号地址。因此,如果我们的程序中使用了dlsym函数,就需要链接到libdl库。

#include 

int main() {
    FILE *fp;
    fp = fopen("1.txt", "r");
    if (fp == NULL) {
        printf("Failed to open file\n");
        return 1;
    }
    printf("File opened successfully\n");
    fclose(fp);
    return 0;
}
$ LD_PRELOAD=./my_fopen.so ./a.out 
hook fopen : open file: 1.txt
File opened successfully

可以使用LD_PRELOAD来加载多个共享库,只需在LD_PRELOAD环境变量中使用冒号“:”将它们的路径分隔开即可。例如:

$ LD_PRELOAD=./lib1.so:./lib2.so my_program

这将在加载“my_program”之前同时加载“./lib1.so”和“./lib2.so”。当使用LD_PRELOAD加载多个共享库时,动态链接器将按照它们在LD_PRELOAD中列出的顺序搜索符号。这意味着如果两个库定义了相同的符号,则在LD_PRELOAD中列出的第一个库将优先使用该符号。

需要注意的是,使用LD_PRELOAD加载多个共享库可能会很棘手,尤其是如果这些库具有冲突的依赖关系或者以不同的方式修改相同的函数。这可能会导致意外的行为甚至崩溃。因此,重要的是要彻底测试和验证使用LD_PRELOAD加载的任何共享库的兼容性。此外,通常最好将使用LD_PRELOAD加载的共享库数量保持在最少数量,并在可能的情况下使用其他调试技术和工具。

你可能感兴趣的:(系统安全,linux,c语言)