KDE6 发布:D-Bus 和 Polkit 齐发
#KDE #local #D-Bus #Polkit目录
- 引言
- KDE KAuth 框架
- 遗留的 fontinst D-Bus 服务
- sddm-kcm6 中文件系统操作的问题
- KWalletManager:保护配置的伪身份验证
- DrKonqi 中的改进
- 结论
- 参考文献
- 变更历史
引言
SUSE 安全团队限制在 openSUSE 发行版及衍生的 SUSE 产品中安装系统范围的 D-Bus 服务和 Polkit 策略。任何包含这些功能的软件包,在添加到生产存储库之前,都需要先经过我们的审查。
去年 11 月,openSUSE KDE 打包人员 就即将到来的 KDE6 主要版本联系了我们,提供了一长串 KDE 组件。这些软件包由于接口重命名或其他重大更改,需要调整 D-Bus 和 Polkit 白名单。一次性查看这么多组件是一次独特的体验,也带来了新的见解,这些将在本文中讨论。
对于不熟悉 D-Bus 和/或 Polkit 的读者,以下各节提供了摘要,以帮助您更好地了解这些系统。
D-Bus 概述
D-Bus 消息总线系统为应用程序中实现远程过程调用提供了一种定义明确的方式。在 Linux 上,它通常仅在本地使用,尽管 D-Bus 规范也允许在网络上传输操作。
D-Bus 服务是一个程序,它提供一个或多个接口,客户端可以调用这些接口来获取信息、触发操作等。D-Bus 规范定义了一组可以传递给 D-Bus 方法调用并从中返回的数据类型。
D-Bus 应用程序通过连接到共享总线来相互通信,总线有两种预定义类型:系统总线和会话总线。执行系统范围任务的服务连接到系统总线。这些服务通常以 root 用户或专用的服务用户身份运行。另一方面,会话总线为每个(图形)用户会话创建,只有以登录用户权限运行的应用程序才能连接到它。会话总线不涉及任何特殊权限。其主要目的是为会话范围的服务(如桌面搜索引擎)提供一个定义明确的 API。
Polkit 概述
Polkit 是一个授权框架,允许(特权)应用程序决定系统中的用户是否被允许执行特定操作。与简单的 root vs. 非-root 决策相比,这些操作提供了更精细化的授权模型。例如,可以有一个用于启用系统蓝牙设备或挂载可移动存储设备的操作。
Polkit 策略配置文件声明了特定应用程序域使用的操作以及对其的身份验证要求。当系统中的一个参与者请求使用 Polkit 的应用程序执行操作时,该应用程序会反过来询问系统范围的 Polkit 守护进程该参与者是否有权限执行此操作。根据上下文,这可能导致在用户的图形会话中显示密码提示,以授权该操作。
Polkit 与 D-Bus 是独立的,但两者的结合是一种非常常见的模式。Polkit 的其他使用方式包括在 setuid-root 二进制文件或类似 sudo 的 pkexec 工具中。
D-Bus 和 Polkit 的安全相关性
D-Bus 和 Polkit 的典型设置如下:一个系统守护进程以完整的 root 权限运行,并在 D-Bus 系统总线上注册一个服务。登录图形会话的未授权用户通过 D-Bus 请求守护进程执行一项活动。这会触发 Polkit 身份验证过程,以确定调用者是否被允许执行此操作。
在安全方面,这种情况可能出错的地方很多。以下各节将探讨可能出现的典型问题。
覆盖所有特权代码路径
系统守护进程实际上需要为其提供的每个敏感 D-Bus 方法正确实现 Polkit 授权检查。Polkit 不是神奇开启的,而是特权组件需要识别所有需要受其保护的代码路径。
一些应用程序会故意提供混合的未授权和已授权 D-Bus 方法。在这些情况下,有时很难考虑到所有可能的副作用和结果,这可能导致在忽略某些内容时出现安全问题。
代表未授权用户以 root 身份行事
特权 D-Bus 服务组件通常需要代表未授权客户端行事。例如,可能是在调用者的主目录中挂载文件系统,或处理调用者提供的文件。这是特权边界穿越的经典案例。此类服务的开发人员通常没有意识到可能出现的问题,尤其是在以 root 身份访问用户控制的路径时。
同样,如果一个特权 D-Bus 服务在共享系统目录中存储来自多个用户的数据,那么通过存储权限过于开放的文件或混合不同的用户上下文,可能会发生信息泄露。
Polkit 的集成可能很困难
Polkit 有其自身的术语和设计原则,需要深入理解才能完全掌握。此外,即使正确请求 Polkit 权限,特权服务仍需要正确评估结果。该领域可能发生的典型错误是,特权服务确实正确请求 Polkit 进行身份验证,但结果被简单地忽略,特权操作仍然继续。
任何人都可以访问 D-Bus 系统总线
默认情况下,所有本地用户都可以访问 D-Bus 系统总线并与大多数特权服务通信。单独的 D-Bus 服务配置文件可以限制允许调用 D-Bus 服务方法的用户的范围。然而,这种情况是例外,因为大多数 D-Bus 服务对所有用户都是可访问的。
这大大增加了攻击面,因为不仅运行授权本地会话的交互式用户帐户可以与这些服务通信,而且例如 nobody 用户帐户也可以。如今,许多在网络上运行的系统守护进程只有有限的特权,甚至使用 systemd 提供的动态分配的用户。如果这些具有低特权的某个网络守护进程可以被利用,那么特权 D-Bus 系统服务中的弱点就有可能进一步提升特权。
这也是 SUSE 安全团队将目光也投向这些不直接连接到网络的组件的原因之一,作为纵深防御策略的一部分。
KDE KAuth 框架
KDE 桌面环境大量使用系统总线和会话总线上的 D-Bus 服务。它在 D-Bus 和 Polkit 之上增加了额外的抽象。基础组件是 KAuth 框架。KAuth 生成 D-Bus 配置文件和一些粘合代码,将 D-Bus 和 Polkit 集成到 KDE 应用程序中。在 KAuth 中,以 root 身份运行的特权 D-Bus 服务被称为 KAuth 助手。
我们对 KDE6 版本进行了 专门的后续审查。SUSE 安全团队的一名前成员在 2017 年在此粘合代码中发现了一个 重大的安全漏洞。由于当时的审计是全面的,我们不再期望在核心授权逻辑中发现任何重大问题,事实上也没有。
QVariantMap 序列化数据的错误使用
KAuth 的一个特点是,在 D-Bus 层面传输的不是本地 D-Bus 数据类型,而是基于 Qt 框架提供的 QVariantMap 数据类型生成的二进制 blob 对象。
在审查过程中,我们注意到 KAuth 中此功能的实现 有点不稳定,因为在 Qt 数据类型反序列化过程中会处理潜在的攻击者控制的数据,甚至在调用实际的 D-Bus 函数回调之前。早在 2019 年,上游作者 就已经发现了这个问题,该问题可能导致如图像数据被反序列化等副作用,而实际上只期望字符串和整数。KAuth 代码目前通过篡改内部 Qt 框架状态来防止此类副作用。
生成的 D-Bus 注入式配置片段的问题
在我们审查工作临近结束时,才意识到 KAuth 的 KDE6 版本引入的一项更改会导致 生成过于开放的 D-Bus 配置文件。D-Bus 的每个包的配置文件片段安装在“/usr/share/dbus-1/system.d”目录下。这些配置文件就像是 D-Bus 系统总线的防火墙配置。它们定义了谁可以为某个接口注册 D-Bus 服务,以及谁可以与其通信。
下面是 systemd-network 的“org.freedesktop.network1.conf”中的一个正确示例
<busconfig>
<policy user="systemd-network">
<allow own="org.freedesktop.network1"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.network1"/>
<allow receive_sender="org.freedesktop.network1"/>
</policy>
</busconfig>
这只允许专用的服务用户“systemd-network”注册“org.freedesktop.network1”D-Bus 接口,而系统中的任何其他用户都可以与其通信。
KAuth KDE6 发布候选版本生成了此配置,而不是
<policy context="default">
<allow send_destination="*"/>
</policy>
其影响很容易被忽略:这表明任何人都可以与 D-Bus 系统总线上的任何内容通信。它还影响了不应受单个 KDE 包的注入式配置片段影响的其他 D-Bus 服务。虽然大多数在系统总线上运行的 D-Bus 服务是“公共”的,即允许任何人与其通信,但一些服务遵循不同的安全模型,其中只有专用用户才能与该服务进行交互。我们将 ratbagd 确定为这样一种 D-Bus 服务,它将受到 KAuth 中此缺陷的负面影响。这表明不相关的软件包的安全态势正面临风险。幸运的是,我们在 KDE6 发布完成之前及时发现了这个问题,并在其到达生产系统之前进行了修复。我们还检查了在 openSUSE Tumbleweed 上提供的所有非 KDE D-Bus 配置文件是否存在相同的问题,但幸运的是没有发现进一步包含此问题的文件。
在某种意义上,这些副作用也是 D-Bus 配置方案的不足之处,因为特定 D-Bus 服务的开发人员并不期望他们的配置文件具有全局影响。logrotate 也存在类似的问题,其中“/etc/logrotate.d”中的注入式配置文件中的设置可能会影响全局设置,从而影响整个系统。这两种情况(D-Bus 和 logrotate)都可能导致难以查找的错误,因为结果还取决于配置文件片段的解析顺序。
遗留的 fontinst D-Bus 服务
我们被要求查看的 KDE6 版本的大多数 KDE 组件在近几年都经过了我们的审查。其中一些是遗留软件包,因为在我们引入 D-Bus 和 Polkit 的打包限制时它们就已经存在了。当时我们没有足够的资源一次性检查所有这些组件。
我们在查看 KDE6 发布版本时遇到的一个遗留组件是“org.kde.fontinst.service”,它是“plasma6-workspace”软件包的一部分。我们发现其中有一个 D-Bus 方法“org.kde.fontinst.manage”,它实际上根据一个“method”字符串输入参数来多路复用大量子方法。这是一种不良设计,因为它破坏了 D-Bus 协议,因此使得各个方法调用的可见性和可管理性降低。更糟糕的是,只有一个 Polkit 操作用于验证隐藏在此单个 D-Bus 方法调用背后的各种代码路径。这样,各种代码路径就只有一种全有或全无的设置。
该服务中可用的子方法几乎构成了一个通用的文件系统 I/O 层,特别是考虑到该服务是以完整的 root 权限运行的。
- install:可用于将任意文件路径复制到任意位置,新文件权限为 0644。
- uninstall:允许删除任意文件路径,只要其父目录具有可写位。
- move:允许移动任意路径,包括新的所有者 uid 和组 gid,到任意新位置。
- toggle:接收原始 XML,似乎还指定了要启用或禁用的字体路径。
- removeFile:顾名思义;另一种删除文件的方式。
- configure:保存修改后的字体目录并调用一个小的 bash 脚本
fontinst_x11,该脚本准备字体目录并触发 X 服务器上的字体刷新。
fontinst 服务的核心业务逻辑应该是管理系统中提供的系统范围字体。理想情况下,为了实现这一点,应该只提供必要的高级逻辑操作,例如:从提供的数据安装字体,按名称删除系统字体。复制、删除和移动任意文件远远超出了该服务应有的范围。
单个 Polkit 操作“org.kde.fontinst.manage”默认需要 auth_admin_keep 授权,即任何想要调用此方法的人都需要提供管理员凭据。然而,如果管理员决定降低这些要求,因为用户应该能够例如在系统中安装新字体,那么此接口不仅允许这样做,还可以通过复制任意文件(例如创建新的“/etc/shadow”文件)来获得完全的 root 权限。
此服务需要重大重新设计。KDE 上游未能及时为 KDE6 发布完成此项工作。我们希望它仍然会发生,因为 API 处于相当令人担忧的状态。
“意外”的 Polkit 设置的困扰
fontinst 服务中关于 auth_admin 设置的情况,是我们审查 D-Bus 服务和 Polkit 操作时看到的常见模式。开发人员认为,为 Polkit 操作要求 auth_admin 身份验证足以证明代码的合理性,即以 root 身份执行过于通用的 API 或不安全的文件系统操作。在某些情况下,可以说一个操作永远不应有比 auth_admin 更弱的身份验证要求,因为它否则会导致不可控的安全问题。但不要忘记,Polkit 是一个可配置的身份验证框架。应用程序会提供默认设置,但系统集成商和管理员可以更改这些要求。
我们所知的唯一提供 明确机制 让管理员通过配置文件和覆盖来覆盖 Polkit 默认设置(针对各个操作)的是 (open)SUSE Linux 发行版。这通过 polkit-default-privs 包实现。我们的经验表明,上游开发者大多既不考虑降低 Polkit 身份验证要求的安全后果,也不测试当为加固目的提高身份验证要求时会发生什么。提高身份验证要求会导致额外的密码提示,而一些应用程序的 Polkit 操作工作流程在这种情况下会导致非常糟糕的行为。
一个常见的例子是像 Flatpak 这样的包管理器,它尝试在图形会话登录时获取 Polkit 身份验证以刷新存储库。开发人员只使用默认的 yes Polkit 身份验证要求进行测试,这使得用户对该身份验证过程不可见。当提高到 auth_admin 时,登录时突然弹出密码提示,用户感到困惑和恼怒。有方法可以解决这个问题:例如,使用 Polkit 的服务可以询问它是否可以在没有用户交互的情况下授权操作。如果不是这种情况,那么包管理器可以选择不立即刷新存储库。还可以使用“org.freedesktop.policykit.imply”注解将多个操作组合在一起进行身份验证,以避免在单个工作流程中出现多个密码提示。
可以理解,上游开发者很难测试许多不同 Polkit 配置的配置管理。提高对该领域普遍问题的认识将有助于从根本上避免这些问题。似乎开发人员只是想将身份验证“塞进”他们的软件中,一旦完成就停止思考。诚然,对于新手来说,Polkit 和 D-Bus 远非简单。尽管如此,每一次身份验证过程都应该经过深思熟虑。对于实现 Polkit 的开发人员来说,要吸取的教训是:
- Polkit 是一个可配置的身份验证框架,开发人员预期的设置可能与运行时实际发生的不符。
- 在建模 Polkit 操作时,应利用其细粒度的可能性,允许用户微调各个活动的要求。
- 应用程序中的每一次 Polkit 授权都应该从两个方向进行思考:如果身份验证要求降低会发生什么,以及如果提高身份验证要求会发生什么?
- 尚未讨论的另一个方面是显示给用户的身份验证消息。它们应该清楚地说明正在授权的确切内容,以便非技术用户能够理解。Polkit 还支持消息中的占位符,用于填充运行时信息,例如正在操作的文件。遗憾的是,此功能在实践中很少使用。
sddm-kcm6 中文件系统操作的问题
此组件是 SDDM 显示管理器的一个 KDE 配置模块 (KCM)。它包含一个 D-Bus 服务“org.kde.kcontrol.kcmsddm.conf”。我们过去已经审查过它,并为 KDE6 版本再次进行了审查。该服务存在两个主要问题,将在后续章节讨论。
未授权的 D-Bus 客户端提供的文件系统路径上的不安全操作
sddm-kcm6 KAuth 助手提供的多个 D-Bus 方法将文件系统路径作为输入参数。将此类路径传递给特权 D-Bus 服务是另一个经常遇到的有问题模式。在 openConfig() 函数中,如果 SDDM 主题配置文件路径不存在,则会由助手创建。如果它已存在,则会对该路径执行 chmod() 操作,模式为 0600,这也遵循符号链接。要了解这可能有多么麻烦,请考虑如果将“/etc/shadow”作为主题配置文件路径传递会发生什么。
以 root 身份操作受未授权用户控制的文件,以正确处理非常困难,需要谨慎使用底层系统调用。开发人员通常甚至没有意识到这个问题。KDE 组件过去在该领域曾遇到过一系列问题。我们认为这有更深层的原因,即 Qt 框架的文件系统 API 的设计,它一方面不允许完全控制底层系统调用(因为 Qt 也是一个平台抽象层),另一方面又没有准确记录其在此方面的 API 的行为。此外,Qt 框架本身也没有意识到它以 root 身份运行,可能正在操作其他用户拥有的文件。Qt 库旨在实现功能丰富的 GUI 应用程序,并且并不真正考虑处理不受信任的输入、以提高的权限运行以及跨越权限边界。
从一开始就避免路径访问问题的优雅方法是,不要通过 D-Bus 传递文件路径,而是传递已经打开的文件描述符。这之所以可能,是因为 D-Bus 内部使用 UNIX 域套接字,并且它们可以用于传递文件描述符。因此,客户端不是向服务传递一个建议“打开此文件,相信我,没问题”的字符串,而是将一个使用其低权限打开的文件描述符传递给特权服务。这样,许多路径访问问题可以瞬间解决。然而,有些情况仍然需要小心,例如,当需要执行递归文件系统操作时。
不幸的是,KDE 使用的 KAuth 框架 在此方面显示出局限性。由于 KAuth 助手 D-Bus API 只传输由 QVariantMap 序列化产生的二进制 blob,因此目前无法传递已打开的文件描述符。
由 sddm 服务用户拥有的配置文件中的更改
另一个问题不是 D-Bus API 中的,而是在 sync() 和 reset() D-Bus 方法的实现中。一旦处理了来自客户端的任何输入参数,助手就会在属于 sddm 服务用户的目录中进行操作。这里是一些从 reset() 和 sync() 函数中提取的精简代码
// from SddmAuthHelper::reset()
QString sddmHomeDirPath = KUser("sddm").homeDir();
QDir sddmConfigLocation(sddmHomeDirPath + QStringLiteral("/.config"));
QFile::remove(sddmConfigLocation.path() + QStringLiteral("/kdeglobals"));
QFile::remove(sddmConfigLocation.path() + QStringLiteral("/plasmarc"));
QDir(sddmHomeDirPath + "/.local/share/kscreen/").removeRecursively();
// from SddmAuthHelper::sync()
QString sddmHomeDirPath = KUser("sddm").homeDir();
QDir sddmCacheLocation(sddmHomeDirPath + QStringLiteral("/.cache"));
if (sddmCacheLocation.exists()) {
sddmCacheLocation.removeRecursively();
}
QDir sddmConfigLocation(sddmHomeDirPath + QStringLiteral("/.config"));
if (!args[QStringLiteral("kscreen-config")].isNull()) {
const QString destinationDir = sddmHomeDirPath + "/.local/share/kscreen/";
QSet<QString> done;
copyDirectoryRecursively(args[QStringLiteral("kscreen-config")].toString(), destinationDir, done);
}
被泄露的 sddm 服务用户可以利用这些操作来牟利
- 它可以导致拒绝服务,例如通过放置目录符号链接,使 D-Bus 服务在完全不同的文件系统位置运行。但是,此攻击是有限的,因为删除调用中使用的最终路径组件需要匹配,例如
kscreen。 - 通过在“~/.local/share/kscreen”中放置符号链接,“kscreen-config”可以被复制到任意位置。
为了使这些操作安全,最好暂时将特权降级到 sddm 用户。
未来的方向
KDE 上游未能及时为 KDE6 版本完成此 D-Bus 服务的重新设计。在这种情况下,sddm 用户主目录中的不安全操作甚至可以正式分配 CVE。由于所有 D-Bus 方法都受到 auth_admin Polkit 身份验证要求的保护,因此在默认安装中至少无法利用这些问题。
KWalletManager:保护配置的伪身份验证
KWalletManager 是 KDE 的密码管理器。它提供图形界面,并且正如预期的那样,在已登录用户的图形用户会话上下文中运行。它提供了一个“savehelper”服务,提供一个 D-Bus 方法“org.kde.kcontrol.kcmkwallet5.save”。那么,以 root 身份运行的服务助手在这里需要保存什么?让我们来看一下实现
ActionReply SaveHelper::save(const QVariantMap &args)
{
Q_UNUSED(args);
const qint64 uid = QCoreApplication::applicationPid();
qDebug() << "executing uid=" << uid;
return ActionReply::SuccessReply();
}
仔细审视这段代码,会发现它什么都没做。我们 询问了上游删除这个未使用的助手,但我们被告知这不是错误,而是故意的。他们想防止以下攻击场景:用户离开电脑未锁定,陌生人走过来,并且,竟然想更改 KWalletManager 的设置。为了防止这种情况发生,GUI 要求服务助手通过 Polkit 的 auth_self 授权来验证操作,如果失败则不继续。
然而,这无法阻止真正的攻击者,因为 KWalletManager 的配置存储在未授权用户的家目录中,仍然可以直接编辑,或者使用修改版的 KWalletManager 来进行编辑,它根本不会要求此身份验证。更不用说在这种情况下攻击者可以做所有其他事情了。那么,应该在哪里划定界限呢?我们甚至不认为这是一种加固,这是一种虚假的安全,而且令人困惑。如果确实需要这种虚假身份验证,那么至少应该找到一种方法来实现它,而无需运行一个什么都不做的 root 身份验证助手。上游似乎不同意,但我们已要求我们的打包人员通过补丁从我们的打包中删除此逻辑。
DrKonqi 中的改进
DrKonqi 是 KDE 的崩溃处理实用程序。如今,它与 systemd-coredump 交互以访问应用程序的核心转储。我们 在 2022 年对其进行的审查 导致了对 systemd-coredump 本身的发现。同时,DrKonqi 获得了额外的 D-Bus 服务逻辑,用于将私有核心转储(例如,来自以 root 身份运行的进程)复制到未授权用户的会话中进行分析。
此实现对于 KDE 组件来说很不寻常,因为它不依赖 KAuth:它直接使用 Qt 框架的 D-Bus 和 Polkit 功能。这可能是因为 KAuth 在传递文件描述符方面存在不足,如上所述。单个 excavateFromToDirFd() D-Bus 方法实际上接受一个文件描述符。它应该是一个指向未授权调用者控制的目录的文件描述符,核心转储将被复制到该目录。尽管这意味着 DrKonqi 无法从 KAuth 的通用框架功能中受益,但从安全角度来看,它是一个很好的例子,说明如何提高以 root 身份运行并在文件系统上操作的 D-Bus 服务的健壮性。
不幸的是,即使有了文件描述符,也会出现问题,正如本例所示。目录的权限处理与常规文件不同。目录通常只能以只读模式 (O_RDONLY) 打开。写入权限仅在尝试写入时进行检查,例如在 DrKonqi 助手的情况下调用 renameat()。这太晚了。未授权的调用者可以打开任何它具有读取访问权限的目录并将其传递给 D-Bus 服务。以 root 身份运行的 D-Bus 服务现在可以在该目录中愉快地创建新文件,即使调用者对此目录没有任何写入权限。
目前正在与上游进行 建设性的讨论,该讨论已促使此 D-Bus 服务在细节上得到各种改进,即将合并。目录文件描述符的问题是在过程后期才发现的,但希望很快能找到问题的解决方案。
结论
D-Bus 和 Polkit 都存在一些需要很好地理解和管理的复杂性。这很重要,因为作为纵深防御措施,它甚至超越了 Linux 本地系统的安全性。在 KAuth 框架等之上添加额外的层可能会导致长期问题,正如当前 KAuth API 对传递文件描述符缺乏支持所见。
KDE 打包人员和上游团队提前就 KDE6 发布候选版本及其引入的更改与我们联系,这一点很有帮助。在某些方面,例如生成糟糕的 D-Bus KAuth 配置文件,上游迅速做出反应并应用了修复,从而避免了有问题的代码在 KDE6 的生产版本中发布。在其他方面,例如遗留的 fontinst D-Bus 服务或 sddm-kcm D-Bus 服务,修复 API 问题的复杂性显然太高,以至于上游无法及时拿出更好的解决方案。我们决定不为这些服务中的发现请求 CVE 分配,因为在默认 Polkit 配置下,攻击向量对于普通用户来说是无法触及的。
到目前为止,大多数 KDE6 软件包应该已经到达 openSUSE Tumbleweed,并可在生产环境中使用。
参考文献
变更历史
| 2024-04-05 | 轻微拼写修正;为 Unsafe Operations in sddm-kcm6 插入了介绍性段落。 |