在Linux上跟踪运行进程的困难

2020-08-10 02:55:57

每个人都知道如何跟踪哪些进程在Linux上运行,但几乎没有人准确地跟踪它们。事实上,这篇文章中列出的所有方法都有或多或少的不足。让我们定义要求:

在合理的情况下,我们不应该需要针对不同的内核版本修改或重新编译代码。

额外的好处是:如果主机是Kubernetes节点或运行docker,那么我们应该能够确定进程属于哪个pod/容器。要做到这一点,知道进程的cgroup ID通常就足够了。1。

让我们来看看可以解决此问题的常见Linux API。为简单起见,我们将专注于检测执行的syscall。完整的解决方案还需要监控分叉/克隆系统调用及其变体以及execveat。

使用NetLink进程连接器。连接器将为生命周期较短的进程发送通知,但通知只包括诸如进程的PID之类的数字数据,而不包括诸如可执行路径之类的数据。因此,您将返回到从/proc读取数据,并且对于生命周期较短的进程具有相同的争用条件。

使用Linux审计API。这是目前最好的解决方案。审计API存在于所有现代内核中,提供完整的可执行路径,并且不会错过短暂的进程。只有两个缺点。首先,一次只有一个usermode程序可以与内核审计API通信。如果您正在开发企业安全解决方案,并且有客户自己通过auditd或osquery使用审计API,这将是一件痛苦的事情。2其次,审计API不支持容器,尽管内核邮件列表多年来一直在讨论如何解决该问题。

使用跟踪点3。内核包含几个相关的跟踪点,这些跟踪点在execve syscall中的不同点上执行。它们是:sched_process_exec、open_exec、sys_enter_execve、sys_exit_execve。4这些跟踪点比以前的解决方案更好,因为它们将跟踪短暂的进程,但是当exec的参数是相对路径时,这些跟踪点都不会提供可执行文件的完整路径。换句话说,如果用户运行cd/bin&;&;/ls,则路径将报告为./ls而不是/bin/ls。下面是一个简单的演示:

#enable the sched_process_exec tracepointsudo-scd/sys/kernel/debug/tracingecho 1>;events/sched/sched_process_exec/enable#通过相对路径运行ls/bin&;&;。/ls#从sched_process_exec跟踪点获取数据#请注意,我们看不到完整的路径cd-cat跟踪|grep ls#禁用tracepointecho 0&

5.与跟踪点不同,有很多可能的函数,您可以在其中插入在执行系统调用期间将命中的kprobe。但是,我在execve的调用图中找不到将进程的PID和可执行文件的完整路径作为函数参数的单个函数。因此,我们在相对路径方面遇到了与跟踪点解决方案相同的问题。这里有一些聪明的破解方法-毕竟,kprobe可以从内核的callstack读取数据-但是这些解决方案在不同的内核版本中并不稳定,所以我排除了它们。

将eBPF程序与tracepoints/kspects/kretspects一起使用6.这打开了一些新的选项。现在,我们可以在每次运行execve syscall时在内核中运行任意代码。从理论上讲,这应该允许我们从内核提取任何我们想要的信息,并将其发送到用户模式。有两种方法可以获得这些数据,但都不符合我们的要求:

从内核结构(如task_struct或linux_binprm)读取数据。我们确实可以这样获取可执行文件的完整路径7,但是读取内核结构会使我们依赖于内核版本。我们的eBPF程序需要知道struct成员的偏移量,因此必须使用每个内核版本的内核头对其进行编译。这通常是通过在运行时编译eBPF程序来解决的,但这也带来了自身的问题,比如要求您在每台机器上都有可用的内核标头。

使用eBPF帮助器函数从内核获取数据。这在包含您使用的帮助器的所有内核版本中都是兼容的。在这种方法中,您永远不会直接访问内核结构-而是使用助手API来获取数据。只有一个问题:没有eBPF帮助器函数可以获取可执行文件的完整路径。(但是,在最近的内核版本中,有一个eBPF助手函数来获取cgroup ID,这对于将进程映射到容器很有用。)。

在libc中对每个正在运行的可执行文件和挂钩EXEC调用使用LD_PRELOAD。说真的,别这么做。它不适用于静态编译的可执行文件,很容易被恶意代码绕过,并且具有相当强的侵入性。

在execve、fork/clone和chdir上使用跟踪点不仅可以跟踪所有进程的创建,还可以跟踪它们的当前工作目录。对于每个execve,查找进程的工作目录并将其与execve的参数组合以获得完整路径。如果这样做,请确保使用eBPF映射,并将所有逻辑放入eBPF程序中,以避免事件以错误顺序到达用户模式的争用情况。

使用基于ptrace的解决方案。这些对于生产代码来说太具侵入性了。但是,如果您使用此路由,则使用ptrace+seccomp和SECCOMP_RET_TRACE标志。然后,seccomp可以截取内核中的所有execve syscall,并将它们传递给用户模式调试器,该调试器可以在告诉seccomp照常继续执行execve之前记录execve调用。

使用AppArmor。您可以编写AppArmor配置文件,禁止进程执行任何其他可执行文件。如果将配置文件置于抱怨模式,则AppArmor实际上不会阻止进程执行-它只会在配置文件被违反时发出警报。如果我们将我们的配置文件附加到每个正在运行的进程,那么我们将拥有一个工作但非常丑陋和骇人听闻的解决方案。你也许不该这么做。

使用ps-这只从/proc轮询,因此具有通常的争用条件。

使用基于eBPF的execsnoop-这只是一个基于kbe/kretbe的解决方案,因此它对上面讨论的内核版本具有相同的依赖性。此外,execsnoop甚至没有扩展相对路径,因此我们一无所获。

使用旧的非eBPF版本的execsnoop-这也不起作用。它只是一个简单的kbe。

使用eBPF助手函数GET_FD_PATH-该函数尚不存在,但是一旦将其添加到内核中,就会有一些帮助。您仍然必须以一种不涉及从内核结构读取的方式获取可执行文件的FD。

这里介绍的API都不是十全十美的。对于您应该使用哪种解决方案以及何时使用,以下是我的建议:

如果可以,请通过auditd或go-audit使用审计API。这将记录所有进程,包括生命周期较短的进程,您将毫不费力地获得完整的可执行路径。如果某人已经通过与您不同的用户模式工具使用审计API,则此解决方案不起作用。如果是那样的话,请继续往下读。

如果您不关心完整路径,并且想要一个不需要编写任何代码的快速、现成的解决方案,那么可以使用execsnoop。这有在运行时需要内核头的缺点。

如果您不关心完整路径,并且愿意做更多的工作来避免需要内核头,那么可以使用上面提到的跟踪点之一。有多种方法可以连接到这些跟踪点并将它们的数据传输到用户模式-无论是通过上面显示的文件系统接口、通过带有eBPF映射的eBPF程序,还是通过perf工具。我将在另一篇文章中介绍这些选项。需要记住的主要事情是:如果您使用eBPF程序,请确保它可以静态编译,这样您就不会像您试图避免的那样对内核头文件具有相同的依赖关系。这意味着您不能访问内核结构,也不能使用像BCC这样在运行时编译eBPF程序的框架。

如果您不关心短暂的进程,并且以前的解决方案不适合您的使用情形,那么可以将NetLink进程连接器与/proc结合使用。

我是不是忘了一个显而易见的解决方案?给我发信息!我还没有在这个网站上建立评论系统,但是你可以在LinkedIn上给我留言。

从内核的角度来看,没有容器或容器ID这样的东西。内核只知道cgroup、网络名称空间、进程名称空间和其他独立的内核API,容器运行时(如docker)恰好使用这些API实现了容器化。当试图通过内核ID标识容器时,您需要一个内核标识符,每个容器正好有一个。对于坞站,cgroup ID满足该要求。[返回]。

理论上,用户模式多路复用器(如auditd和go-audit)可以缓解此问题,但对于企业解决方案,您仍然不知道客户是否正在使用多路复用器,如果是,则不知道是哪一个,也不知道是否存在其他直接连接到审计API的安全解决方案。[返回]。

跟踪点是在设置的位置静态编译到内核中的探测器。每个探测器都可以单独启用,以便在内核到达该探测器的位置时发出通知。[返回]。

为了获得这个列表,我运行cat/sys/kernel/tracting/available_events|grep exec,然后根据对内核源代码的浏览过滤输出[return]。

KProbe允许您从几乎任何内核位置提取调试信息。您可以将它们视为发出信息但不会停止执行的内核断点。[返回]。

换句话说,使用tracepoints/kspects/kretProbe作为挂钩机制,但将eBPF程序设置为在挂钩上运行,而不是在老式的处理程序上运行。[返回]

例如,在sched_process_exec上放置一个跟踪点,并使用有界的eBPF循环遍历bprm->;file->;f_path.dentry中的Dentry链,通过性能循环缓冲区[return]一次将其发送到用户模式。