CVE-2024-53104

Posted by : on

Category : cve   learning


Table of Contents

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

Introduction

CVE-2024-53104는 리눅스 커널의 USB Video Class (UVC) 드라이버에서 비디오 스트리밍 인터페이스 중 디지털 비디오 (digital video, DV) 등을 처리하는 과정에서 발생한 out-of-bound (OOB) 취약점이다[1, 2].

UVC는 USB로 연결된 비디오 장치 (웹캠 등)에 대한 명세로, 리눅스 커널에는 uvcvideo라는 커널 드라이버 모듈로 포함되어 있다. 그리고 DV 포맷은 캠코더 등에 사용되는 비디오 포맷이다[3, 4, 5].

본 글에서는 리눅스 커널 5.15를 기준으로 본 취약점을 살펴볼 것이다.

Root Cause Analysis

리눅스 커널의 UVC 드라이버는 uvc_parse_streaming 함수에서 포맷, 프레임, 인터벌의 개수를 세고 ({1}) 이들을 위한 메모리를 kzalloc 함수로 할당한다 ({2}). 그리고 uvc_parse_format 함수를 호출하여 포맷과 프레임을 파싱한다 ({3}). 이떄 UVC_VS_FORMAT_DV 포맷의 경우에는 포맷 당 더미 프레임 하나만 존재하여 포맷과 프레임의 개수가 같음을 기억하자.

    static int uvc_parse_streaming(struct uvc_device *dev,
                                   struct usb_interface *intf)
    {
        struct uvc_streaming *streaming = NULL;
        struct uvc_format *format;
        struct uvc_frame *frame;
        struct usb_host_interface *alts = &intf->altsetting[0];
        unsigned char *_buffer, *buffer = alts->extra;
        int _buflen, buflen = alts->extralen;
        unsigned int nformats = 0, nframes = 0, nintervals = 0;
        unsigned int size, i, n, p;
        u32 *interval;
        u16 psize;
        int ret = -EINVAL;
    
        /* ... */
    
        /* Skip the standard interface descriptors. */
        while (buflen > 2 && buffer[1] != USB_DT_CS_INTERFACE) {
            buflen -= buffer[0];
            buffer += buffer[0];
        }
    
        if (buflen <= 2) {
            uvc_dbg(dev, DESCR,
                    "no class-specific streaming interface descriptors found\n");
            goto error;
        }
    
        /* Parse the header descriptor. */
        switch (buffer[2]) {
        case UVC_VS_OUTPUT_HEADER:
            streaming->type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
            size = 9;
            break;
    
        case UVC_VS_INPUT_HEADER:
            streaming->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
            size = 13;
            break;
    
        default:
            uvc_dbg(dev, DESCR,
                    "device %d videostreaming interface %d HEADER descriptor not found\n",
                    dev->udev->devnum, alts->desc.bInterfaceNumber);
            goto error;
        }
    
        p = buflen >= 4 ? buffer[3] : 0;
        n = buflen >= size ? buffer[size-1] : 0;
    
        if (buflen < size + p*n) {
            uvc_dbg(dev, DESCR,
                    "device %d videostreaming interface %d HEADER descriptor is invalid\n",
                    dev->udev->devnum, alts->desc.bInterfaceNumber);
            goto error;
        }
    
        streaming->header.bNumFormats = p;
        streaming->header.bEndpointAddress = buffer[6];
        if (buffer[2] == UVC_VS_INPUT_HEADER) {
            streaming->header.bmInfo = buffer[7];
            streaming->header.bTerminalLink = buffer[8];
            streaming->header.bStillCaptureMethod = buffer[9];
            streaming->header.bTriggerSupport = buffer[10];
            streaming->header.bTriggerUsage = buffer[11];
        } else {
            streaming->header.bTerminalLink = buffer[7];
        }
        streaming->header.bControlSize = n;
    
        streaming->header.bmaControls = kmemdup(&buffer[size], p * n,
                                                GFP_KERNEL);
        if (streaming->header.bmaControls == NULL) {
            ret = -ENOMEM;
            goto error;
        }
    
        buflen -= buffer[0];
        buffer += buffer[0];
    
        _buffer = buffer;
        _buflen = buflen;
    
        /* Count the format and frame descriptors. */
        while (_buflen > 2 && _buffer[1] == USB_DT_CS_INTERFACE) { /* {1} */
            switch (_buffer[2]) {
            case UVC_VS_FORMAT_UNCOMPRESSED:
            case UVC_VS_FORMAT_MJPEG:
            case UVC_VS_FORMAT_FRAME_BASED:
                nformats++;
                break;
    
            case UVC_VS_FORMAT_DV:
                /* DV format has no frame descriptor. We will create a
                 * dummy frame descriptor with a dummy frame interval.
                 */
                nformats++;
                nframes++;
                nintervals++;
                break;
    
            case UVC_VS_FORMAT_MPEG2TS:
            case UVC_VS_FORMAT_STREAM_BASED:
                uvc_dbg(dev, DESCR,
                        "device %d videostreaming interface %d FORMAT %u is not supported\n",
                        dev->udev->devnum,
                        alts->desc.bInterfaceNumber, _buffer[2]);
                break;
    
            case UVC_VS_FRAME_UNCOMPRESSED:
            case UVC_VS_FRAME_MJPEG:
                nframes++;
                if (_buflen > 25)
                    nintervals += _buffer[25] ? _buffer[25] : 3;
                break;
    
            case UVC_VS_FRAME_FRAME_BASED:
                nframes++;
                if (_buflen > 21)
                    nintervals += _buffer[21] ? _buffer[21] : 3;
                break;
            }
    
            _buflen -= _buffer[0];
            _buffer += _buffer[0];
        }
    
        if (nformats == 0) {
            uvc_dbg(dev, DESCR,
                    "device %d videostreaming interface %d has no supported formats defined\n",
                    dev->udev->devnum, alts->desc.bInterfaceNumber);
            goto error;
        }
    
        size = nformats * sizeof(*format) + nframes * sizeof(*frame)
            + nintervals * sizeof(*interval);
        format = kzalloc(size, GFP_KERNEL); /* {2} */
        if (format == NULL) {
            ret = -ENOMEM;
            goto error;
        }
    
        frame = (struct uvc_frame *)&format[nformats];
        interval = (u32 *)&frame[nframes];
    
        streaming->format = format;
        streaming->nformats = nformats;
    
        /* Parse the format descriptors. */
        while (buflen > 2 && buffer[1] == USB_DT_CS_INTERFACE) {
            switch (buffer[2]) {
            case UVC_VS_FORMAT_UNCOMPRESSED:
            case UVC_VS_FORMAT_MJPEG:
            case UVC_VS_FORMAT_DV:
            case UVC_VS_FORMAT_FRAME_BASED:
                format->frame = frame;
                ret = uvc_parse_format(dev, streaming, format,
                                       &interval, buffer, buflen); /* {3} */
                if (ret < 0)
                    goto error;
    
                frame += format->nframes;
                format++;
    
                buflen -= ret;
                buffer += ret;
                continue;
    
            default:
                break;
            }
    
            buflen -= buffer[0];
            buffer += buffer[0];
        }
    
        /* ... */
    }

위 함수에서 uvc_parse_format 함수를 호출하면 이 함수는 포맷을 파싱한 ({4}) 후에 프레임 기술자 (frame descriptor)를 파싱한다 ({5}).

    static int uvc_parse_format(struct uvc_device *dev,
                                struct uvc_streaming *streaming, struct uvc_format *format,
                                u32 **intervals, unsigned char *buffer, int buflen)
    {
        struct usb_interface *intf = streaming->intf;
        struct usb_host_interface *alts = intf->cur_altsetting;
        struct uvc_format_desc *fmtdesc;
        struct uvc_frame *frame;
        const unsigned char *start = buffer;
        unsigned int width_multiplier = 1;
        unsigned int interval;
        unsigned int i, n;
        u8 ftype;
    
        format->type = buffer[2];
        format->index = buffer[3];
    
        switch (buffer[2]) {        /* {4} */
        case UVC_VS_FORMAT_UNCOMPRESSED:
        case UVC_VS_FORMAT_FRAME_BASED:
            /* ... */
            break;
    
        case UVC_VS_FORMAT_MJPEG:
            /* ... */
            break;
    
        case UVC_VS_FORMAT_DV:
            if (buflen < 9) {
                uvc_dbg(dev, DESCR,
                        "device %d videostreaming interface %d FORMAT error\n",
                        dev->udev->devnum,
                        alts->desc.bInterfaceNumber);
                return -EINVAL;
            }
    
            switch (buffer[8] & 0x7f) {
            case 0:
                strscpy(format->name, "SD-DV", sizeof(format->name));
                break;
            case 1:
                strscpy(format->name, "SDL-DV", sizeof(format->name));
                break;
            case 2:
                strscpy(format->name, "HD-DV", sizeof(format->name));
                break;
            default:
                uvc_dbg(dev, DESCR,
                        "device %d videostreaming interface %d: unknown DV format %u\n",
                        dev->udev->devnum,
                        alts->desc.bInterfaceNumber, buffer[8]);
                return -EINVAL;
            }
    
            strlcat(format->name, buffer[8] & (1 << 7) ? " 60Hz" : " 50Hz",
                    sizeof(format->name));
    
            format->fcc = V4L2_PIX_FMT_DV;
            format->flags = UVC_FMT_FLAG_COMPRESSED | UVC_FMT_FLAG_STREAM;
            format->bpp = 0;
            ftype = 0;
    
            /* Create a dummy frame descriptor. */
            frame = &format->frame[0];
            memset(&format->frame[0], 0, sizeof(format->frame[0]));
            frame->bFrameIntervalType = 1;
            frame->dwDefaultFrameInterval = 1;
            frame->dwFrameInterval = *intervals;
            *(*intervals)++ = 1;
            format->nframes = 1;
            break;
    
        case UVC_VS_FORMAT_MPEG2TS:
        case UVC_VS_FORMAT_STREAM_BASED:
            /* Not supported yet. */
        default:
            uvc_dbg(dev, DESCR,
                    "device %d videostreaming interface %d unsupported format %u\n",
                    dev->udev->devnum, alts->desc.bInterfaceNumber,
                    buffer[2]);
            return -EINVAL;
        }
    
        uvc_dbg(dev, DESCR, "Found format %s\n", format->name);
    
        buflen -= buffer[0];
        buffer += buffer[0];
    
        /* Parse the frame descriptors. Only uncompressed, MJPEG and frame
         * based formats have frame descriptors.
         */
        while (buflen > 2 && buffer[1] == USB_DT_CS_INTERFACE &&
               buffer[2] == ftype) { /* {5} */
            frame = &format->frame[format->nframes];
    
            /* ... */
    
            buflen -= buffer[0];
            buffer += buffer[0];
        }
    
        return buffer - start;
    }

이때 위 함수에서 UVC_VS_FORMAT_DV을 파싱할 때는 더미 프레임을 같이 만든다. 그리고 이 포맷은 포맷 당 더미 프레임 하나이므로 (포맷과 프레임의 개수가 같음) 더이상 파싱할 프레임이 없다. 즉, UVC_VS_FORMAT_DV의 경우에는 while 문에 진입하면 안된다는 것이다. 하지만 위 코드에서 while 문의 조건식은 이를 방지하지 못할 수 있다. 그럼 파싱할 프레임이 없음에도 작업을 진행하게 되고 uvc_parse_format 함수의 반환값이 부정확해지게 된다.

Patch

아래 패치를 보면 상기의 UVC_VS_FORMAT_DV를 파싱할 때 ftype 변수를 0으로 초기화한다는 것에서 착안하여 이 경우에는 프레임을 파싱하지 못하도록 (while 문에 진입하지 못하도록) 수정하였음을 알 수 있다[2].

    media: uvcvideo: Skip parsing frames of type UVC_VS_UNDEFINED in uvc_parse_format
    commit ecf2b43018da9579842c774b7f35dbe11b5c38dd upstream.
    
    This can lead to out of bounds writes since frames of this type were not
    taken into account when calculating the size of the frames buffer in
    uvc_parse_streaming.
    
    Fixes: c0efd232929c ("V4L/DVB (8145a): USB Video Class driver")
    Signed-off-by: Benoit Sevens <bsevens@google.com>
    Cc: stable@vger.kernel.org
    Acked-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
    Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
    Signed-off-by: Hans Verkuil <hverkuil@xs4all.nl>
    Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
    Diffstat
    -rw-r--r--	drivers/media/usb/uvc/uvc_driver.c	2	
    1 files changed, 1 insertions, 1 deletions
    diff --git a/drivers/media/usb/uvc/uvc_driver.c b/drivers/media/usb/uvc/uvc_driver.c
    index 0fac689c6350b2..13db0026dc1aad 100644
    --- a/drivers/media/usb/uvc/uvc_driver.c
    +++ b/drivers/media/usb/uvc/uvc_driver.c
    @@ -371,7 +371,7 @@ static int uvc_parse_format(struct uvc_device *dev,
         * Parse the frame descriptors. Only uncompressed, MJPEG and frame
         * based formats have frame descriptors.
         */
    -	while (buflen > 2 && buffer[1] == USB_DT_CS_INTERFACE &&
    +	while (ftype && buflen > 2 && buffer[1] == USB_DT_CS_INTERFACE &&
               buffer[2] == ftype) {
            unsigned int maxIntervalIndex;

References

  1. “CVE-2024-53104 Detail,” NATIONAL VULNERABILITY DATABASE. [Online]. Available: https://nvd.nist.gov/vuln/detail/CVE-2024-53104
  2. Benoit Sevens, Nov. 2024, “media: uvcvideo: Skip parsing frames of type UVC_VS_UNDEFINED in uvc_parse_format,” [Online]. Available: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=1ee9d9122801eb688783acd07791f2906b87cb4f
  3. USB Implementers Forum, “Universal Serial Bus Device Class Definition for Video Devices,” Sep. 2003, [Revised Jun. 2012].
  4. Sep. 2016, “uvcvideo,” LinuxTVWiki. [Online]. Available: https://www.linuxtv.org/wiki/index.php/Uvcvideo
  5. Douglas Dixon, “DV: Digital Video Format for the Masses (3/2000),” Manifest Technology. [Online]. Available: https://www.manifest-tech.com/media_pc/dv_tech.htm

About oMAcS
oMAcS

...

Email : david232818@gmail.com

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

Useful Links