CVE-2024-23848

Posted by : on

Category : learning


Contents

  1. Introduction
  2. Background
  3. Root Cause Analysis
  4. References

Introduction

본 취약점은 linux의 HDMI Consumer Electronics Control (CEC) framework에서 발생한 취약점으로, CEC 디바이스를 release할 때 적절한 lock이 걸려 있지 않아 발생하였다[1, 2].

CEC는 HDMI에서 사용되는 프로토콜 중 하나로, HDMI connectors는 이를 위한 핀을 하나 제공한다. 이 프로토콜은 HDMI 케이블로 연결된 디바이스들 간의 통신에 사용된다[3].

CEC를 사용하기 위해서는 관련 디바이스 파일을 열어야 하며, 특정 동작의 경우 관련 권한이 요구될 수 있다. CEC를 사용하는 디바이스들은 CEC message를 송수신하여 통신하게 된다[3].

CEC message는 큐에 저장되었다가 송수신되는데, 이 큐는 CEC filehandle의 멤버로 존재한다. 그런데 CEC 디바이스를 release할 때 CEC filehandle에 대한 lock이 존재하지 않아 race condition이 발생할 수 있다. 그리고 release 과정에서 filehandle이 free되므로 UAF가 발생할 수 있다[2, 4].

Background

CEC는 HDMI로 연결된 디바이스들이 통신하기 위한 프로토콜로, CEC message를 사용하여 통신하게 된다[3, 4].

CEC를 사용하는 방법은 다른 디바이스를 사용하는 방법과 다르지 않다. 즉, 디바이스 파일을 open system call을 사용해서 열고 ioctl system call을 사용하여 디바이스를 제어하고 close system call을 사용해서 디바이스를 release하는 것이다. 이 중 ioctl system call을 호출할 때 사용 가능한 command 중 CEC message와 관련된 것은 다음과 같다[2]:

@ CEC_ADAP_S_PHYS_ADDR: Sets a new physical address; it requires CEC_CAP_PHYS_ADDR and file descriptor to be in initiator mode

@ CEC_S_MODE: There are Initiator modes and Follower modes; Initiator mode is CEC_MODE_NO_INITIATOR, CEC_MODE_INITIATOR (default), CEC_MODE_EXCL_INITIATOR

@ CEC_RECEIVE: Received messasge can be a message received form another CEC device or the result of an earlier non-blocking transmit

@ CEC_TRANSMIT: Send CEC message; it requires CEC_CAP_TRANSMIT

이때 수신된 CEC message를 큐에 추가하는 과정은 커널 내부 스레드로 동작한다. 이는 다음 call trace를 통해 알 수 있다[4]:

ioctl() /* system call with CEC_ADP_S_PHYS_ADDR command */
cec_ioctl() /* with CEC_ADAP_S_PHYS_ADDR command */
cec_adap_s_phys_addr()
__cec_s_phys_addr()
cec_adap_enable()
adap->ops->adap_enable() /* this is cec_pin_adap_enable() */
kthread_run() /* with cec_pin_thread_func(), it works as a thread */
cec_received_msg_ts()
cec_receive_notify()
cec_queue_msg_fh()

그리고 다음 코드를 통해 수신된 CEC message가 큐에 저장됨을 알 수 있다[4]:

/*
 * Queue a new message for this filehandle.
 *
 * We keep a queue of at most CEC_MAX_MSG_RX_QUEUE_SZ messages. If the
 * queue becomes full, then drop the oldest message and keep track
 * of how many messages we've dropped.
 */
static void cec_queue_msg_fh(struct cec_fh *fh, const struct cec_msg *msg)
{
	static const struct cec_event ev_lost_msgs = {
		.event = CEC_EVENT_LOST_MSGS,
		.flags = 0,
		{
			.lost_msgs = { 1 },
		},
	};
	struct cec_msg_entry *entry;

	mutex_lock(&fh->lock);
	entry = kmalloc(sizeof(*entry), GFP_KERNEL);
	if (entry) {
		entry->msg = *msg;
		/* Add new msg at the end of the queue */
		list_add_tail(&entry->list, &fh->msgs);

		if (fh->queued_msgs < CEC_MAX_MSG_RX_QUEUE_SZ) {
			/* All is fine if there is enough room */
			fh->queued_msgs++;
			mutex_unlock(&fh->lock);
			wake_up_interruptible(&fh->wait);
			return;
		}

		/*
		 * if the message queue is full, then drop the oldest one and
		 * send a lost message event.
		 */
		entry = list_first_entry(&fh->msgs, struct cec_msg_entry, list);
		list_del(&entry->list);
		kfree(entry);
	}
	mutex_unlock(&fh->lock);

	/*
	 * We lost a message, either because kmalloc failed or the queue
	 * was full.
	 */
	cec_queue_event_fh(fh, &ev_lost_msgs, ktime_get_ns());
}

CEC device는 close system call이 호출되었을 때 release된다. 이때 메모리를 정리하는 작업이 수행되며, 이는 다음 코드를 통해 알 수 있다[4]:

/* Override for the release function */
static int cec_release(struct inode *inode, struct file *filp)
{
	struct cec_devnode *devnode = cec_devnode_data(filp);
	struct cec_adapter *adap = to_cec_adapter(devnode);
	struct cec_fh *fh = filp->private_data;
	unsigned int i;

	mutex_lock(&adap->lock);
	if (adap->cec_initiator == fh)
		adap->cec_initiator = NULL;
	if (adap->cec_follower == fh) {
		adap->cec_follower = NULL;
		adap->passthrough = false;
	}
	if (fh->mode_follower == CEC_MODE_FOLLOWER)
		adap->follower_cnt--;
	if (fh->mode_follower == CEC_MODE_MONITOR_PIN)
		cec_monitor_pin_cnt_dec(adap);
	if (fh->mode_follower == CEC_MODE_MONITOR_ALL)
		cec_monitor_all_cnt_dec(adap);
	mutex_unlock(&adap->lock);

	mutex_lock(&devnode->lock);
	mutex_lock(&devnode->lock_fhs);
	list_del(&fh->list);
	mutex_unlock(&devnode->lock_fhs);
	mutex_unlock(&devnode->lock);

	/* Unhook pending transmits from this filehandle. */
	mutex_lock(&adap->lock);
	while (!list_empty(&fh->xfer_list)) {
		struct cec_data *data =
			list_first_entry(&fh->xfer_list, struct cec_data, xfer_list);

		data->blocking = false;
		data->fh = NULL;
		list_del_init(&data->xfer_list);
	}
	mutex_unlock(&adap->lock);
	while (!list_empty(&fh->msgs)) {
		struct cec_msg_entry *entry =
			list_first_entry(&fh->msgs, struct cec_msg_entry, list);

		list_del(&entry->list);
		kfree(entry);
	}
	for (i = CEC_NUM_CORE_EVENTS; i < CEC_NUM_EVENTS; i++) {
		while (!list_empty(&fh->events[i])) {
			struct cec_event_entry *entry =
				list_first_entry(&fh->events[i],
						 struct cec_event_entry, list);

			list_del(&entry->list);
			kfree(entry);
		}
	}
	kfree(fh);

	cec_put_device(devnode);
	filp->private_data = NULL;
	return 0;
}

Root Cause Analysis

상기의 cec_release 함수의 코드를 다시 살펴보면, fh에 대한 lock이 없음을 알 수 있다[4].

/* Override for the release function */
static int cec_release(struct inode *inode, struct file *filp)
{
	struct cec_devnode *devnode = cec_devnode_data(filp);
	struct cec_adapter *adap = to_cec_adapter(devnode);
	struct cec_fh *fh = filp->private_data;
	unsigned int i;

	mutex_lock(&adap->lock);
	
	/* ... */
	
	mutex_unlock(&adap->lock);

	mutex_lock(&devnode->lock);
	mutex_lock(&devnode->lock_fhs);
	list_del(&fh->list);
	mutex_unlock(&devnode->lock_fhs);
	mutex_unlock(&devnode->lock);

	/* Unhook pending transmits from this filehandle. */
	mutex_lock(&adap->lock);
	
	/* ... */
	
	mutex_unlock(&adap->lock);
	
	/* ... */
	
	kfree(fh);

	cec_put_device(devnode);
	filp->private_data = NULL;
	return 0;
}

그런데 상기의 cec_queue_msg_fh 함수에는 fh에 lock을 걸고 fh->msgs에 접근하여 큐에 메시지를 추가하는 작업이 존재한다[4].

/*
 * Queue a new message for this filehandle.
 *
 * We keep a queue of at most CEC_MAX_MSG_RX_QUEUE_SZ messages. If the
 * queue becomes full, then drop the oldest message and keep track
 * of how many messages we've dropped.
 */
static void cec_queue_msg_fh(struct cec_fh *fh, const struct cec_msg *msg)
{
	static const struct cec_event ev_lost_msgs = {
		.event = CEC_EVENT_LOST_MSGS,
		.flags = 0,
		{
			.lost_msgs = { 1 },
		},
	};
	struct cec_msg_entry *entry;

	mutex_lock(&fh->lock);
	entry = kmalloc(sizeof(*entry), GFP_KERNEL);
	if (entry) {
		entry->msg = *msg;
		/* Add new msg at the end of the queue */
		list_add_tail(&entry->list, &fh->msgs);
		
		/* ... */
	}
	mutex_unlock(&fh->lock);

	/*
	 * We lost a message, either because kmalloc failed or the queue
	 * was full.
	 */
	cec_queue_event_fh(fh, &ev_lost_msgs, ktime_get_ns());
}

그럼 우리는 상기의 두 함수를 race 시켜서 fh가 free된 상태에서 fh->msgs에 접근하도록 만들 수 있을 것이다.

   thread 1     |                      thread 2
---------------------------------------------------------------------   
   kfree(fh)    |
                |   mutex_lock(fh);
                |   list_add_tail(&entry->list, &fh->msgs); ---> UAF

따라서 race condition에 의한 UAF가 발생할 수 있다.

References

  1. “CVE-2024-23848 Detail,” 2024. [Online]. Available: https://nvd.nist.gov/vuln/detail/CVE-2024-23848, [Accessed Jan. 30, 2024].
  2. Hans Verkuil, “[Linux Kernel Bugs] KASAN: slab-use-after-free Read in cec_queue_msg_fh and 4 other crashes in the cec device (cec_ioctl),” 2024. Available: https://lore.kernel.org/lkml/e9f42704-2f99-4f2c-ade5-f952e5fd53e5%40xs4all.nl/, [Accessed Jan. 30, 2024].
  3. “Linux Media Subsystem Documentation,” 2016. [Online]. Available: https://www.kernel.org/doc/html/v4.9/media/uapi/cec/cec-intro.html, [Accessed Jan. 31, 2024].
  4. torvalds, “Linux kernel,” 2024. [Online]. Available: https://github.com/torvalds/linux, [Accessed Jan. 30, 2024].

About oMAcS
oMAcS

...

Email : david232818@gmail.com

Website : https://omacs.prose.sh/

Useful Links