Linux系统调用监控:深入内核探索之旅

本文详细介绍了如何利用Kprobes和Kallsyms技术监控Linux系统调用,包括内核模块开发、符号查找、系统调用表修改以及实际调试过程,适合对Linux内核和系统安全感兴趣的开发者阅读。

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工具非常酷,我肯定会在某个时候再次使用它,但它并没有立即有用(或者我认为如此)来让我查看特定系统调用何时被使用,我最初的探测结果在日志中大多是垃圾。

Kallsyms

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

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

1
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系统调用理解的目标之外,我现在找到了一种用似乎任何我想要的东西覆盖系统调用(以及其他内核结构)的方法。除了简单的监控,我如何能够扩展(并不可避免地破坏)系统调用功能?更进一步,除了系统调用表,我还可以覆盖哪些其他类型的内核结构和内存?而且,关键的是,这会对我的可怜电脑做什么?我没有期望一个晚上的折腾会带来任何这些,但我兴奋地看到我能想出什么。

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

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计