ksmbd内核SMB服务器模糊测试改进与漏洞挖掘深度解析

本文深入探讨了对Linux内核ksmbd SMB服务器模块进行模糊测试的技术改进,包括状态管理、协议规范实现、多种模糊测试策略以及发现的23个安全漏洞,详细介绍了如何扩展攻击面并利用syzkaller提升覆盖率。

ksmbd - 模糊测试改进与漏洞发现 (2/3)

引言

这是对先前发表文章的后续研究。

我们的初步研究发现了数个无需身份验证的漏洞,但我们仅仅触及了攻击面的表层。即使在修补代码以绕过身份验证后,大多数有趣的操作仍需要与初始阶段我们省略的处理程序和状态进行交互。在本部分,我们将解释如何增加覆盖率并应用不同的模糊测试策略来识别更多漏洞。

配置依赖的攻击面

一些功能需要额外的配置选项。我们尝试启用许多可用功能,以最大化暴露的攻击面。这帮助我们触发了在最小配置示例中被禁用的代码路径。然而,为了简化设置,我们没有考虑Kerberos支持或RDMA等功能。这些可以作为未来改进的目标。

配置相关功能

以下功能有助于扩展攻击面。默认情况下,只有机会锁(oplocks)是启用的。

  • G = 仅全局作用域
  • S = 每个共享,但也可以全局设置为默认值
  • 持久句柄 (G)
  • 机会锁 (S)
  • 服务器多通道支持 (G)
  • SMB2租约 (G)
  • VFS对象 (S)

从代码角度来看,除了 smb2pdu.c 之外,还涉及以下源文件:

  • ndr.c – 用于SMB结构的NDR编码/解码
  • oplock.c – 机会锁请求和中断处理
  • smbacl.c – SMB ACL的解析和执行
  • vfs.c – 虚拟文件系统操作接口
  • vfs_cache.c – 用于文件和目录查找的缓存层

fs/smb/server 目录中的其余文件要么是标准通信的一部分,要么需要更复杂的设置才能执行,例如各种身份验证方案的情况。

模糊测试器改进

SMB3期望在大多数操作之前进行有效的会话设置,并且其身份验证流程是多步骤的,需要正确的顺序。为实现有效的Kerberos身份验证进行模糊测试是不切实际的。

如第一部分所述,我们修补了NTLMv2身份验证以便与资源交互。我们还明确允许了访客账户,并指定了 map to guest = bad user,以便在凭据无效时回退到“guest”。在报告了 CVE-2024-50285: ksmbd: check outstanding simultaneous SMB operations 后,信用额度限制变得更加严格,因此我们也将其修补掉以避免速率限制。

当我们使用更大的语料库重新启动syzkaller时,几分钟后,所有剩余的候选测试用例都被拒绝了。经过一番调查,我们发现问题在于默认的 max connections = 128,我们必须将其增加到最大值 65536。没有更改其他限制。

状态管理

SMB交互是有状态的,依赖于会话、TreeID和FileID。模糊测试需要模拟有效的状态转换,例如 smb2_create ⇢ smb2_ioctl ⇢ smb2_close。当我们发起诸如 smb2_tree_connectsmb2_sess_setupsmb2_create 等操作时,我们在伪系统调用中手动解析响应以提取资源标识符,并在后续调用中重用它们。我们的测试工具被编程为每个伪系统调用发送多条消息。

下方展示了资源解析的示例代码:

 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
// 处理响应。不包含 +4B 的PDU长度
void process_buffer(int msg_no, const char *buffer, size_t received) {
  // .. 省略 ..

    // 提取SMB2命令
  uint16_t cmd_rsp = u16((const uint8_t *)(buffer + CMD_OFFSET));
  debug("Response command: 0x%04x\n", cmd_rsp);

  switch (cmd_rsp) {
    case SMB2_TREE_CONNECT:
      if (received >= TREE_ID_OFFSET + sizeof(uint32_t)) {
        tree_id = u32((const uint8_t *)(buffer + TREE_ID_OFFSET));
        debug("Obtained tree_id: 0x%x\n", tree_id);
      }
      break;

    case SMB2_SESS_SETUP:
      // 第一次会话设置响应携带 session_id
      if (msg_no == 0x01 &&
          received >= SESSION_ID_OFFSET + sizeof(uint64_t)) {
        session_id = u64((const uint8_t *)(buffer + SESSION_ID_OFFSET));
        debug("Obtained session_id: 0x%llx\n", session_id);
      }
      break;

    case SMB2_CREATE:
      if (received >= CREATE_VFID_OFFSET + sizeof(uint64_t)) {
        persistent_file_id = u64((const uint8_t *)(buffer + CREATE_PFID_OFFSET));
        volatile_file_id   = u64((const uint8_t *)(buffer + CREATE_VFID_OFFSET));
        debug("Obtained p_fid: 0x%llx, v_fid: 0x%llx\n",
              persistent_file_id, volatile_file_id);
      }
      break;

    default:
      debug("Unknown command (0x%04x)\n", cmd_rsp);
      break;
  }
}

我们不得不解决的另一个问题是,ksmbd依赖于全局状态-内存池或会话表,这使得模糊测试的确定性降低。我们尝试启用实验性的 reset_acc_state 功能来重置累积状态,但这显著降低了模糊测试的速度。我们决定不过分关心可复现性,因为每个漏洞通常会在几十甚至上百个测试用例中出现。对于其他情况,我们使用下面描述的聚焦模糊测试。

协议规范

我们将测试工具基于官方的SMB协议规范,通过为所有支持的SMB命令实现语法。微软作为其开放规范计划的一部分,发布了SMB和其他协议的详细技术文档。

例如,SMB2 IOCTL请求的线格式如下所示:

 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
+-----------------------------------------------+
|          Header Prefix (SMB2Header_Prefix)    |
+-----------------------------------------------+
|          Command (0xb)                        |
+-----------------------------------------------+
|          Header Suffix (SMB2Header_Suffix)    |
+-----------------------------------------------+
|          StructureSize (57)                   |
+-----------------------------------------------+
|          Reserved (0)                         |
+-----------------------------------------------+
|          CtlCode                              |
+-----------------------------------------------+
|          PersistentFileId (0x4)               |
+-----------------------------------------------+
|          VolatileFileId (0x0)                 |
+-----------------------------------------------+
|          InputOffset                          |
+-----------------------------------------------+
|          InputCount                           |
+-----------------------------------------------+
|          MaxInputResponse (65536)             |
+-----------------------------------------------+
|          OutputOffset                         |
+-----------------------------------------------+
|          OutputCount                          |
+-----------------------------------------------+
|          MaxOutputResponse (65536)            |
+-----------------------------------------------+
|          Flags                                |
+-----------------------------------------------+
|          Reserved2 (0)                        |
+-----------------------------------------------+
|          Input (variable)                     |
+-----------------------------------------------+
|          Output (variable)                    |
+-----------------------------------------------+

然后,我们手动将此规范重写为我们的语法,这使得我们的测试工具能够自动构建有效的SMB2 IOCTL请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
smb2_ioctl_req {
        Header_Prefix           SMB2Header_Prefix
        Command                 const[0xb, int16]
        Header_Suffix           SMB2Header_Suffix
        StructureSize           const[57, int16]
        Reserved                const[0, int16]
        CtlCode                 union_control_codes
        PersistentFileId        const[0x4, int64]
        VolatileFileId          const[0x0, int64]
        InputOffset             offsetof[Input, int32]
        InputCount              bytesize[Input, int32]
        MaxInputResponse        const[65536, int32]
        OutputOffset            offsetof[Output, int32]
        OutputCount             len[Output, int32]
        MaxOutputResponse       const[65536, int32]
        Flags                   int32[0:1]
        Reserved2               const[0, int32]
        Input                   array[int8]
        Output                  array[int8]
} [packed]

在翻译过程中,我们对照源代码进行了最终检查,以识别和验证可能存在的不匹配。

模糊测试策略

由于我们很好奇仅使用默认的syzkaller配置和从头生成的语料库可能会错过哪些漏洞,因此我们探索了不同的模糊测试方法,每种方法都在以下小节中描述。

FocusAreas

有时,我们触发了一个无法复现的漏洞,并且从崩溃日志中无法立即清楚其发生原因。在其他情况下,我们想要专注于覆盖率较低的分析函数。实验性的 focus_areas 函数正好允许这样做。

例如,通过以以下方式针对 smb_check_perm_dacl

1
2
3
4
5
"focus_areas": [
  {"filter": {"functions": ["smb_check_perm_dacl"]}, "weight": 20.0},
  {"filter": {"files": ["^fs/smb/server/"]}, "weight": 2.0},
  {"weight": 1.0}
]

我们识别了多个整数溢出,并能够快速建议和确认补丁。

为了到达易受攻击的代码,syzkaller构建了一个通过验证并导致整数溢出的ACL。用Python重写后,它看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def build_sd():
    sd = bytearray(0x14)

    sd[0x00] = 0x00
    sd[0x01] = 0x00
    struct.pack_into("<H", sd, 0x02, 0x0001)
    struct.pack_into("<I", sd, 0x04, 0x78)
    struct.pack_into("<I", sd, 0x08, 0x00)
    struct.pack_into("<I", sd, 0x0C, 0x10000)
    struct.pack_into("<I", sd, 0x10, 0xFFFFFFFF) # dacloffset

    while len(sd) < 0x78:
        sd += b"A"

    sd += b"\x01\x01\x00\x00\x00\x00\x00\x00"
    sd += b"\xCC" * 64

    return bytes(sd)

sd = build_sd()
print(f"[+] Final SD length: {len(sd)}")

ANYBLOB

anyTypes 结构在模糊测试期间内部使用,并且文档较少 - 可能是因为它并不打算直接使用。它定义在 prog/any.go 中,可以表示多种结构:

1
2
3
4
5
6
type anyTypes struct {
	union  *UnionType
	array  *ArrayType
	blob   *BufferType
    // .. 省略..
}

在提交 9fe8aa4 中实现,其用例是将复杂结构压缩为扁平字节数组,并仅应用通用变异。

阅读测试用例能更清楚地了解其工作原理,其中:

1
foo$any_in(&(0x7f0000000000)={0x11, 0x11223344, 0x2233, 0x1122334455667788, {0x1, 0x7, 0x1, 0x1, 0x1bc, 0x4}, [{@res32=0x0, @i8=0x44, "aabb"}, {@res64=0x1, @i32=0x11223344, "1122334455667788"}, {@res8=0x2, @i8=0x55, "cc"}]})

转换为

1
foo$any_in(&(0x7f0000000000)=ANY=[@ANYBLOB="1100000044332211223300000000000088776655443322117d00bc11", @ANYRES32=0x0, @ANYBLOB="0000000044aabb00", @ANYRES64=0x1, @ANYBLOB="443322111122334455667788", @ANYRES8=0x2, @ANYBLOB="0000000000000055cc0000"])`

翻译是模糊测试过程的一部分自动进行的。

在运行模糊测试器数周后,它停止了产生新的覆盖率。我们没有手动编写遵循语法并到达新路径的输入,而是使用了ANYBLOB,这使我们能够轻松生成它们。

ANYBLOB表示为BufferType数据类型,我们使用从此处和此处获取的公开pcap文件来生成新的语料库。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json
import os

# tshark -r smb2_dac_sample.pcap -Y "smb || smb2" -T json -e tcp.payload > packets.json

os.makedirs("corpus", exist_ok=True)

def load_packets(json_file):
    with open(json_file, 'r') as file:
        data = json.load(file)
    
    packets = [entry["_source"]["layers"]["tcp.payload"] for entry in data]
    
    return packets

if __name__ == "__main__":
    json_file = "packets.json"
    packets = load_packets(json_file)
    
    for i, packet in enumerate(packets):
        pdu_size = len(packet[0])
        filename = f"corpus/packet_{i:03d}.txt"
        with open(filename, "w") as f:
            f.write(f"syz_ksmbd_send_req(&(0x7f0000000340)=ANY=[@ANYBLOB=\"{packet[0]}\"], {hex(pdu_size)}, 0x0, 0x0)")

之后,我们使用 syz-db 将所有候选测试用例打包到语料库数据库中,并恢复了模糊测试。

这样,我们立即触发了 ksmbd: fix use-after-free in ksmbd_sessions_deregister(),并将整体覆盖率提高了几个百分点。

超越KASAN的Sanitizer覆盖率

除了KASAN,我们还尝试了其他Sanitizer,如KUBSAN和KCSAN。没有显著改进:KCSAN产生了许多误报,或者报告了无关组件中的漏洞,似乎没有安全影响。有趣的是,KUBSAN能够识别出一个KASAN未检测到的问题:

1
id = le32_to_cpu(psid->sub_auth[psid->num_subauth - 1]);

在这种情况下,用户能够将 psid->num_subauth 设置为0,这导致了错误的读取 psid->sub_auth[-1]。尽管这次访问仍在同一个结构分配(smb_sid)内,但UBSAN的数组索引边界检查考虑了数组的声明边界:

1
2
3
4
5
6
struct smb_sid {
	__u8 revision; /* revision level */
	__u8 num_subauth;
	__u8 authority[NUM_AUTHS];
	__le32 sub_auth[SID_MAX_SUB_AUTHORITIES]; /* sub_auth[num_subauth] */
} __attribute__((packed));

因此能够捕获到这个漏洞。

覆盖率

一个未解决的问题是多进程模糊测试。由于各种锁定机制,并且因为我们重用了相同的身份验证状态,我们注意到当只使用一个进程时,模糊测试更加稳定,覆盖率增长更快。我们在单个调用内发送了多个请求,但最初担心这会让我们错过竞态条件。

如果检查执行日志,我们会看到syzkaller在一个进程内创建了多个线程,就像调用标准系统调用时一样:

1
2
3
4
5
6
1.887619984s ago: executing program 0 (id=1628):
syz_ksmbd_send_req(&(0x7f0000000d40)={0xee, @smb2_read_req={{}, 0x8, {0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, "fbac8eef056a860726ca964fb4f60999"}, 0x31, 0x6, 0x2, 0x7e, 0x70, 0x4, 0x0, 0xffffffff, 0x2, 0x7, 0xee, 0x0, "1cad48fb0cba2f253915fe074290eb3e10ed9ac895dde2a575e4caabc1f3a537e265fea8a440acfd66cf5e249b1ccaae941160f24282c81c9df0260d0403bb44b0461da80509bd756c155b191718caa5eabd4bd89aa9bed58bf87d42ef49bca4c9f08f22d495b601c9c025631b815bf6cbeb0aa4785aec4abf776d75e5be"}}, 0xf2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
syz_ksmbd_send_req(&(0x7f0000000900)=ANY=[@ANYRES16=<r0=>0x0], 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0) (async, rerun: 32)
syz_ksmbd_send_req(&(0x7f0000001440)=ANY=[@ANYBLOB="000008c0fe534d4240000000000000000b0001000000000000000000030000000000000000000000010000000100000000000000684155244ffb955e3201e88679ed735a39000000040214000400000000000000000000000000000078000000480800000000010000000000000000000000010001"], 0x8c4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0) (async, rerun: 32)
syz_ksmbd_send_req(&(0x7f0000000200)={0x58, @smb2_oplock_break_req={{}, 0x12, {0x1, 0x0, 0x0, 0x9, 0x0, 0x1, 0x1, "3c66dd1fe856ec397e7f8d7c8c293fd6"}, 0x24}}, 0x5c, &(0x7f0000000000)=ANY=[@ANYBLOB="00000080fe534d424000010000000000050001000800000000000000040000000000000000000000010000000100000000000000b31fae29f7ea148ad156304f457214a539000000020000000000000000000000000000000000000000000002"], 0x84, &(0x7f0000000100)=ANY=[@ANYBLOB="00000062fe534d4240000000000000000e00010000000000000000000700000000000000000000000100000001000000000000000002000000ffff0000000000000000002100030a08000000040000000000000000000000000000006000020009000000aedf"], 0x66, 0x0, 0x0) (async)
...

注意模糊测试过程中自动添加的 async 关键字,它允许在不阻塞的情况下并行运行命令,在此提交 fd8caa5 中实现。因此,没有UAF因为看似缺乏并行性而被遗漏。

最终,基于syzkaller的基准测试,我们在20个虚拟机中每秒执行20-30个进程,这仍然可能意味着运行数百条命令。作为参考,我们使用了一台平均配置的服务器 - 没有专门针对模糊测试性能进行优化。

我们使用syzkaller内置的函数级指标来衡量覆盖率。虽然我们知道这没有捕获对像SMB这样的协议至关重要的状态转换,但它仍然提供了对已执行代码的有用近似。总体而言,fs/smb/server 目录达到了约60%的覆盖率。对于处理大多数SMB命令解析和分发的 smb2pdu.c 来说,我们达到了70%。

下面的截图显示了关键文件的覆盖率。 (截图描述:一张覆盖率统计图表,显示了fs/smb/server目录下各个源文件的代码覆盖率百分比,其中smb2pdu.c覆盖率较高。)

发现的漏洞

在我们的研究期间,我们总共报告了 23个漏洞。大多数漏洞是释放后使用或越界读写发现。鉴于这个数量,其影响自然各不相同。例如,fix the warning from __kernel_write_iter 只是一个简单的警告,仅在特定设置 (kernel.panic_on_warn) 下可用于DoS;validate zero num_subauth before sub_auth is accessed 是一个简单的越界1字节读取;而 prevent rename with empty string 只会导致内核异常。

还有一些问题需要更仔细的分析才能确定可利用性(例如,fix type confusion via race condition when using ipc_msg_send_request)。然而,在评估了有潜力的候选漏洞后,我们能够识别出一些强大的原语,允许攻击者至少利用该发现本地获取远程代码执行。

已识别的问题列表报告如下:

描述 提交 CVE
通过验证 *pos 防止越界流写入 0ca6df4 CVE-2025-37947
防止使用空字符串重命名 53e3e5b CVE-2025-37956
修复 ksmbd_session_rpc_open 中的释放后使用 a1f46c9 CVE-2025-37926
修复来自 __kernel_write_iter 的警告 b37f2f3 CVE-2025-37775
修复 smb_break_all_levII_oplock() 中的释放后使用 18b4fac CVE-2025-37776
修复 __smb2_lease_break_noti() 中的释放后使用 21a4e47 CVE-2025-37777
在访问 sub_auth 前验证 num_subauth 是否为零 bf21e29 CVE-2025-22038
修复 dacloffset 边界检查中的溢出 beff0bc CVE-2025-22039
修复 ksmbd_sessions_deregister() 中的释放后使用 15a9605 CVE-2025-22041
修复 r_count 递减/递增不匹配 ddb7ea3 CVE-2025-22074
为创建租约上下文添加边界检查 bab703e CVE-2025-22042
为持久句柄上下文添加边界检查 542027e CVE-2025-22043
防止在机会锁中断通知期间释放连接 3aa660c CVE-2025-21955
修复 ksmbd_free_work_struct 中的释放后使用 bb39ed4 CVE-2025-21967
修复 smb2_lock 中的释放后使用 84d2d16 CVE-2025-21945
修复 smb2_lock 中关于trap的bug e26e2d2 CVE-2025-21944
修复 parse_sec_desc() 中的越界访问 d6e13e1 CVE-2025-21946
修复使用 ipc_msg_send_request 时通过竞态条件导致的类型混淆 e2ff19f CVE-2025-21947
对齐 aux_payload_buf 以避免加密操作中的OOB读取 06a0254 -
检查未完成的并发SMB操作 0a77d94 CVE-2024-50285
修复 smb3_preauth_hash_rsp 中的slab释放后使用 b8fc56f CVE-2024-50283
修复 ksmbd_smb2_session_create 中的slab释放后使用 c119f4e CVE-2024-50286
修复 smb2_allocate_rsp_buf 中的slab越界访问 0a77715 CVE-2024-26980

请注意,我们意识到自2024年2月Linux内核成为CNA以来围绕CVE分配的争议。我个人认为,虽然存在许多有争议的情况,但当前的方法是务实的:现在为具有潜在安全影响的修复分配CVE,特别是内存破坏和其他可能被利用的漏洞类别。

有关更多信息,整个过程在这个精彩的演示文稿或相关文章中有详细描述。最后,CVE审批的投票过程在 vulns.git 仓库中实现。

结论

我们的研究产生了数十个漏洞,尽管通常不鼓励使用伪系统调用,并且它有几个缺点。例如,在所有情况下,我们都必须通过查找相关的崩溃日志条目、生成C程序并手动最小化它们来执行分类过程。

由于系统调用可以使用资源进行绑定,这种方法也可以应用于ksmbd,它涉及发送数据包。对于未来的研究,探索这个方向是理想的 - SMB命令可以产生资源,然后这些资源被输入到不同的命令中。由于时间限制,我们遵循了伪系统调用的方法,依赖于自定义补丁。

在下一个也是最后一个部分,我们将专注于利用 CVE-2025-37947

参考资料

其他相关文章

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