漏洞分析一百篇-05-WindowsOLE应用程序EQBEDT32上栈溢出漏洞

Author Avatar
leo00000 7月 04, 2018

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 EditionMoniker Magic: Running Scripts Directly in Microsoft Office,看雪上有大佬的译文,这里就以CVE-2017-11882为例,在OLE攻击面中分析与利用。

OLE编程与逆向

OLE编程

推荐两个资源COM编程精彩实例.pdfIT民工的博客。这里说一些大概,更多细节可以从资源中了解到。
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服务器的一些特征:

  1. 会调用CoInitialize()进行COM初始化工作,对于OLE是OleInitialize()函数
  2. OLE中会实现一些特定功能的接口函数,至少包括IUnknown、IPersist、IPersistStorage、IOleObject等。关于更多接口信息参考链接。对这些接口的初始化工作里OleInitialize()不远。
  3. 初始化完成后会调用CoRegisterClassObject将EXE对象注册到表中供其他程序使用。
  4. 会调用一些关键函数,如: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模块,这里做两点笔记:

  1. primer接口,参考https://blog.rapid7.com/2012/12/17/metasploit-hooks/
  2. 利用regsvr32 scrobj.dll,执行powershell脚本,参考https://betanews.com/2016/04/25/bypass-applocker-security/

在github提供完整的[EXP]msf_CVE_2017_11882.rb