CVE-2024-0145
A heap based buffer overflow vulnerability exists in the way Ndecomp parameter is used when parsing JPEG2000 files in NVIDIA nvJPEG2000 0.8.0 library. A specially crafted JPEG2000 file can lead to a 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-122 - Heap-based Buffer Overflow
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;
uint8_t Ssiz[Csiz];
uint8_t XRsiz[Csiz];
uint8_t YRsiz[Csiz]; }; ```
At 0x9A8EE
the code retrieves the Xsiz/XOsiz/XTOsiz
and Ysiz/YTsiz/YTOsiz
fields of the SIZ
segment from the input file and calculates the total number of tiles present in the file:
loc_9A8EE:
mov ecx, [rdi+7Ch] # XTsiz
mov eax, [rdi+6Ch] # Xsiz
xor edx, edx
mov rbx, rsi
mov esi, [rdi+80h] # YTsoz
mov r15, rdi
add eax, ecx
sub eax, 1
sub eax, [rdi+84h]. # XTOsiz
div ecx
xor edx, edx
mov ecx, eax
mov eax, [rdi+70h] # Ysiz
add eax, esi
sub eax, 1
sub eax, [rdi+88h]. # YTOsiz
div esi
imul ecx, eax
mov edx, ecx
For clarity, the equivalent pseudocode is provided below:
def get_num_tiles(siz):
width = (siz.YTsiz + siz.Ysiz - 1 - siz.YTOsiz) / siz.YTsiz
height = (siz.XTsiz + siz.Xsiz - 1 - siz.XTOsiz) / siz.XTsiz
return int(width) * int(height)
For simplicity we take the case where num_tiles = 1
.
Then at 0x9C052
the code proceeds to allocate an array based on the calculated number of tiles, multiplying the number of tiles with 0x3d0 = 976
:
loc_9C052:
imul rbx, 3D0h
mov rdi, rbx
call __Znwm ; operator new(ulong)
The COD
segment holds the default coding style parameters for every component of a JPEG2000 image. The parameters can be overridden by subsequent COC
segments for specific components.
struct COD_segment {
uint16_t Lcod;
uint8_t Scod;
uint32_t SGcod;
uint8_t Ndecomp;
uint8_t blockWidth;
uint8_t blockHeight;
uint8_t blockStyle;
uint8_t Transformation;
};
The Ndecomp
field holds the default number of decomposition levels for all components. At 0xA29B0
the code initializes the previously allocated array with 0x0F0F0F0F0F0F0F0F
.
loc_A4280:
movzx edx, byte ptr [rbx+0Fh]
mov rax, 0F0F0F0F0F0F0F0Fh
add rdx, 1
cmp edx, 8
jnb short loc_A42F8
loc_A42F8:
lea rdi, [rbx+20h]
mov rcx, r14
mov [rbx+18h], rax (1) rbx+0x18 = buffer+0x388
mov [r14+rdx-8], rax (2) r14 = buffer+0x388, rdx = Ndecomp
and rdi, 0FFFFFFFFFFFFFFF8h
sub rcx, rdi
add ecx, edx
shr ecx, 3
cmp edx, 8
rep stosq (3)
mov rax, 0F0F0F0F0F0F0F0Fh
jb short loc_A42B7
loc_A4326:
lea rdi, [rbx+41h]
mov [rbx+39h], rax (4). rbx+0x39 = buffer+0x3a9
mov [rbp+rdx-8], rax (5) rbp = buffer+0x3a9, rdx = Ndecomp
and rdi, 0FFFFFFFFFFFFFFF8h
sub rbp, rdi
add edx, ebp
shr edx, 3
mov ecx, edx
rep stosq (6)
jmp loc_A4227
The code performs a memory write at (1) at the start of the array and then at (2) at the end of the array using the Ndecomp
value as an index. Then at (3) it uses the Ndecomp
value as a counter for the rep stosq
operation. As a reminder, the rep stosq
performs a memory write to the pointer of rdi
with the value in rax
for rcx
times. The same operations happen an (4) (5) and (6). The above assembly snippet is an inline implementation of the memset()
and is equivalent to:
memset(buffer+0x388, 0x0F, Ndecomp+1)
memset(buffer+0x3a9, 0x0F, Ndecomp+1)
The buffer previously allocated has a size of 0x3d0 * num_tiles
. Since the code performs no checks for Ndecomp
a heap buffer overflow can occur, corrupting adjacent memory with the value of 0x0F
. Since the buffer size is 0x3d0
, the minimum size for Ndecomp
to corrupt adjacent memory is 0x3d0-0x3a9=0x27
. The data written is not controlled by the attacker but it can lead to corrupting a structure that holds size fields or the lower bytes of a function pointer that can lead to code execution.
==3557== Memcheck, a memory error detector
==3557== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==3557== Using Valgrind-3.21.0 and LibVEX; rerun with -h for copyright info
==3557== Command: ../../../nvjpeg2k_fuzz ./readCOD-1cod.jp2
==3557==
==3557== Invalid write of size 8
==3557== at 0x424ED3: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41BDAA: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
==3557== Address 0x5122a01 is 17 bytes after a block of size 976 alloc'd
==3557== at 0x4848F95: operator new(unsigned long) (vg_replace_malloc.c:472)
==3557== by 0x41CC30: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41BF59: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
==3557==
==3557== Invalid write of size 8
==3557== at 0x424EE7: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41BDAA: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
==3557== Address 0x51229f0 is 0 bytes after a block of size 976 alloc'd
==3557== at 0x4848F95: operator new(unsigned long) (vg_replace_malloc.c:472)
==3557== by 0x41CC30: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41BF59: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
==3557==
==3557== Invalid write of size 8
==3557== at 0x424EFE: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41BDAA: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
==3557== Address 0x5122a22 is 14 bytes before a block of size 32 alloc'd
==3557== at 0x4848F95: operator new(unsigned long) (vg_replace_malloc.c:472)
==3557== by 0x41C802: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41B6B6: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
==3557==
==3557== Invalid write of size 8
==3557== at 0x424F11: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41BDAA: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
==3557== Address 0x51229f0 is 0 bytes after a block of size 976 alloc'd
==3557== at 0x4848F95: operator new(unsigned long) (vg_replace_malloc.c:472)
==3557== by 0x41CC30: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x41BF59: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x4095AC: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==3557== by 0x403873: main (nvjpeg2k_fuzz.cpp:141)
2024-11-21 - Vendor Disclosure
2025-02-11 - Vendor Patch Release
2025-02-11 - Public Release
Discovered by Dimitrios Tatsis of Cisco Talos.