Talos Vulnerability Report

TALOS-2024-2095

NVIDIA nvJPEG2000 Coding Style Component index out-of-bounds write vulnerability

February 11, 2025
CVE Number

CVE-2024-0143

SUMMARY

A memory corruption vulnerability exists in the Coding Style Component handling of the NVIDIA nvJPEG2000 library version 0.8.0. A specially crafted JPEG2000 file can lead to an out-of-bounds write with arbitrary data which can lead to further memory corruption and arbitrary code execution. An attacker can provide a malicious file to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

NVIDIA nvJPEG2000 0.8.0

PRODUCT URLS

nvJPEG2000 - https://developer.nvidia.com/nvjpeg

CVSSv3 SCORE

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-787 - Out-of-bounds Write

DETAILS

The nvJPEG2000 library is provided by NVIDIA as a high-performance JPEG2000 encoding and decoding library. The prerequisite is a CUDA enabled GPU in the system that allows faster processing than traditional CPU implementations.

JPEG2000 is an image compression standard with the intent of superseding JPEG, offering a higher compression ratio.

The JPEG2000 file format specification can be found in ISO/IEC 15444-1. Generally, the layout of the file follows a typical TLV (Type-Length-Value) structure. The specification defines a Box as a triplet of type, length and contents, with the contents possibly including nested Box structures. Additionally, a codestream is special kind of Box structure that contains different types of Segments that contain the compressed image data as well as information needed for the decoding process. Similar to the Box structures, Segments also use a TLV layout for their representation.

┌───────────────────────┐
│  Signature            │
│ ┌─────────────┐       │
│ │Filetype Box │       │
│ │    Length   │       │
│ │    Type     │       │
│ │    Contents │       │
│ ┌─────────────┐       │
│ │Header Box   │       │
│ │    Length   │       │
│ │    Type     │       │
│ │    Contents │       │
│ └─────────────┘       │
│ ┌─────────────────┐   │
│ │Codestream box   │   │
│ │    Length       │   │
│ │    Type         │   │
│ │    Segment      │   │
│ │       Marker    │   │
│ │       Length    │   │
│ │       Contents  │   │
│ └─────────────────┘   │
│  ...                  │
└───────────────────────┘

The SIZ Segment contains important information regarding the compressed image, like width, height, bit depth, vertical and horizontal offsets etc. Additionally, it contains the Csiz field that denotes the number of components present.

struct SIZ_segment {
    uint8_t marker[] = [0xFF, 0x51];
    uint16_t length;

    uint16 Rsiz;
    uint32 Xsiz;
    uint32 Ysiz;
    uint32 XOsiz;
    uint32 YOsiz;
    uint32 XTsiz;
    uint32 YTsiz;
    uint32 XTOsiz;
    uint32 YTOsiz;
    uint16 Csiz;
    ubyte  Ssiz[Csiz];
    ubyte  XRsiz[Csiz];
    ubyte  YRsiz[Csiz];
};

At offset 0x9AB96 the library reads the number of components from the Csiz field of the SIZ segment. Then using a number of lea instructions, it multiplies Csiz by 0x4c to calculate the size of a newly allocated buffer holding parameters for each component.

.text:000000000009AB96                 movzx   ebp, word ptr [r15+8Ch]       ; Csiz
.text:000000000009AB9E                 pxor    xmm0, xmm0
.text:000000000009ABA2                 mov     qword ptr [rsp+60h], 0
.text:000000000009ABAB                 movaps  xmmword ptr [rsp+50h], xmm0
.text:000000000009ABB0                 lea     rax, [rbp+rbp*8+0]            ; rax = Csiz * 9
.text:000000000009ABB5                 test    rbp, rbp
.text:000000000009ABB8                 lea     rsi, [rbp+rax*2+0]            ; rsi = Csiz + rax * 2 = Csiz * 19
.text:000000000009ABBD                 lea     r12, ds:0[rsi*4]              ; r12 = rsi * 4 = Csiz * 76 = Csiz * 0x4c
.text:000000000009ABC5                 jz      short loc_9AC33
.text:000000000009ABC7                 mov     rdi, r12        ; unsigned __int64
.text:000000000009ABCA                 call    __Znwm          ; operator new(ulong)

In a JPEG2000 image the COC segment also known as the Coding Style Component segment denotes various parameters regarding the compressions of a particular component. The components that these parameters relate to is stored in the Ccoc field. If Csiz is less than 256, Ccoc is read as 1 byte from input. Else, it is read as 2 bytes from input in order to be able to address the maximum number of components supported by the standard which is 65535. For simplicity’s sake we take the case for Csiz = 1.

struct COC_segment {
            uint8_t marker[] = [0xFF, 0x53]
            uint16_t Lcoc;

            ubyte Ccoc;

            ubyte Scoc;
            ubyte Ndecomp;
            ubyte blockWidth;
            ubyte blockHeight;
            ubyte blockStyle;
            ubyte Transformation;
            ...
};

Summing up, the index for the component is stored in Ccoc. Then the code attempts to access the parameters from the previously allocated buffer using Ccoc as an index to the array, resulting in r12 holding pointer to this memory location.

.text:00000000000A283E                 movzx   eax, byte ptr [rsp+1C8h+var_1A8]       ; rax = Ccoc
.text:00000000000A2843                 lea     rdx, [rax+rax*8]                       ; rdx = rax * 9
.text:00000000000A2847                 mov     rbx, [rbx]
.text:00000000000A284A                 mov     rsi, r13
.text:00000000000A284D                 mov     rdi, rbp
.text:00000000000A2850                 lea     r14, [rax+rdx*2].                      ; r14 = rax + rdx * 2 = rax * 19
.text:00000000000A2854                 mov     edx, 1
.text:00000000000A2859                 shl     r14, 2                                 ; r14 = rax * 0x4c
.text:00000000000A285D                 lea     r12, [rbx+r14]                         ; calculate pointer to array object
.text:00000000000A2861                 mov     byte ptr [r12+4Ah], 1                  ; possible out of bounds write

Since the library does not do any form of bounds checking, if the Ccoc parameter is larger than Csiz then an out of bounds write can happen that can lead to remote code execution. At offset 0xA2861 we see an out of bounds write with a fixed value that can be more than enough for an attacker to exploit the vulnerability.

The assembly instructions that follow 0xA2861 copy bytes from the input file to the memory location pointed to by r12. Evidently, this is an out of bounds write with a controlled index and fully controlled 4 bytes of data which can be a powerful primitive for an attacker.

call    qword ptr [rax]                    ; Read next byte from file
mov     [r12], al
...
call    qword ptr [rax]                    ; Read next byte from file
add     eax, 2
mov     [r12+1], al
...
call    qword ptr [rax]                    ; Read next byte from file
movzx   eax, byte ptr [rsp+1C8h+var_1A8]
mov     [r12+2], al
...
call    qword ptr [rax]                    ; Read next byte from file
movzx   eax, byte ptr [rsp+1C8h+var_1A8]
mov     [r12+3], al

Crash Information

==300131== Memcheck, a memory error detector
==300131== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==300131== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==300131== Command: ../../nvjpeg2k_fuzz min01.jp2
==300131==
==300131== Invalid write of size 1
==300131==    at 0x423411: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x40958C: nvjpeg2kStreamParse (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==300131==  Address 0x517d9aa is 4,554 bytes inside an unallocated block of size 3,946,496 in arena "client"
==300131==
==300131== Invalid write of size 1
==300131==    at 0x423444: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x40958C: nvjpeg2kStreamParse (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==300131==  Address 0x517d960 is 4,480 bytes inside an unallocated block of size 3,946,496 in arena "client"
==300131==
==300131== Invalid write of size 1
==300131==    at 0x423461: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x40958C: nvjpeg2kStreamParse (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==300131==  Address 0x517d961 is 4,481 bytes inside an unallocated block of size 3,946,496 in arena "client"
==300131==
==300131== Invalid write of size 1
==300131==    at 0x42347F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x40958C: nvjpeg2kStreamParse (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==300131==  Address 0x517d962 is 4,482 bytes inside an unallocated block of size 3,946,496 in arena "client"
==300131==
==300131== Invalid write of size 1
==300131==    at 0x423491: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x40958C: nvjpeg2kStreamParse (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==300131==  Address 0x517d963 is 4,483 bytes inside an unallocated block of size 3,946,496 in arena "client"
==300131==
==300131== Invalid write of size 4
==300131==    at 0x4234C6: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x40958C: nvjpeg2kStreamParse (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==300131==    by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==300131==  Address 0x517d964 is 4,484 bytes inside an unallocated block of size 3,946,496 in arena "client"
TIMELINE

2024-10-24 - Vendor Disclosure
2025-02-11 - Vendor Patch Release
2025-02-11 - Public Release

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.