Talos Vulnerability Report

TALOS-2017-0497

Simple DirectMedia Layer SDL2_image load_xcf_tile_rle Decompression Code Execution Vulnerability

March 1, 2018
CVE Number

CVE-2017-14448

Summary

An exploitable code execution vulnerability exists in the XCF image rendering functionality of SDL2_image-2.0.2. A specially crafted XCF image can cause a heap overflow resulting in code execution. An attacker can display a specially crafted image to trigger this vulnerability.

Tested Versions

Simple DirectMedia Layer SDL2_image 2.0.2

Product URLs

https://www.libsdl.org/projects/SDL_image/

CVSSv3 Score

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

CWE

CWE-122: Heap-based Buffer Overflow

Details

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 XCF file (the native file type for Gimp), the LibSDL2 library implements a custom RLE inflater for the compressed XCF data. The code that handlese this is listed below:

//IMG_xcf.c
static unsigned char * load_xcf_tile_rle (SDL_RWops * src, Uint32 len, int bpp, int x, int y) 
	unsigned char * load, * t, * data, * d;
	Uint32 reallen;
      		int i, size, count, j, length;
  		unsigned char val;
t = load = (unsigned char *) SDL_malloc (len); //[1] reallen = SDL_RWread (src, t, 1, len); //[2]
	data = (unsigned char *) SDL_malloc (x*y*bpp);  //[3]
	[...]

From the above, it should be noted that the bpp,x, and y parameters are all under the file’s control, and also that the len parameter is calculated from x*y*6. At [1], memory is allocated to read in the compressed RLE data at [2]. Interestingly, the reallen parameter is never checked, so if the read fails, this RLE decompression will occur on the bytes already located in memory. At [3], space is allocated for the decompressed pixel data to be written. As expected, the size is the dimensions multiplied by the bytes-per-pixel. The code then proceeds to loop and populate the pixel data in the new buffer. Continuing from the previous code:

[...]
data = (unsigned char *) SDL_malloc (x*y*bpp);
for (i = 0; i < bpp; i++) { //[1]
		d    = data + i;      //[2]
		size = x*y;           
		count = 0;

		while (size > 0) { //size == amount of pixels (=>total size == x*y*bpp) //[3]
[...]

The outer loop at [1] designates that it is writing to the i’th byte for each pixel. Thus, if there are 8 bytes per pixel, the loop is going to be writing to the 0th,8th,16th…. N8th byte for the array of pixels, which is done for optimization purposes. The ‘d’ variable at [2] will be the pointer used to write into the destination, and the ‘size’ variable at [3] is used to determine if every Nbbp’th byte has been written to or not. Continuing on from this block:

[...]
while (size > 0) { //size == amount of pixels (=>total size == x*y*bpp) 
 		val = *t++;       // [1]
  		length = val;

  		if (length >= 128) { // Cast unsigned char => signed char
    			length = 255 - (length - 1); 
    			if (length == 128) { // [2]
      				length = (*t << 8) + t[1];
      				t += 2;
    			}

[…]

Using the ‘size’ variable to determine how many pixels have been written to, the code starts to read in the RLE compressed data and write to the newly allocated memory at ‘data’. The first byte read at [1], ‘length’, is used to determine how many pixels to write into the image. There are three different cases for this byte, the above code covers the cases of ‘length’ >= 0x80. If ‘length’ > 0x80, then the length is two’s complimented and the program assumes that the following data has not been compressed, and writes in different consecutive bytes. For example, if the following byte sequence was first read in, 0xFE 0xAA 0xBB, then the program would write 0xAA to the i’th byte of the allocated image memory, and 0xBB to the (x*bpp)+i byte, for x in range(0,length). After this, the program would look for another ‘length’ byte, since (0xFF – (0xFE-1)) == 2.

If the ‘length’ byte is 0x80, this designates that the program must read in the next two bytes as the length instead. Thus, for the byte sequence 0x80 0x30 0x30 0xAA 0xBB… The program would read in 0x3030 as the amount of bytes to write, and then write 0xAA to the i’th byte, 0xBB to the (lengthbpp)+i byte and so forth, until it writes 0x3030 bytes, ending at data+(0x3030bpp)+i.

In cases where ‘length’ < 0x80, it’s almost the same, except that the program assumes a compressed string of pixel data. Thus, for a byte sequence of 0x7f 0x30 0x30 0xAA, the program would write 0x30 to i’th byte of the first 0x7f pixels, and then 0xAA to the i’th byte of the next 0x30 pixels.

count += length;  // not used, lol
    	size -= length;     // [1] 

    	while (length-- > 0) { // [2] 
      		*d = *t++;
      		d += bpp;
}

But before all this writing occurs at [2], after the bytes have been parsed, the size variable is decremented by the length variable at [1]. Remember that the ‘size variable is used to determine if the program should use to continue writing. Unfortunately, there are no checks in between the size subtraction and the data writing, so if the length variable goes past the total size of the allocated buffer, the write will still occur, resulting in a heap-based overflow.

Crash Information

Program received signal SIGSEGV, Segmentation fault. 0x00007ffff784a196 in load_xcf_tile_rle (src=0xc63b00, len=0x309b, bpp=0x3, x=0x40, y=0x40) at IMG_xcf.c:510 warning: Source file is more recent than executable. 510 size -= length; —————[ registers ]—- $rax : 0x0000000000c8d000 $rbx : 0x0000000000000000 $rcx : 0x00007ffff781f620 -> 0x0000000100000000 $rdx : 0x0000000000000000 $rsp : 0x00007fffffffde80 -> 0x00007fffffffdef0 -> 0x00007fffffffdfa0 -> 0x00007fffffffe060 -> 0x00007fffffffe0a0 -> 0x00007fffffffe0d0 -> 0x00007fffffffe110 -> 0x0000000000000000 $rbp : 0x00007fffffffdef0 -> 0x00007fffffffdfa0 -> 0x00007fffffffe060 -> 0x00007fffffffe0a0 -> 0x00007fffffffe0d0 -> 0x00007fffffffe110 -> 0x0000000000000000 $rsi : 0x0000000000000039 $rdi : 0x00007ffff7dd4540 -> 0x000000000000003a (“:”?) $rip : 0x00007ffff784a196 -> <load_xcf_tile_rle+279> mov BYTE PTR [rax], dl $r8 : 0x0000000000000003 $r9 : 0x0000000000c656f0 -> 0x4220308041203080 $r10 : 0x000000000000023a $r11 : 0x0000000000000000 $r12 : 0x0000000000400a10 -> <_start+0> xor ebp, ebp $r13 : 0x00007fffffffe1f0 -> 0x0000000000000002 $r14 : 0x0000000000000000 $r15 : 0x0000000000000000 $eflags: [carry parity adjust zero sign trap INTERRUPT direction overflow RESUME virtualx86 identification] ——————-[ stack ]—- 0x00007fffffffde80|+0x00: 0x00007fffffffdef0 -> 0x00007fffffffdfa0 -> 0x00007fffffffe060 -> 0x00007fffffffe0a0 -> 0x00007fffffffe0d0 -> 0x00007fffffffe110 -> 0x0000000000000000 <-$rsp 0x00007fffffffde88|+0x08: 0x0000004000000040 (“@”?) 0x00007fffffffde90|+0x10: 0x0000309b00000003 0x00007fffffffde98|+0x18: 0x0000000000c63b00 -> 0x00007ffff7aace48 -> <stdio_size+0> push rbp 0x00007fffffffdea0|+0x20: 0x0000000000000000 0x00007fffffffdea8|+0x28: 0x80007ffff74ec97d 0x00007fffffffdeb0|+0x30: 0x0000000000c6d520 -> 0x00307f0080810041 (“A”?) 0x00007fffffffdeb8|+0x38: 0x00000010f7aacf03 ——–[ code:i386:x86-64 ]—- 0x7ffff784a183 <load_xcf_tile_rle+260> mov rax, QWORD PTR [rbp-0x8] 0x7ffff784a187 <load_xcf_tile_rle+264> lea rdx, [rax+0x1] 0x7ffff784a18b <load_xcf_tile_rle+268> mov QWORD PTR [rbp-0x8], rdx 0x7ffff784a18f <load_xcf_tile_rle+272> movzx edx, BYTE PTR [rax] 0x7ffff784a192 <load_xcf_tile_rle+275> mov rax, QWORD PTR [rbp-0x10] ->0x7ffff784a196 <load_xcf_tile_rle+279> mov BYTE PTR [rax], dl 0x7ffff784a198 <load_xcf_tile_rle+281> mov eax, DWORD PTR [rbp-0x60] 0x7ffff784a19b <load_xcf_tile_rle+284> cdqe
0x7ffff784a19d <load_xcf_tile_rle+286> add QWORD PTR [rbp-0x10], rax 0x7ffff784a1a1 <load_xcf_tile_rle+290> mov eax, DWORD PTR [rbp-0x24] 0x7ffff784a1a4 <load_xcf_tile_rle+293> lea edx, [rax-0x1] —-[ source:IMG_xcf.c+510 ]—- 506 t += 2; 507 } 508
509 count += length; // size=-0xc3f0L, length=0x2afaL -> 510 size -= length; 511
512 while (length– > 0) { //when we overstep, length == 0x27. 513 *d = *t++; 514 d += bpp; —————–[ threads ]—- [#0] Id 5, Name: “img_read_plain”, stopped, reason: SIGSEGV [#1] Id 4, Name: “img_read_plain”, stopped, reason: SIGSEGV [#2] Id 3, Name: “img_read_plain”, stopped, reason: SIGSEGV [#3] Id 2, Name: “img_read_plain”, stopped, reason: SIGSEGV [#4] Id 1, Name: “img_read_plain”, stopped, reason: SIGSEGV ——————-[ trace ]—- [#0] 0x7ffff784a196->Name: load_xcf_tile_rle(src=0xc63b00, len=0x309b, bpp=0x3, x=0x40, y=0x40) [#1] 0x7ffff784a55d->Name: do_layer_surface(surface=0xc4fae0, src=0xc63b00, head=0xc4f020, layer=0x818d30, load_tile=0x7ffff784a07f ) [#2] 0x7ffff784ae24->Name: IMG_LoadXCF_RW(src=0xc63b00) [#3] 0x7ffff78287ef->Name: IMG_LoadTyped_RW(src=0xc63b00, freesrc=0x1, type=0x7fffffffe4eb "(x_ x)") [#4] 0x7ffff78285e0->Name: IMG_Load(file=0x7fffffffe4ea "(x_ x)") [#5] 0x400b85->Name: main(argc=0x2, argv=0x7fffffffe1f8) ------------------------------------------------------------------------------------------------------------------------

Timeline

2017-11-28 - Vendor Disclosure
2018-03-01 - Public Release

Credit

Discovered by Lilith (>;_;)> of Cisco Talos.