
CVE-2022-41218에 대한 취약점 세부 정보
이 취약점은 drivers/media/dvb-core/dmxdev.c에서 발생하는 경쟁 조건 취약점입니다. 따라서 이 dmxdev를 사용하는 usb 장치 드라이버에서 경쟁 조건이 발생할 수 있습니다.
이 취약점은 3개의 UAF를 트리거하는 데 사용될 수 있습니다.
먼저, 익스플로잇 순서는 다음과 같습니다(drivers/media/usb/ttusb-dec/ttusb_dec.c를 사용하여 디버깅했습니다):```
cpu0 cpu1 cpu 2
1. dvb_dvr_open()
2. ttusb_dec_disconnect()
ttusb_dec_exit_dvb()
dvb_dmxdev_release()
wait_event(dmxdev->dvr_dvbdev->wait_queue, …)
3. dvb_demux_open()
dvb_demux_ioctl()
dvb_usercopy()
copy_from_user() <- userfaultfd stuck
4. dvb_dvr_release()
5. vfree(dmxdev->filter)
6. copy_from_user() <- userfaultfd release
dvb_demux_do_ioctl()
struct dmxdev *dmxdev = dmxdevfilter->dev; <- UAF!!
상세한 익스플로잇 흐름은 다음과 같습니다:
1. 디바이스 노드 중 dvr0에 대해 open()을 호출합니다. 이는 dvb_dvr_open()을 호출하여 `dvbdev->users++`가 실행되도록 합니다.
이 경우, usb 디바이스를 제거하여 ttusb_dec_disconnect()가 실행되면 `wait_event(dmxdev->dvr_dvbdev->wait_queue, …)` 조건이 포착됩니다.
2. USB 디바이스를 물리적으로 제거합니다.
이 경우, ttusb_dec_disconnect() 함수가 실행되는 동안 `wait_event(dmxdev->dvr_dvbdev->wait_queue, …)`에서 대기합니다.
3. demux0 노드에 대해 open()을 호출하여 dvb_demux_open() 함수가 호출되도록 합니다.
이 함수도 단계 1의 dvb_dvr_open() 함수와 유사하게 `dvbdev->users++`를 실행합니다.
그러나 현재 대기 중인 dvb_dmxdev_release() 함수가 이미 `dvbdev->users`에 대한 검사를 통과했기 때문에 잘못된 참조 카운팅이 발생합니다:```
void dvb_dmxdev_release(struct dmxdev *dmxdev)
{
dmxdev->exit = 1;
if (dmxdev->dvbdev->users > 1) { // This check is bypassed. improper reference counting.
wait_event(dmxdev->dvbdev->wait_queue,
dmxdev->dvbdev->users == 1);
}
if (dmxdev->dvr_dvbdev->users > 1) {
wait_event(dmxdev->dvr_dvbdev->wait_queue, // Currently wait()ing here.
dmxdev->dvr_dvbdev->users == 1);
}
dvb_unregister_device(dmxdev->dvbdev);
dvb_unregister_device(dmxdev->dvr_dvbdev);
vfree(dmxdev->filter);
dmxdev->filter = NULL;
dmxdev->demux->close(dmxdev->demux);
}
그런 다음 demux0에서 ioctl()을 호출합니다. 호출 시, 세 번째 인자는 userfaultfd(또는 FUSE fs)에 의해 설정된 사용자 공간 주소를 제출합니다. 이제 ioctl이 실행되는 동안 dvb_usercopy()의 copy_from_user()에서 멈추게 됩니다:``` int dvb_usercopy(struct file *file, unsigned int cmd, unsigned long arg, int (*func)(struct file *file, unsigned int cmd, void *arg)) { char sbuf[128]; void *mbuf = NULL; void *parg = NULL; int err = -EINVAL;
/* Copy arguments into temp kernel buffer */
switch (_IOC_DIR(cmd)) {
case _IOC_NONE:
/*
* For this command, the pointer is actually an integer
* argument.
*/
parg = (void *) arg;
break;
case _IOC_READ: /* some v4l ioctls are marked wrong ... */
case _IOC_WRITE:
case (_IOC_WRITE | _IOC_READ):
if (_IOC_SIZE(cmd) <= sizeof(sbuf)) {
parg = sbuf;
} else {
/* too big to allocate from stack */
mbuf = kmalloc(_IOC_SIZE(cmd), GFP_KERNEL);
if (NULL == mbuf)
return -ENOMEM;
parg = mbuf;
}
err = -EFAULT;
if (copy_from_user(parg, (void __user *)arg, _IOC_SIZE(cmd))) // here
goto out;
break;
}
4. dvr0 노드를 open()한 스레드에서 fd를 close()합니다.
그러면 dvb_dvr_release()가 호출되고, `dvbdev->users--`와 `wake_up(&dvbdev->wait_queue)`가 실행되어 .disconnect 흐름인 dvb_dmxdev_release() 함수를 깨웁니다.
5. .disconnect 흐름에서 dvb_dmxdev_release() 함수는 `vfree(dmxdev->filter)`를 실행합니다.
6. userfaultfd를 설정한 demux0 ioctl 스레드에서 userfaultfd를 해제합니다.
이로 인해 .disconnect에서 방금 해제한 `dmxdev->filter` 주소를 읽어 UAF가 발생합니다.
이제 UAF는 dvb_demux_do_ioctl()의 모든 경우에 사용될 수 있으며, eBPF와 연결하면 LPE도 가능합니다.
커널 로그는 다음과 같습니다: