警告:
我必须明确警告所有试图这样做的人。有一个共享库挂钩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);
}
注意这个“解决方案”的两件事:
- 此代码绕过
(__libc_)dlsym() 内部完成的锁定,因此为了使此线程安全,您应该添加一些锁定。
-
_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");
如果您确实需要在共享对象中同时挂钩dlsym 和dlvsym,那么这种方法当然也行不通。
更新:同时挂钩 dlsym() 和 dlvsym()
出于好奇,我想了一些方法来挂钩这两种 glibc 符号查询方法,然后我想出了一个解决方案,它使用了一个链接到 libdl 的附加包装库。这个想法是拦截器库可以在运行时使用带有RTLD_LOCAL | RTLD_DEEPBIND标志的dlopen()动态加载这个库,这将为这个对象创建一个单独的链接器范围,也包含libdl,因此dlsym和dlvsym 将被解析为原始方法,而不是拦截器库中的方法。现在的问题是我们的拦截器库不能直接调用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,...) 来查找注入器库本身的路径,并使用它来搜索包装器库。