Contents #
- Introduction
- Background
- Root Cause Analysis
- 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]:
1ioctl() /* system call with CEC_ADP_S_PHYS_ADDR command */
2cec_ioctl() /* with CEC_ADAP_S_PHYS_ADDR command */
3cec_adap_s_phys_addr()
4__cec_s_phys_addr()
5cec_adap_enable()
6adap->ops->adap_enable() /* this is cec_pin_adap_enable() */
7kthread_run() /* with cec_pin_thread_func(), it works as a thread */
8cec_received_msg_ts()
9cec_receive_notify()
10cec_queue_msg_fh()
그리고 다음 코드를 통해 수신된 CEC message가 큐에 저장됨을 알 수 있다[4]:
1/*
2 * Queue a new message for this filehandle.
3 *
4 * We keep a queue of at most CEC_MAX_MSG_RX_QUEUE_SZ messages. If the
5 * queue becomes full, then drop the oldest message and keep track
6 * of how many messages we've dropped.
7 */
8static void cec_queue_msg_fh(struct cec_fh *fh, const struct cec_msg *msg)
9{
10 static const struct cec_event ev_lost_msgs = {
11 .event = CEC_EVENT_LOST_MSGS,
12 .flags = 0,
13 {
14 .lost_msgs = { 1 },
15 },
16 };
17 struct cec_msg_entry *entry;
18
19 mutex_lock(&fh->lock);
20 entry = kmalloc(sizeof(*entry), GFP_KERNEL);
21 if (entry) {
22 entry->msg = *msg;
23 /* Add new msg at the end of the queue */
24 list_add_tail(&entry->list, &fh->msgs);
25
26 if (fh->queued_msgs < CEC_MAX_MSG_RX_QUEUE_SZ) {
27 /* All is fine if there is enough room */
28 fh->queued_msgs++;
29 mutex_unlock(&fh->lock);
30 wake_up_interruptible(&fh->wait);
31 return;
32 }
33
34 /*
35 * if the message queue is full, then drop the oldest one and
36 * send a lost message event.
37 */
38 entry = list_first_entry(&fh->msgs, struct cec_msg_entry, list);
39 list_del(&entry->list);
40 kfree(entry);
41 }
42 mutex_unlock(&fh->lock);
43
44 /*
45 * We lost a message, either because kmalloc failed or the queue
46 * was full.
47 */
48 cec_queue_event_fh(fh, &ev_lost_msgs, ktime_get_ns());
49}
CEC device는 close
system call이 호출되었을 때 release된다. 이때 메모리를 정리하는 작업이 수행되며, 이는 다음 코드를 통해 알 수 있다[4]:
1/* Override for the release function */
2static int cec_release(struct inode *inode, struct file *filp)
3{
4 struct cec_devnode *devnode = cec_devnode_data(filp);
5 struct cec_adapter *adap = to_cec_adapter(devnode);
6 struct cec_fh *fh = filp->private_data;
7 unsigned int i;
8
9 mutex_lock(&adap->lock);
10 if (adap->cec_initiator == fh)
11 adap->cec_initiator = NULL;
12 if (adap->cec_follower == fh) {
13 adap->cec_follower = NULL;
14 adap->passthrough = false;
15 }
16 if (fh->mode_follower == CEC_MODE_FOLLOWER)
17 adap->follower_cnt--;
18 if (fh->mode_follower == CEC_MODE_MONITOR_PIN)
19 cec_monitor_pin_cnt_dec(adap);
20 if (fh->mode_follower == CEC_MODE_MONITOR_ALL)
21 cec_monitor_all_cnt_dec(adap);
22 mutex_unlock(&adap->lock);
23
24 mutex_lock(&devnode->lock);
25 mutex_lock(&devnode->lock_fhs);
26 list_del(&fh->list);
27 mutex_unlock(&devnode->lock_fhs);
28 mutex_unlock(&devnode->lock);
29
30 /* Unhook pending transmits from this filehandle. */
31 mutex_lock(&adap->lock);
32 while (!list_empty(&fh->xfer_list)) {
33 struct cec_data *data =
34 list_first_entry(&fh->xfer_list, struct cec_data, xfer_list);
35
36 data->blocking = false;
37 data->fh = NULL;
38 list_del_init(&data->xfer_list);
39 }
40 mutex_unlock(&adap->lock);
41 while (!list_empty(&fh->msgs)) {
42 struct cec_msg_entry *entry =
43 list_first_entry(&fh->msgs, struct cec_msg_entry, list);
44
45 list_del(&entry->list);
46 kfree(entry);
47 }
48 for (i = CEC_NUM_CORE_EVENTS; i < CEC_NUM_EVENTS; i++) {
49 while (!list_empty(&fh->events[i])) {
50 struct cec_event_entry *entry =
51 list_first_entry(&fh->events[i],
52 struct cec_event_entry, list);
53
54 list_del(&entry->list);
55 kfree(entry);
56 }
57 }
58 kfree(fh);
59
60 cec_put_device(devnode);
61 filp->private_data = NULL;
62 return 0;
63}
Root Cause Analysis #
상기의 cec_release
함수의 코드를 다시 살펴보면, fh
에 대한 lock이 없음을 알 수 있다[4].
1/* Override for the release function */
2static int cec_release(struct inode *inode, struct file *filp)
3{
4 struct cec_devnode *devnode = cec_devnode_data(filp);
5 struct cec_adapter *adap = to_cec_adapter(devnode);
6 struct cec_fh *fh = filp->private_data;
7 unsigned int i;
8
9 mutex_lock(&adap->lock);
10
11 /* ... */
12
13 mutex_unlock(&adap->lock);
14
15 mutex_lock(&devnode->lock);
16 mutex_lock(&devnode->lock_fhs);
17 list_del(&fh->list);
18 mutex_unlock(&devnode->lock_fhs);
19 mutex_unlock(&devnode->lock);
20
21 /* Unhook pending transmits from this filehandle. */
22 mutex_lock(&adap->lock);
23
24 /* ... */
25
26 mutex_unlock(&adap->lock);
27
28 /* ... */
29
30 kfree(fh);
31
32 cec_put_device(devnode);
33 filp->private_data = NULL;
34 return 0;
35}
그런데 상기의 cec_queue_msg_fh
함수에는 fh
에 lock을 걸고 fh->msgs
에 접근하여 큐에 메시지를 추가하는 작업이 존재한다[4].
1/*
2 * Queue a new message for this filehandle.
3 *
4 * We keep a queue of at most CEC_MAX_MSG_RX_QUEUE_SZ messages. If the
5 * queue becomes full, then drop the oldest message and keep track
6 * of how many messages we've dropped.
7 */
8static void cec_queue_msg_fh(struct cec_fh *fh, const struct cec_msg *msg)
9{
10 static const struct cec_event ev_lost_msgs = {
11 .event = CEC_EVENT_LOST_MSGS,
12 .flags = 0,
13 {
14 .lost_msgs = { 1 },
15 },
16 };
17 struct cec_msg_entry *entry;
18
19 mutex_lock(&fh->lock);
20 entry = kmalloc(sizeof(*entry), GFP_KERNEL);
21 if (entry) {
22 entry->msg = *msg;
23 /* Add new msg at the end of the queue */
24 list_add_tail(&entry->list, &fh->msgs);
25
26 /* ... */
27 }
28 mutex_unlock(&fh->lock);
29
30 /*
31 * We lost a message, either because kmalloc failed or the queue
32 * was full.
33 */
34 cec_queue_event_fh(fh, &ev_lost_msgs, ktime_get_ns());
35}
그럼 우리는 상기의 두 함수를 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 #
- "CVE-2024-23848 Detail," 2024. [Online]. Available: https://nvd.nist.gov/vuln/detail/CVE-2024-23848, [Accessed Jan. 30, 2024].
- 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]. - "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].
- torvalds, "Linux kernel," 2024. [Online]. Available: https://github.com/torvalds/linux, [Accessed Jan. 30, 2024].