我写了一个LKM,它在你的内核中实现了可信路径执行(TPE):
https://github.com/cormander/tpe-lkm
当我将 WRAP_SYSCALLS 定义为1时,我遇到了偶尔的内核OOPS(在本问题的最后描述),并且在我的机智结束时尝试跟踪它。
一点背景:
由于LSM框架不会导出其符号,因此我必须对如何将TPE检查插入正在运行的内核进行创作。我写了一个find_symbol_address()函数,它给了我所需的任何函数的地址,并且它工作得非常好。我可以调用这样的函数:
int (*my_printk)(const char *fmt, ...);
my_printk = find_symbol_address("printk");
(*my_printk)("Hello, world!\n");
它工作正常。我使用此方法找到 security_file_mmap , security_file_mprotect 和 security_bprm_check 函数。
然后我用 asm 覆盖这些函数,跳转到我的函数进行TPE检查。问题是,当前加载的LSM将不再执行它对该函数的挂钩的代码,因为它已被完全劫持。
以下是我所做的一个例子:
int tpe_security_bprm_check(struct linux_binprm *bprm) {
int ret = 0;
if (bprm->file) {
ret = tpe_allow_file(bprm->file);
if (IS_ERR(ret))
goto out;
}
#if WRAP_SYSCALLS
stop_my_code(&cs_security_bprm_check);
ret = cs_security_bprm_check.ptr(bprm);
start_my_code(&cs_security_bprm_check);
#endif
out:
return ret;
}
注意 #if WRAP_SYSCALLS 部分之间的部分(默认情况下定义为0)。如果设置为1,则调用LSM的钩子,因为我将原始代码写回 asm 跳转并调用该函数,但是我遇到了一个带有“无效操作码”的偶尔内核OOPS:
invalid opcode: 0000 [#1] SMP
RIP: 0010:[<ffffffff8117b006>] [<ffffffff8117b006>] security_bprm_check+0x6/0x310
我不知道问题是什么。我尝试了几种不同类型的锁定方法(详见 start / stop_my_code 内部)无济于事。要触发内核OOPS,请编写一个简单的bash while循环,无休止地启动后台“ls”命令。大约一分钟后,它就会发生。
我在RHEL6内核上测试它,也适用于Ubuntu 10.04 LTS(2.6.32 x86_64)。
虽然这个方法到目前为止最成功,但我尝试了另一种简单地将内核函数复制到我用 kmalloc 创建的指针的方法,但是当我尝试执行它时,我得到: 内核尝试执行受NX保护的页面 - 漏洞利用尝试? (uid:0)。如果有人能告诉我如何kmalloc空间并将其标记为可执行文件,那么这也有助于我解决上述问题。
感谢任何帮助!
答案 0 :(得分:9)
1.看来,在调用函数之前,security_bprm_check()
的开头并未完全恢复。 oops发生在security_bprm_check+0x6
,即在你放置跳跃之后,所以看来,跳跃的某些部分仍然存在于那一刻。我现在不能说为什么会发生这种情况。
看一下x86上Kernel Probes (KProbes)的实现,它可能会给你一些提示。有关详细信息,另请参阅description of KProbes。 KProbes需要以安全的方式修补和恢复几乎任意的内核代码才能完成工作。
2.现在提到的另一种方法是复制功能。以下是一个黑客攻击,内核开发人员会不赞成,但如果没有别的办法,这可能会有所帮助。
您可以分配内存以将函数复制到分配内核模块代码的内存的同一区域。该区域默认情况下应该是可执行的。同样,KProbes使用这个技巧来分配他们的绕道缓冲区。
内存由module_alloc()
函数分配,并由module_free()
释放。这些功能当然不会导出,但您可以使用与security_file_mmap()
等相同的方式查找地址。仅仅是好奇,您使用的是kallsyms_on_each_symbol()
,对吗?
如果以这种方式分配内存,这也可以帮助避免另一个不那么明显的问题。在x86-64上,可用于kmalloc和模块代码的内存地址区域彼此相距很远(见Documentation/x86/x86_64/mm.txt),超出任何相对跳转的范围。如果内存映射到模块的地址区域,则可以使用近端相对跳转和调用来调用复制的函数。这种方式也避免了RIP相对寻址的类似问题。
编辑:请注意,在x86上,如果将某段代码复制到不同的内存区域并希望它在那里运行,则可能需要对该代码进行一些更改。至少你需要修复在复制代码之外传输控制的相对调用和跳转(例如对另一个函数的调用等)以及RIP相对寻址的指令。
除此之外,代码中可能还有其他结构需要修复。例如,编译器可能已经通过表优化了一些甚至所有switch
语句到跳转。也就是说,每个case
的代码块的地址保存在内存中的表中,而switch变量是该表的索引。这样,您的模块将执行类似jmp <table_start>(%reg, N)
的操作,而不是许多比较(N是指针的大小,以字节为单位)。也就是说,只需跳转到表中相应元素中的地址即可。因为在复制之前为代码创建了这样的表,所以可能需要修复,否则这样的跳转会将执行恢复到原始代码而不是复制代码。