潜入/proc/pid/mem

2020-10-27 21:07:50

几个月前,在读到Cloudflare实习生班级人数翻了一番的消息后,我很快重拾简历,申请了一份实习工作。长话短说:现在,几个月后,我发现自己开始研究Linux内核代码,并在Linux容器运行时gVisor中添加了一个相当酷的特性。

我的实习是在新兴技术和孵化小组的一个涉及gVisor的项目上进行的。一位同事联系了我的团队,说他无法读取沙箱中堆栈跟踪的调试符号。例如,当隔离进程崩溃时,我们在日志中看到:

*检查故障堆栈轨迹:*@0x7ff5f69e50bd(未知)@0x7ff5f69e9c9c(未知)@0x7ff5f69e4dbd(未知)@0x7ff5f69e55a9(未知)@0x5564b27912da(未知)@0x7ff5f650ecca(未知)@0x5564b27910fa(未知)。

显然,这并不是很有用。我迫不及待地自愿修复这个堆栈展开代码-这能有多难呢?

经过一些调试之后,我们发现项目中使用的日志库打开了/proc/self/mem,以便在每个内存映射区域的开头查找ELF标头。这是计算偏移量以找到调试符号的正确地址所必需的。

事实证明,这种机制相当常见。堆栈展开代码通常在奇怪的上下文中运行-如SIGSEGV处理程序-因此不适合来回挖掘实际内存地址来读取ELF。这可能会引发另一个SIGSEGV。SIGSEGV处理程序内的SIGSEGV意味着要么通过SEGFAULT的默认处理程序终止,要么反复递归到同一处理程序(如果设置了SA_NODEFER),从而导致堆栈溢出。

然而,在gVisor内部,每次调用/proc/self/mem上的open()都会导致ENOENT,因为整个/proc/self/mem文件都丢失了。为了提供健壮的沙箱,gVisor必须小心地重新实现Linux内核接口。这个特定的/proc文件根本没有在gVisor的沙箱组件之一Sentry的虚拟文件系统中实现。Marek在项目聊天中询问了开发人员,并得到了确认-他们会很高兴接受实现此文件的补丁。

最简单的解决办法是对展开的行为做一个小的本地补丁,然而我发现自己潜入Linux内核,试图弄清楚mem文件是如何工作的,试图在Sentry的VFS中实现它。

该文件本身非常强大,因为它允许对进程的虚拟地址空间进行原始访问。根据手册页,有文档记录的文件操作是open()、read()和lSeek()。典型的用例是调试任务或转储进程内存。

当进程想要打开文件时,内核执行文件权限检查,查找与mem相关的操作,并调用名为proc_mem_open的方法。它检索关联的任务并调用名为mm_access的方法。

/**获取对任务mm的引用(如果它还没有离开),并使用传递给它的模式参数ptrace_May_access*成功。*/。

看起来比较直截了当,对吧?Mm_access的特殊之处在于,它验证当前任务对内存所属任务的权限。如果当前任务和目标任务不共享同一内存管理器,内核将调用名为__ptrace_May_access的方法。

/**我们可以检查给定的任务吗?*此检查既用于附加ptrace*,也用于允许访问/proc中的敏感信息。**ptrace_Attach拒绝/proc允许的几种情况*因为无法设置必要的父/子关系*或停止指定的任务。**/。

根据手册页,希望从不相关的/proc/[PID]/mem文件读取的进程应该具有访问模式ptrace_mode_ATTACH_FSCREDS。此检查不会验证进程是否通过ptrace_ATTACH附加,而是验证它是否具有使用指定凭据模式附加的权限。

浏览完函数后,您将看到,如果当前任务与目标任务属于同一线程组,则允许进程访问;如果不满足以下条件,则允许进程访问(取决于是否设置了ptrace_mode_FSCREDS或ptrace_mode_REALCREDS,我们将使用文件系统UID/GID,通常与有效的UID/GID相同),或者使用真实的UID/GID;如果当前任务与目标任务属于同一线程组,则允许进程访问;如果不满足以下条件,则拒绝访问(取决于是否设置了ptrace_mode_FSCREDS或ptrace_mode_REALCREDS,我们将使用文件系统UID/GID,通常与有效UID/GID相同):

当前任务的凭据(UID,GID)与目标进程的凭据(真实、有效和保存的Set-UID/GID)匹配

在下一次检查中,如果当前任务在目标任务的用户命名空间内既没有CAP_SYS_PTRACE,也没有将目标的Dumpable属性设置为SUID_DUMP_USER,则拒绝访问。通常需要Dumpable属性来允许生成核心转储。

在完成这三项检查之后,我们还将通过通用的Linux安全模块(和其他LSM)来验证我们的访问模式是否正常。您可能知道LSM是SELinux和AppArmor。COMMONCAP LSM根据有效或允许的进程能力(取决于模式为FSCREDS或REALCREDS)执行检查,允许在以下情况下进行访问。

当前任务的功能是目标任务功能的超集,或者。

当前任务和目标任务的凭据在给定凭据模式下匹配,目标任务是可转储的,它们在相同的用户命名空间中运行,并且目标任务的能力是当前任务能力的子集。

我强烈建议您阅读ptrace手册页,更深入地挖掘不同的模式、选项和检查。

由于所有访问检查都在打开文件时进行,因此读取文件非常简单。当对mem文件调用read()时,它会调用mem_rw(它实际上既可以进行读操作,也可以进行写操作)。

为了避免使用大量内存,mem_rw在循环中执行复制,并在中间页中缓冲数据。Memrw具有隐藏的超能力,即,它使用FOLL_FORCE来避免对用户拥有的页面进行权限检查(处理标记为不可读/不可写、可读和可写的页面)。

如果目标任务在打开文件描述符后退出,则执行read()将始终成功,并读取0字节

如果从目标任务的内存到中间页的初始拷贝失败,它并不总是返回错误,但只有在没有读取数据的情况下才会返回错误。

幸运的是,gVisor已经将ptrace_May_access实现为kernel.task.CanTrace,因此可以避免重新实现所有的ptrace访问逻辑。但是,由于缺乏对ptrace_mode_FSCREDS的支持(这仍然是一个悬而未决的问题),gVisor中的实现不那么复杂。

当新的文件描述符打开()时,会调用虚拟inode的GetFile方法,因此这是访问检查自然发生的地方。访问检查成功后,该方法返回fs.File。Fs.File实现了您预期的所有文件操作,比如read()和write()。例如,gVisor还提供了大量用于快速构建工作文件结构的原语,这样就不必重新实现泛型lSeek()。

如果任务调用对fs.File的read()调用,则read方法将检索该文件的Task的内存管理器。使用类似于io.Writer和io.Reader的接口,使用舒适的Copin和CopyOut方法访问任务的内存管理器非常容易。

*检查故障堆栈跟踪:*@0x7f190c9e70bd Google::LogMessage::Fail()@0x7f190c9ebc9c Google::LogMessage::SendToLog()@0x7f190c9e6dbd Google::LogMessage::Flush()@0x7f190c9e75a9 Google::LogMessageFtal::~LogMessageFtal()@0x55d6f718c2da main@0x7f190c510cca_libc_start_main@0x55d6f718c0c0start_main@0x7f190c510cca_libc_start_main@0x55d6f718c0c0start。

全面胜利!/proc/<;pid>;/mem文件是深入了解进程内存内容的重要机制。在出现复杂和不可预见的故障时,堆叠开卷机进行工作是必不可少的。由于进程内存包含高度敏感的信息,因此对文件的数据访问由一组文档不完善的复杂规则决定。稍加努力,您就可以在gVisor的沙箱中模拟/proc/[pid]/mem,其中该进程只能访问gVisor作者已经实现的procf子集,因此,您可以在发生崩溃时访问易于读取的堆栈跟踪。

深潜编程Linux