译者:eridanus96
预估稿费:180RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
一直以来,我希望能深入了解Linux内核内部是如何工作的。为实现这一点,有一个比较好的思路是写一个小的Rootkit PoC。大家可以在这里找到相关Rootkit代码。
这是一个非常简单的Rootkit。其功能是,隐藏特定前缀的文件使其不可见。然而,假如我们知道这些文件或文件夹的位置,就仍然可以访问它们。但通过“ls -a”命令以及文件管理器是无法看到的。此外,通过lsmod或者/proc/modules也无法将它们列出。
关于ls
我们首先尝试一下如何借助ls来找到这些文件。我们知道,当某个程序需要借助网络、文件系统或其他系统特定活动进行工作时,它就必须经过内核。也就是说,在此时它将使用系统调用。我们可以在这个表格中,查到64位Linux系统的系统调用。
为了找出ls所使用的系统调用,我们使用了一个名为Strace的工具。Strace将会列出程序所使用的系统调用。当我们执行strace ls后,会出现很多程序连接产生的杂项,但如果我们继续向下查看, 会发现有以下几行:
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 11 entries */, 32768) = 344
getdents(3, /* 0 entries */, 32768) = 0
close(3) = 0
由此看来,我们需要重点分析的系统调用是getdents。在执行后,ls可能会调用libc函数、readdir,但最终还是会调用getdents。
在具体分析getdents之前,首先让我们来讨论一下如何进入内核。
可装载内核模块(LKM)
在Linux系统中,如果我们想在内核中运行代码,我们可以借助于可装载内核模块(LKM)。本文将不会花用太多篇幅阐述它是如何工作的,大家可以参考这一篇文章(链接)。我们的思路是将自定义的模块加载到内核。一些有用的内核符号就会被内核导出,我们便能够去使用它们,或者也可以通过kallsyms_lookup_name函数来获取。在内核中,我们需要拦截getdents调用,并对它返回的值进行更改。此外,我们也希望可以稍微隐藏一下自己的行为,由于该模块是在系统中,因此就不会那么明显。
那么,接下来要解决的问题就是我们要如何Hook getdents,以及Linux内核会如何响应系统调用。
系统调用表
在内核之中,存在一个系统调用表。其中的系统调用编号(系统调用发生时rax的值)是其Handler在其表中的偏移量。在Windows系统中,由于PatchGuard内核保护系统的存在,系统调用表是无法接触到的。但在Linux系统中,我们就可以避开它。
需要注意的是,如果我们将系统调用表弄乱,将会造成非常严重的问题,这样的PoC无疑是愚蠢的,所以还是要考虑将Hook放置在其他地方。
系统调用表位于sys_call_table,它是系统内核的一块区间,其作用是将调用号和服务连接起来,当系统调用某一个进程时,就会通过sys_call_table查找到该程序。然而,它并不是一个可导出并供使用的Linux内核符号,因此,我们有下面这4种方式可供选择:
- 暴力搜索。这种方式更适用于32位系统上,但在64位系统上也是理论上可行的。
- 找到使用系统调用表的函数。有几个使用sys_call_table符号的函数,如果我们通过这些函数进行解析,就可以找到它们的引用。
- 在其他地方寻找。其实,如果不在内核中寻找,也是非常容易找到的。
- 不使用系统调用表。这是一个最好的方案,如果我们不在系统调用表上Hook,我们还可以将Hook放在Handler上。
针对这个简单的模块,我们会选择上面的第3种方式。不使用系统调用表的方式也比较有趣,我将会在后续另写一篇文章进行讲解。对于第3种方式,我们只需要读取并分析/boot/System.map-$(uname -r)文件即可。我们的这一操作,可以在将自身添加到内核的同时进行,由此就确保会得到正确的地址。在我的代码中,build_and_install.sh进行了这项工作。我生成了一个将使用可装载内核模块(LKM)编译的头文件。
smap="/boot/System.map-$(uname -r)"
echo -e "#pragma once" > ./sysgen.h
echo -e "#include <linux/fs.h>" >> ./sysgen.h
symbline=$(cat $smap | grep '\Wsys_call_table$')
set $symbline
echo -e "void** sys_call_table = (void**)0x$1;" >> ./sysgen.h
关于Hook
系统调用表是只读的,但当我们在内核中的时候,这并不会成为较大的阻碍因素。在内核中,CR0是一个控制寄存器,可以修改处理器的操作方式。其中的第16位是写保护标志所在的位置,如果该标志为0,CPU就可以让内核写入只读页。Linux为我们提供了两个很有帮助的函数,可以用于修改CR0寄存器,分别是write_cr0和read_cr0。
在我的代码中,我通过 write_cr0(read_cr0() & (~WRITE_PROTECT_FLAG)); 关闭了写保护机制,随后在 #define WRITE_PROTECT_FLAG (1<<16)通过 write_cr0(read_cr0() | WRITE_PROTECT_FLAG);将其重新打开。
随后,将当前getdents Handler的入口保存,这样我就可以在删除模块时恢复原样。之后,我们使用 sys_call_table[GETDENTS_SYSCALL_NUM] = sys_getdents_new;写了一个新的Handler。
其中的sys_getdents_new函数,只负责运行原始处理程序,并检查其结果,删除任何以我们指定前缀开头或是与我们模块有关的条目。
从/proc/modules实现隐藏
在Linux系统中,/proc/文件系统作为用户空间与内核之间的接口。它们并不是传统意义上的文件,在打开、写入或读取proc文件时,它会调用内核中的处理程序,动态地得到所需信息并进行相应操作。通过Hook /proc/modules文件的Read Handler,我们可以筛选出任何引用到我们模块的相应行。为了实现这一点,首先需要知道/proc/modules在内核中注册的位置。
我在Github上搜索了“proc_create(“modules”)“的源代码,并找到了以下几行:
static int __init proc_modules_init(void){
proc_create("modules", 0, NULL, &proc_modules_operations);
return 0;
}
由此看来,proc_modules_operations是一个包含Handler函数的结构。尽管它并不是一个导出的符号,但我们已经在使用System.map文件了,就不妨再使用一次它。
一旦它被导入,放置Hook的过程就会非常简单,proc_modules_operations->read = proc_modules_read_new;。在proc_modules_read_new中,我们还进行和之前一样的读取,并筛选出我们需要的相应行。
结论
至此,我们就拥有了第一个Linux rootkit。此后,我们还可以通过在系统调用表之外进行Hook,以及通过在内核中隐藏模块来改进。
希望上述内容已经讲解得足够清楚明白,如果大家有任何问题,或者发现我的代码中存在任何Bug,请随时提出。我也期待能有更多人提出Rootkit方面的更深入分析并相互学习。