Advances in Kernel Hacking II
在kernel hacking领域有好多观点没有多大用处。但是,这并不意味着不能使用它们解决实际的问题,而它们能够解决的通常又不是人们最初希望解决的。 在收到执行某个可执行二进制文件的请求时,如果你重定向执行请求,转而执行其它的二进制可执行文件,将使用户很难察觉。我们通过几个内核模块就可以实现执行的重定向,从而使某个可执行文件只有在执行时,才会被替代,而真正的可执行文件根本不做任何修改。 如果使用这种技术安装系统后门,由于不对任何的文件进行修改,因此将使Tripwire[1]和aide[2]等数据完整性检测工具无法察觉。而另一方面,如果把执行重定向技术用于蜜罐(honeypot)系统,可以起到迷惑攻击者的作用。 即使经过多年活跃的内核开发,通过可加载内核(Loadable Kernel Module)实现执行重定向功能也只是使用同样的技术。因此,某些系统管理员能够很轻易地快速察觉系统的后门,而其他人对这种危险却无动于衷。不过,目前这种危险还并不存在。 下面,我将展示五种实现执行重定向的方式,附录A包含它们的示例代码。这些示例代码并不能用于实际,只是为了描述一些思想。 读一下参考4或者参考5对于理解这些源代码很有帮助。
这些示例代码只是描述了如何在一个LKM中实现这些技术,而且这都是在Linux系统的实现。不过,这些技术并不局限于Linux,大部分技术只要稍做修改可以移植到任何的UNIX系统。 3.1.Classic 这里之所以包含这个技术,纯粹是出于完整性考虑,它是通过代替系统调用处理函数实现的。细节请看附录A中的代码classic.c,这里就不必多做赘述了。KNARK(参考3)rootkit采用了这个技术,有关的技术细节在参考6中有解释。采用这个技术的隐蔽性不好,只要检查指向系统调用表的地址就能够检测到。 3.2.Obvious 系统调用和硬件的架构无关。在内核的源代码中,do_execve函数的作用是执行应用程序(在~/fs/exec.c文件中)。而execve系统调用可以理解为do_execve函数的一个封装。这里,我们将通过代替do_execve函数实现执行的重定向:
n_do_execve (char *file, char **arvp, char **envp,
struct pt_regs *regs)
...
if (!strcmp (file, O_REDIR_PATH)) {
file = strdup (N_REDIR_PATH);
}
restore_do_execve ();
ret = do_execve (file, arvp, envp, regs);
redirect_do_execve ();
...
为了实际的执行重定向,我们替代do_execve内核函数并随心所欲地用新的文件名代替原来的文件名。这显然也是一种封装execve系统调用的方式。有关的技术细节请参考附录A中的obvious.c文件。据我所知,目前还没有LKM采用这种技术。 检测采用这种技术的后门不象采用classic技术的那样容易。检查函数的开头是否包含一个跳转指令是个不错的想法。 注:采用这种技术,首先需要用一段代码(static char n_handler[] = "xb8x00x00x00x00xffxe0")替代目标函数的开头七个字节的指令,n_handler[]表示如下汇编指令:
mov $0x0,%eax
jmp *%eax
然后,在模块的init_module()函数中使用*(long *) &n_handler[1] = (long) n_do_execve在n_handler[]中填入n_do_execve函数的地址。n_handler[]将变成
mov $0x4xxxxxxx,%eax
jmp *%eax
3.3.Waiter 在执行一个程序之前,系统首先要打开需要执行的文件。内核提供了一个函数open_exec完成这个任务。它将打开要执行的二进制文件并做一些完整性检查工作。 由于open_exec需要完整的路径才能打开要执行的二进制文件,因此我们可以任意地使用我们要执行的文件代替原来的文件名,然后调用原来的函数(open_exec)。open_exec在do_execve函数中调用。 这种技术的实现方式和上一种差不多,因此也是可以检测的。 3.4.Nexus 一旦二进制可执行文件被打开,就可以读入内存了,对吗?事实上,在这之前还要搜索对应的二进制格式处理器,由这个处理器处理这个二进制文件。通常,这个操作完成,就将启动一个新的进程。 一个二进制格式处理器如下定义(参考~/include/linux/binfmts.h文件):
/*
* This structure defines the functions that are
* used to load the binary formats that linux
* accepts.
*/
struct linux_binfmt {
struct linux_binfmt * next;
struct module *module;
int (*load_binary)(struct linux_binprm *,
struct pt_regs * regs);
int (*load_shlib)(struct file *);
int (*core_dump)(long signr, struct pt_regs * regs,
struct file * file);
unsigned long min_coredump; /* minimal dump size */
};
在这个结构中提供了三个函数指针。load_shlib用于加载动态连接库;core_dump用于处理内核转储(core dump)文件;load_binary用来加载要执行的二进制文件。我们可以在load_binary上面做手脚,用新的处理函数替代这个指针。 我们的load_binary函数如下所示:
int n_load_binary (struct linux_binprm *bin,
struct pt_regs *regs) {
int ret;
if (!strcmp (bin->filename, O_REDIR_PATH)) {
/*
* 如果是要重定向的二进制文件,
* 只要关闭原有文件描述符打开新的文件。
*/
filp_close (bin->file, 0);
bin->file = open_exec (N_REDIR_PATH);
prepare_binprm (bin);
goto out;
}
out:
return old_load_binary (bin, regs);
}
更多相关文章
|
推荐文章
精彩文章
|