Talos Vulnerability Report

TALOS-2024-2080

NVIDIA nvJPEG2000 cSIZ out-of-bounds write vulnerability

February 11, 2025
CVE Number

CVE-2024-0142

SUMMARY

A memory corruption vulnerability exists in the Image Decoding functionality of NVIDIA nvJPEG2000 0.8.0. A specially crafted .jp2 file can lead to an out-of-bounds write. 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 {
    uint16_t marker;
    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 0x9BD7E we see the code taking the Csiz field of the SIZ Segment and multiplying it by 0x314, passing the result as a parameter to new(). It is safe to assume that here the code allocates an array of Components, with 0x314 being the size in bytes for each internal corresponding structure.

loc_9BD7E:
imul    rbp, 314h
mov     rdi, rbp
call    __Znwm          ; operator new(ulong)
mov     r10, [r12+8]

After new() returns, we have a pointer to a buffer with a size of Csiz * 0x314. Inspecting at runtime with a debugger, we see:

Thread 1 "nvjpeg2000_deco" hit Breakpoint 1, 0x00007ffff749bd7e in ?? ()
   from /lib/x86_64-linux-gnu/libnvjpeg2k.so.0
$1 = "Csiz allocation object count"
rbp            0x3                 0x3
$2 = "Allocated buffer"
rax            0x555555a9a760      93824997762928

The code then continues to parse the rest of the file, at some point encountering the QCC or the Quantization Component Segment. Each QCC segment contains information for the component with the index Cqcc specified within.

struct QCC_segment {
    uint16_t marker;
    uint16_t length;

    ubyte Cqcc;
    ubyte Sqcc;
    ubyte Spqcc[];
};

At code offset 0xA11EB, the library reads the Cqcc field that holds the Component index and then proceeds to find the Component in the previously allocated array in order to perform operations on it. As expected, it takes the Cqcc field, multiplies it by 0x314 to find the offset to the corresponding Component and then proceeds to add that offset to R12 that holds the pointer to the Component array allocated earlier.

loc_A11EB:
imul    rdx, 314h
sub     esi, 2
mov     rdi, rbp
add     rdx, [r12]
movzx   esi, si
call    sub_A0260

In the debugger we see:

Thread 1 "nvjpeg2000_deco" hit Breakpoint 3, 0x00007ffff74a11eb in ?? ()
   from /lib/x86_64-linux-gnu/libnvjpeg2k.so.0
$3 = "Cqcc object index"
rdx            0x40                64
$4 = "Buffer used for calculation"
0x555555a87f50: 0x0000555555a9a760

Then the newly calculated pointer is used to perform operations on the Component structure.

sub_A0260:
...
mov     r14, rdx
..
mov     byte ptr [rdx+310h], 1
...
mov     [r14+4], edx
...

Note however that the library does not check that the Cqcc index is less than the Csiz parameter in the SIZ Segment. As a result, the calculated pointer exceeds the bounds of the Component array, leading to an out of bounds write. With control over out of bounds index and values written, this can result in further memory corruption and arbitrary code execution.

Crash Information

In the GDB output we get:

free(): invalid pointer

Thread 2 "nvjpeg2000_deco" received signal SIGABRT, Aborted.
[Switching to Thread 0x7ffff3a00000 (LWP 2331203)]
0x00007ffff669eb1c in pthread_kill () from /lib/x86_64-linux-gnu/libc.so.6

Running under Valgrind we see:

==2306288== Memcheck, a memory error detector
==2306288== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==2306288== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==2306288== Command: ./nvjpeg2000_decode -i crash                                      
==2306288== Warning: noted but unhandled ioctl 0x30000001 with no direction hints.
==2306288==    This could cause spurious value errors to appear.
==2306288==    See README_MISSING_SYSCALL_OR_IOCTL for guidance on writing a proper wrapper.
==2306288== Warning: set address range perms: large range [0x200000000, 0x400200000) (noaccess)
==2306288== Warning: set address range perms: large range [0x7bad000, 0x27bac000) (noaccess)
Using GPU - Quadro P4000 with CC 6.1
==2306288== Warning: set address range perms: large range [0x10006000000, 0x10106000000) (noaccess)
==2306288== Invalid write of size 1
==2306288==    at 0x4AA027D: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==2306288==    by 0x4AA1203: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==2306288==    by 0x4A9A053: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==2306288==    by 0x4A9BB4B: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==2306288==    by 0x4A889DC: nvjpeg2kStreamParse (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==2306288==    by 0x111762: decode_images(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx
11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, std::vector<char, std::allocator<char> > const&, unsigned long, decode_params_t&) (n
vjpeg2000DecodeSample.cpp:65)
==2306288==    by 0x111D1B: process_images(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cx
x11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >&, decode_params_t&, double&) (nvjpeg2000DecodeSample.cpp:151)
==2306288==    by 0x112533: main (nvjpeg2000DecodeSample.cpp:293)
==2306288==  Address 0xc227a00 is 48,784 bytes inside an unallocated block of size 1,983,696 in arena "client"
==2306288==
TIMELINE

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

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.