CVE-2024-0144
A heap-based buffer overflow vulnerability exists in the Ndecomp field handling of NVIDIA nvJPEG2000 0.8.0. A specially crafted JPEG2000 file can lead to overwrite of adjacent heap memory 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-120 - Buffer Copy without Checking Size of Input (‘Classic 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 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 component index 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 = 2
.
Additionally, the Ndecomp
field holds the number of decomposition levels for this particular component.
struct COC_segment {
uint8_t marker[2] = [0xFF, 0x53]
uint16_t Lcoc;
uint8_t Ccoc;
uint8_t Scoc;
uint8_t Ndecomp;
uint8_t blockWidth;
uint8_t blockHeight;
uint8_t blockStyle;
uint8_t Transformation;
...
};
At offset 0xA29EC
we see an interesting piece of assembly code responsible for parsing the COC
segment of a JPEG2000 file. Here rdx
holds the Ndecomp
value increased by 1 and rbx
is the heap allocated buffer with a size of Csiz*0x4c
.
.text:00000000000A29EC cmp edx, 8 # rdx = Ndecomp+1
.text:00000000000A29EF lea rcx, [rbx+r14+29h] # rbx = heap buffer, r14 = Ccoc*0x4c
.text:00000000000A29F4 mov rax, 0F0F0F0F0F0F0F0Fh # value used in rep stosq
.text:00000000000A2A90 lea rdi, [rcx+8] # rep stosq destination
.text:00000000000A2A94 mov [rcx], rax # [buffer + Ccoc*0x4c + 0x29], first 8 bytes of the buffer
.text:00000000000A2A97 (1) mov [rdx+rcx-8], rax # [buffer + Ccoc*0x4c + 0x29 + (Ndecomp+1) - 8], last 8 bytes of the buffer
.text:00000000000A2A9C and rdi, 0FFFFFFFFFFFFFFF8h
.text:00000000000A2AA0 sub rcx, rdi # get the remainder to 8
.text:00000000000A2AA3 add edx, ecx
.text:00000000000A2AA5 shr edx, 3 # Ndecomp+1-remainder / 8
.text:00000000000A2AA8 mov ecx, edx # set as counter
.text:00000000000A2AAA (2) rep stosq # memset(buffer, 0x0F0F0F0F0F0F0F0F, (Ndecomp+1-rem)/8)
What the above code does is to access the object with index Ccoc
, add 0x29
to access a specific buffer in the object and fill it with the value 0x0F
, assuming it holds Ndecomp+1
bytes. It is an optimized inline implementation of the memset()
function.
The code performs a memory write at offset 0xA2A97
(1) by using the Ndecomp
value as an index to the last 8-byte value in the array. Then at (2) 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. Essentially, the above assembly snippet is equivalent to a
memset(&buffer[Ccoc]+0x29, 0x0F, Ndecomp+1)
Since the Ndecomp
value is not being checked, a heap buffer overflow can occur that can corrupt adjacent objects in memory. Although the value being written is not actually controlled, an attacker could corrupt a size field in a struct that can lead to a more convenient exploitation primitive. As another example, the attacker could corrupt the low bytes of a pointer that can lead to further memory corruption.
==148121== Memcheck, a memory error detector
==148121== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==148121== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==148121== Command: ../../nvjpeg2k_fuzz ./poc.jp2
==148121==
==148121== Invalid write of size 8
==148121== at 0x423647: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x40958C: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==148121== Address 0x515edd1 is 9 bytes after a block of size 152 alloc'd
==148121== at 0x4846F95: operator new(unsigned long) (vg_replace_malloc.c:487)
==148121== by 0x41B77E: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x40958C: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==148121==
==148121== Invalid write of size 8
==148121== at 0x42365A: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x41BD5F: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x40958C: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==148121== Address 0x515edc8 is 0 bytes after a block of size 152 alloc'd
==148121== at 0x4846F95: operator new(unsigned long) (vg_replace_malloc.c:487)
==148121== by 0x41B77E: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x40958C: ??? (in /usr/lib/x86_64-linux-gnu/libnvjpeg2k/12/libnvjpeg2k.so.0.8.0.38)
==148121== by 0x403873: main (nvjpeg2k_fuzz.cpp:117)
==148121==
==148121==
==148121== HEAP SUMMARY:
==148121== in use at exit: 0 bytes in 0 blocks
==148121== total heap usage: 248 allocs, 248 frees, 105,773 bytes allocated
==148121==
==148121== All heap blocks were freed -- no leaks are possible
==148121==
==148121== For lists of detected and suppressed errors, rerun with: -s
==148121== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)
2024-11-01 - Vendor Disclosure
2025-02-11 - Vendor Patch Release
2025-02-11 - Public Release
Discovered by Dimitrios Tatsis of Cisco Talos.