ksmbd漏洞研究:深入Linux内核SMB服务器的安全挑战

本文详细分析了Linux内核SMB服务器ksmbd的多个安全漏洞,包括CVE-2024-50286、CVE-2024-50283和CVE-2024-50285,探讨了其架构、fuzzing方法和漏洞利用条件,涉及UAF、同步问题和内存耗尽等关键技术点。

ksmbd漏洞研究

07 Jan 2025 - 发布者:Norbert Szetei

引言

在Doyensec,我们决定对SMB3内核服务器(ksmbd)进行漏洞研究活动,这是Linux内核的一个组件。最初,它作为实验性功能启用,但在内核版本6.6中,实验性标志被移除,并保持稳定状态。

ksmbd分割任务以优化性能,在内核空间处理关键文件操作,在用户空间通过ksmbd.mountd处理非性能相关任务,如DCE/RPC和用户账户管理。服务器使用多线程架构来高效并行处理SMB请求,利用内核工作线程实现可扩展性,并通过用户空间集成进行配置和RPC处理。

ksmbd默认未启用,但它是学习SMB协议同时探索Linux内部机制(如网络、内存管理和线程)的绝佳目标。

ksmbd内核组件直接绑定到端口445以处理SMB流量。内核与ksmbd.mountd用户空间进程之间的通信通过Netlink接口进行,这是Linux中用于内核到用户空间通信的基于套接字的机制。尽管ksmbd.mountd以root权限运行,但由于其直接可达性,我们专注于直接针对内核。

架构示意图可在邮件列表中找到,并显示如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
               |--- ...
       --------|--- ksmbd/3 - Client 3
       |-------|--- ksmbd/2 - Client 2
       |       |         ____________________________________________________
       |       |        |- Client 1                                          |
<--- Socket ---|--- ksmbd/1   <<= Authentication : NTLM/NTLM2, Kerberos      |
       |       |      | |     <<= SMB engine : SMB2, SMB2.1, SMB3, SMB3.0.2, |
       |       |      | |                SMB3.1.1                            |
       |       |      | |____________________________________________________|
       |       |      |
       |       |      |--- VFS --- Local Filesystem
       |       |
KERNEL |--- ksmbd/0(forker kthread)
---------------||---------------------------------------------------------------
USER           ||
               || communication using NETLINK
               ||  ______________________________________________
               || |                                              |
        ksmbd.mountd <<= DCE/RPC(srvsvc, wkssvc, samr, lsarpc)   |
               ^  |  <<= configure shares setting, user accounts |
               |  |______________________________________________|
               |
               |------ smb.conf(config file)
               |
               |------ ksmbdpwd.db(user account/password file)
                            ^
  ksmbd.adduser ------------|

关于此主题已发布多项研究,包括Thalium和pwning.tech的研究。后者详细解释了如何使用syzkaller从零开始进行fuzzing。尽管文章语法简单,但它为我们后续的改进提供了优秀的起点。

我们首先使用标准SMB客户端拦截和分析合法通信。这使我们能够扩展syzkaller语法,以包含在smb2pdu.c中实现的附加命令。

在fuzzing过程中,我们遇到了几个挑战,其中一个在pwning.tech文章中已解决。最初,我们需要标记数据包以识别syzkaller实例(procid)。此标记仅对第一个数据包是必需的,因为后续数据包共享相同的套接字连接。为了解决这个问题,我们修改了第一个(协商)请求,附加了代表syzkaller实例号的8字节。之后,我们发送未标记的后续数据包。

syzkaller的另一个限制是无法使用malloc()进行动态内存分配,这使在伪系统调用中实现身份验证变得复杂。为了绕过这一点,我们修补了相关的身份验证(NTLMv2)和数据包签名验证检查,允许我们在没有有效签名的情况下绕过协商和会话设置。这使能够调用其他命令,如ioctl处理逻辑。

为了创建更多样化和有效的测试用例,我们最初使用strace提取通信,或手动制作数据包。为此,我们使用了Kaitai Struct,通过其Web界面或可视化工具。当数据包被内核拒绝时,Kaitai允许我们快速识别和解决问题。

漏洞发现

在我们的研究中,我们识别了多个安全问题,其中三个在本文中描述。这些漏洞有一个共同特点——它们可以在会话设置阶段无需身份验证的情况下被利用。利用它们需要对通信过程有基本了解。

通信过程

在KSMBD初始化期间(无论是内置到内核还是作为外部模块),调用启动函数create_socket()来监听传入流量:

1
2
3
4
5
6
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/transport_tcp.c#L484
	ret = kernel_listen(ksmbd_socket, KSMBD_SOCKET_BACKLOG);
	if (ret) {
		pr_err("Port listen() error: %d\n", ret);
		goto out_error;
	}

实际数据处理发生在ksmbd_tcp_new_connection()函数和生成的每连接线程(ksmbd:%u)中。此函数还分配代表连接的struct ksmbd_conn:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/transport_tcp.c#L203
static int ksmbd_tcp_new_connection(struct socket *client_sk)
{
	// ..
	handler = kthread_run(ksmbd_conn_handler_loop,
			      KSMBD_TRANS(t)->conn,
			      "ksmbd:%u",
			      ksmbd_tcp_get_port(csin));
	// ..
}

ksmbd_conn_handler_loop至关重要,因为它处理读取、验证和处理SMB协议消息(PDU)。在没有错误的情况下,它调用更具体的处理函数之一:

1
2
3
4
5
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/connection.c#L395
		if (default_conn_ops.process_fn(conn)) {
			pr_err("Cannot handle request\n");
			break;
		}

处理函数将SMB请求添加到工作线程队列:

1
2
3
4
5
// ksmbd_server_process_request
static int ksmbd_server_process_request(struct ksmbd_conn *conn)
{
	return queue_ksmbd_work(conn);
}

这发生在queue_ksmbd_work内部,它分配ksmbd_work结构,该结构包装会话、连接和所有SMB相关数据,同时执行早期初始化。

在Linux内核中,将工作项添加到工作队列需要使用INIT_WORK()宏进行初始化,该宏将项链接到处理时要执行的回调函数。此处执行如下:

1
2
3
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/server.c#L312
	INIT_WORK(&work->work, handle_ksmbd_work);
	ksmbd_queue_work(work);

我们现在接近处理SMB PDU操作。最后一步是handle_ksmbd_work从请求中提取命令号

1
2
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/server.c#L213
rc = __process_request(work, conn, &command);

并执行相关的命令处理程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/server.c#L108
static int __process_request(struct ksmbd_work *work, struct ksmbd_conn *conn,
			     u16 *cmd)
{
	// ..
	command = conn->ops->get_cmd_val(work);
	*cmd = command;
	// ..

	cmds = &conn->cmds[command];
	// ..
	ret = cmds->proc(work);

以下是调用的过程列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/smb2ops.c#L171
	[SMB2_NEGOTIATE_HE]	=	{ .proc = smb2_negotiate_request, },
	[SMB2_SESSION_SETUP_HE] =	{ .proc = smb2_sess_setup, },
	[SMB2_TREE_CONNECT_HE]  =	{ .proc = smb2_tree_connect,},
	[SMB2_TREE_DISCONNECT_HE]  =	{ .proc = smb2_tree_disconnect,},
	[SMB2_LOGOFF_HE]	=	{ .proc = smb2_session_logoff,},
	[SMB2_CREATE_HE]	=	{ .proc = smb2_open},
	[SMB2_QUERY_INFO_HE]	=	{ .proc = smb2_query_info},
	[SMB2_QUERY_DIRECTORY_HE] =	{ .proc = smb2_query_dir},
	[SMB2_CLOSE_HE]		=	{ .proc = smb2_close},
	[SMB2_ECHO_HE]		=	{ .proc = smb2_echo},
	[SMB2_SET_INFO_HE]      =       { .proc = smb2_set_info},
	[SMB2_READ_HE]		=	{ .proc = smb2_read},
	[SMB2_WRITE_HE]		=	{ .proc = smb2_write},
	[SMB2_FLUSH_HE]		=	{ .proc = smb2_flush},
	[SMB2_CANCEL_HE]	=	{ .proc = smb2_cancel},
	[SMB2_LOCK_HE]		=	{ .proc = smb2_lock},
	[SMB2_IOCTL_HE]		=	{ .proc = smb2_ioctl},
	[SMB2_OPLOCK_BREAK_HE]	=	{ .proc = smb2_oplock_break},
	[SMB2_CHANGE_NOTIFY_HE]	=	{ .proc = smb2_notify},

在解释了如何到达PDU函数后,我们可以继续讨论由此产生的错误。

CVE-2024-50286

该漏洞源于ksmbd中sessions_table管理的不当同步。具体来说,代码缺少sessions_table_lock来保护会话过期和会话注册期间的并发访问。此问题引入了竞争条件,其中多个线程可以同时访问和修改sessions_table,导致cache kmalloc-512中的Use-After-Free(UAF)。

sessions_table实现为哈希表,它存储连接的所有活动SMB会话,使用会话标识符(sess->id)作为键。

在会话注册期间,发生以下流程:

  • 为连接创建新会话。
  • 在注册会话之前,工作线程调用ksmbd_expire_session以删除过期会话,避免陈旧会话消耗资源。
  • 清理完成后,新会话添加到连接的会话列表中。

对此表的操作,如添加(hash_add)和删除会话(hash_del),缺乏适当的同步,创建了竞争条件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/smb2pdu.c#L1663
int smb2_sess_setup(struct ksmbd_work *work)
{
	// .. 
	ksmbd_conn_lock(conn);
	if (!req->hdr.SessionId) {
		sess = ksmbd_smb2_session_create(); // [1]
		if (!sess) {
			rc = -ENOMEM;
			goto out_err;
		}
		rsp->hdr.SessionId = cpu_to_le64(sess->id);
		rc = ksmbd_session_register(conn, sess); // [2]
		if (rc)
			goto out_err;

		conn->binding = false;

在[1],创建会话,通过分配sess对象:

1
2
3
4
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/mgmt/user_session.c#L381
	sess = kzalloc(sizeof(struct ksmbd_session), GFP_KERNEL);
	if (!sess)
		return NULL;

此时,在大量同时连接期间,一些会话可能过期。当调用[2]的ksmbd_session_register时,它调用ksmbd_expire_session [3]:

1
2
3
4
5
6
7
8
9
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/mgmt/user_session.c#L192
int ksmbd_session_register(struct ksmbd_conn *conn,
			   struct ksmbd_session *sess)
{
	sess->dialect = conn->dialect;
	memcpy(sess->ClientGUID, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE);
	ksmbd_expire_session(conn); // [3]
	return xa_err(xa_store(&conn->sessions, sess->id, sess, GFP_KERNEL));
}

由于没有实现表锁定,过期的sess对象可能从表中移除([4])并释放([5]):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/mgmt/user_session.c#L173
static void ksmbd_expire_session(struct ksmbd_conn *conn)
{
	unsigned long id;
	struct ksmbd_session *sess;

	down_write(&conn->session_lock);
	xa_for_each(&conn->sessions, id, sess) {
		if (atomic_read(&sess->refcnt) == 0 &&
		    (sess->state != SMB2_SESSION_VALID ||
		     time_after(jiffies,
			       sess->last_active + SMB2_SESSION_TIMEOUT))) {
			xa_erase(&conn->sessions, sess->id);
			hash_del(&sess->hlist); // [4]
			ksmbd_session_destroy(sess); // [5]
			continue;
		}
	}
	up_write(&conn->session_lock);
}

然而,在另一个线程中,当连接在ksmbd_server_terminate_conn中终止时,可能调用清理,通过调用ksmbd_sessions_deregister,操作同一表且没有适当的锁([6]):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/mgmt/user_session.c#L213
void ksmbd_sessions_deregister(struct ksmbd_conn *conn)
{
	struct ksmbd_session *sess;
	unsigned long id;

	down_write(&sessions_table_lock);
	// .. ignored, since the connection is not binding
	up_write(&sessions_table_lock);

	down_write(&conn->session_lock);
	xa_for_each(&conn->sessions, id, sess) {
		unsigned long chann_id;
		struct channel *chann;

		xa_for_each(&sess->ksmbd_chann_list, chann_id, chann) {
			if (chann->conn != conn)
				ksmbd_conn_set_exiting(chann->conn);
		}

		ksmbd_chann_del(conn, sess);
		if (xa_empty(&sess->ksmbd_chann_list)) {
			xa_erase(&conn->sessions, sess->id);
			hash_del(&sess->hlist); // [6] 
			ksmbd_session_destroy(sess);
		}
	}
	up_write(&conn->session_lock);
}

一种可能的流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Thread A                         | Thread B
---------------------------------|-----------------------------
ksmbd_session_register           | 
ksmbd_expire_session             |  
                                 | ksmbd_server_terminate_conn
                                 | ksmbd_sessions_deregister
ksmbd_session_destroy(sess)      |   |
    |                            |   |
    hash_del(&sess->hlist);      |   |
    kfree(sess);                 |   |
                                 |   hash_del(&sess->hlist);

启用KASAN时,问题通过以下崩溃显现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
BUG: KASAN: slab-use-after-free in __hlist_del include/linux/list.h:990 [inline]
BUG: KASAN: slab-use-after-free in hlist_del_init include/linux/list.h:1016 [inline]
BUG: KASAN: slab-use-after-free in hash_del include/linux/hashtable.h:107 [inline]
BUG: KASAN: slab-use-after-free in ksmbd_sessions_deregister+0x569/0x5f0 fs/smb/server/mgmt/user_session.c:247
Write of size 8 at addr ffff888126050c70 by task ksmbd:51780/39072

BUG: KASAN: slab-use-after-free in hlist_add_head include/linux/list.h:1034 [inline]
BUG: KASAN: slab-use-after-free in __session_create fs/smb/server/mgmt/user_session.c:420 [inline]
BUG: KASAN: slab-use-after-free in ksmbd_smb2_session_create+0x74a/0x750 fs/smb/server/mgmt/user_session.c:432
Write of size 8 at addr ffff88816df5d070 by task kworker/5:2/139

两个问题都导致在偏移112处发生越界(OOB)写入。

CVE-2024-50283: ksmbd: 修复smb3_preauth_hash_rsp中的slab-use-after-free

该漏洞在提交7aa8804c0b中引入,当实现会话的引用计数以避免UAF时:

1
2
3
4
5
6
7
// https://github.com/torvalds/linux/blob/7aa8804c0b67b3cb263a472d17f2cb50d7f1a930/fs/smb/server/server.c
send:
	if (work->sess)
		ksmbd_user_session_put(work->sess);
	if (work->tcon)
		ksmbd_tree_connect_put(work->tcon);
	smb3_preauth_hash_rsp
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计