从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ

图片[1]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

作者:维阵漏洞研究员–lawhack

微软的office产品内置了一个名为公式编辑器的组件,该组件主要用来对文档中的Object Linking and Embedding(OLE)对象进行编辑,由于时间久远,该组件也长久没有更新,自从2017年开始,爆出了cve-2017-11882等高危漏洞,用户打开一个特定的rtf文档就会被劫持,触发漏洞导致主机被控制。而WPS内部同样有公式编辑器这一组件,这一组件和office的有所不同,今天笔者对该组件的加载过程进行了逆向分析,并能够通过自己编写的程序主动启动公式编辑器并加载、显示文件中的ole对象。

应用版本

wps office:11.1.0.10000

EqnEdit.exe:2007.12.5.0

分析过程

目标分析

首先确定了ole对象的载体文档为rtf,因为根据office之前爆出的漏洞,在rtf文档通过添加objupdate等关键字,可以使用户在打开文档时,直接触发公式编辑器组件的加载而不必用户去双击激活对应的ole对象。但是在这里WPS并没有提供这样的”便利”,打开构造好的样本,WPS并没有加载公式编辑器组件,还是需要主动双击或者右键激活的方式来加载组件。接下来就看下wps中哪些应用库负责加载该公式编辑器组件。

整体性分析

利用ProcessMonitor等程序对组件的加载过程进行整体性分析。首先关注的点是rtf文档的加载过程,打开wps office后,利用procmon监控wps的执行,打开对应的rtf文档(Embed Equatoin.rtf),procmon的显示如下:

图片[2]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

查看函数调用栈如下:

图片[3]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

并没有值得注意的,仅仅是获取文件的信息,和rtf文档的解析无关,紧接着,看到如下的文件操作:

图片[4]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

查看函数调用栈如下:

图片[5]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

可以看到wpsmain.dll中调用了StgOpenStorage函数,该函数主要用来打开一个复合文档文件,并将其进行结构化存储,保存在IStorage类型的结构体中,在ole2中是通用的对复合文档的存储方式。继续看下去,如下:

图片[6]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

调用栈如下:

图片[7]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

通过名字不难猜测,rtfreader.dll主要用来对rtf文档进行解析。接下来会看到有趣的一点,如下:

图片[8]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

虽然在rtfreader中并没有主动激活公式编辑器组件的功能,在这里还是通过com接口对EqnEdit.exe程序所在的文件进行了访问,如果打开procmon中的注册表信息查看功能,我们将看到更多相关的信息,对我们理解整个EqnEdit程序的加载过程略有帮助。如下:

图片[9]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

查看调用栈,发现大部分行为主要集中于OleConvertOLESTREAMToIStorage这个函数中:

图片[10]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

这个函数主要用来将复合文档中的ole1流对象转换为ole2结构化存储对象,说明此时rtfreader已经对rtf文档的公式编辑器对象(Equation Native Obj)进行了解析、保存等操作,但是并没有主动调用公式编辑器进行加载、显示。接下来双击WPS文档中显示的公式编辑器对象,同时在procmon中添加processname为EqnEdit.exe,则可以看到行为如下:

图片[11]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

可以看到EqnEdit被创建,查看详细发现父进程并不是wps.exe,而是svchost.exe:

图片[12]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科图片[13]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

因为在wps中是通过com接口调用EqnEdit,因此是由特定的服务DcomLaunch负责将LocalServer类型的程序载体启动,对com应用比较熟的话可能对此更加了解。

查看对应的调用栈如下:

图片[14]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

可以看出是由函数OleLoad负责加载的,但是其实真正创建EqnEdit程序的并不是这个api,而是由OleRun这个函数负责创建的,后面再细说。

这样我们大概了解了整个EqnEdit程序的加载流程,由rtfreader.dll负责对rtf文档进行解析,将其中的公式编辑器对象内容提取并保存为ole2的结构化存储模式,随后由kso.dll负责加载、激活公式编辑器组件,让公式编辑器能够正常显示。接下来就看下具体的函数是哪些了,后面需要自己写代码将公式编辑器加载起来。

代码级分析

首先查看rtfreader.dll利用到的具体函数,定位到调用OleConvertOLESTREAMToIStorage的函数中,ida显示如下:

图片[15]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科图片[16]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

由于之前用Dynamorio跑过WPS,因此可以看到程序覆盖状况,标记深色的说明是程序执行过的代码。这里利用windbg进行调试,可以看到第二个参数就是rtf中的公式编辑器对象,如下:

图片[17]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

在010editor中打开rtf文档,可以看到对应的obj内容如下:

图片[18]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

rtf在保存数据时都是以16进制字符串保存的,所以要看原内容将其转换下即可:

图片[19]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

可以看出和调试器中显示的参数内容一致。这里做出猜测,需要进行转换的数据内容就是这里了,在后续写程序时,复合文档所需要的内容也就是这部分,而不是整个rtf文档。

随后就是申请全局内存并通过调用CreateStreamOnHGlobal 函数将数据保存为IStream类型,通过StgCreateDocfile创建结构化存储文档,并通过OleConvertOLESTREAMToIStorage进行转换。

图片[20]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

但这里需要注意的是第一个参数OLESTREAM是需要自己实现内部的读写函数。

图片[21]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

接下来程序会创建一个objinfo流,如下:

图片[22]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

抛开解析rtf文档外,基本上rtfreader为加载EqnEdit程序所需要做的就这些了,接下来看下kso.dll的主要实现。

定位到KAxOleObjectSite::_loadObject函数,ida查看如下:

图片[23]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

在104AA00D函数中,负责ole加载工作,如下:

图片[24]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

图片[25]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

看下OleLoad的函数参数如下:

图片[26]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

其中pStg来源于在rtfreader中转换后的IStorage结构化存储数据,猜测是同类的,如下:

图片[27]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

0:000> dds 05dc0818  l405dc0818  76a2121c coml2!CExposedDocFile::`vftable'05dc081c  76a21200 coml2!CExposedDocFile::`vftable'05dc0820  05dc081805dc0824  05dc08400:000> dds 76a2121c l476a2121c  76a2b750 coml2!CExposedDocFile::QueryInterface76a21220  76a2b260 coml2!CExposedDocFile::AddRef76a21224  76a31c90 coml2!CExposedDocFile::Release76a21228  76a429f0 coml2!CExposedDocFile::CreateStream
0:000> dds 0e2b1c68 0e2b1c68  76a2121c coml2!CExposedDocFile::`vftable'0e2b1c6c  76a21200 coml2!CExposedDocFile::`vftable'0e2b1c70  0e2b1c680e2b1c74  0e2b1c90

虽说地址发生了变化,但内容应该是一致的,第二个参数是要启动的com组件的接口标识符,这里EqnEdit.exe的CLSID为{0002CE21-0000-0000-C000-000000000046},而要交互的接口的iid为{00000112-0000-0000-C000-000000000046},表明要获取的接口类型为IOleObject,在oleviewdotnet中也可以看到,如下:

图片[28]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

通过这个接口暴露出的函数我们可以让EqnEdit.exe程序窗口显示。

返回到KAxOleObjectSite::_loadObject函数中,可以看到,通过OleLoad函数拿到了OLEOBJECT对象,接着通过调用OleRun函数来启动对应的com程序即EqnEdit.exe程序,此时,如果是在调试状态的话,当运行了OleRun函数后,可以观察到EqnEdit.exe已经正常启动。

eax=06ab2094 ebx=0ef20d30 ecx=06f93928 edx=6c97e984 esi=06f93928 edi=06f93944eip=6cd66682 esp=0019c484 ebp=0019c4b0 iopl=0         nv up ei pl nz na po nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202kso!KAxOleObjectSite::_loadObject+0x7d:6cd66682 ff1560f2ea6e    call    dword ptr [kso!KKeyboardHook::s_setPopupWidgets+0xf78 (6eeaf260)] ds:002b:6eeaf260={ole32!OleRun (7702bfb0)}0:000> peax=00000000 ebx=0ef20d30 ecx=0019c450 edx=00000001 esi=06f93928 edi=06f93944eip=6cd66688 esp=0019c488 ebp=0019c4b0 iopl=0         nv up ei pl nz na po nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202kso!KAxOleObjectSite::_loadObject+0x83:6cd66688 89450c          mov     dword ptr [ebp+0Ch],eax ss:002b:0019c4bc=00000000

图片[29]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

但此时窗口并没有显示,真正要激活窗口还需要执行对应的动作,主要实现在KAxOleObjectSite::DoVerb中,如下:

图片[30]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科图片[31]-从逆向到开发,启动wps公式编辑器 – 作者:极光无限SZ-安全小百科

只需要执行默认的0号动作即可将EqnEdit.exe窗口激活显示,调试时信息如下:

0:000> reax=00000000 ebx=06f93958 ecx=7707d250 edx=770031c8 esi=06f93928 edi=0c838468eip=6cd671bd esp=0019c55c ebp=0019c59c iopl=0         nv up ei pl nz na po nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202kso!KAxOleObjectSite::DoVerb+0x106:6cd671bd ffd1            call    ecx {ole32!CDefObject::DoVerb (7707d250)}0:000> dd esp l80019c55c  06ab2094 00000000 00000000 06f939580019c56c  00000000 00372608 00000000 a70b1960

那么大概的流程以及关键的代码函数都比较清楚了,接下来就是开发过程。

开发过程

在开发前需要了解整个com客户端的开发过程,推荐阅读ole2高级编程这本书,虽然比较过时,但是内容非常丰富,主要看第九章OLE相关的。如果仅是简单的实现激活EqnEdit程序的话就没必要实现所有的组件如IAdviseSink、IOleClientSite。

到这里依旧不完全确定前面所了解的内容是否能够最终激活EqnEdit程序,需要我们去一步步调试去解决出现的问题。

1.首先要调用ole2.0相关api,需要包含Ole2.0头文件,并且包含对应的lib库。

#include#pragma comment( lib, "ole32.lib" )

2.调用OleInitialize()函数,如果没有调用的话,后面会报错。

3.读取本地的公式编辑器二进制数据,这里我将对应的内容提取到文件中,只需读取该文件即可。

4.将二进制数据转换为IStorage结构化存储数据,通过调用CreateStreamOnHGlobal、StgCreateDocfile、OleConvertOLESTREAMToIStorage函数即可。

5.实现IAdviseSink、IOleClientSite接口,笔者使用纯C的方式来实现,如下:

IOleClientSiteVtbl* myclientvtbl = new IOleClientSiteVtbl;    IOleClientSite myclientsite;  myclientsite.lpVtbl = myclientvtbl;    myclientsite.lpVtbl->QueryInterface = queryinterface;    myclientsite.lpVtbl->AddRef = addref;    myclientsite.lpVtbl->Release = release;    myclientsite.lpVtbl->SaveObject = saveobj;    myclientsite.lpVtbl->GetMoniker = getmoniker;    myclientsite.lpVtbl->GetContainer = getcontainer;    myclientsite.lpVtbl->OnShowWindow = onshowwindow;    myclientsite.lpVtbl->ShowObject = showobj;    myclientsite.lpVtbl->RequestNewObjectLayout = requestnew;

6.调用OleLoad函数获取对应的OleObject对象:

res = OleLoad(docstorage, IID_IOleObject, (LPOLECLIENTSITE)newclientsite, (LPVOID*)&myoleobj);

7.设置hostnames:

myoleobj->lpVtbl->SetHostNames(myoleobj,L"WPS文字", L"newtest.rtf");

8.调用OleRun执行对应的com对象:

OleRun((LPUNKNOWN)myoleobj);

9.调用OleSetContainedObject 进行通知:

res = OleSetContainedObject((LPUNKNOWN)myoleobj, 1);

10.调用OleObject对象的doverb接口函数,激活EqnEdit.exe:

res = myoleobj->lpVtbl->DoVerb(myoleobj,0, 0, (LPOLECLIENTSITE)&myclientsite, 0, (HWND)0, 0);

到了这一步,基本上EqnEdit.exe就会弹出并且将要解析的符号内容显示出来。

11.最后调用OleUninitialize结束,当然最好在之前将所有申请的资源进行释放。

视频演示:

https://www.bilibili.com/video/BV13p4y1k7CM

今天的文章到这里就结束了,期待更多分享。

来源:freebuf.com 2020-10-30 18:48:15 by: 极光无限SZ

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论