LWN | Restricting path name lookup with openat2()

LWN.net

题目:Restricting path name lookup with openat2()
  利用openat2()来限制文件路径查询  lwn.net
作者:Jonathan Corbet
时间:August 22, 2019
索引:
  [Kernel] 文件系统/虚拟文件系统层
  [Kernel] 系统调用/openat2()
  [Security] Linux内核/虚拟文件系统层

根据文件路径查询具体的文件对象看起来是一个相当简单的工作,然而,这其实是内核所实现的最复杂的功能之一。如果执行正确功能的(用户空间)代码访问潜在恶意用户控制的路径,那事情就变得复杂了。自2014年起就有一些工作希望能让open()openat()系统调用更加安全,当时是试图添加O_BENEATH标志,不过后续仍有无数问题需要处理。Aleksa Sarai已经在这个领域研究过一段时间了,现在他给出的结论是需要创建一个新的openat() API,命名为openat2(),才能真正解决问题。

增加openat2()的直接原因是希望能让进程安全的打开一个路径,哪怕此路径是由攻击者控制的也无法破坏系统。实际操作中,这就意味着需要在根据路径查找文件的这部分工作中增加一些限制。过去人们试图给openat()增加标志来解决这个问题,不过无法彻底解决:openat()不会检查它不认识的标志;可用的flag bit也不多了。利用无法识别的标志来进行攻击也十分有名。如果某个进程希望使用限制路径访问的标志,那它首先就需要去确认内核是否支持这个标志;否则就需要接受内核在碰到不支持的标志时可能会出现的安全漏洞。

一种不合适的做法是更改openat()在收到不认识的标志时返回错误值,因为这会破坏大量现有程序。所以唯一的方案就是来建立一个新的系统调用确保会检查新标志,这就是openat2()

struct open_how {
    __u32 flags;
    union {
        __u16 mode;
	__u16 upgrade_mask;
    };
    __u16 resolve;
    __u64 reserved[7]; /* must be zeroed */
};
int openat2(int dfd, const char *filename, const struct open_how *how);

(注意,这个接口形式是内核暴露在用户空间的接口,通常C函数库会封装为另一套更加方便调用的API。)

Sarai没有简单的增加一个参数用作标志设置,而是把所有会影响行为的信息都放到一个独立结构里面去了。这样openat2()就跟open()openat()的行为不一样了,因为openat2()永远都是使用固定个数的参数。flags字段包含了open()openat()已经支持的o_flags内容,不过如果在flags字段使用了未知的bit则会报错。mode字段的含义仍是指定创建新文件时的权限信息。

resolve字段则是包含了一组新的控制路径查找行为的标志。在这次的补丁集面已经实现了的标志包括:

  • RESOLVE_NO_XDEV:路径查找的时候确保不会跨过挂载点(也包括bind mount)。换言之,打开的文件必须是跟dfd描述符在同一个mount的文件系统里。如果dfdAT_FDCWD的话则跟当前工作目录在同一个mount的文件系统里。
  • RESOLVE_NO_MAGICLINKS:在一个文件系统目录里面只有少数几类对象,包括普通文件、目录、设备、FIFO、符号链接。Sarai 希望使用这个选项来处理另一类多年来未得到正式承认的类型“magic link”。比如说/proc/PID/fd目录下面的那些链接,它们都是由kernel实现,具有一些很特别的属性。使用这个flag能够防止路径查找的时候去遍历进入那些magic link,这样就可以避免某些类型的攻击。例如,容器内部的程序利用/proc/目录下面的指向已经打开文件的描述符的链接来访问到容器外的数据。
  • RESOLVE_NO_SYMLINKS:禁止跟随符号链接(包含magic links)继续查找。这个选项跟O_NOFOLLOW flag还是有区别的,因为它会禁止在查找过程中任何位置来跟随链接跳转,而O_NOFOLLOW只会对路径里面最后一级(意指最终文件或者目录)有效。
  • RESOLVE_BENEATH:查找过程必须局限在起始点一下的目录树中,如果利用类似“../”来试图跳出这支目录树的话,就会返回错误。
  • RESOLVE_IN_ROOT:使用这个flag的话,表现就好像是首先chroot()到起始位置了一样。这样所有绝对路径的效果都是相对于起始位置的,而“../”也无法走到这个目录之外。之前做的为了让RESOLVE_IN_ROOT避免race condition的一些改动导致chroot()受到影响,细节见这里: lwn.net/ml/linux-kernel

本次补丁组中提及了好几次magic link相关的问题。RESOLVE_NO_MAGICLINKS参数会确保查找过程中不会跟随magic link深入进去,看起来还有不少场景其实是需要能跟随magic link的。这里的问题是如果允许跟随magic link深入的话,可能会有风险。例如二月份报出的runc container breakout vulnerability就是因为恶意代码利用/proc/PID/exe链接来打开了runc程序,并拥有了写入权限。简单来说,这会导致container容器的限制失效。

第一个patch更改了open()在碰到magic link时候行为,包括所有open()的变种函数都有影响。此前的做法是忽略magic link本身的权限bit配置,现在则会受这些权限bit设置的控制。距离来说,/proc/PID/exe的权限可以在kernel里配置好,确保没法用write权限来打开此文件,这样就能组织runc breakout攻击。

此外还在openat2()里面提供了另一个功能(包括openat()也有这个改进了),那就是增加了一个O_EMPTYPATH标志,用来要求kernel忽略path这个参数。这样调用这个函数的时候就只会重新打开dfd参数所传入的文件描述符,只不过是用新的mode参数来打开。例如很常见的一个应用场景,就是此前已经用O_PATH标志打开过的一个文件仅仅能做一些文件描述符级别的操作,无法访问文件内容,那么重新打开一次的话就能够访问文件内容或者元数据了。此前程序要这么做的话,会通过对/proc/PID/fd路径来重新打开,不过使用O_EMPTYPATH之后哪怕无法访问/proc目录也能实现这个目的了。

最后一点,在这种重新打开用O_PATH flag打开过的文件进行这种“升级”操作的时候,这个新的API还允许施加一些限制。在用openat2()通过O_PATH参数打开文件获取文件描述符的时候,可以利用struct open_howupgrade_mask成员来配置今后再次打开这个文件描述符时有哪些访问限制。如果使用了UPGRADE_NOREAD,则会禁止重新打开的时候授予读权限。而UPGRADE_NOWRITE则可以避免获得写权限。这样就能限制恶意程序在对O_PATH文件描述符进行重新打开时的危害范围了。

这个补丁组的前几个版本已经经过了很多讨论。这次最新版本贴出来之后很安静,说明此前提出的大多数反对意见都已经得到解决了,或者也许reviewer都还在参加Linux Security Summit所以没仔细看邮件。无论如何,我们都很需要有办法来对文件路径查找的过程进行限制,所以或迟或久,这个补丁组总会合入的,只是看还需要做多大改动而已。

此补丁集的先前版本已经产生了相当多的讨论。 此发布后的相对安静可能反映出这样一个事实:随着时间的推移提出的大多数问题已经解决了,或者只是可能评论的人去参加Linux安全峰会所以没仔细看电子邮件。无论哪种方式,都有明确的需求对限制文件路径名的遍历的能力,因此或早或晚,此补丁集的某些版本似乎很可能会进入主线。

版权声明:
作者:dorence
链接:https://wp.dorence.top/archives/197
来源:极客模拟
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录