ksmbd 条件竞争漏洞挖掘:思路与案例

ksmbd 条件竞争漏洞挖掘:思路与案例

  ksmbd 条件竞争漏洞挖掘:思路与案例.drawio

  本文介绍从代码审计的角度分析、挖掘条件竞争、UAF 漏洞思路,并以 ksmbd 为实例介绍审计的过程和几个经典漏洞案例。

  分析代码版本为:linux-6.5.5

  相关漏洞在一年前已修复完毕.

掌握背景:Linux 内核条件竞争 UAF 常见场景

  首先我们看一下 Linux 内核下 UAF 漏洞产生的几种常见情况,UAF 的核心原理是 内存被释放了,程序仍让能使用这块内存,导致该现象的常见场景:

  • 指针在程序中被拷贝(比如存放到不同的对象中、放到链表中),其中一个指针释放后另一个指针没有被清理
  • 程序中并发访问导致内存对象还在使用时被其他线程释放.

image

  对于 Linux 内核/驱动而言,目前最常见的 UAF 是由于条件竞争导致的,即一个线程在使用某块内存时其他线程将其释放了。

  那么 Linux 内核为什么会有条件竞争问题呢,其根本原因如下:

  • Linux 以进程为调度主体,不同的进程可能会同时运行
  • 不同的进程实体,通过系统调用进入内核后共用同一个内核里面的资源,比如物理内存、内核堆内存等

下图表示一个多核系统上两个进程同时在不同的 CPU 核运行,访问内核中的共享变量,实际上由于调度中断单核情况下也存在并发场景

image

  并发执行+共享对象 是条件竞争的根因,因此驱动在开发设计时要考虑到并发场景,利用锁、引用计数等机制让资源在并发访问时不出问题.

  常见的并发场景:

并发场景 解释
用户进程之间 多线程并发系统调用,比如IOCTL、MMAP、READ、WRITE 等
用户进程与内核线程之间 内核线程中访问的共享对象,可能被用户态进程修改、释放
内核线程之间 不同内核线程之间使用共享对象

  以 IOCTL 为例用户态进程通过 SVC 指令进入内核,首先进入 ioctl 系统调用入口,然后在 vfs_ioctl​ 里面会调用 f_op->unlocked_ioctl 注册的函数指针,进入每个文件/驱动自己实现的回调中。

image

image

  由于多核可以并发 SVC 同时执行系统调用,且从系统调用入口到驱动回调中没有锁保护,所以在各个驱动接口中需要自己实现锁保护并发资源访问

  同理以 mmap 回调为例,SVC 进入内核后会进入 vm_mmap_pgoff 里面会获取当前进程 mm 对象的写锁,然后才通过 do_mmap 执行驱动对应的 mmap 回调

image

  分析系统调用 ---> 驱动回调的调用路径之间锁的使用情况可知:

  • 同一个进程的不同线程共享 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;
}

image

  通过分析这篇文章可以得到一些信息:

  • 定位 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

    1. kernel_accept(iface->ksmbd_socket, &client_sk,
    2. 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 连接下的各种协议状态

image

  两个对象互相保存对方的指针,方便从一个对象中拿到另一个对象进行操作,对象的大概作用:

  • ksmbd_transport:负责链路数据的收发,比如从网络连接中读取数据
  • ksmbd_conn:管理整个 smb 连接的状态,比如登录、文件操作,会话密钥等,每个 TCP 连接对应一个 conn 对象

  其中 ksmbd_conn 是非常核心的对象,在 SMB 请求处理的各个环节都能看到, ksmbd_conn_handler_loop 的大概处理流程如下

  • ksmbd_conn_handler_loop --> 每个连接都会创建一个内核线程执行该函数

    • while (ksmbd_conn_alive(conn))

      1. char hdr_buf[4] = {0,};

      2. size = t->ops->read(t, hdr_buf, sizeof(hdr_buf), -1);

      3. pdu_size = get_rfc1002_len(hdr_buf);

        • return be32_to_cpu(*((__be32 *)buf)) & 0xffffff;
      4. conn->request_buf = kvmalloc(size, GFP_KERNEL);

      5. memcpy(conn->request_buf, hdr_buf, sizeof(hdr_buf)); // 设置 rfc1002_len 到新的 req_buf

      6. size = t->ops->read(t, conn->request_buf + 4, pdu_size, 2);

      7. default_conn_ops.process_fn --> ksmbd_server_process_request --> queue_ksmbd_work

        1. work = ksmbd_alloc_work_struct(); // conn 的每个请求都启动一个 work 处理?

        2. work->request_buf = conn->request_buf;

        3. INIT_WORK(&work->work, handle_ksmbd_work);

        4. ksmbd_queue_work(work); // 调度 work 执行

          • queue_work(ksmbd_wq, &work->work);

  conn 下每收到一个请求都会新建一个 work,然后把 work 放到 ksmbd_wq, workqueue 会动态分配到不同 worker 执行。这边在介绍一下内核的 workqueue 机制,workqueue 和 work 的关系如下:

image

  核心概念是:work 先注册到 workqueue ,然后具体由 worker 执行,在代码中每个 worker 对应一个 work_thread 内核线程,一个 workqueue 里面会存在多个 worker,这些 worker 之间并发执行.

  因此同一时刻可能会有 handle_ksmbd_work 实例访问同一个 conn 对象,这样就有了 RACE 的可能.

image

  继续分析 handle_ksmbd_work 的大体逻辑:

  • handle_ksmbd_work

    • __handle_ksmbd_work

      1. conn->ops->allocate_rsp_buf(work)

      2. conn->ops->is_transform_hdr 校验 trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM;

      3. is_transform_hdr 满足条件 走 smb3_decrypt_req

      4. rc = conn->ops->init_rsp_hdr(work);

      5. conn->ops->check_user_session -->v2 setup 和 v1 SMB_COM_NEGOTIATE_EX​ 请求可以通过

      6. __process_request

        1. ksmbd_verify_smb_message
        2. cmds = &conn->cmds[command];
        3. 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 的关系如下图所示:

image

  • ksmbd_conn 对象里面的 sessions 数组中保存了当前连接的会话对象(ksmbd_session)
  • ksmbd_session 对象的 file_table 保存了打开的所有文件对象(ksmbd_file)
  • ksmbd_file 对象的 filp 指向了真正打开的 VFS 文件对象(struct file)

  下面看一下 ksmbd_file 对象的创建和初始化过程,相关代码如下:

  • smb2_open

    1. 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
    2. << RACE 时间窗 >>

    3. ksmbd_open_durable_fd(fp);

    4. 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 场景下的时序关系如下图:

image

  触发后的 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;
}

  让我们以并发的思维人脑模拟执行一下上面的代码:

  1. 假设两个线程 A B 并发进入 ksmbd_close_fd,且此时 id 对应 fp 的引用计数为 1
  2. 由于持有的是 read_lock 读锁,所以两个线程可以同时拿到 fp 并进入 atomic_dec_and_test
  3. 由于 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 的场景如下:

  1. 线程 A 持有 fp (比如通过 smb2_read),此时 fp->refcount = 2
  2. 5 个线程 B1 B2 .... B5,同时进入 ksmbd_close_fd 就会尝试最多减 5 次引用计数,导致 fp->refcount = 0,被释放
  3. 线程 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

  有了上面的经验,我开始关注代码中对对象的使用:

  1. 使用共享对象是是否持有引用计数
  2. 能否并发释放

  于是在浏览代码时发现 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);

          1. tcon = xa_load(&sess->tree_conns, id);
          2. 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

      1. fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId);

      2. filp = fp->filp;

      3. down_read(&conn_list_lock);

      4. 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)

          1. spin_unlock(&conn->llist_lock);
          2. up_read(&conn_list_lock);
          3. kfree(cmp_lock); // 释放锁 [0]
          4. goto out_check_cl;
        • spin_unlock(&conn->llist_lock);

      5. up_read(&conn_list_lock);

    • rc == FILE_LOCK_DEFERRED

      • x
    • else

      1. spin_lock(&work->conn->llist_lock);
      2. list_add_tail(&smb_lock->clist, &work->conn->lock_list);
      3. list_add_tail(&smb_lock->flist, &fp->lock_list);
      4. spin_unlock(&work->conn->llist_lock); // [1] smb_lock 加到了链表中
      5. * 时间窗 *
      6. 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 场景如下:

  1. 两个线程 A B 同时执行到 test_and_set_bit 前
  2. 线程 A 先执行将 tcon->status 设置为 TREE_CONN_EXPIRE
  3. 并通过 ksmbd_tree_conn_disconnect 释放 work->tcon
  4. 线程 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开始分析一个目标,并发现其中可能的条件竞争攻击面,最后结合多个实际的漏洞案例对漏洞挖掘、分析进行讲解。

  ‍


  1. smb2_close 条件竞争 UAF

    ↩︎
posted @ 2024-12-13 19:04  hac425  阅读(18)  评论(0编辑  收藏  举报