Linux系统调用监控与内核探测技术解析

59 阅读6分钟

Linux系统调用监控

最近我深入研究了Linux系统,特别专注于探索Linux内核。我发现Michael Kerrisk所著的《Linux编程接口》(TLPI)是一本极佳的参考手册,涵盖了系统调用(syscalls)的应用。要快速了解Linux系统调用,可以在终端使用man 2 intro命令查看介绍性手册页(或在线查看页面)。在阅读TLPI时,我渴望更深入地探索系统调用本身的实现。为此,我最近阅读了大量内核源代码,并将代码片段串联起来以更好地理解某些功能。

我的首要目标是找到检测特定系统调用使用时机的方法,并在执行前查看传递给它的信息。作为整体探索的延伸,我最终希望创建一个可加载内核模块(LKM),能够监听给定的系统调用并至少记录观察到的调用。理想情况下,该模块还应打印调用参数的详细信息。最终我得到了一段可以扩展用于监控任意系统调用的代码,以及研究Linux内核安全的机制。

Kprobes

初步研究让我发现了一个名为Kprobes的内核追踪功能,可用于挂钩大多数内核符号。据我理解,在内核中注册kprobe会使其在内存中目标符号之前插入,并在此过程中保存符号信息。注册的kprobe然后执行在kp->pre_handler中编写的任何代码,运行指令,最后执行在可选的kp->post_handler中可能编写的代码。我认为Kprobes功能非常酷,肯定会在某个时候再次使用它,但起初它对于查看特定系统调用使用时机并不直接有用(或者我这么认为),而且最初的探测在日志中 mostly 产生了垃圾信息。

Kallsyms

更多研究最终让我找到了kallsyms,这是一个用于提取内核符号的功能。我找到了一个似乎完全符合需求的代码片段。使用kallsyms_lookup_name()函数,内核模块查找系统调用表的地址。通过使用该表的地址(在启用对表的读写访问后,感谢Stack Overflow上的一个答案),你可以用自己的定义临时替换系统调用的定义,在我的情况下,只是在调用使用时包含一个printk写入日志文件。我知道这只是触及了这个功能的表面,但更深入的研究可以在其他时间进行(希望如此)。

于是,我编译了模块并尝试加载它。编译成功(好兆头),但尝试加载模块时出现了一个奇怪的错误(坏兆头):

ERROR: modpost: "kallsyms_lookup_name" [/home/moth/.../watcher.ko] undefined!

很好。不知道为什么kallsyms_lookup_name未定义,但我必须调查一下。

经过一些额外研究,我找到了答案。事实证明,自内核版本5.7.0以来,内核不再全局导出该符号。有了这个知识,我现在必须找到替代解决方案。

Kprobes(再探)

我在一个内核黑客GitHub仓库上发现了一个问题,讨论了我遇到的相同情况,线程中的人们讨论出了一些真正辉煌的东西。还记得kprobes以及它们对这个项目并不直接有用吗?开玩笑的——结果证明这个功能非常有用,只是方式与我最初预期不同。在kprobe结构中返回的其中一件事是地址,即探针在内存中的位置。也许你已经能看到这走向了。我们可以使用kprobe来检索kallsyms_lookup_name()函数的地址,因为kprobes基本上可以看到任何内核结构。然后我们可以将该kprobe的地址视为函数本身,从而绕过内核将其暴露给我们的需要。

整合一切

这应该是制作一个工作概念验证所需的一切。将我找到的代码片段烘焙到模块中,它现在可以用于读/写系统调用表,并将处理程序插入到我想要查看的任何系统调用中。目前,我一直以getuid()为目标,因为它相对简单。在一个终端窗口中,我用insmod插入模块,运行id命令(依赖于getuid()系统调用),然后用rmmod移除模块。

加载模块、运行ID命令、移除模块

到目前为止相当简单。在另一个运行dmesg -wH的终端中,我看到模块设置信息,包括kallsyms_lookup_name()sys_call_tablegetuid()系统调用的地址。然后模块在安静下来之前看到三个getuid()调用。几秒钟后,我运行id命令,系统调用被识别并记录。又几秒钟后,我运行rmmod,这导致拦截的系统调用的剩余部分。

成功的系统调用观察

我起初不确定其他getuid()调用来自哪里,但最终意识到这很可能是因为我使用sudo命令插入或移除模块。

这似乎工作得非常好,我有想法将其扩展为对其他潜在项目更有用的东西。此外,dmesg输出本身目前并不很有用,所以下一步将是输出寄存器值和我能想到的任何其他相关信息。

结论(和代码)

这里有一个重要的注意事项。Kprobes功能是一个可以选择禁用的特性。如果它没有启用,并且你感觉冒险,你可以使用Kprobes文档中指定的选项编译内核,以确保你可以加载模块来使用该功能。Red Hat系列似乎默认启用了必需的功能。根据Debian(或任何其他)系列,你的情况可能有所不同。

总的来说,这是一个有趣的练习和优秀的学习经验。它是世界上最有用的东西吗?不是。是否有其他更强大的解决方案可用?几乎肯定有。我认为SystemTap会很合适。也就是说,理解如何挂钩到内核,以及在整个项目中学到的一切,对于我未来从事其他项目将是无价的。

说到未来的其他项目……接下来是什么?除了我最初加深对Linux系统调用理解的目标之外,我现在找到了一种用似乎任何我想要的东西覆盖系统调用(以及其他内核结构)的方法。除了简单的监控,我如何能够扩展(并不可避免地破坏)系统调用功能?进一步超越系统调用表,我还能覆盖哪些其他类型的内核结构和内存?而且,关键的是,这对我可怜的电脑会做什么?我没有期望一个晚上的修补会导致任何这些,但我很兴奋能看到我能想出什么。

好了,足够的人类语言了。是时候来点计算机语言了。如果你感兴趣,可以在这里找到模块代码。请注意评论中列出的链接,因为如果没有找到它们,我不会走这么远。