Talos Vulnerability Report

TALOS-2024-2108

NVIDIA nvJPEG2000 Ndecomp heap-based buffer overflow vulnerability

February 11, 2025
CVE Number

CVE-2024-0144

SUMMARY

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.

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-120 - Buffer Copy without Checking Size of Input (‘Classic 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 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.

Crash Information

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

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

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.