【问题标题】:How can I intercept dlsym calls using LD_PRELOAD?如何使用 LD_PRELOAD 拦截 dlsym 调用?
【发布时间】:2013-03-14 00:04:00
【问题描述】:

我想拦截应用程序对 dlsym 的调用。我曾尝试在 .so 中声明我正在预加载 dlsym ,并使用 dlsym 本身来获取它的真实地址,但由于很明显的原因,这不起作用。

有没有比获取进程的内存映射并使用 libelf 在加载的 libdl.so 中找到 dlsym 的真实位置更简单的方法?

【问题讨论】:

  • 代码是问题的一部分还是答案?您可以编辑您的问题或将其作为答案发布。

标签: c++ c intercept ld-preload dlsym


【解决方案1】:

警告

我必须明确警告所有试图这样做的人。有一个共享库挂钩dlsym 的一般前提有几个明显的缺点。最大的问题是原始的dlsym 实现如果 glibc 将在内部使用堆栈展开技术来找出调用该函数的加载模块。如果拦截共享库然后代表原始应用程序调用原始dlsym,这将中断使用RTLD_NEXT之类的东西的查找,因为现在当前模块不是最初调用的模块,但是你的钩子库。

也许可以以正确的方式实现这一点,但这需要更多的工作。在没有尝试过的情况下,我认为使用dlinfo 获取链接映射的链表,您可以单独遍历所有模块,并为每个模块执行单独的dlsym,以获得RTLD_NEXT 行为正确。为此,您仍然需要获取调用者的地址,您可以通过旧的 backtrace(3) 系列函数获得该地址。

我 2013 年的旧答案

我偶然发现 hdante 与评论者的回答相同的问题:调用 __libc_dlsym() 直接因段错误而崩溃。在阅读了一些 glibc 资源之后,我想出了以下 hack 作为解决方法:

extern void *_dl_sym(void *, const char *, void *);
extern void *dlsym(void *handle, const char *name)
{
    /* my target binary is even asking for dlsym() via dlsym()... */
    if (!strcmp(name,"dlsym")) 
        return (void*)dlsym;
    return _dl_sym(handle, name, dlsym);
}

注意这个“解决方案”的两件事:

  1. 此代码绕过(__libc_)dlsym() 内部完成的锁定,因此为了使此线程安全,您应该添加一些锁定。
  2. _dl_sym()的第三个参数是调用者的地址,glibc似乎是通过堆栈展开来重构这个值,但我只是使用了函数本身的地址。调用者地址在内部用于查找调用者所在的链接映射,以正确获取 RTLD_NEXT 之类的内容(并且,使用 NULL 作为第三个参数将使调用失败并在使用 RTLD_NEXT 时出现错误)。但是,我还没有研究过 glibc 的 unwindind 功能,所以我不能 100% 确定上面的代码会做正确的事情,而且它可能只是碰巧起作用......

目前提出的解决方案有一些明显的缺点:_dl_sym() 在某些情况下的行为与预期的dlsym() 完全不同。例如,尝试解析一个不存在的符号会退出程序,而不是仅仅返回 NULL。要解决这个问题,可以使用_dl_sym() 来获取指向原始dlsym() 的指针并将其用于其他所有内容(例如在“标准”LD_PRELOAD 钩子方法中,根本不钩子dlsym):

extern void *_dl_sym(void *, const char *, void *);
extern void *dlsym(void *handle, const char *name)
{
    static void * (*real_dlsym)(void *, const char *)=NULL;
    if (real_dlsym == NULL)
        real_dlsym=_dl_sym(RTLD_NEXT, "dlsym", dlsym);
    /* my target binary is even asking for dlsym() via dlsym()... */
    if (!strcmp(name,"dlsym")) 
        return (void*)dlsym;
    return real_dlsym(handle,name);
}

2021 年更新 / glibc-2.34

从 glibc 2.34 开始,函数 _dl_sym() 不再公开导出。我可以建议的另一种方法是改用dlvsym(),这通常是 glibc API 和 ABI 的一部分。唯一的缺点是您现在需要确切的版本来请求dlsym 符号。幸运的是,这也是 glibc ABI 的一部分,不幸的是,它因架构而异。但是,glibc 源的根文件夹中的 grep 'GLIBC_.*\bdlsym\b' -r sysdeps 会告诉您您需要什么:

[...]
sysdeps/unix/sysv/linux/i386/libc.abilist:GLIBC_2.0 dlsym F
sysdeps/unix/sysv/linux/i386/libc.abilist:GLIBC_2.34 dlsym F
[...]
sysdeps/unix/sysv/linux/x86_64/64/libc.abilist:GLIBC_2.2.5 dlsym F
sysdeps/unix/sysv/linux/x86_64/64/libc.abilist:GLIBC_2.34 dlsym F

Glibc-2.34 实际上引入了这个函数的新版本,但为了向后兼容,旧版本仍然保留。

对于 x86_64,您可以使用:

real_dlsym=dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.2.5");

而且,如果你们都想在同一进程中获取最新版本,以及可能的另一个拦截器,则可以使用该版本再次执行未版本化查询:

real_dlsym=real_dlsym(RTLD_NEXT, "dlsym");

如果您确实需要在共享对象中同时挂钩dlsymdlvsym,那么这种方法当然也行不通。

更新:同时挂钩 dlsym()dlvsym()

出于好奇,我想了一些方法来挂钩这两种 glibc 符号查询方法,然后我想出了一个解决方案,它使用了一个链接到 libdl 的附加包装库。这个想法是拦截器库可以在运行时使用带有RTLD_LOCAL | RTLD_DEEPBIND标志的dlopen()动态加载这个库,这将为这个对象创建一个单独的链接器范围,也包含libdl,因此dlsymdlvsym 将被解析为原始方法,而不是拦截器库中的方法。现在的问题是我们的拦截器库不能直接调用wrapper库里面的任何函数,因为我们不能使用dlsym,这是我们原来的问题。

但是,共享库可以有一个初始化函数,链接器将在dlopen() 返回之前调用该函数。我们只需要将包装库的初始化函数中的一些信息传递给拦截器库即可。由于两者都在同一个进程中,我们可以为此使用环境块。

这是我想出的代码:

dlsym_wrapper.h:

#ifndef DLSYM_WRAPPER_H
#define DLSYM_WRAPPER_H

#define DLSYM_WRAPPER_ENVNAME "DLSYM_WRAPPER_ORIG_FPTR"
#define DLSYM_WRAPPER_NAME "dlsym_wrapper.so"
typedef void* (*DLSYM_PROC_T)(void*, const char*);

#endif

dlsym_wrapper.c,编译为dlsym_wrapper.so

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

#include "dlsym_wrapper.h"

__attribute__((constructor))
static void dlsym_wrapper_init()
{
    if (getenv(DLSYM_WRAPPER_ENVNAME) == NULL) {
        /* big enough to hold our pointer as hex string, plus a NUL-terminator */
        char buf[sizeof(DLSYM_PROC_T)*2 + 3];
        DLSYM_PROC_T dlsym_ptr=dlsym;
        if (snprintf(buf, sizeof(buf), "%p", dlsym_ptr) < (int)sizeof(buf)) {
            buf[sizeof(buf)-1] = 0;
            if (setenv(DLSYM_WRAPPER_ENVNAME, buf, 1)) {
                // error, setenv failed ...
            }
        } else {
            // error, writing pointer hex string failed ...
        }
    } else {
        // error: environment variable already set ...
    }
}

拦截器库中的一个函数用于获取指向 原始dlsym()(应该只调用一次,由互斥锁保护):

static void *dlsym_wrapper_get_dlsym
{
    char dlsym_wrapper_name = DLSYM_WRAPPER_NAME;
    void *wrapper;
    const char * ptr_str;
    void *res = NULL;
    void *ptr = NULL;

    if (getenv(DLSYM_WRAPPER_ENVNAME)) {
        // error: already defined, shoudn't be...
    }
    wrapper = dlopen(dlsym_wrapper_name, RTLD_LAZY | RTLD_LOCAL | RTLD_DEEPBIND | RTLD_NOLOAD);
    if (wrapper) {
        // error: dlsym_wrapper.so already loaded ...
        // it is important that we load it by ourselves to a sepearte linker scope
    }
    wrapper = dlopen(dlsym_wrapper_name, RTLD_LAZY | RTLD_LOCAL | RTLD_DEEPBIND);
    if (!wrapper) {
        // error: dlsym_wrapper.so can't be loaded
    }

    ptr_str = getenv(DLSYM_WRAPPER_ENVNAME);
    if (!ptr_str) {
        // error: dlsym_wrapper.so failed...
    }
    if (sscanf(ptr_str, "%p", &ptr) == 1) {
        if (ptr) {
            // success!
            res = ptr;
        } else {
            // error: got invalid pointer ...
        }
    } else {
        // error: failed to parse pointer...
    }

    // this is a bit evil: close the wrapper. we can be sure
    // that libdl still is used, as this mosule uses it (dlopen)
    dlclose(wrapper);

    return res;
}

这当然假设dlsym_wrapper.so 在库搜索路径中。但是,您可能更喜欢使用完整路径通过LD_PRELOAD 注入拦截器库,而根本不修改LD_LIBRARY_PATH。为此,您可以添加dladdr(dlsym_wrapper_get_dlsym,...) 来查找注入器库本身的路径,并使用它来搜索包装器库。

【讨论】:

  • 您的real_dlsym 接受3 个参数,但您只需使用real_dlsym(handle,ptr) 调用它,其中ptr 甚至不是变量。
  • @lama12345:你是对的。我更新了我的答案以修复那些复制和粘贴错误。
  • 对您来说有两个坏消息:1. 您的代码是正确的,但对于RTLD_NEXT,您的代码并不总是正确。 2. Citrix 向/etc/ld.so.preload 添加了一个库,该库显然复制并粘贴了您的答案中的代码,而没有注意到您的警告,这对人们造成了破坏(link 1link 2)。
  • @JosephSible-ReinstateMonica 感谢您的提醒。不幸的是,我对此无能为力。正如我很久以前在答案中所写的那样,一个人应该只使用这个技巧来获得指向dlsym 本身的指针,并让 that 进行所有其他查找的处理(这也将得到像RTLD_NEXT 这样的标志对...)
【解决方案2】:

http://www.linuxforu.com/2011/08/lets-hook-a-library-function/

来自正文:

当您需要在钩子中调用 __libc_dlsym(句柄、符号)时,请注意自身调用 dlsym() 的函数。

extern void *__libc_dlsym (void *, const char *);
void *dlsym(void *handle, const char *symbol)
{
    printf("Ha Ha...dlsym() Hooked\n");
    void* result = __libc_dlsym(handle, symbol); /* now, this will call dlsym() library function */
    return result;
}

【讨论】:

  • 这似乎是段错误,我什至尝试单独编译该代码,结果相同
  • 相同的@user1588911
猜你喜欢
  • 1970-01-01
  • 2015-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-25
  • 2018-07-08
相关资源
最近更新 更多