Talos Vulnerability Report

TALOS-2024-2113

NVIDIA nvJPEG2000 Default Coding Styles Ndecomp buffer overflow vulnerability

February 11, 2025
CVE Number

CVE-2024-0145

SUMMARY

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.

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-122 - Heap-based Buffer Overflow

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;
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.

Crash Information

Valgrind Output

==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)
TIMELINE

2024-11-21 - Vendor Disclosure
2025-02-11 - Vendor Patch Release
2025-02-11 - Public Release

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.