Function Interposition in Linux(hook)

你是否想多改变库代码的工作方式,不替换整个库或者重新编译它。例如,你想包裹一层mallocfree函数来记录分配的日志,为了查找内存泄露。你可以重写那段使用了malloc/free的代码,或者修改libc,这两者听起来都不是很吸引人的方式

这个教程将告诉你用自己实现的wrapper来代替库中的函数,这被叫做函数打桩(function interposition),它可以在任何程序中完成,而无需重新编译程序或库。

首先,一些背景知识:动态链接。当一个程序使用动态链接库编译时,有一列表记录未定义的符号(symbol)包含在二进制中的,还有一个列表记录程序所链接的库(library)。符号和库两者没什么关联,这两个列表仅仅用来告诉loader那些库需要被加载、那些符号需要被解析。在运行时(Runtime),每个符号用提供的第一个库来解析,这意味着如果我们将包含我们wrapper函数的库在其他库的前面加载,程序中的未定义符号表将解析到我们的wrapper函数,而不是真正的函数

我们如何让一个程序加载它没有链接的库?幸运的是,这是最简单的部分。环境变量LD_PRELOAD为加载器提供了一个库列表,以便在其他任何操作之前加载。假设我们有一个名为libjmalloc的共享库。因此包括mallocfree的替换。我们想在程序foo中使用它,所以我们像这样运行它:

LD_PRELOAD=/home/jay/libjmalloc.so   ./foo

这个loader将表现的像foo链接了libmalloc.so一样,我们给它一个到库的绝对路径,这样它就不会去搜索/usr/lib这样的普通位置。如果要预加载多个库,请使用冒号分隔它们的名称。

到目前为止都很好,但是如果我们想在自己实现的版本中使用原版的malloc呢?例如,我们仅仅想在调用malloc/free时打印一条信息,但是内存管理仍然用原来的malloc/free。然而我们不能在wrapper中直接调用libc版的malloc,因为编译器会将它解释为对wrapper本身的递归调用。解决方案是使用dlsym动态加载指向malloc的指针

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

void* malloc(size_t size)
{
    static void* (*real_malloc)(size_t) = NULL;
    if (!real_malloc)
        real_malloc = dlsym(RTLD_NEXT, "malloc");

    void *p = real_malloc(size);
    fprintf(stderr, "malloc(%d) = %p\n", size, p);
    return p;
}

我们编译一下:

gcc -shared -ldl -fPIC jmalloc.c -o libjmalloc.so

dlfcn.h 申明函数将动态加载符号表,而不是链接到程序中。这些函数的一个主要用法就是加载plugins。例如在这里,我们认为libc是一个提供了malloc(我们把它赋给real_malloc)的插件。我们使用dlsym加载这个符号,它有两个参数:(a library handle, a symbol name)。通常情况下,我们使用dlopen获取一个有效的库句柄,但是因为malloc所在的libc库默认就是被链接,所以我们只需要传一个RTLD_NEXT它告诉动态链接器:在下一个支持它的库中解析符号(而不是在当前调用dlsyum的这个库)RTLD_NEXTGNU特有的,所以记得在include dlfcn.h 之前定义_GNU_SOURCE

此时,您可以替换大部分的库函数。但是,有一些函数不能使用此方法插入。例如,如果您想为dlsym本身创建一个包装器,该怎么办?您还不能在内部包装任何库函数dlsym调用。

如果你确实需要包装这些函数,GNU链接器提供了一个有用的选项 --wrap。如果你给它一个符号dlsym,它会将所有的dlsym调用替换成__wrap_dlsym,用real_dlsym调用真正的dlsym,这种方法的缺点是,您需要重新链接使用wrapper任何程序。上面的例子可以被写成这样:

#include <stdint.h>
#include <stdio.h>

void* __real_malloc(size_t);
void* __wrap_malloc(size_t size)
{
    void *p = __real_malloc(size);
    fprintf(stderr, "malloc(%d) = %p\n");
    return p;
}

写在最后的一点东西。首先,LD_PRELOAD会因为安全原因被SUID权限的程序忽略,由于函数插入可以让程序执行任何您想要的操作,因此Linux阻止您修改代表其他用户或组运行的程序的行为。其次,您不能插入静态链接库的函数调用,因为这些调用在运行时之前已解析。例如,如果libc中的某个函数调用malloc,它将永远不会调用其他库中的包装函数。

除了这些限制之外,函数插入是一种非常强大的技术,可用于监控程序或修改其行为。愉快的介入!

ref:
https://jayconrod.com/posts/23/tutorial-function-interposition-in-linux

posted @ 2022-08-19 11:25  Rogn  阅读(108)  评论(0编辑  收藏  举报