引言
这是对先前发布文章的后续研究。我们最初的研究发现了一些无需认证的漏洞,但仅触及了攻击表面的皮毛。即使在修补代码绕过认证后,大多数有趣操作仍需与最初忽略的处理程序和状态进行交互。在本部分中,我们解释如何增加覆盖率并应用不同的模糊测试策略来识别更多漏洞。
配置依赖的攻击面
以下功能有助于扩大攻击面。默认情况下仅启用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_connect、smb2_sess_setup或smb2_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命令实现语法。Microsoft作为其开放规范计划的一部分,发布了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
// .. 省略..
}
|
在commit 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关键字,它允许在不阻塞的情况下并行运行命令,在此commit 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只会导致内核异常。
还有其他问题,可利用性需要更周到的分析(例如,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 |
请注意,我们知道关于CVE分配的争议,因为Linux内核在2024年2月成为CNA。我个人的看法是,虽然有许多有争议的情况,但当前的方法是务实的:现在为具有潜在安全影响的修复分配CVE,特别是内存损坏和其他可能被利用的漏洞类。
有关更多信息,整个过程在此精彩演示或相关文章中有详细描述。最后,CVE批准投票过程在vulns.git存储库中实现。
结论
我们的研究产生了数十个漏洞,尽管通常不鼓励使用伪系统调用并且有几个缺点。例如,在所有情况下,我们必须通过查找相关崩溃日志条目、生成C程序并手动最小化它们来手动执行分类过程。
由于系统调用可以使用资源绑定,这种方法也可以应用于涉及发送数据包的ksmbd。未来的研究探索这个方向将是理想的 - SMB命令可以产生资源,然后将其馈送到不同的命令中。由于时间限制,我们遵循了伪系统调用方法,依赖于自定义补丁。
对于下一个也是最后一个部分,我们专注于利用CVE-2025-37947。
参考文献