从逆向到开发,启动wps公式编辑器

  • A+
所属分类:逆向工程

从逆向到开发,启动wps公式编辑器

从逆向到开发,启动wps公式编辑器

一次对WPS内部公式编辑器加载过程的逆向分析。


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


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

- 01 -

应用版本


wps office:11.1.0.10000
EqnEdit.exe:2007.12.5.0


- 02 -

分析过程


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

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

查看函数调用栈如下:
从逆向到开发,启动wps公式编辑器

此处并没有值得注意的,仅仅是获取文件的信息,和rtf文档的解析无关。

紧接着,看到如下的文件操作:
从逆向到开发,启动wps公式编辑器
 
查看函数调用栈如下:
从逆向到开发,启动wps公式编辑器

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

通过名字不难猜测,rtfreader.dll主要用来对rtf文档进行解析。接下来会看到有趣的一点,如下:
从逆向到开发,启动wps公式编辑器

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

查看调用栈,发现大部分行为主要集中于
OleConvertOLESTREAMToIStorage这个函数中:
从逆向到开发,启动wps公式编辑器

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

可以看到EqnEdit被创建,查看详细发现父进程并不是wps.exe,而是svchost.exe:
从逆向到开发,启动wps公式编辑器
 从逆向到开发,启动wps公式编辑器

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

查看对应的调用栈如下:
从逆向到开发,启动wps公式编辑器

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

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

代码级分析
首先查看rtfreader.dll利用到的具体函数,定位到调用
OleConvertOLESTREAMToIStorage的函数中,ida显示如下:
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器

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

在010editor中打开rtf文档,可以看到对应的obj内容如下:
从逆向到开发,启动wps公式编辑器

rtf在保存数据时都是以16进制字符串保存的,所以要看原内容将其转换下即可:
从逆向到开发,启动wps公式编辑器

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

随后就是申请全局内存并通过调用CreateStreamOnHGlobal 函数将数据保存为IStream类型,通过StgCreateDocfile创建结构化存储文档,并通过
OleConvertOLESTREAMToIStorage进行转换。
从逆向到开发,启动wps公式编辑器

但这里需要注意的是第一个参数OLESTREAM是需要自己实现内部的读写函数。
从逆向到开发,启动wps公式编辑器

接下来程序会创建一个objinfo流:
从逆向到开发,启动wps公式编辑器

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

定位到KAxOleObjectSite::_loadObject函数,查看ida:
从逆向到开发,启动wps公式编辑器

在104AA00D函数中,负责ole加载工作:
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器

OleLoad的函数参数如下:
从逆向到开发,启动wps公式编辑器

其中pStg来源于在rtfreader中转换后的IStorage结构化存储数据,我猜测是同类的:
从逆向到开发,启动wps公式编辑器
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中也可以看到,如下:
从逆向到开发,启动wps公式编辑器

通过这个接口暴露出的函数我们可以让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

从逆向到开发,启动wps公式编辑器

但此时窗口并没有显示,真正要激活窗口还需要执行对应的动作,主要实现在
KAxOleObjectSite::DoVerb中,如下:
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器

只需要执行默认的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

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

- 03 -

开发过程


在开发前需要了解整个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结束,当然最好在之前将所有申请的资源进行释放。

- 04 -
视频演示

今天的文章到这里就结束了,敬请期待后续分享。

往期精选

从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器
从逆向到开发,启动wps公式编辑器

本文始发于微信公众号(极光无限):从逆向到开发,启动wps公式编辑器

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: