我想拦截应用程序对 dlsym 的调用。我曾尝试在 .so 中声明我正在预加载 dlsym ,并使用 dlsym 本身来获取它的真实地址,但由于非常明显的原因,这不起作用。
有没有比获取进程的内存映射并使用 libelf 在加载的 libdl.so 中找到 dlsym 的真实位置更容易的方法?
我想拦截应用程序对 dlsym 的调用。我曾尝试在 .so 中声明我正在预加载 dlsym ,并使用 dlsym 本身来获取它的真实地址,但由于非常明显的原因,这不起作用。
有没有比获取进程的内存映射并使用 libelf 在加载的 libdl.so 中找到 dlsym 的真实位置更容易的方法?
警告:
我必须明确警告所有试图这样做的人。具有共享库挂钩的一般前提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 的一部分,不幸的是,它因架构而异。但是,grep 'GLIBC_.*\bdlsym\b' -r sysdeps
glibc 源的根文件夹中的 a 会告诉您您需要什么:
[...]
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
. 这个想法是拦截器库可以在运行时使用标志动态加载这个库dlopen()
,RTLD_LOCAL | RTLD_DEEPBIND
这将为这个对象创建一个单独的链接器范围,也包含libdl
, 这样dlsym
anddlvsym
将被解析为原始方法,而不是那个在拦截器库中。现在的问题是我们的拦截器库不能直接调用包装库内部的任何函数,因为我们不能使用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,...)
以查找注入器库本身的路径,并将其用于搜索包装器库。
http://www.linuxforu.com/2011/08/lets-hook-a-library-function/
从文中:
当您需要在钩子中调用 __libc_dlsym (handle, symbol) 时,请注意本身调用 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;
}