ksmbd漏洞研究
发布日期:2025年1月7日
作者:Norbert Szetei
引言
在Doyensec,我们决定对Linux内核组件SMB3内核服务器(ksmbd)进行漏洞研究活动。最初,它作为实验性功能启用,但在内核版本6.6中,实验性标志被移除,并保持稳定状态。
ksmbd通过任务拆分来优化性能,在内核空间处理关键文件操作,在用户空间通过ksmbd.mountd处理非性能相关任务(如DCE/RPC和用户账户管理)。服务器采用多线程架构,利用内核工作线程实现可扩展性,并通过用户空间集成进行配置和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从零开始进行模糊测试。尽管文章语法简单,但它为我们后续的改进提供了优秀的起点。
我们首先使用标准SMB客户端拦截和分析合法通信。这使我们能够扩展syzkaller语法,包含smb2pdu.c中实现的附加命令。
在模糊测试过程中,我们遇到了几个挑战,其中一个在pwning.tech文章中已解决。最初,我们需要标记数据包以识别syzkaller实例(procid)。此标记仅对第一个数据包是必需的,因为后续数据包共享相同的套接字连接。为解决此问题,我们修改了第一个(协商)请求,附加8字节表示syzkaller实例号,然后发送未标记的后续数据包。
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内部发生,它分配包装会话、连接和所有SMB相关数据的ksmbd_work结构,同时执行早期初始化。
在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中的使用后释放(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
8
9
10
11
12
13
14
15
|
// 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(work); // [8]
if (work->sess && work->sess->enc && work->encrypted &&
conn->ops->encrypt_resp) {
rc = conn->ops->encrypt_resp(work);
if (rc < 0)
conn->ops->set_rsp_status(work, STATUS_DATA_ERROR);
}
ksmbd_conn_write(work);
|
此处,ksmbd_user_session_put递减sess->refcnt,如果值达到零,内核允许释放sess对象([7]):
1
2
3
4
5
6
7
8
9
10
11
|
// https://github.com/torvalds/linux/blob/7aa8804c0b67b3cb263a472d17f2cb50d7f1a930/fs/smb/server/mgmt/user_session.c#L296
void ksmbd_user_session_put(struct ksmbd_session *sess)
{
if (!sess)
return;
if (atomic_read(&sess->refcnt) <= 0)
WARN_ON(1);
else
atomic_dec(&sess->refcnt); // [7]
}
|
随后的smb3_preauth_hash_rsp函数([8])访问sess对象而不验证它是否已被释放([9]):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// https://github.com/torvalds/linux/blob/7aa8804c0b67b3cb263a472d17f2cb50d7f1a930/fs/smb/server/smb2pdu.c#L8859
if (le16_to_cpu(rsp->Command) == SMB2_SESSION_SETUP_HE && sess) {
__u8 *hash_value;
if (conn->binding) {
struct preauth_session *preauth_sess;
preauth_sess = ksmbd_preauth_session_lookup(conn, sess->id);
if (!preauth_sess)
return;
hash_value = preauth_sess->Preauth_HashValue;
} else {
hash_value = sess->Preauth_HashValue; // [9]
if (!hash_value)
return;
}
ksmbd_gen_preauth_integrity_hash(conn, work->response_buf,
hash_value);
}
|
这可能导致在访问已释放对象时出现使用后释放(UAF)条件,如KASAN检测到:
1
2
|
BUG: KASAN: slab-use-after-free in smb3_preauth_hash_rsp (fs/smb/server/smb2pdu.c:8875)
Read of size 8 at addr ffff88812f5c8c38 by task kworker/0:9/308
|
CVE-2024-50285:ksmbd:检查未完成的同步SMB操作
在报告错误并确认修复后,我们在发送大量数据包时发现了另一个问题。每次在套接字连接期间调用queue_ksmbd_work时,它通过ksmbd_alloc_work_struct分配数据:
1
2
3
4
5
6
|
// https://elixir.bootlin.com/linux/v6.11/source/fs/smb/server/ksmbd_work.c#L21
struct ksmbd_work *ksmbd_alloc_work_struct(void)
{
struct ksmbd_work *work = kmem_cache_zalloc(work_cache, GFP_KERNEL);
// ..
}
|
在SMB中,信用额度旨在控制客户端可以发送的请求数量。但是,受影响的代码在强制执行信用额度限制之前执行。
通过远程套接字发送这些数据包大约两分钟后,系统始终遇到内核恐慌并重启:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
[ 287.957806] Out of memory and no killable processes...
[ 287.957813] Kernel panic - not syncing: System is deadlocked on memory
[ 287.957824] CPU: 2 UID: 0 PID: 2214 Comm: ksmbd:52086 Tainted: G B 6.12.0-rc5-00181-g6c52d4da1c74-dirty #26
[ 287.957848] Tainted: [B]=BAD_PAGE
[ 287.957854] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
[ 287.957863] Call Trace:
[ 287.957869] <TASK>
[ 287.957876] dump_stack_lvl (lib/dump_stack.c:124 (discriminator 1))
[ 287.957895] panic (kernel/panic.c:354)
[ 287.957913] ? __pfx_panic (kernel/panic.c:288)
[ 287.957932] ? out_of_memory (mm/oom_kill.c:1170)
[ 287.957964] ? out_of_memory (mm/oom_kill.c:1169)
[ 287.957989] out_of_memory (mm/oom_kill.c:74 mm/oom_kill.c:1169)
[ 287.958014] ? mutex_trylock (./arch/x86/include/asm/atomic64_64.h:101 ./include/linux/atomic/atomic-arch-fallback.h:4296 ./include/linux/atomic/atomic-long.h:1482 ./include/linux/atomic/atomic-instrumented.h:4458 kernel/locking/mutex.c:129 kernel/locking/mutex.c:152 kernel/locking/mutex.c:1092)
|
原因是ksmbd不断创建线程,在分叉超过2000个线程后,ksmbd_work_cache耗尽了可用内存。
这可以通过使用slabstat或检查/proc/slabinfo来确认。活动对象的数量稳步增加,最终耗尽内核内存并导致系统重启:
1
2
3
4
5
6
7
|
# ps auxww | grep -i ksmbd | wc -l
2069
# head -2 /proc/slabinfo; grep ksmbd_work_cache /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
ksmbd_work_cache 16999731 16999731 384 21 2 : tunables 0 0 0 : slabdata 809511 809511 0
|
此问题未被syzkaller识别,而是通过手动测试触发代码发现。
结论
尽管syzkaller识别并触发了两个漏洞,但未能生成重现器,需要手动分析崩溃报告。这些问题无需身份验证即可访问,模糊测试的进一步改进可能会发现来自难以正确实现的复杂锁定机制或其他因素的额外错误。由于时间限制,我们未尝试为UAF创建完全有效的利用。
参考文献