Talos Vulnerability Report


Simple DirectMedia Layer SDL2_Image IMG_LoadPCX_RW Information Disclosure Vulnerability

April 10, 2018
CVE Number



An exploitable information disclosure vulnerability exists in the PCX image rendering functionality of SDL2_image-2.0.2. A specially crafted PCX image can cause an out-of-bounds read on the heap, resulting in information disclosure . An attacker can display a specially crafted image to trigger this vulnerability.

Tested Versions

Simple DirectMedia Layer SDL2_image 2.0.2

Product URLs


CVSSv3 Score

5.3 - CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N


CWE-126: Buffer Over-read


LibSDL is a multi-platform library for easy access to low level hardware and graphics, providing support for a large amount of games, software, and emulators. The last known count of software using LibSDL (from 2012) listed the number at upwards of 120. The LibSDL2_Image library is an optional component that deals specifically with parsing and displaying a variety of image file formats, creating a single and uniform API for image processing, regardless of the type.

When parsing and storing an PCX image file, the LibSDL2 library implements a custom RLE inflater for the compressed PCX data. In order to this, however, the library must first parse the PCX headers, such that it can allocate the correct amount of memory for the color data buffer. The structure of the PCX headers is listed below:

struct PCXheader {
    Uint8 Manufacturer;
    Uint8 Version;
    Uint8 Encoding;
    Uint8 BitsPerPixel;
    Sint16 Xmin, Ymin, Xmax, Ymax;
    Sint16 HDpi, VDpi;
    Uint8 Colormap[48];
    Uint8 Reserved;
    Uint8 NPlanes;
    Sint16 BytesPerLine;
    Sint16 PaletteInfo;
    Sint16 HscreenSize;
    Sint16 VscreenSize;
    Uint8 Filler[54];

It should be noted that these values are all taken directly from the PCX file itself, as one might expect. Also, for the purposes of this bug, the most important fields are the Xmin, Xmax, and BytesPerLine parameters, however (pcxh.BitsPerPixel == 8 && pcxh.NPlanes == 3) must also be true in order to hit the buggy code path.

When creating the SDL_Surface object used to store the image information, the dimensions of the image are generated as such:

// IMG_pcx.c:118
/* Create the surface of the appropriate type */
    width = (pcxh.Xmax - pcxh.Xmin) + 1;
    height = (pcxh.Ymax - pcxh.Ymin) + 1;
surface = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height,
               bits, Rmask, Gmask, Bmask, Amask); //[1]

The width is multiplied by the height inside of [1], in order to generate the appropriate size buffer for the surface.pixels buffer, which is where the image data ends up being stored. But immediately after the pixel buffer is malloced with size (width*height), the library also mallocs another buffer:

bpl = pcxh.NPlanes * pcxh.BytesPerLine; //[1]
    if (bpl > surface->pitch) {
            error = "bytes per line is too large (corrupt?)";
    buf = (Uint8 *)SDL_malloc(bpl); //[2]
    row = (Uint8 *)surface->pixels;

At [1], the size of this new buffer is calculated. As mentioned before, pcxh.Nplanes must be 3, however there is no such restriction on the pcxh.BytesPerLine parameter, which is read in from the input image. For purposes of the discussion, assume pcxh.BytesPerLine = 0x200. Thus, when the buf variable is populated with heap memory at [2], the size of the heap chunk is 0x600. The library then reads in the actual image data from the file into the buf variable, one row at a time. After this, the data is taken from the tmp buffer 'buf' and de-interlaced into the actual SDL_Surface.pixels buffer from before.

else if(src_bits == 24) {
        /* de-interlace planes */
        Uint8 *innerSrc = buf;
        int plane;
            for(plane = 0; plane < pcxh.NPlanes; plane++) { // plane == 3
                int x;
                dst = row + plane;
            for(x = 0; x < width; x++) { //[1]
                    *dst = *innerSrc++;   //[2]
                        dst += pcxh.NPlanes;

During the de-interlace phase, at [2], the temporary buffer 'buf' is walked with the innerSrc pointer, with each row taking 'width' bytes [1]. When looking back, we can see that the 'width' variable is created as such: width = (pcxh.Xmax - pcxh.Xmin) + 1;, while the 'buf' buffer is created as such: SDL_malloc(pcxh.NPlanes * pcxh.BytesPerLine). Since there's not really any checks or comparisons between the sizes of the buffer an width, if width is greater than the BytesPerLine, the loop starts to read out of bounds into the rest of the heap, resulting in an information disclosure, as the SDL_surface.pixel buffer now contains heap data.

Crash Information

Program received signal SIGSEGV, Segmentation fault.

0x00007f2d47d6f0c8 in IMG_LoadPCX_RW (src=0x60700000dfb0) at IMG_pcx.c:208
warning: Source file is more recent than executable.
208                         *dst = *innerSrc++; // ASAN crash here, rax == 0x61b000020000
-------------------------------------------------------------------------[ registers ]----
$rax   : 0x000061b000020000 -> 0x0000000000000000 -> 0x0000000000000000
$rbx   : 0x00007fffe8f637e0 -> 0x00007fffe8f65374 -> 0x736172635f6e6f6e -> 0x736172635f6e6f6e ("non_cras"?)
$rcx   : 0x000000000000005a -> 0x000000000000005a
$rdx   : 0x000061b000020001 -> 0x0000000000000000 -> 0x0000000000000000
$rsp   : 0x00007fffe8f63500 -> 0x00007fffe8f635c0 -> 0x000006000801050a -> 0x0000000000000000 -> 0x0000000000000000
$rbp   : 0x00007fffe8f63660 -> 0x00007fffe8f636a0 -> 0x00007fffe8f636d0 -> 0x0000000000000002 -> 0x0000000000000002
$rsi   : 0x000061600000fd60 -> 0x0000000000000000 -> 0x0000000000000000
$rdi   : 0x00007fffe8f63520 -> 0x000000000801050a -> 0x000000000801050a
$rip   : 0x00007f2d47d6f0c8 -> <IMG_LoadPCX_RW+1288> movzx edx, BYTE PTR [rax]
$r8    : 0x000061600000fd60 -> 0x0000000000000000 -> 0x0000000000000000
$r9    : 0x00007fffe8f6351f -> 0x0000000801050a5a -> 0x0000000000000000 -> 0x0000000000000000
$r10   : 0x0000000000000020 -> 0x0000000000000020
$r11   : 0x000061b00000fc00 -> 0x0000000000000000 -> 0x0000000000000000
$r12   : 0x00000000004bd3e4 -> <_start+0> xor ebp, ebp
$r13   : 0x00007fffe8f637d0 -> 0x0000000000000002 -> 0x0000000000000002
$r14   : 0xfffffffffffffff8 -> 0xfffffffffffffff8
$r15   : 0x0000000000000000 -> 0x0000000000000000
$eflags: [CARRY parity adjust zero SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
-----------------------------------------------------------------------------[ stack ]----
0x00007fffe8f63500|+0x00: 0x00007fffe8f635c0 -> 0x000006000801050a -> 0x0000000000000000 -> 0x0000000000000000  <-$rsp
0x00007fffe8f63508|+0x08: 0x000060700000dfb0 -> 0x00007f2d47fede48 -> 0x20ec8348e5894855 -> 0x20ec8348e5894855
0x00007fffe8f63510|+0x10: 0x000061600000fc80 -> 0xbebebebefbad2488
0x00007fffe8f63518|+0x18: 0x5a0061600000fc80 -> 0x5a0061600000fc80
0x00007fffe8f63520|+0x20: 0x000000000801050a -> 0x000000000801050a      <-$rdi
0x00007fffe8f63528|+0x28: 0x0048004801ff09ff -> 0x0048004801ff09ff
0x00007fffe8f63530|+0x30: 0x0000000000000000 -> 0x0000000000000000
0x00007fffe8f63538|+0x38: 0x0000000000000000 -> 0x0000000000000000
------------------------------------------------------------------[ code:i386:x86-64 ]----
0x7f2d47d6f0b3 <IMG_LoadPCX_RW+1267> mov    DWORD PTR [rbp-0x78], 0x0
0x7f2d47d6f0ba <IMG_LoadPCX_RW+1274> jmp    0x7f2d47d6f0e3 <IMG_LoadPCX_RW+1315>
0x7f2d47d6f0bc <IMG_LoadPCX_RW+1276> mov    rax, QWORD PTR [rbp-0x70]
0x7f2d47d6f0c0 <IMG_LoadPCX_RW+1280> lea    rdx, [rax+0x1]
0x7f2d47d6f0c4 <IMG_LoadPCX_RW+1284> mov    QWORD PTR [rbp-0x70], rdx
 ->0x7f2d47d6f0c8 <IMG_LoadPCX_RW+1288> movzx  edx, BYTE PTR [rax]
0x7f2d47d6f0cb <IMG_LoadPCX_RW+1291> mov    rax, QWORD PTR [rbp-0x50]
0x7f2d47d6f0cf <IMG_LoadPCX_RW+1295> mov    BYTE PTR [rax], dl
0x7f2d47d6f0d1 <IMG_LoadPCX_RW+1297> movzx  eax, BYTE PTR [rbp-0xff]
0x7f2d47d6f0d8 <IMG_LoadPCX_RW+1304> movzx  eax, al
0x7f2d47d6f0db <IMG_LoadPCX_RW+1307> add    QWORD PTR [rbp-0x50], rax
--------------------------------------------------------------[ source:IMG_pcx.c+208 ]----
204              for(plane = 0; plane < pcxh.NPlanes; plane++) {
205                  int x;
206                  dst = row + plane;
207                  for(x = 0; x < width; x++) {
            // innerSrc=0x00007fffe8f635f0 -> [...] -> 0x0000000000000000, dst=0x00007fffe8f63610 -> [...] -> 0x00006500005d0000
-> 208                       *dst = *innerSrc++; 
209                      dst += pcxh.NPlanes; 
210                  }
211              }
212          }
---------------------------------------------------------------------------[ threads ]----
[#0] Id 1, Name: "", stopped, reason: SIGSEGV
-----------------------------------------------------------------------------[ trace ]----
[#0] 0x7f2d47d6f0c8->Name: IMG_LoadPCX_RW(src=0x60700000dfb0)
[#1] 0x7f2d47d697ef->Name: IMG_LoadTyped_RW(src=0x60700000dfb0, freesrc=0x1, type=0x0)
[#2] 0x7f2d47d695e0->Name: IMG_Load(file=0x7fffe8f65374 "non_crashes/0436f5c6ebc8db621a0280296203bb14")
[#3] 0x4bd553->Name: main(argc=<optimized out>, argv=<optimized out>)


2018-02-06 - Vendor Disclosure
2018-02-07 - Vendor patched
2018-04-10 - Public Release


Discovered by Lilith of Cisco Talos.