ksmbd 条件竞争漏洞挖掘:思路与案例
ksmbd 条件竞争漏洞挖掘:思路与案例
本文介绍从代码审计的角度分析、挖掘条件竞争、UAF 漏洞思路,并以 ksmbd 为实例介绍审计的过程和几个经典漏洞案例。
分析代码版本为:linux-6.5.5
相关漏洞在一年前已修复完毕.
掌握背景:Linux 内核条件竞争 UAF 常见场景
首先我们看一下 Linux 内核下 UAF 漏洞产生的几种常见情况,UAF 的核心原理是 内存被释放了,程序仍让能使用这块内存,导致该现象的常见场景:
- 指针在程序中被拷贝(比如存放到不同的对象中、放到链表中),其中一个指针释放后另一个指针没有被清理
- 程序中并发访问导致内存对象还在使用时被其他线程释放.
对于 Linux 内核/驱动而言,目前最常见的 UAF 是由于条件竞争导致的,即一个线程在使用某块内存时其他线程将其释放了。
那么 Linux 内核为什么会有条件竞争问题呢,其根本原因如下:
- Linux 以进程为调度主体,不同的进程可能会同时运行
- 不同的进程实体,通过系统调用进入内核后共用同一个内核里面的资源,比如物理内存、内核堆内存等
下图表示一个多核系统上两个进程同时在不同的 CPU 核运行,访问内核中的共享变量,实际上由于调度中断单核情况下也存在并发场景
并发执行+共享对象 是条件竞争的根因,因此驱动在开发设计时要考虑到并发场景,利用锁、引用计数等机制让资源在并发访问时不出问题.
常见的并发场景:
并发场景 | 解释 |
---|---|
用户进程之间 | 多线程并发系统调用,比如IOCTL、MMAP、READ、WRITE 等 |
用户进程与内核线程之间 | 内核线程中访问的共享对象,可能被用户态进程修改、释放 |
内核线程之间 | 不同内核线程之间使用共享对象 |
以 IOCTL 为例用户态进程通过 SVC 指令进入内核,首先进入 ioctl 系统调用入口,然后在 vfs_ioctl
里面会调用 f_op->unlocked_ioctl 注册的函数指针,进入每个文件/驱动自己实现的回调中。
由于多核可以并发 SVC 同时执行系统调用,且从系统调用入口到驱动回调中没有锁保护,所以在各个驱动接口中需要自己实现锁保护并发资源访问。
同理以 mmap 回调为例,SVC 进入内核后会进入 vm_mmap_pgoff 里面会获取当前进程 mm 对象的写锁,然后才通过 do_mmap 执行驱动对应的 mmap 回调
分析系统调用 ---> 驱动回调的调用路径之间锁的使用情况可知:
- 同一个进程的不同线程共享 mm 对象(即使 mm 指针一样)所以线程之间无法并发 mmap,但是 mmap 和其他回调比如 ioctl 是可以互相并发的.
- fork 出来的子进程或者通过 IPC 将 fd 共享给其他进程情况下,可以通过多进程并发同时进入驱动的 mmap 回调中.
挖掘条件竞争漏洞,首先就需要通过分析内核代码,清楚了解目标函数执行上下文中是否已经有锁保护,有哪些锁保护,然后以并发执行的视角分析并发场景下的各个代码时序,判断是否存在 UAF。
分析目标:梳理目标软件脉络
分析 ksmbd 的背景是去年偶然间看到一篇介绍通过 syzkaller fuzz ksmbd 协议的文章:Tickling ksmbd: fuzzing SMB in the Linux kernel ,其核心原理是新增一个伪系统调用 syz_ksmbd_send_req 用来将 syzkaller 生成的数据喂给内核的协议中解析
#define KSMBD_BUF_SIZE 16000
static long syz_ksmbd_send_req(volatile long a0, volatile long a1, volatile long a2, volatile long a3)
{
int sockfd;
int packet_reqlen;
int errno;
struct sockaddr_in serv_addr;
char packet_req[KSMBD_BUF_SIZE]; // max frame size
debug("[*]{syz_ksmbd_send_req} entered ksmbd send...\n");
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
memset(&serv_addr, '\0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(445);
errno = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// prepend kcov handle to packet
packet_reqlen = a1 + 8 > KSMBD_BUF_SIZE ? KSMBD_BUF_SIZE - 8 : a1;
*(unsigned long*)packet_req = procid + 1;
memcpy(packet_req + 8, (char*)a0, packet_reqlen);
if (write(sockfd, (char*)packet_req, packet_reqlen + 8) < 0)
return -4;
if (read(sockfd, (char*)a2, a3) < 0)
return -5;
if (close(sockfd) < 0)
return -6;
debug("[+]{syz_ksmbd_send_req} successfully returned\n");
return 0;
}
通过分析这篇文章可以得到一些信息:
- 定位 ksmbd 处理数据包的入口,即 ksmbd_conn_handler_loop
- 还存在一些简单的内存越界漏洞,代表可能还会存在一些较复杂、或者隐藏较深的漏洞
基于这些信息开始对 ksmbd 的源码进行分析,分析思路:
- 从入口出追踪客户端发送的网络数据流,梳理出请求处理过程中的数据流过程、校验逻辑.
- 分析数据解析过程,尝试挖掘内存越界、溢出等漏洞
- 分析请求处理时的一些上下文约束,比如是否可并发、是否有锁,对象生命周期管理,尝试挖掘条件竞争、UAF漏洞
从 ksmbd_conn_handler_loop 往上追踪,这是在 ksmbd_tcp_new_connection 创建的内核线程回调函数
static int ksmbd_tcp_new_connection(struct socket *client_sk)
{
t = alloc_transport(client_sk);
csin = KSMBD_TCP_PEER_SOCKADDR(KSMBD_TRANS(t)->conn);
KSMBD_TRANS(t)->handler = kthread_run(ksmbd_conn_handler_loop,
KSMBD_TRANS(t)->conn,
"ksmbd:%u",
ksmbd_tcp_get_port(csin));
其调用路径:
-
kthread = kthread_run(ksmbd_kthread_fn, (void *)iface, "ksmbd-%s",ksmbd_kthread_fn
- kernel_accept(iface->ksmbd_socket, &client_sk,
- ksmbd_tcp_new_connection(client_sk);
因此可以知道每当有一个客户端连接 445 端口时,ksmbd_kthread_fn 就会通过 ksmbd_tcp_new_connection 创建一个内核线程,然后 ksmbd_conn_handler_loop 里面处理每个 socket 请求的业务.
在 ksmbd_tcp_new_connection --> alloc_transport 会为每一个连接创建两个关键的对象(ksmbd_transport
和 ksmbd_conn
),用于管理 tcp 连接下的各种协议状态
两个对象互相保存对方的指针,方便从一个对象中拿到另一个对象进行操作,对象的大概作用:
- ksmbd_transport:负责链路数据的收发,比如从网络连接中读取数据
- ksmbd_conn:管理整个 smb 连接的状态,比如登录、文件操作,会话密钥等,每个 TCP 连接对应一个 conn 对象
其中 ksmbd_conn 是非常核心的对象,在 SMB 请求处理的各个环节都能看到, ksmbd_conn_handler_loop 的大概处理流程如下
-
ksmbd_conn_handler_loop --> 每个连接都会创建一个内核线程执行该函数
-
while (ksmbd_conn_alive(conn))
-
char hdr_buf[4] = {0,};
-
size = t->ops->read(t, hdr_buf, sizeof(hdr_buf), -1);
-
pdu_size = get_rfc1002_len(hdr_buf);
- return be32_to_cpu(*((__be32 *)buf)) & 0xffffff;
-
conn->request_buf = kvmalloc(size, GFP_KERNEL);
-
memcpy(conn->request_buf, hdr_buf, sizeof(hdr_buf)); // 设置 rfc1002_len 到新的 req_buf
-
size = t->ops->read(t, conn->request_buf + 4, pdu_size, 2);
-
default_conn_ops.process_fn --> ksmbd_server_process_request --> queue_ksmbd_work
-
work = ksmbd_alloc_work_struct(); // conn 的每个请求都启动一个 work 处理?
-
work->request_buf = conn->request_buf;
-
INIT_WORK(&work->work, handle_ksmbd_work);
-
ksmbd_queue_work(work); // 调度 work 执行
- queue_work(ksmbd_wq, &work->work);
-
-
-
conn 下每收到一个请求都会新建一个 work,然后把 work 放到 ksmbd_wq, workqueue 会动态分配到不同 worker 执行。这边在介绍一下内核的 workqueue 机制,workqueue 和 work 的关系如下:
核心概念是:work 先注册到 workqueue ,然后具体由 worker 执行,在代码中每个 worker 对应一个 work_thread 内核线程,一个 workqueue 里面会存在多个 worker,这些 worker 之间并发执行.
因此同一时刻可能会有 handle_ksmbd_work 实例访问同一个 conn 对象,这样就有了 RACE 的可能.
继续分析 handle_ksmbd_work 的大体逻辑:
-
handle_ksmbd_work
-
__handle_ksmbd_work
-
conn->ops->allocate_rsp_buf(work)
-
conn->ops->is_transform_hdr 校验 trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM;
-
is_transform_hdr 满足条件 走 smb3_decrypt_req
-
rc = conn->ops->init_rsp_hdr(work);
-
conn->ops->check_user_session -->v2 setup 和 v1
SMB_COM_NEGOTIATE_EX
请求可以通过 -
__process_request
- ksmbd_verify_smb_message
- cmds = &conn->cmds[command];
- ret = cmds->proc(work) ;
-
-
handle_ksmbd_work 主要逻辑就是解析 work->request_buf 中的网络报文数据,根据里面的请求类型、参数调用对应的请求处理函数进行处理(conn->cmds
),同时可以看到 cmds->proc 执行时上下文是没有锁的,因此如果 cmd 里面如果有访问到共享变量就需要自行加锁避免并发.
cmds 中可用的回调函数如下
static struct smb_version_cmds smb2_0_server_cmds[NUMBER_OF_SMB2_COMMANDS] = {
[SMB2_NEGOTIATE_HE] = { .proc = smb2_negotiate_request, },
[SMB2_SESSION_SETUP_HE] = { .proc = smb2_sess_setup, },
[SMB2_TREE_CONNECT_HE] = { .proc = smb2_tree_connect,},
[SMB2_TREE_DISCONNECT_HE] = { .proc = smb2_tree_disconnect,},
[SMB2_LOGOFF_HE] = { .proc = smb2_session_logoff,},
[SMB2_CREATE_HE] = { .proc = smb2_open},
[SMB2_QUERY_INFO_HE] = { .proc = smb2_query_info},
[SMB2_QUERY_DIRECTORY_HE] = { .proc = smb2_query_dir},
[SMB2_CLOSE_HE] = { .proc = smb2_close},
[SMB2_ECHO_HE] = { .proc = smb2_echo},
[SMB2_SET_INFO_HE] = { .proc = smb2_set_info},
[SMB2_READ_HE] = { .proc = smb2_read},
[SMB2_WRITE_HE] = { .proc = smb2_write},
[SMB2_FLUSH_HE] = { .proc = smb2_flush},
[SMB2_CANCEL_HE] = { .proc = smb2_cancel},
[SMB2_LOCK_HE] = { .proc = smb2_lock},
[SMB2_IOCTL_HE] = { .proc = smb2_ioctl},
[SMB2_OPLOCK_BREAK_HE] = { .proc = smb2_oplock_break},
[SMB2_CHANGE_NOTIFY_HE] = { .proc = smb2_notify},
};
这些回调函数就会根据请求和 conn 对象实现 smb 协议的业务逻辑,之后就可以对这些回调函数进行审计,这里再次回顾一下这些回调函数执行的上下文状态:
- 回调函数会在不同的 worker 线程中被调用,存在并发性
- 同一个连接的不同请求可能并发处理,处理时会访问同一个 conn 对象
经过分析 ksmbd 中的共享变量、对象也主要是集中在 conn 对象中(类似于 file_operation 回调的共享 filp 对象),因此在分析条件竞争漏洞时可重点关注对 conn 对象的访问、操作。
实例分析:加深理解
本节以一些真实案例介绍条件竞争漏洞的挖掘、分析经验
smb2_open 条件竞争 UAF
smb2_open 的命令字为 SMB2_CREATE_HE,其用途对标的是 Linux 用户态的 open 函数,用于打开远程 smb 服务器上的一个文件。
[SMB2_CREATE_HE] = { .proc = smb2_open},
函数的代码很长,大致逻辑是首先从数据包中提取出要打开的文件名和打开的模式,对文件名校验后通过内核 vfs 子系统的 API 打开共享目录下的文件。
对该函数进行审计的思路是:
- 常规数据解析类漏洞,比如堆栈溢出等
- 文件名校验逻辑是否有误,导致目录穿越
- 对象管理是否有误,导致 UAF
对数据解析和文件名校验、打开逻辑进行分析没有发现问题,分析其对象管理时,发现 smb2_open 打开文件后会分配 struct ksmbd_file 对象管理打开文件对应的 struct file 对象,ksmbd_file 和 ksmbd_cnn 的关系如下图所示:
- ksmbd_conn 对象里面的 sessions 数组中保存了当前连接的会话对象(ksmbd_session)
- ksmbd_session 对象的 file_table 保存了打开的所有文件对象(ksmbd_file)
- ksmbd_file 对象的 filp 指向了真正打开的 VFS 文件对象(struct file)
下面看一下 ksmbd_file 对象的创建和初始化过程,相关代码如下:
-
smb2_open
-
struct ksmbd_file *fp = ksmbd_open_fd(work, filp);
- fp = kmem_cache_zalloc(filp_cache, GFP_KERNEL);
- atomic_set(&fp->refcount, 1);
- ret = __open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID); // --> 把 fp 插入到 sess->file_table
-
<< RACE 时间窗 >>
-
ksmbd_open_durable_fd(fp);
-
fp->cdoption = req->CreateDisposition;
-
首先调用 ksmbd_open_fd 分配 fp,其中会调用 kmem_cache_zalloc 分配 ksmbd_file 对象,然后通过 __open_id 将 fp 存放到 ksmbd_session 的 file_table 里面(work->sess->file_table), work->sess 是进入回调函数前从 conn 对象中获取的
struct ksmbd_session *ksmbd_session_lookup(struct ksmbd_conn *conn,
unsigned long long id)
{
struct ksmbd_session *sess;
sess = xa_load(&conn->sessions, id);
if (sess)
sess->last_active = jiffies;
return sess;
}
work->sess = ksmbd_session_lookup_all(conn, sess_id);
ksmbd_open_fd 返回后设置 fp 对象的其他字段,注意到此时 fp 已经被放入了 sess->file_table ,此时其他线程也可以同时获取该对象,而且此时 fp 的引用计数为 1。
下面可以看一下 smb_close 的实现,其核心逻辑位于 ksmbd_close_fd
int ksmbd_close_fd(struct ksmbd_work *work, u64 id)
{
struct ksmbd_file *fp;
struct ksmbd_file_table *ft;
ft = &work->sess->file_table;
read_lock(&ft->lock);
fp = idr_find(ft->idr, id);
if (fp) {
set_close_state_blocked_works(fp);
if (!atomic_dec_and_test(&fp->refcount))
fp = NULL;
}
read_unlock(&ft->lock);
__put_fd_final(work, fp);
return 0;
}
当 fp->refcount 减一后为 0 时会进入 __put_fd_final 释放 fp 的内存,因此可以在线程 A 执行 smb2_open 时,其他线程通过 smb2_close 释放 fp 就能导致 UAF。
RACE 场景下的时序关系如下图:
触发后的 kasan 日志如下
[ 224.236369] BUG: KASAN: slab-use-after-free in __open_id+0xfc/0x160 [ksmbd]
[ 224.236457] Write of size 8 at addr ffff8881bf504788 by task kworker/6:1/90
[ 224.236469] CPU: 6 PID: 90 Comm: kworker/6:1 Tainted: G OE 6.5.4 #1
[ 224.236478] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/22/2020
[ 224.236485] Workqueue: ksmbd-io handle_ksmbd_work [ksmbd]
[ 224.236572] Call Trace:
[ 224.236577] <TASK>
[ 224.236583] dump_stack_lvl+0x48/0x70
[ 224.236595] print_report+0xd2/0x660
[ 224.236605] ? __virt_addr_valid+0x103/0x180
[ 224.236617] ? kasan_complete_mode_report_info+0x8a/0x230
[ 224.236639] ? __open_id+0xfc/0x160 [ksmbd]
[ 224.236721] kasan_report+0xd0/0x120
[ 224.236731] ? __open_id+0xfc/0x160 [ksmbd]
[ 224.236816] __asan_store8+0x8e/0xe0
[ 224.236825] __open_id+0xfc/0x160 [ksmbd]
[ 224.236908] ksmbd_open_durable_fd+0x21/0x40 [ksmbd]
[ 224.236991] smb2_open+0x1276/0x3d00 [ksmbd]
[ 224.237083] ? __pfx_smb2_open+0x10/0x10 [ksmbd]
[ 224.237167] ? ksmbd_release_crypto_ctx+0xd1/0x100 [ksmbd]
[ 224.237281] ? ksmbd_crypt_message+0x48d/0xc70 [ksmbd]
[ 224.237368] ? __pfx_ksmbd_crypt_message+0x10/0x10 [ksmbd]
[ 224.237463] ? xas_descend+0x82/0x130
[ 224.237473] ? xas_descend+0x82/0x130
[ 224.237481] ? xas_start+0x8a/0x1d0
[ 224.237490] ? __rcu_read_unlock+0x51/0x80
[ 224.237507] ? ksmbd_smb2_check_message+0xa56/0xc90 [ksmbd]
[ 224.237595] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 224.237684] process_one_work+0x4d3/0x840
[ 224.237700] worker_thread+0x91/0x6e0
[ 224.237715] ? __pfx_worker_thread+0x10/0x10
[ 224.237726] kthread+0x188/0x1d0
[ 224.237735] ? __pfx_kthread+0x10/0x10
[ 224.237744] ret_from_fork+0x44/0x80
[ 224.237754] ? __pfx_kthread+0x10/0x10
[ 224.237763] ret_from_fork_asm+0x1b/0x30
[ 224.237777] </TASK>
[ 224.237785] Allocated by task 90:
[ 224.237790] kasan_save_stack+0x38/0x70
[ 224.237800] kasan_set_track+0x25/0x40
[ 224.237809] kasan_save_alloc_info+0x1e/0x40
[ 224.237818] __kasan_slab_alloc+0x9d/0xa0
[ 224.237824] kmem_cache_alloc+0x17f/0x3c0
[ 224.237833] ksmbd_open_fd+0x2d/0x550 [ksmbd]
[ 224.237916] smb2_open+0x1200/0x3d00 [ksmbd]
[ 224.237999] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 224.238082] process_one_work+0x4d3/0x840
[ 224.238091] worker_thread+0x91/0x6e0
[ 224.238100] kthread+0x188/0x1d0
[ 224.238106] ret_from_fork+0x44/0x80
[ 224.238114] ret_from_fork_asm+0x1b/0x30
[ 224.238124] Freed by task 774:
[ 224.238128] kasan_save_stack+0x38/0x70
[ 224.238137] kasan_set_track+0x25/0x40
[ 224.238146] kasan_save_free_info+0x2b/0x60
[ 224.238155] ____kasan_slab_free+0x180/0x1f0
[ 224.238164] __kasan_slab_free+0x12/0x30
[ 224.238170] slab_free_freelist_hook+0xd2/0x1a0
[ 224.238178] kmem_cache_free+0x1b2/0x360
[ 224.238187] __ksmbd_close_fd+0x34a/0x490 [ksmbd]
[ 224.238280] ksmbd_close_fd+0xb0/0x110 [ksmbd]
[ 224.238362] smb2_close+0x2fc/0x690 [ksmbd]
[ 224.238445] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 224.238528] process_one_work+0x4d3/0x840
[ 224.238537] worker_thread+0x91/0x6e0
[ 224.238546] kthread+0x188/0x1d0
[ 224.238552] ret_from_fork+0x44/0x80
[ 224.238560] ret_from_fork_asm+0x1b/0x30
smb2_close 条件竞争 UAF
发现 smb2_open 的漏洞后,忽然想到如果并发 smb2_close 会出现什么样的效果.再次回顾下 smb2_close --> ksmbd_close_fd 的代码
int ksmbd_close_fd(struct ksmbd_work *work, u64 id)
{
struct ksmbd_file *fp;
struct ksmbd_file_table *ft;
ft = &work->sess->file_table;
read_lock(&ft->lock);
fp = idr_find(ft->idr, id);
if (fp) {
set_close_state_blocked_works(fp);
if (!atomic_dec_and_test(&fp->refcount))
fp = NULL;
}
read_unlock(&ft->lock);
__put_fd_final(work, fp);
return 0;
}
让我们以并发的思维人脑模拟执行一下上面的代码:
- 假设两个线程 A B 并发进入 ksmbd_close_fd,且此时 id 对应 fp 的引用计数为 1
- 由于持有的是 read_lock 读锁,所以两个线程可以同时拿到 fp 并进入 atomic_dec_and_test
- 由于 atomic_dec_and_test 的逻辑其中一个线程会进入 fp = NULL 分支,所以只有一个线程能正常释放 fp.
因此上述场景是无法产生 UAF 的,那假如线程 A 先进入 __put_fd_final ,然后线程 B 执行到 atomic_dec_and_test ,然后线程 A 在 __put_fd_final 里面释放 fp 是否可行呢?
由于 __put_fd_final 释放 fp 前会先获取 ft->lock 的写锁,将 fp 从 idr 中移除后才会去释放 fp,因此无法构造上面的场景,因为写锁的获取要等所有读锁释放后才能获取。
单纯并发 ksmbd_close_fd 不可行,那利用其他接口配合这个逻辑是否可以产生不一样的效果呢,经过思考和尝试,当其他线程在使用 fp 时,多个线程进入 close 就可以把 fp 提前释放。
以 smb2_read 为例,函数首先通过 ksmbd_lookup_fd_slow 获取 fp 并增加其引用计数,使用完成后会通过 ksmbd_fd_put 释放引用
fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId);
nbytes = ksmbd_vfs_read(work, fp, length, &offset);
ksmbd_fd_put(work, fp);
并发导致 UAF 的场景如下:
- 线程 A 持有 fp (比如通过 smb2_read),此时 fp->refcount = 2
- 5 个线程 B1 B2 .... B5,同时进入 ksmbd_close_fd 就会尝试最多减 5 次引用计数,导致 fp->refcount = 0,被释放
- 线程 A 后面使用 fp 时就是被释放的 fp
触发漏洞后的 kasan 日志如下
[ 115.537085] BUG: KASAN: slab-use-after-free in smb2_read+0x241/0x850 [ksmbd]
[ 115.537205] Read of size 4 at addr ffff8881ac7099cc by task kworker/6:2/76
[ 115.537218] CPU: 6 PID: 76 Comm: kworker/6:2 Tainted: G OE 6.5.4 #1
[ 115.537227] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/22/2020
[ 115.537235] Workqueue: ksmbd-io handle_ksmbd_work [ksmbd]
[ 115.537321] Call Trace:
[ 115.537327] <TASK>
[ 115.537333] dump_stack_lvl+0x48/0x70
[ 115.537349] print_report+0xd2/0x660
[ 115.537361] ? __virt_addr_valid+0x103/0x180
[ 115.537375] ? kasan_complete_mode_report_info+0x8a/0x230
[ 115.537387] ? smb2_read+0x241/0x850 [ksmbd]
[ 115.537472] kasan_report+0xd0/0x120
[ 115.537482] ? smb2_read+0x241/0x850 [ksmbd]
[ 115.537570] __asan_load4+0x8e/0xd0
[ 115.537579] smb2_read+0x241/0x850 [ksmbd]
[ 115.537680] ? __pfx_smb2_read+0x10/0x10 [ksmbd]
[ 115.537782] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 115.537870] process_one_work+0x4d3/0x840
[ 115.537889] worker_thread+0x91/0x6e0
[ 115.537904] ? __pfx_worker_thread+0x10/0x10
[ 115.537915] kthread+0x188/0x1d0
[ 115.537925] ? __pfx_kthread+0x10/0x10
[ 115.537934] ret_from_fork+0x44/0x80
[ 115.537946] ? __pfx_kthread+0x10/0x10
[ 115.537955] ret_from_fork_asm+0x1b/0x30
[ 115.537969] </TASK>
[ 115.537977] Allocated by task 76:
[ 115.537983] kasan_save_stack+0x38/0x70
[ 115.537994] kasan_set_track+0x25/0x40
[ 115.538003] kasan_save_alloc_info+0x1e/0x40
[ 115.538012] __kasan_slab_alloc+0x9d/0xa0
[ 115.538019] kmem_cache_alloc+0x17f/0x3c0
[ 115.538028] ksmbd_open_fd+0x2d/0x550 [ksmbd]
[ 115.538110] smb2_open+0x1200/0x3ca0 [ksmbd]
[ 115.538193] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 115.538298] process_one_work+0x4d3/0x840
[ 115.538307] worker_thread+0x91/0x6e0
[ 115.538316] kthread+0x188/0x1d0
[ 115.538323] ret_from_fork+0x44/0x80
[ 115.538331] ret_from_fork_asm+0x1b/0x30
[ 115.538341] Freed by task 1114:
[ 115.538346] kasan_save_stack+0x38/0x70
[ 115.538355] kasan_set_track+0x25/0x40
[ 115.538364] kasan_save_free_info+0x2b/0x60
[ 115.538373] ____kasan_slab_free+0x180/0x1f0
[ 115.538382] __kasan_slab_free+0x12/0x30
[ 115.538388] slab_free_freelist_hook+0xd2/0x1a0
[ 115.538396] kmem_cache_free+0x1b2/0x360
[ 115.538405] __ksmbd_close_fd+0x34a/0x490 [ksmbd]
[ 115.538487] ksmbd_close_fd+0xb0/0x110 [ksmbd]
[ 115.538569] smb2_close+0x2fc/0x690 [ksmbd]
[ 115.538652] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 115.538734] process_one_work+0x4d3/0x840
[ 115.538743] worker_thread+0x91/0x6e0
[ 115.538752] kthread+0x188/0x1d0
[ 115.538758] ret_from_fork+0x44/0x80
[ 115.538777] ret_from_fork_asm+0x1b/0x30
smb2_write 条件竞争 UAF
有了上面的经验,我开始关注代码中对对象的使用:
- 使用共享对象是是否持有引用计数
- 能否并发释放
于是在浏览代码时发现 smb2_write 上来就是使用了 work->tcon 对象
int smb2_write(struct ksmbd_work *work)
{
WORK_BUFFERS(work, req, rsp);
if (test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_PIPE)) {
ksmbd_debug(SMB, "IPC pipe write request\n");
return smb2_write_pipe(work);
}
追踪一下赋值点
-
__handle_ksmbd_work
-
rc = conn->ops->get_ksmbd_tcon(work);
-
smb2_get_ksmbd_tcon
-
work->tcon = ksmbd_tree_conn_lookup(work->sess, tree_id);
- tcon = xa_load(&sess->tree_conns, id);
- return tcon;
-
-
-
int smb2_get_ksmbd_tcon(struct ksmbd_work *work)
{
struct smb2_hdr *req_hdr = ksmbd_req_buf_next(work);
unsigned int tree_id;
work->tcon = ksmbd_tree_conn_lookup(work->sess, tree_id);
return 1;
}
可以看到 work->tcon 没有持有 tcon 对象的引用计数,那么能够并发释放吗,其并发逻辑位于
-
smb2_tree_disconnect
-
struct ksmbd_tree_connect <span style="font-weight: bold;" data-type="strong">*tcon = work->tcon;
-
test_and_set_bit(TREE_CONN_EXPIRE, &tcon->status)
-
ksmbd_tree_conn_disconnect(sess, tcon);
- ksmbd_share_config_put(tree_conn->share_conf);
- kfree(tree_conn);
-
smb2_tree_disconnect 为 __handle_ksmbd_work 的 cmd 回调,其中也没有锁保护,因此可以并发释放 tcon.
因此在 smb2_write 执行时,多线程发起 smb2_tree_disconnect 请求就能在 smb2_write 使用 tcon 时将其释放,导致 UAF,Crash 的日志如下:
[ 5521.190232] BUG: KASAN: slab-use-after-free in smb2_write+0x16e/0x840 [ksmbd]
[ 5521.190327] Read of size 8 at addr ffff8881c5ef6708 by task kworker/6:0/1913
[ 5521.190341] CPU: 6 PID: 1913 Comm: kworker/6:0 Tainted: G OE 6.5.4 #1
[ 5521.190350] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/22/2020
[ 5521.190358] Workqueue: ksmbd-io handle_ksmbd_work [ksmbd]
[ 5521.190446] Call Trace:
[ 5521.190451] <TASK>
[ 5521.190457] dump_stack_lvl+0x48/0x70
[ 5521.190473] print_report+0xd2/0x660
[ 5521.190485] ? __virt_addr_valid+0x103/0x180
[ 5521.190499] ? kasan_complete_mode_report_info+0x8a/0x230
[ 5521.190511] ? smb2_write+0x16e/0x840 [ksmbd]
[ 5521.190596] kasan_report+0xd0/0x120
[ 5521.190606] ? smb2_write+0x16e/0x840 [ksmbd]
[ 5521.190694] __asan_load8+0x8b/0xe0
[ 5521.190704] smb2_write+0x16e/0x840 [ksmbd]
[ 5521.190790] ? _raw_spin_lock+0x82/0xf0
[ 5521.190807] ? __pfx_smb2_write+0x10/0x10 [ksmbd]
[ 5521.190894] ? ksmbd_smb2_check_message+0xa56/0xc90 [ksmbd]
[ 5521.191001] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 5521.191090] process_one_work+0x4d3/0x840
[ 5521.191109] worker_thread+0x91/0x6e0
[ 5521.191124] ? __pfx_worker_thread+0x10/0x10
[ 5521.191135] kthread+0x188/0x1d0
[ 5521.191145] ? __pfx_kthread+0x10/0x10
[ 5521.191154] ret_from_fork+0x44/0x80
[ 5521.191166] ? __pfx_kthread+0x10/0x10
[ 5521.191175] ret_from_fork_asm+0x1b/0x30
[ 5521.191189] </TASK>
[ 5521.191197] Allocated by task 1913:
[ 5521.191203] kasan_save_stack+0x38/0x70
[ 5521.191214] kasan_set_track+0x25/0x40
[ 5521.191223] kasan_save_alloc_info+0x1e/0x40
[ 5521.191231] __kasan_kmalloc+0xc3/0xd0
[ 5521.191240] kmalloc_trace+0x48/0xc0
[ 5521.191249] ksmbd_tree_conn_connect+0x75/0x2c0 [ksmbd]
[ 5521.191335] smb2_tree_connect+0x11d/0x4c0 [ksmbd]
[ 5521.191419] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 5521.191502] process_one_work+0x4d3/0x840
[ 5521.191511] worker_thread+0x91/0x6e0
[ 5521.191520] kthread+0x188/0x1d0
[ 5521.191526] ret_from_fork+0x44/0x80
[ 5521.191534] ret_from_fork_asm+0x1b/0x30
[ 5521.191544] Freed by task 1922:
[ 5521.191549] kasan_save_stack+0x38/0x70
[ 5521.191558] kasan_set_track+0x25/0x40
[ 5521.191567] kasan_save_free_info+0x2b/0x60
[ 5521.191575] ____kasan_slab_free+0x180/0x1f0
[ 5521.191585] __kasan_slab_free+0x12/0x30
[ 5521.191591] slab_free_freelist_hook+0xd2/0x1a0
[ 5521.191599] __kmem_cache_free+0x1a2/0x2f0
[ 5521.191608] kfree+0x78/0x120
[ 5521.191616] ksmbd_tree_conn_disconnect+0x94/0xb0 [ksmbd]
[ 5521.191701] smb2_tree_disconnect+0x183/0x1b0 [ksmbd]
[ 5521.191785] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 5521.191868] process_one_work+0x4d3/0x840
[ 5521.191877] worker_thread+0x91/0x6e0
[ 5521.191886] kthread+0x188/0x1d0
[ 5521.191892] ret_from_fork+0x44/0x80
[ 5521.191913] ret_from_fork_asm+0x1b/0x30
smb2_lock 条件竞争 UAF
下面看一个稍微复杂一点的例子,对于复杂代码我们依然采用一样的策略,关注对象使用、锁、引用计数, smb2_lock 的关键代码如下:
-
smb2_lock
-
flags & SMB2_LOCKFLAG_UNLOCK
-
fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId);
-
filp = fp->filp;
-
down_read(&conn_list_lock);
-
list_for_each_entry(conn, &conn_list, conns_list)
-
spin_lock(&conn->llist_lock);
-
list_for_each_entry_safe(cmp_lock, tmp2, &conn->lock_list, clist)
- spin_unlock(&conn->llist_lock);
- up_read(&conn_list_lock);
- kfree(cmp_lock); // 释放锁 [0]
- goto out_check_cl;
-
spin_unlock(&conn->llist_lock);
-
-
up_read(&conn_list_lock);
-
-
rc == FILE_LOCK_DEFERRED
- x
-
else
- spin_lock(&work->conn->llist_lock);
- list_add_tail(&smb_lock->clist, &work->conn->lock_list);
- list_add_tail(&smb_lock->flist, &fp->lock_list);
- spin_unlock(&work->conn->llist_lock); // [1] smb_lock 加到了链表中
- * 时间窗 *
- list_add(&smb_lock->llist, &rollback_list); // [2] 使用 smb_lock
-
这里涉及的对象为存放在 conn->lock_list 中的 smb_lock 对象,在 【1】 -- 【2】 之间其他线程,通过 SMB2_LOCKFLAG_UNLOCK 进入 【0】 释放 smb_lock 就会导致 UAF.
产生漏洞的本质原因是 [1] 分支提前将 smb_lock 对象放入了 work->conn->lock_list 这样在其释放 llist_lock 后,其他线程就能释放 smb_lock,[2] 处使用的 smb_lock 就是已经被释放的对象。
这也是一种常见的条件竞争常见,即提前将对象放入了共享资源池中,后续使用时一旦被并发释放就会导致 UAF.
panic 日志如下
[ 192.743133] BUG: KASAN: slab-use-after-free in smb2_lock+0x17a7/0x2010 [ksmbd]
[ 192.743228] Write of size 8 at addr ffff88810a5ca028 by task kworker/6:2/76
[ 192.743241] CPU: 6 PID: 76 Comm: kworker/6:2 Tainted: G OE 6.5.4 #1
[ 192.743250] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 07/22/2020
[ 192.743258] Workqueue: ksmbd-io handle_ksmbd_work [ksmbd]
[ 192.743345] Call Trace:
[ 192.743350] <TASK>
[ 192.743357] dump_stack_lvl+0x48/0x70
[ 192.743372] print_report+0xd2/0x660
[ 192.743384] ? __virt_addr_valid+0x103/0x180
[ 192.743398] ? kasan_complete_mode_report_info+0x8a/0x230
[ 192.743422] ? smb2_lock+0x17a7/0x2010 [ksmbd]
[ 192.743507] kasan_report+0xd0/0x120
[ 192.743518] ? smb2_lock+0x17a7/0x2010 [ksmbd]
[ 192.743605] __asan_store8+0x8e/0xe0
[ 192.743615] smb2_lock+0x17a7/0x2010 [ksmbd]
[ 192.743700] ? xas_descend+0x82/0x130
[ 192.743710] ? __rcu_read_unlock+0x51/0x80
[ 192.743730] ? __pfx_smb2_lock+0x10/0x10 [ksmbd]
[ 192.743814] ? ksmbd_smb2_check_message+0xa56/0xc90 [ksmbd]
[ 192.743902] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 192.743990] process_one_work+0x4d3/0x840
[ 192.744009] worker_thread+0x91/0x6e0
[ 192.744024] ? __pfx_worker_thread+0x10/0x10
[ 192.744035] kthread+0x188/0x1d0
[ 192.744045] ? __pfx_kthread+0x10/0x10
[ 192.744054] ret_from_fork+0x44/0x80
[ 192.744066] ? __pfx_kthread+0x10/0x10
[ 192.744075] ret_from_fork_asm+0x1b/0x30
[ 192.744089] </TASK>
[ 192.744097] Allocated by task 76:
[ 192.744103] kasan_save_stack+0x38/0x70
[ 192.744114] kasan_set_track+0x25/0x40
[ 192.744123] kasan_save_alloc_info+0x1e/0x40
[ 192.744132] __kasan_kmalloc+0xc3/0xd0
[ 192.744141] kmalloc_trace+0x48/0xc0
[ 192.744151] smb2_lock+0x4c6/0x2010 [ksmbd]
[ 192.744233] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 192.744316] process_one_work+0x4d3/0x840
[ 192.744325] worker_thread+0x91/0x6e0
[ 192.744334] kthread+0x188/0x1d0
[ 192.744340] ret_from_fork+0x44/0x80
[ 192.744349] ret_from_fork_asm+0x1b/0x30
[ 192.744358] Freed by task 83:
[ 192.744363] kasan_save_stack+0x38/0x70
[ 192.744372] kasan_set_track+0x25/0x40
[ 192.744382] kasan_save_free_info+0x2b/0x60
[ 192.744390] ____kasan_slab_free+0x180/0x1f0
[ 192.744411] __kasan_slab_free+0x12/0x30
[ 192.744418] slab_free_freelist_hook+0xd2/0x1a0
[ 192.744426] __kmem_cache_free+0x1a2/0x2f0
[ 192.744436] kfree+0x78/0x120
[ 192.744443] smb2_lock+0x1488/0x2010 [ksmbd]
[ 192.744526] handle_ksmbd_work+0x2a7/0x800 [ksmbd]
[ 192.744608] process_one_work+0x4d3/0x840
[ 192.744617] worker_thread+0x91/0x6e0
[ 192.744626] kthread+0x188/0x1d0
[ 192.744632] ret_from_fork+0x44/0x80
[ 192.744641] ret_from_fork_asm+0x1b/0x30
[ 192.744650] Last potentially related work creation:
[ 192.744655] kasan_save_stack+0x38/0x70
[ 192.744664] __kasan_record_aux_stack+0xb3/0xd0
[ 192.744673] kasan_record_aux_stack_noalloc+0xb/0x20
[ 192.744682] kvfree_call_rcu+0x2d/0x4e0
[ 192.744690] kernfs_unlink_open_file+0x18b/0x1a0
[ 192.744699] kernfs_fop_release+0x6d/0x180
[ 192.744707] __fput+0x1e1/0x480
[ 192.744716] ____fput+0xe/0x20
[ 192.744725] task_work_run+0x109/0x190
[ 192.744733] exit_to_user_mode_prepare+0x16b/0x190
[ 192.744743] syscall_exit_to_user_mode+0x29/0x60
[ 192.744755] do_syscall_64+0x67/0x90
[ 192.744764] entry_SYSCALL_64_after_hwframe+0x6e/0xd8
[ 192.744780] Second to last potentially related work creation:
[ 192.744784] kasan_save_stack+0x38/0x70
[ 192.744794] __kasan_record_aux_stack+0xb3/0xd0
[ 192.744802] kasan_record_aux_stack_noalloc+0xb/0x20
[ 192.744811] kvfree_call_rcu+0x2d/0x4e0
[ 192.744819] kernfs_unlink_open_file+0x18b/0x1a0
[ 192.744827] kernfs_fop_release+0x6d/0x180
[ 192.744834] __fput+0x1e1/0x480
[ 192.744842] ____fput+0xe/0x20
[ 192.744851] task_work_run+0x109/0x190
[ 192.744858] exit_to_user_mode_prepare+0x16b/0x190
[ 192.744867] syscall_exit_to_user_mode+0x29/0x60
[ 192.744877] do_syscall_64+0x67/0x90
[ 192.744885] entry_SYSCALL_64_after_hwframe+0x6e/0xd8
smb20_oplock_break_ack 条件竞争 UAF
这个漏洞的模式稍微有点区别,smb20_oplock_break_ack 会在在释放 fp 和 opinfo 的引用后继续使用 opinfo
static void smb20_oplock_break_ack(struct ksmbd_work *work)
{
fp = ksmbd_lookup_fd_slow(work, volatile_id, persistent_id);
opinfo = opinfo_get(fp);
// 【0】use fp and opinfo with refcount
opinfo_put(opinfo);
ksmbd_fd_put(work, fp);
// 【1】use opinfo after drop refcount
opinfo->op_state = OPLOCK_STATE_NONE;
wake_up_interruptible_all(&opinfo->oplock_q);
rsp->StructureSize = cpu_to_le16(24);
rsp->OplockLevel = rsp_oplevel;
rsp->Reserved = 0;
rsp->Reserved2 = 0;
rsp->VolatileFid = volatile_id;
rsp->PersistentFid = persistent_id;
inc_rfc1001_len(work->response_buf, 24);
return;
}
【0】 中的代码是正确的,使用对象时应该要在持有引用计数的情况下使用,避免被其他线程 RACE 释放,但是 【1】 处时 opinfo 的引用计数已经释放,其他线程可以并发释放 opinfo,这样后续对 opinfo 的操作就会导致 UAF.
opinfo 会在 fp 被释放时进行释放,关键调用栈如下:
-
__ksmbd_close_fd
-
close_id_del_oplock
-
opinfo_del(opinfo);
-
opinfo_put
-
call_rcu(&opinfo->rcu_head, opinfo_free_rcu);
- free_opinfo(opinfo);
-
-
-
-
smb2_tree_disconnect 条件竞争 UAF
这个漏洞和 smb2_close 条件竞争 UAF[1] 很像,看看关键代码如下:
int smb2_tree_disconnect(struct ksmbd_work *work)
{
struct smb2_tree_disconnect_rsp *rsp;
struct smb2_tree_disconnect_req *req;
struct ksmbd_session *sess = work->sess;
struct ksmbd_tree_connect *tcon = work->tcon;
WORK_BUFFERS(work, req, rsp);
rsp->StructureSize = cpu_to_le16(4);
inc_rfc1001_len(work->response_buf, 4);
if (!tcon || test_and_set_bit(TREE_CONN_EXPIRE, &tcon->status)) {
rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED;
smb2_set_err_rsp(work);
return 0;
}
ksmbd_close_tree_conn_fds(work);
ksmbd_tree_conn_disconnect(sess, tcon);
work->tcon = NULL;
return 0;
}
和 smb2_close 的区别是这里没有读写锁的保护,而是利用 test_and_set_bit 原子操作来避免 tcon 被多次释放,但其实 UAF 的位置也就是 test_and_set_bit
race 场景如下:
- 两个线程 A B 同时执行到 test_and_set_bit 前
- 线程 A 先执行将 tcon->status 设置为 TREE_CONN_EXPIRE
- 并通过 ksmbd_tree_conn_disconnect 释放 work->tcon
- 线程 B 执行 test_and_set_bit 时, tcon 已经被释放,导致 UAF
通过这两个例子可以看出,一个正确的 free 逻辑需要考虑的情况比较复杂,漏洞挖掘人员也应该重点关注。
ksmbd_session_lookup_all 条件竞争 UAF
ksmbd_session_lookup_all --> ksmbd_session_lookup 中会在无锁情况访问 sess ,在 [0] [1] 之间其他线程释放 sess 就会 UAF
struct ksmbd_session *ksmbd_session_lookup(struct ksmbd_conn *conn,
unsigned long long id)
{
struct ksmbd_session *sess;
sess = xa_load(&conn->sessions, id);
// [0] race window begin
// [1] race window end
if (sess)
sess->last_active = jiffies;
return sess;
}
总结
本文介绍以 ksmbd 为例介绍如何从0开始分析一个目标,并发现其中可能的条件竞争攻击面,最后结合多个实际的漏洞案例对漏洞挖掘、分析进行讲解。
smb2_close 条件竞争 UAF
↩︎