以 root 身份连接到用户 dbus

3

如果我们正常打开Python解释器并输入以下内容:

import dbus
bus = dbus.SessionBus()
bus.list_names()

我们可以在用户会话的 D-Bus 上看到所有服务。现在假设我们想要在同一个脚本中执行一些只有 root 用户才能进行的操作,以确定通过 dbus 传递的信息,因此我们使用 sudo python 命令以超级用户权限运行解释器,并执行相同的操作。这时,我们只能看到 root 用户会话 dbus 上的短列表,并尝试使用 get_object 连接任何位于用户 dbus 上的内容会导致“未找到”错误。
import os

os.seteuid(int(os.environ['SUDO_UID']))

但是这只会使得SessionBus()返回一个org.freedesktop.DBus.Error.NoReply,所以这可能是无意义的。是否有一种方法可以使用Python DBus绑定作为超级用户连接到用户的dbus服务?

3个回答

5

我对DBus的知识很少,但那个问题引起了我的好奇心。

TL;DR: 使用目标用户的套接字地址和seteuid来获取访问权限,请使用dbus.bus.BusConnection

第一个问题:DBus连接到哪个套接字以获得会话总线?

$ cat list_bus.py 
import dbus
print(dbus.SessionBus().list_names())
$ strace -o list_bus.trace python3 list_bus.py
$ grep ^connect list_bus.trace 
connect(3, {sa_family=AF_UNIX, sun_path="/run/user/1000/bus"}, 20) = 0

也许它依赖于环境变量?明白了!
$ env|grep /run/user/1000/bus
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

从root账户跟踪行为可以看出它不知道连接的地址。通过搜索变量名称,我找到了D-Bus规范,其中的"Well-known Message Bus Instances"章节。

第二个问题:我们是否可以直接连接到套接字而不需要D-Bus库猜测正确的地址?dbus-python教程中指出:

对于特殊目的,您可能会使用非默认的总线,或者使用dbus-python 0.81.0中添加的一些新API来连接不是总线的连接。

查看更改日志,似乎是指这些内容:

Bus拥有一个超类dbus.bus.BusConnection(与总线守护进程的连接,但没有共享连接语义或任何已弃用的API),以便那些想要子类化总线守护进程连接的人受益。

让我们试一下:

$ python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
145

如何获取 Root 权限?

# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3/dist-packages/dbus/bus.py", line 124, in __new__
    bus = cls._new_for_bus(address_or_type, mainloop=mainloop)
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.NoReply: Did not
  receive a reply. Possible causes include: the remote application did not send
  a reply, the message bus security policy blocked the reply, the reply timeout
  expired, or the network connection was broken.
>>> import os
>>> os.seteuid(1000)
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143

所以这个问题的答案是: 使用BusConnection代替SessionBus并显式指定地址,再结合使用seteuid来获得访问权限。

奖励: 不使用seteuid以root身份连接

我还想知道是否可能直接作为root用户访问总线,而不需要使用seteuid。经过一些搜索,我找到了一个systemd ticket,其中有如下评论:

dbus-daemon是强制访问的组件...(但你可以放置一个xml策略文件来实现这一点)。

这让我想起了一个askubuntu问题,讨论如何修改站点本地会话总线策略。

只是为了玩玩,我在一个终端中运行了以下命令:

$ cp /usr/share/dbus-1/session.conf session.conf
$ (edit session.conf to modify the include for local customization)
$ diff /usr/share/dbus-1/session.conf session.conf
50c50
<   <include ignore_missing="yes">/etc/dbus-1/session-local.conf</include>
---
>   <include ignore_missing="yes">session-local.conf</include>
$ cat > session-local.conf
<busconfig>
  <policy context="mandatory">
    <allow user="root"/>
  </policy>
</busconfig>
$ dbus-daemon --config-file session.conf --print-address
unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98
$ dbus-daemon --config-file session.conf --print-address
unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98

在另一个终端中,我无法以root用户身份连接到此总线:
# python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
>>> from dbus.bus import BusConnection
>>> address = "unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98"
>>> BusConnection(address).list_names()
dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String(':1.0')], signature=dbus.Signature('s'))

这还应该使得在全局安装 session-local.conf 时能够访问系统上的 所有 会话总线:

# cp session-local.conf /etc/dbus-1/session-local.conf
# kill -HUP 1865   # reload config of my users session dbus-daemon
# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143

现在它可以工作了 - 现在root用户可以连接到任何会话总线,而不需要使用seteuid。别忘了

# rm /etc/dbus-1/session-local.conf 

如果您的root用户不需要这种权限。

这是一个非常好的答案,将对未来有所帮助,特别感谢花时间解释dbus策略 \o/ 我想知道是否可能只允许root访问所有会话,为什么这不是常见情况?虽然这是原始问题的最佳答案,但最好完全避免以root身份运行python,并在必要时提升权限。 - teprrr
解决了问题,并提供了大量上下文。关于teprrr提到的默认值问题,通常用户dbus是应用程序软件,不需要root用户关心或需要发送dbus信号,因此root通常无法看到用户dbus。 - day

1

Bluehorn的回答帮助了我。我想分享我的解决方案。我刚学Python几个月(之前只会shell脚本),所以如果这种方法是错误的,只是在我的系统上运行,请告诉我。

这些是我编写的守护进程的部分,用于监视Linux中的CPU温度并控制风扇速度,因此需要root权限。不确定在多个用户登录时以普通用户身份运行时是否能正常工作。我猜测不会……


import os, pwd
from dbus import SessionBus, Interface
from dbus.bus import BusConnection

# Subclassing dbus.Interface because why not

class Dbus(Interface):
    def __init__(self, uid):
        method = 'org.freedesktop.Notifications'
        path = '/' + method.replace('.', '/')
        if os.getuid() == uid:
            obj = SessionBus().get_object( method, path )
        else:
            os.seteuid(uid)
            obj = BusConnection( "unix:path=/run/user/" + str(uid) + "/bus" )
            obj.get_object( method, path )

        super().__init__(obj, method)

# Did this so my notifications would work
# when running as root or non root

class DbusNotificationHandler:

    app_icon = r"/path/to/my/apps/icon.png"
    name     = "MacFanD"

    def __init__(self):
        loggedIn, users = [ os.getlogin() ], []
        for login in pwd.getpwall():
            if login.pw_name in loggedIn:
                users.append( login.pw_uid )

        self.users = []
        for i in users:
            self.users.append( Dbus(i) )

    def notification(self, msg, mtype=None):
        if not isinstance(msg, list) or len(msg) < 2:
            raise TypeError("Expecting a list of 2 for 'msg' parameter")

        hint = {}

        if mtype == 'temp':
            icon = 'dialog-warning'
            hint['urgency'] = 2
            db_id = 498237618
            timeout = 0
        elif mtype == 'warn':
            icon = 'dialog-warning'
            hint['urgency'] = 2
            db_id = 0
            timeout = 5000
        else:
            icon = self.app_icon
            hint['urgency'] = 1
            db_id = 0
            timeout = 5000

        for db in self.users:
            db.Notify( self.name, db_id, icon, msg[0], msg[1:], [], hint, timeout )

handler = DbusNotificationHandler()
notify = handler.notification

msg = [ "Daemon Started", "Daemon is now running - %s"%os.getpid() ]
notify(msg)

temp = "95 Celsius"
msg = [ "High Temp Warning", "CPU temperature has reached %s"%temp ]
notify(msg, 'warn')

0

您可以设置 DBUS_SESSION_BUS_ADDRESS 环境变量来选择要连接的 dbus 会话。

权限不正确(即缺少 seteuid)会导致立即出现 NoReply,而未定义 DBUS_SESSION_BUS_ADDRESS 则会响应 Using X11 for dbus-daemon autolaunch was disabled at compile time, set your DBUS_SESSION_BUS_ADDRESS instead

这是我使用的测试代码:

import os
import dbus

# become user
uid = os.environ["SUDO_UID"]
print(f"I'm {os.geteuid()}, becoming {uid}")
os.seteuid(int(uid))

# set the dbus address
os.environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path=/run/user/{uid}/bus"

bus = dbus.SessionBus()
print(bus.list_names())

# I'm 0, becoming 1000
# dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String('org.fr .. <snip>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接