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]:
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
- “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].