ksmbd模糊测试改进与漏洞发现深度解析

本文深入探讨了针对Linux内核SMB服务器ksmbd的模糊测试改进策略,包括状态管理、协议规范实现、多种模糊测试方法的应用,并详细介绍了由此发现的二十多个安全漏洞及其对应CVE编号。

简介

这是对先前发表文章的后续。我们最初的研究发现了几个无需认证的漏洞,但这只是浅尝辄止地触及了攻击面。即使在修补代码以绕过认证之后,大多数有趣的操作仍需与最初被忽略的处理程序和状态进行交互。在本文中,我们将解释如何增加覆盖范围并应用不同的模糊测试策略来发现更多漏洞。

配置相关的攻击面

某些功能需要额外的配置选项。我们尝试启用许多可用功能以最大化暴露的攻击面。这帮助我们触发了在最小化配置示例中被禁用的代码路径。然而,为了简化设置,我们未考虑诸如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以便在凭证无效时回退到“来宾”身份。在报告CVE-2024-50285: ksmbd: check outstanding simultaneous SMB operations之后,信用额度限制变得更加严格,因此我们也将其修补掉以避免速率限制。

当我们用更大的语料库重新启动syzkaller时,几分钟后,所有剩余的候选输入都被拒绝。经过一番调查,我们发现这是由于默认的max connections = 128,我们不得不将其增加到最大值65536。其他限制未作更改。

状态管理

SMB交互是有状态的,依赖于会话、TreeID和FileID。模糊测试需要模拟有效的状态转换,例如smb2_createsmb2_ioctlsmb2_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请求的线格式如下所示:

然后,我们手动将此规范重写为我们的语法,使我们的测试工具能够自动构建有效的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的消毒剂覆盖

除了KASAN,我们还尝试了其他消毒剂,如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个VM中每秒执行20-30个进程,这仍然可能意味着运行数百条命令。作为参考,我们使用了一台平均配置的服务器 - 没有特别针对模糊测试性能进行优化。

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

下图显示了关键文件的覆盖率。

发现的漏洞

在我们的研究期间,我们总共报告了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只会导致内核oops。

还有一些问题,其可利用性需要更仔细的分析(例如,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中的陷阱错误 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内核成为CVE编号机构(CNA)以来围绕CVE分配的争议。我个人认为,虽然存在许多有争议的情况,但当前的方法是务实的:现在为具有潜在安全影响的修复分配CVE,特别是内存损坏和其他可能被利用的漏洞类别。

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

结论

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

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

在下一篇也是最后一篇文章中,我们将重点讨论CVE-2025-37947的利用。

参考资料

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