漏洞分析一百篇-05-WindowsOLE应用程序EQBEDT32上栈溢出漏洞
CVE-2017-11882是windows OLE应用程序EQNEDT32.exe上的一个漏洞,可以实现远程代码执行。
前言
有关于OLE漏洞最近几年不断的曝光出来,CVE-2012-0158、CVE-2014-4114、CVE-2015-1641、CVE-2015-1641、CVE-2017-11882、CVE-2018-0802等等,盛产CVE而且利用难度不高,影响还是比较大的。Haifei Li(haifei.li@intel.com)和Bing Sun(bing.sun@intel.com)在这方面有两篇很好的文章:Attacking Interoperability: An OLE Edition和Moniker Magic: Running Scripts Directly in Microsoft Office,看雪上有大佬的译文,这里就以CVE-2017-11882为例,在OLE攻击面中分析与利用。
OLE编程与逆向
OLE编程
推荐两个资源COM编程精彩实例.pdf和IT民工的博客。这里说一些大概,更多细节可以从资源中了解到。
OLE(Object Linking and Embedding)是COM的一部分,COM由两部分组成,COM客户端和COM服务器,以CVE-2017-11882来说,office中的word进程winword.exe就是COM客户端,嵌入其中的EQNEDT32.exe就是COM服务器。COM服务器有两类,一类是DLL将自己映射到客户端进程空间,叫做进程中服务器;另外一类是EXE有着自己独立的进程空间,服务器和客户端通过LPC进行通信,叫做进程外服务器,也叫本地服务器。CVE-2017-11882中EQNEDT32.exe就是进程外服务器。
通过了解进COM程外服务器编程,我们知道OLE服务器的一些特征:
- 会调用CoInitialize()进行COM初始化工作,对于OLE是OleInitialize()函数
- OLE中会实现一些特定功能的接口函数,至少包括IUnknown、IPersist、IPersistStorage、IOleObject等。关于更多接口信息参考链接。对这些接口的初始化工作里OleInitialize()不远。
- 初始化完成后会调用CoRegisterClassObject将EXE对象注册到表中供其他程序使用。
- 会调用一些关键函数,如:coml2!CExposedDocFile::OpenStream(), coml2!CExposedStream::Read()
关于OLE客户端,如何获取CLSID和数据,在An OLE Edition论文中做了很全面的解释。将整个过程简单的表示出来,如下图:
OLE逆向
有了上面的基础,逆向就会相对简单一些。这里的逆向我们主要做的是静态分析工作,动态调试留到漏洞分析去讲。静态分析在IDA中我们主要做两个工作,识别初始化过程和接口函数。
识别初始化过程
在前面正向编程过程中我们知道一个关键函数OleInitialize()、CoRegisterClassObject(),定位到函数sub_40415B,该函数就是服务器程序的初始化函数了。
识别接口函数
接口函数的初始化工作应该离OleInitialize函数不会太远,可以肯定在sub_40415B初始化函数中了。通过OleView工具,我们看到了Microsoft Equation3.0注册了9个接口,
其中IclientSecurity和IMultiQI接口是供UWP应用使用的,对与desktop app我们会实现其余的7个接口,对比接口特征然后我们定位到sub_40440A,有很明显的分支结构,有大量的虚函数,我们判定sub_40440A应该是接口函数的初始化函数,然后参照IPersistStorageVtbl定义对比接口数量,IPersistStorage是10个,其中有10个虚函数的分支有两个(loc_404749和loc_4048B4),继续比较函数功能发现分支loc_404749是用来初始化IPersistStorage接口的,loc_4048B4是用来初始化IOleInPlaceActiveObject接口的。
其中IPersistStorageVtbl定义能通过MSDN查到,在Objidl.h中:
EXTERN_C const IID IID_IPersistStorage;
#if defined(__cplusplus) && !defined(CINTERFACE)
MIDL_INTERFACE("0000010a-0000-0000-C000-000000000046")
IPersistStorage : public IPersist
{
public:
virtual HRESULT STDMETHODCALLTYPE IsDirty( void) = 0;
virtual HRESULT STDMETHODCALLTYPE InitNew(
/* [unique][in] */ __RPC__in_opt IStorage *pStg) = 0;
virtual HRESULT STDMETHODCALLTYPE Load(
/* [unique][in] */ __RPC__in_opt IStorage *pStg) = 0;
virtual HRESULT STDMETHODCALLTYPE Save(
/* [unique][in] */ __RPC__in_opt IStorage *pStgSave,
/* [in] */ BOOL fSameAsLoad) = 0;
virtual HRESULT STDMETHODCALLTYPE SaveCompleted(
/* [unique][in] */ __RPC__in_opt IStorage *pStgNew) = 0;
virtual HRESULT STDMETHODCALLTYPE HandsOffStorage( void) = 0;
};
#else /* C style interface */
typedef struct IPersistStorageVtbl
{
BEGIN_INTERFACE
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
__RPC__in IPersistStorage * This,
/* [in] */ __RPC__in REFIID riid,
/* [annotation][iid_is][out] */
_COM_Outptr_ void **ppvObject);
ULONG ( STDMETHODCALLTYPE *AddRef )(
__RPC__in IPersistStorage * This);
ULONG ( STDMETHODCALLTYPE *Release )(
__RPC__in IPersistStorage * This);
HRESULT ( STDMETHODCALLTYPE *GetClassID )(
__RPC__in IPersistStorage * This,
/* [out] */ __RPC__out CLSID *pClassID);
HRESULT ( STDMETHODCALLTYPE *IsDirty )(
__RPC__in IPersistStorage * This);
HRESULT ( STDMETHODCALLTYPE *InitNew )(
__RPC__in IPersistStorage * This,
/* [unique][in] */ __RPC__in_opt IStorage *pStg);
HRESULT ( STDMETHODCALLTYPE *Load )(
__RPC__in IPersistStorage * This,
/* [unique][in] */ __RPC__in_opt IStorage *pStg);
HRESULT ( STDMETHODCALLTYPE *Save )(
__RPC__in IPersistStorage * This,
/* [unique][in] */ __RPC__in_opt IStorage *pStgSave,
/* [in] */ BOOL fSameAsLoad);
HRESULT ( STDMETHODCALLTYPE *SaveCompleted )(
__RPC__in IPersistStorage * This,
/* [unique][in] */ __RPC__in_opt IStorage *pStgNew);
HRESULT ( STDMETHODCALLTYPE *HandsOffStorage )(
__RPC__in IPersistStorage * This);
END_INTERFACE
} IPersistStorageVtbl;
interface IPersistStorage
{
CONST_VTBL struct IPersistStorageVtbl *lpVtbl;
};
而且在头文件能识别出个接口的IID,在IDA中标注出来,IDA也能自动帮我们完成这些工作。
typedef struct _GUID {
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[8];
} GUID;
typedef GUID IID;
typedef IID* REFIID;

漏洞分析
我们知道CVE-2017-11882和之前的漏洞一样,也是在IPersistStorage::Load()中出了问题,我们使用CFlags附加调试EQNEDT32.exe,在该函数下断点开始分析,利用栈回溯的方法执行到弹出计算器。查看调用栈如图:
整理出过程中关键的几个函数IPersistStorage::Load()中通过CExposedStream::Read(),分两次分别读入了EQNOLEFILEHDR和MTEFData,并在sub_42F8FF函数中处理MTEFData数据:
处理MTEFData会进入sub_43755C,该函数主要是循环读入record,根据record tag处理record content,当record tag=0x8(即读到FONT tag时)会进入sub_43A720,在这个函数中循环使用setjmp实现,和平常不同:
之后会进入到sub_43B418,该函数会读取fontName到指针lpLogFont。
最终在sub_41160F中,进行字符串拷贝时造成了栈溢出,变量&v12=ebp-2Ch,超过这个值时就会造成栈溢出了。
漏洞利用
在动态调试的过程中,我们能清楚看到程序对结构体的处理,当我们手头没有一份公开的结构体说明的时候,比较好的办法就是构造一个样本,提取其中的结构体。如果能从网上找到一些解释说明,对我们来说当然更好,这里有一份MTEF3格式和RTF格式的解释说明就直接拿来用了。
在实际漏洞挖掘中,漏洞分析本身就是构造POC的过程,通过分析我们知道漏洞主要问题出在对文档中Equation Native Stream Data的处理,Equation Native Stream Data = EQNOLEFILEHDR + MTEFData,其中MTEFData = MTEFHead + MTEF Byte Stream。参考一份正常样本,假设我们要触发EQNEDT32.EXE中的漏洞,构造出EQNIKEFUKEHDR必须满足:
struct EQNOLEFILEHDR {
WORD cbHdr; // 格式头长度,固定为0x1C。
DWORD version; // 固定为0x00020000。
WORD cf; // 该公式对象的剪贴板格式。
DWORD cbObject; // MTEF数据的长度,不包括头部。
DWORD reserved1; // 未公开
DWORD reserved2; // 未公开
DWORD reserved3; // 未公开
DWORD reserved4; // 未公开
};
同时我们知道问题具体是在MTEFByte中对FONT RECORD的处理,那就可以直接从正常样本中提取MTEF HEAD:
偏移量 | 说明 | 构造样本值 |
---|---|---|
0 | MTEF版本号 | 0x03 |
1 | 该数据的生成系统 | 0x01 表示Windows系统 |
2 | 该数据的生成产品 | 0x01 表示有公式编辑器生成 |
3 | 产品主版本号 | 0x03 |
4 | 产品子版本号 | 0x0A |
然后是对MTEF Byte Stream的构造,我们要满足下面的条件,并且为了触发漏洞,我们要构造FONT Record:
参考下图有MTEF Byte Stream = SIZE record + LINE record + FONT record(tag + typeface + style + name)。
之后就是很熟悉的一个过程了,对于完全没有缓解措施的应用程序,大家应该是信手拈来了,直接覆盖返回值,这里比较巧的是call WinEXE地址是0x00430c12,地址小端的写法刚好是字符串结尾。在winEXE中调用的参数,就是fontName。
Metasploit框架
长期没有写Ruby了,手生练一练。构造EXP时候主要解决一个问题就是如何扩展到metesploit的payload模块,我们的想法就是通过webclient服务,让靶机下载payload然后利用powershell执行payload。
在exploit模块中利用到了powershell和http模块,这里做两点笔记:
- primer接口,参考https://blog.rapid7.com/2012/12/17/metasploit-hooks/
- 利用regsvr32 scrobj.dll,执行powershell脚本,参考https://betanews.com/2016/04/25/bypass-applocker-security/
在github提供完整的[EXP]msf_CVE_2017_11882.rb。