CVE-2024-0143
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.
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 {
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
==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"
2024-10-24 - Vendor Disclosure
2025-02-11 - Vendor Patch Release
2025-02-11 - Public Release
Discovered by Dimitrios Tatsis of Cisco Talos.