CVE-2024-0142
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.
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
nvJPEG2000 - https://developer.nvidia.com/nvjpeg
9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-787 - Out-of-bounds Write
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.
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==
2024-10-08 - Vendor Disclosure
2025-02-11 - Vendor Patch Release
2025-02-11 - Public Release
Discovered by Dimitrios Tatsis of Cisco Talos.