CVE-2018-3838
An exploitable information vulnerability exists in the XCF image rendering functionality of SDL2_image-2.0.2. A specially crafted XCF 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.
Simple DirectMedia Layer SDL2_image 2.0.2
https://www.libsdl.org/projects/SDL_image/
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 XCF file (the native file type for Gimp), the LibSDL2 library implements a custom RLE inflater for the compressed XCF data. The code that handles 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, along with the len
field, which is derived from the calling function as such:
do_layer_surface(SDL_Surface * surface, SDL_RWops * src, xcf_header * head, xcf_layer * layer, load_tile_type load_tile) {
SDL_RWseek(src, layer->hierarchy_file_offset, RW_SEEK_SET);
hierarchy = read_xcf_hierarchy(src); // [1]
level = NULL;
for (i = 0; hierarchy->level_file_offsets[i]; i++) {
SDL_RWseek(src, hierarchy->level_file_offsets[i], RW_SEEK_SET);
level = read_xcf_level(src); //[2]
ty = tx = 0;
for (j = 0; level->tile_file_offsets[j]; j++) {
SDL_RWseek(src, level->tile_file_offsets[j], RW_SEEK_SET);
[...]
if (level->tile_file_offsets[j + 1]) {
tile = load_tile(src, level->tile_file_offsets[j + 1] - level->tile_file_offsets[j], hierarchy->bpp, ox, oy); // [3]
} else {
tile = load_tile(src, ox * oy * 6, hierarchy->bpp, ox, oy);
}
[...]
To sum up the above, each XCF surface can have multiple layers, and these layers have a hierarchy, which is read in at [1] and dictates the order in which the image layers are rendered. Then, each layer has a set of tiles (which are just blocks of image data), which must be read in first before rendering. These tiles can be different sizes, so an array of file offsets are read in at [2], such that the parsing can be done correctly. After this, the library loops over the tile offsets, and seeks for the tile data in the file, reading the tile data at [3]. ‘load_tile’ is a function pointer to the load_xcf_tile_rle function that we were at before. Of most note for this bug, is the fact that the ‘len’ parameter from the load_xcf_tile_rle function is calculated from level->tile_file_offsets[j + 1] – level->tile_file_offsets[j]
, as long as level->tile_file_offsets[j+1] is != 0.
Thus, if we make our XCF file have two consecutive tile_file_offsets that are equivalent, our call to load_xcf_tile_rle becomes load_xcf_tile_rle(src,0,hierarchy->bpp,ox,oy). This causes issues further down inside load_xcf_tile_rle as we end up doing a call to malloc(0) for our source buffer:
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]
Since SDL_malloc is a simple wrapper around normal malloc, this depends on the specific implementation of malloc for the system, but at least on Debian Linux, m0560874168f86c7aeeee2edcf2cea202alloc(0) returns a valid buffer of the minimum chunk size possible for the given architecture, which is usually 16 bytes on a 64 byte system. But regardless of the exact byte count, the amount of bytes read in from this malloc(0) buffer for data is going to be far greater for any image of reasonable size, as the destination buffer’s size is xybpp. This results in a large user-controlled amount of data being read from the heap and populated into the XCF tile for further processing, creating an out of bounds read.
(Note: Compiled with ASAN)
Program received signal SIGSEGV, Segmentation fault.
0x00007f692455e127 in load_xcf_tile_rle (src=0x60700000dfb0, len=0x0, bpp=0x3, x=0x40, y=0x40) at IMG_xcf.c:496
warning: Source file is more recent than executable.
496
-------------------------------------------------------------------------[ registers ]----
$rax : 0x0000602000010000 -> 0x0000000000000000 -> 0x0000000000000000
$rbx : 0x00007ffd66fd8c30 -> 0x00007ffd66fda37f -> 0x3134373830363530 -> 0x3134373830363530 ("05608741"?)
$rcx : 0x22ffffff00000000 -> 0x22ffffff00000000
$rdx : 0x0000602000010001 -> 0x0000000000000000 -> 0x0000000000000000
$rsp : 0x00007ffd66fd88d0 -> 0x00007ffd66fd8940 -> 0x00007ffd66fd89f0 -> 0x00007ffd66fd8ab0 -> 0x00007ffd66fd8af0 -> 0x00007ffd66fd8b20
-> 0x0000000000000002 ->
0x0000000000000002
$rbp : 0x00007ffd66fd8940 -> 0x00007ffd66fd89f0 -> 0x00007ffd66fd8ab0 -> 0x00007ffd66fd8af0 -> 0x00007ffd66fd8b20 ->
0x0000000000000002 -> 0x0000000000000002
$rsi : 0x0000000000000013 -> 0x0000000000000013
$rdi : 0x00007f6924ae8540 -> 0x0000000000000014 -> 0x0000000000000014
$rip : 0x00007f692455e127 -> <load_xcf_tile_rle+168> movzx eax, BYTE PTR [rax]
$r8 : 0x0000000000000000 -> 0x0000000000000000
$r9 : 0x000000000000001e -> 0x000000000000001e
$r10 : 0x00000000004bd553 -> 0x6349643375c08548 -> 0x6349643375c08548
$r11 : 0x0000000000000008 -> 0x0000000000000008
$r12 : 0x00000000004bd3e4 -> <_start+0> xor ebp, ebp
$r13 : 0x00007ffd66fd8c20 -> 0x0000000000000002 -> 0x0000000000000002
$r14 : 0xfffffffffffffff8 -> 0xfffffffffffffff8
$r15 : 0x0000000000000000 -> 0x0000000000000000
$eflags: [carry PARITY adjust zero sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
-----------------------------------------------------------------------------[ stack ]----
0x00007ffd66fd88d0|+0x00: 0x00007ffd66fd8940 -> 0x00007ffd66fd89f0 -> 0x00007ffd66fd8ab0 -> 0x00007ffd66fd8af0 -> 0x00007ffd66fd8b20 ->
0x0000000000000002 -> 0x0000000000000002
<-$rsp
0x00007ffd66fd88d8|+0x08: 0x0000004000000040 -> 0x0000000000000000 -> 0x0000000000000000
0x00007ffd66fd88e0|+0x10: 0x0000000000000003 -> 0x0000000000000003
0x00007ffd66fd88e8|+0x18: 0x000060700000dfb0 -> 0x00007f69247c0e48 -> 0x20ec8348e5894855 -> 0x20ec8348e5894855
0x00007ffd66fd88f0|+0x20: 0x0000000000000000 -> 0x0000000000000000
0x00007ffd66fd88f8|+0x28: 0x00007f69236c097d -> 0x01c4f6038bc28948 -> 0x01c4f6038bc28948
0x00007ffd66fd8900|+0x30: 0x00006270001b2100 -> 0x0000be0000be0000 -> 0x0000be0000be0000
0x00007ffd66fd8908|+0x38: 0x00000000247c0f03 -> 0x00000000247c0f03
------------------------------------------------------------------[ code:i386:x86-64 ]----
0x7f692455e10f <load_xcf_tile_rle+144> mov DWORD PTR [rbp-0x1c], 0x0
0x7f692455e116 <load_xcf_tile_rle+151> jmp 0x7f692455e22a <load_xcf_tile_rle+427>
0x7f692455e11b <load_xcf_tile_rle+156> mov rax, QWORD PTR [rbp-0x8]
0x7f692455e11f <load_xcf_tile_rle+160> lea rdx, [rax+0x1]
0x7f692455e123 <load_xcf_tile_rle+164> mov QWORD PTR [rbp-0x8], rdx
->0x7f692455e127 <load_xcf_tile_rle+168> movzx eax, BYTE PTR [rax]
0x7f692455e12a <load_xcf_tile_rle+171> mov BYTE PTR [rbp-0x41], al
0x7f692455e12d <load_xcf_tile_rle+174> movzx eax, BYTE PTR [rbp-0x41]
0x7f692455e131 <load_xcf_tile_rle+178> mov DWORD PTR [rbp-0x24], eax
0x7f692455e134 <load_xcf_tile_rle+181> cmp DWORD PTR [rbp-0x24], 0x7f
0x7f692455e138 <load_xcf_tile_rle+185> jle 0x7f692455e1b0 <load_xcf_tile_rle+305>
--------------------------------------------------------------[ source:IMG_xcf.c+496 ]----
492 for (i = 0; i < bpp; i++) { //ah, we write every 'i'th byte in the Surface
493 d = data + i; //d is the offending variable...
494 size = x*y; //0x1000
495 count = 0;
-> 496
497 while (size > 0) { //size == amount of pixels (=>total size == x*y*bpp)
498 val = *t++; // #! t => OOB. Size starts at 0x1000
499
500 length = val; //length == length of consecutive pixels that are same color?
---------------------------------------------------------------------------[ threads ]----
[#0] Id 1, Name: "", stopped, reason: SIGSEGV
-----------------------------------------------------------------------------[ trace ]----
[#0] 0x7f692455e127->Name: load_xcf_tile_rle(src=0x60700000dfb0, len=0x0, bpp=0x3, x=0x40, y=0x40)
[#1] 0x7f692455e55d->Name: do_layer_surface(surface=0x60800000be20, src=0x60700000dfb0, head=0x60700000df40, layer=0x60600000eea0,
load_tile=0x7f692455e07f <load_xcf_tile_rle>)
[#2] 0x7f692455ee24->Name: IMG_LoadXCF_RW(src=0x60700000dfb0)
[#3] 0x7f692453c7ef->Name: IMG_LoadTyped_RW(src=0x60700000dfb0, freesrc=0x1, type=0x0)
[#4] 0x7f692453c5e0->Name: IMG_Load(file=0x7ffd66fda37f "0560874168f86c7aeeee2edcf2cea202")
[#5] 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.