在 Linux 内核中包装函数时遇到问题
我编写了一个 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 空间并将其标记为可执行文件,那也将帮助我解决上述问题。
任何帮助表示赞赏!
I've written a LKM that implements Trusted Path Execution (TPE) into your kernel:
https://github.com/cormander/tpe-lkm
I run into an occasional kernel OOPS (describe at the end of this question) when I define WRAP_SYSCALLS to 1, and am at my wit's end trying to track it down.
A little background:
Since the LSM framework doesn't export its symbols, I had to get creative with how I insert the TPE checking into the running kernel. I wrote a find_symbol_address() function that gives me the address of any function I need, and it works very well. I can call functions like this:
int (*my_printk)(const char *fmt, ...);
my_printk = find_symbol_address("printk");
(*my_printk)("Hello, world!\n");
And it works fine. I use this method to locate the security_file_mmap, security_file_mprotect, and security_bprm_check functions.
I then overwrite those functions with an asm jump to my function to do the TPE check. The problem is, the currently loaded LSM will no longer execute the code for it's hook to that function, because it's been totally hijacked.
Here is an example of what I do:
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;
}
Notice the section between the #if WRAP_SYSCALLS section (it's defined as 0 by default). If set to 1, the LSM's hook is called because I write the original code back over the asm jump and call that function, but I run into an occasional kernel OOPS with an "invalid opcode":
invalid opcode: 0000 [#1] SMP
RIP: 0010:[<ffffffff8117b006>] [<ffffffff8117b006>] security_bprm_check+0x6/0x310
I don't know what the issue is. I've tried several different types of locking methods (see the inside of start/stop_my_code for details) to no avail. To trigger the kernel OOPS, write a simple bash while loop that endlessly starts a backgrounded "ls" command. After a minute or so, it'll happen.
I'm testing this on a RHEL6 kernel, also works on Ubuntu 10.04 LTS (2.6.32 x86_64).
While this method has been the most successful so far, I have tried another method of simply copying the kernel function to a pointer I created with kmalloc but when I try to execute it, I get: kernel tried to execute NX-protected page - exploit attempt? (uid: 0). If anyone can tell me how to kmalloc space and have it marked as executable, that would also help me solve the above problem.
Any help is appreciated!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
1.看来,
security_bprm_check()
的开头在函数调用之前没有完全恢复。哎呀发生在security_bprm_check+0x6
处,即就在您放置在那里的跳转之后,所以看起来跳转的某些部分在那一刻仍然存在。我现在无法说出为什么会发生这种情况。看看Kernel Probes (KProbes)在x86上的实现,它可能会给你一些提示。有关详细信息,另请参阅KProbes 说明。 KProbe 需要以安全的方式修补和恢复几乎任意的内核代码片段才能完成其工作。
2.现在谈谈您提到的另一种方法,即有关函数复制的方法。以下是一些 hack,内核开发人员会不赞成,但如果没有其他方法,这可能会有所帮助。
您可以分配内存以将函数复制到与分配内核模块代码的内存相同的区域。默认情况下该区域应该是可执行的。同样,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(%reg, N)
的内容(N 是指针的大小,以字节为单位),而不是进行多次比较。也就是说,只是跳转到表中适当元素中的地址。由于此类表是在复制代码之前为代码创建的,因此可能需要进行修复,否则此类跳转会将执行带回原始代码而不是复制的代码。1.It seems, the beginning of
security_bprm_check()
is not restored completely before the function is called. The oops happens atsecurity_bprm_check+0x6
, i.e. right after the jump you placed there, so it seems, some part of the jump is still there at that moment. I cannot say now why this can happen.Take a look at the implementation of Kernel Probes (KProbes) on x86, it may give you some hints. See also the description of KProbes for details. KProbes need to patch and restore almost arbitrary pieces of kernel code in a safe way to do their work.
2.Now to the other approach that you mentioned, concerning copying of the function. The following is a bit of a hack and would be frowned upon by the kernel developers but if there is no other way, this might help.
You can allocate memory to copy the functions to from the same area where the memory for the code of the kernel modules is allocated. That area should be executable by default. Again, KProbes use this trick to allocate their detour buffers.
Memory is allocated by
module_alloc()
function and freed bymodule_free()
. These functions are of course not exported but you can find their addresses in the same way as you do forsecurity_file_mmap()
, etc. Just of curiosity, you are usingkallsyms_on_each_symbol()
, right?If you allocate memory this way, this could also help avoid another not so obvious problem. On x86-64, the memory address areas available for kmalloc and for the modules' code are located quite far away from each other (see Documentation/x86/x86_64/mm.txt), beyond the reach of any relative jump. If the memory is mapped to the modules' address area, you can use near relative jumps and calls to call the copied functions. A similar problem with RIP-relative addressing is also avoided this way.
EDIT: Note that on x86, if you copy some piece of code to a different memory area and you would like it to run there, some changes in that code may be necessary. At least you need to fixup the relative calls and jumps that transfer control outside of the copied code (e.g. the calls to another function, etc.) as well as the instructions with RIP-relative addressing.
Apart from that, there may be other structures in the code that need to be fixed up. For example, the compiler might have optimized some or even all
switch
statements to a jump via a table. That is, the addresses of the code blocks for eachcase
are kept in a table in the memory and the switch variable is the index into that table. This way, instead of many comparisons, your module will execute something likejmp <table_start>(%reg, N)
(N is the size of a pointer, in bytes). That is, just a jump to an address that is in the appropriate element of the table. Because such tables are created for the code before you copy it, fixup may be necessary otherwise such jumps will take the execution back to the original piece of code rather than the copied one.