ksmbd模糊测试改进与漏洞挖掘深度解析

引言

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

配置相关的攻击面

某些功能需要额外的配置选项。我们尝试启用许多可用功能以最大化暴露的攻击面。这帮助我们触发了在最小配置示例中被禁用的代码路径。但为简化设置,我们未考虑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后,信用限制变得更加严格,因此我们也修补了该限制以避免速率限制。

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

状态管理

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

资源解析的示例代码如下:

// 处理响应。不包含+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命令实现语法。Microsoft作为其开放规范计划的一部分,发布了SMB和其他协议的详细技术文档。

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

[此处应有SMB2 IOCTL请求的线格式图]

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

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

"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重写后,它看起来像这样:

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中定义,可以表示多个结构:

type anyTypes struct {
  union  *UnionType
  array  *ArrayType
  blob   *BufferType
    // .. 截断..
}

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

阅读测试用例更能说明其工作原理,其中:

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"}]})

翻译为

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

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

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

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未检测到的额外问题:

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

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

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.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关键字,它允许在不阻塞的情况下并行运行命令,在此commit fd8caa5中实现。因此,没有UAF由于看似缺乏并行性而被遗漏。

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

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

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

[此处应有覆盖率截图]

发现的漏洞

在我们的研究期间,我们总共报告了23个漏洞。大多数漏洞是use-after-free或越界读写发现。考虑到这个数量,影响自然不同。例如,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)。然而,在评估有希望的候选后,我们能够识别一些强大的原语,允许攻击者至少本地利用该发现以获得远程代码执行。

识别的问题列表在此报告:

描述 Commit CVE
通过验证*pos防止越界流写入 0ca6df4 CVE-2025-37947
防止使用空字符串重命名 53e3e5b CVE-2025-37956
修复ksmbd_session_rpc_open中的use-after-free a1f46c9 CVE-2025-37926
修复__kernel_write_iter的警告 b37f2f3 CVE-2025-37775
修复smb_break_all_levII_oplock()中的use-after-free 18b4fac CVE-2025-37776
修复__smb2_lease_break_noti()中的use-after-free 21a4e47 CVE-2025-37777
在访问sub_auth之前验证零num_subauth bf21e29 CVE-2025-22038
修复dacloffset边界检查中的溢出 beff0bc CVE-2025-22039
修复ksmbd_sessions_deregister()中的use-after-free 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中的use-after-free bb39ed4 CVE-2025-21967
修复smb2_lock中的use-after-free 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-use-after-free b8fc56f CVE-2024-50283
修复ksmbd_smb2_session_create中的slab-use-after-free c119f4e CVE-2024-50286
修复smb2_allocate_rsp_buf中的slab-out-of-bounds 0a77715 CVE-2024-26980

请注意,我们了解围绕CVE分配的争议,因为Linux内核在2024年2月成为CVE编号机构(CNA)。我个人的看法是,虽然有许多有争议的情况,但当前的方法是务实的:现在为具有潜在安全影响的修复分配CVE,特别是内存损坏和其他可能被利用的漏洞类。

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

结论

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

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

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

参考文献

  • pwning tech - Tickling ksmbd: fuzzing SMB in the Linux kernel
  • https://github.com/google/syzkaller
  • Dongliang Mu - Some explanation of main syzkaller logic, execprog, syz-repro

其他相关文章:

  • ksmbd - Exploiting CVE-2025-37947 (3/3) - 08 Oct 2025
  • !exploitable Episode Two - Enter the Matrix - 27 Feb 2025
  • ksmbd vulnerability research - 07 Jan 2025
  • Introduction to VirtualBox security research - 26 Apr 2022
  • Fuzzing JavaScript Engines with Fuzzilli - 09 Sep 2020
  • Fuzzing TLS certificates from their ASN.1 grammar - 14 May 2020
  • Staring into the Spotlight - 15 Nov 2017
    更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
    对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

公众号二维码

公众号二维码

posted @ 2025-10-20 17:23  qife  阅读(6)  评论(0)    收藏  举报