CVE-2017-8543 Windows Search漏洞分析及POC关键部分

2017-07-18 18:37:00 启明星辰 ADLab

更多资讯和分析文章请关注启明星辰ADLab微信公众号及官方网站(adlab.venustech.com.cn)

漏洞描述




2017年6月,微软发布的补丁修复了多个远程执行漏洞,其中包括CVE-2017-8543 Windows Search搜索漏洞(CNVD-2017-09381,CNNVD-201706-556),该漏洞几乎影响所有的Windows操作系统。对于Windows XP和Windows Server 2003等停止更新的系统,微软也发布了对应的补丁,用户可以手动下载补丁进行安装。

Windows搜索服务(Windows Search Service,WSS)是Windows的一项默认启用的基本服务,用于建立和维护文件系统索引。由于WSS在解析搜索请求时,存在内存越界漏洞,可能导致远程代码执行。



协议分析





当客户端对远程主机发起搜索请求后,它们之间使用Windows搜索协议(Windows Search Protocol,WSP)进行数据交互。交互的消息序列如下所示。其中,CPMConnectIn 消息中包括服务器的名称和索引名称(默认Windows\SYSTEMINDEX)。服务器验证客户端的权限后建立会话,回复CPMConnectOut消息; CPMCreateQueryIn消息用于设置查询的文件目录范围、关键字信息等; CMPSetBindingsIn消息用于设置返回的查询结果内容,例如文件名称、文件类型等;CPMGetRowsIn消息用于请求查询结果。

以上信息的Header需遵循以下格式,Header大小为0x10。

其中,_msg表示消息类型,常用的消息类型如下所示。

与该漏洞成因相关的两个消息是CPMSetBindingsIn和CPMGetRowsIn。首先介绍CPMSetBindingsIn消息,消息的格式如下所示。

struct CPMSetBindingsIn

{

int msg_0;

int status_4;

int ulCheckSum_8;

int ulReserved2_c;

int hCursor_10;

int cbRow_14;

int cbBindingDesc_18;

int dummy_1c;

int cColumns_20;

struct Column aColumns[SIZE];

};

前0x10字节是消息Header;hCursor 是CPMCreateQueryOut消息返回的句柄;cbRow表示row的长度,以字节为单位;aColumns是Column类型结构体数组;cColumns是数组的长度。在这里,每一行(row)代表一条查询结果,每一列(column)代表查询结果属性,例如文件名称、文件类型等。


文件名称

文件类型

...

1条查询结果row[0]

column[0]

column[1]

...

2条查询结果row[1]

column[0]

column[1]

...

....

column[0]

column[1]

...

CPMSetBindingsIn中的Column结构体定义如下:

struct Column

{

    struct CFullPropSpec cCFullPropSpec;

    int   Vtype;

    char  AggregateUsed;  

    char  AggregateType;

    char  ValueUsed;   

    char  padding1;

    short ValueOffset;

    short ValueSize;  

    char  StatusUsed;    

    char  padding2;      

    short StatusOffset;   

    char  LengthUsed;    

    char  padding3;

    short LengthOffset;   

}

struct CFullPropSpec

    {

        char GUID[0x10];

        int ulKind;

        int PrSpec;

     }

其中,GUID标志所代表的属性,例如guidFilename=E05ACF41-5AF70648-BD8759C7-D9248EB9代表文件名称。

Vtype表示column对应的数据类型。常用数据类型如下表,在CPMSetBindingsIn消息中,Vtype一般取值0x0c。

Value

含义

VT_I4

0x0003

A 4-byte signed integer.

VT_UI4

0x0013

A 4-byte unsigned integer.

 

VT_LPSTR

0x001E

A null-terminated string using the system code page.

 

VT_LPWSTR

0x001F

A null-terminated, 16-bit Unicode string. See [UNICODE].

Note The protocol uses UTF-16 LE encoding.

VT_VARIANT

0x000C

CBaseStorageVariant.

 

ValueOffset表示在每一行(row),该column数据存放的偏移位置,ValueSize表示这个column数据所占内存大小。

当收到CPMSetBindings消息时,程序调用DoSetBindings进行数据解析。DoSetBindings是CRequestServer类的成员函数。CRequestServer类中还包括其他解析函数,例如DoCreateQuery、DoGetRows等。数据成员cCProxyMessage_c0即为接收的数据Buffer。


class CRequestServer

{

public:

    void DoConnect(unsigned long len,unsigned long&var)();     //解析CPMConnectIn消息

    void DoCreateQuery(unsigned long len,unsigned long&var);   //解析CPMCreateQueryIn消息

    voidDoSetBindings(unsigned longlen,unsigned long&var); //解析CPMSetBindingsIn消息

    void DoGetRows(unsignedlong len,unsignedlong &var)();  //解析CPMGetRowsIn消息

.....

private:

...

    CVIQuery *pCVIQuery_5c;

    XArray *pXArray_6c;

    CProxyMessagecCProxyMessage_c0; 

...

}

DoSetBindings函数的实现如下所示。

void DoSetBindings(unsignedlong len,unsignedlong &var)

    {

        CPMSetBindingsIn*pCPMSetBindingsIn = &cCProxyMessage_c0;

        pCPMSetBindingsIn->ValidateCheckSum(var_40,len);

        struct CMemDeSerStream* pCMemDeSerStream = new pCMemDeSerStream((char*)pCPMSetBindingsIn);

        class CPidMapper* pCPidMapper=new CPidMapper(0);

        CTableColumnSet * pCTableColumnSet = new CTableColumnSet(pCMemDeSerStream, pCPidMapper);

        pCVIQuery_5c->SetBindings(pCPMSetBindingsIn->hCursor_10,

            pCPMSetBindingsIn->cbRow_14,

            pCTableColumnSet,

            pCPidMapper);

    }

(1)DoSetBindings函数首先初始化pCPMSetBindingsIn指针,使其指向接收的CPMSetBindingsIn数据,然后使用pCPMSetBindingsIn指针初始化CMemDeSerStream类。CMemDeSerStream类用于完成各个字段的读取。

(2)使用pCMemDeSerStream指针初始化CTableColumnSet类。CTableColumnSet类和CPidMapper类都是CCountedDynArray类的派生类。CCountedDynArray是一个数组类,数据成员包含一个指针数组Array_4。CTableColumnSet类构造函数首先调用GetULong获得数组长度cColumns作为循环次数,然后循环解析aColumns数组元素。在while循环中:

  • 解析column结构中的CFullPropSpec结构,将对象指针&CFullPropSpec添加到CPidMapper中:

pCPidMapper->array_4[CurrentIndex]= &cCFullPropSpec

  • 解析column结构中的其他字段,并保存到CTableColumn类,将对象指针pCTableColumn添加到CTableColumnSet中:

pCTableColumnset->array_4[RetIndex]= pCTableColumn

CTableColumnSet(CMemDeSerStream*pCMemDeSerStream, CPidMapper* pCPidMapper)

    {

        int_ColumnCount = pCMemDeSerStream->GetULong();

        SetExactSize(_ColumnCount);

        char GUID[16]={0};

        intcount = 0;

        do{

            CFullPropSpeccCFullPropSpec(pCMemDeSerStream);  //解析CFullPropSpec

            if(0==cCFullPropSpec.IsValid())

                gotoerror;

            intRetIndex = pCPidMapper->NameToPid(&cCFullPropSpec,0,0); 

            CTableColumn *pCTableColumn = new CTableColumn(RetIndex,1);  //解析CTableColumn

            Add(pCTableColumn,RetIndex);            count++;

        }while(count<_ColumnCount);

}

(3)将pCPidMapper和pCTableColumnset作为参数传入到CVIQuery:: SetBindings中。CVIQuery:: SetBindings函数调用了CTableCursor::CheckBindings,在while循环中,依次获取pCTableColumnset中的CTableColumn元素,调用checkBinding检测CTableColumn有效性。

int CheckBindings(CTableColumnSet*pCTableColumnSet,CTableRowAlloc *pCTableRowAlloc,intcbRow)

    {

        int index=0;

        int result;

        if(!pCTableColumnSet->CurrentIndex)

            return 0;

        while(1)

        {

            CTableColumn*pCTableColumn = pCTableColumnSet->Get(index);

            result = CheckBinding(pCTableColumn, pCTableRowAlloc, cbRow);

 

            if ( result < 0 )

                break;

            if ( ++index >= pCTableColumnSet->CurrentIndex)

                return 0;

        }

        return result;

    }

 

    int CheckBinding(CTableColumn*pCTableColumn,CTableRowAlloc *pCTableRowAlloc,intcbRow)

    {

        pCTableColumn->Validate(cbRow,0);

        //.......

    }

CTableCursor::checkBinding调用CTableColumn::Validate进行验证,如果ValueSize + ValueOffset大于cbRow,将抛出异常,以防内存越界。

void validate(intcbRow,bool flag)

    {

        try

        {

            if(ValueSize_06 + ValueOffset_04>cbRow)

                throw 0x80040E08;

        }

 

    }

接下来介绍CPMGetRows消息,CPMGetRowsIn消息格式如下:

struct CPMGetRowsIn

{  

    int msg_0;

    intstatus_4;

    intulCheckSum_8;

    intulReserved2_c;

    inthCursor_10;

    intcRowsToTransfer_14;

    intcbRowWidth_18;

    intcbSeek_1c;

    intcbReserved_20;

    intcbReadBuffer_24;

    intulClientBase_28;

    intfBwdFetch_2c;

    int eType_30;

    intchapt_3C;

    union

    {

        CRowSeekAt cCRowSeekAt;

        CRowSeekAtRatio cCRowSeekAtRatio;

        CRowSeekByBookmark cCRowSeekByBookmark;

        CRowSeekNext cCRowSeekNext;

    }

}

CPMGetRowsOut的消息格式如下:

struct CPMGetRowsOut

{

    int msg_0;

    intstatus_4;

    intulCheckSum_8;

    intulReserved2_c;

    intcRowsReturned_10;

    inteType_14;

    intchapt_18;

    //Rows_offset;

}

在CPMGetRowsIn消息中,cbRowWidth表示row长度,与CPMSetBindingsIn消息中的cbRow意义相同。cbReadBuffer表示用于存放CPMGetRowsOut消息的buffer大小;cbReserved表示Rows数据在CPMGetRowsOut消息中的偏移;eType表示查询的方法,取值范围如下表所示。

在CPMGetRowsOut消息中,对于每一行(row)中的列(column),column数据使用CTableVariant类表示。CTableVariant结构定义如下。其中Vtype表示数据类型,取值范围见前文Vtype常用数据类型表所示。如果Vtype为字符串等变长数据类型,offset则指向的该变长数据偏移位置。CTableVariant结构存放在valueoffset指定的位置,变长数据则存放在内存末尾位置,在后面解析代码中进行说明。

当接收CPMGetRowsIn数据,调用DoGetRows函数,函数实现如下所示。

void DoGetRows(unsigned long len,unsigned long &var)

    {

        CMPGetRowsOut *pCMPGetRowsOut =cCProxyMessage_c0;

        CPMGetRowsIn *pCPMGetRowsIn =&cCProxyMessage_c0;

        pCPMGetRowsIn->ValidateCheckSum(var_40,len);

        char*pCPMGetRowsIn_eType_30 = &pCPMGetRowsIn->eType_30;

        char*pCPMGetRowsIn_eType_cbseek= (char*)&pCPMGetRowsIn->eType_30 +                             pCPMGetRowsIn->cbSeek_1c;

        structCMemDeSerStream* pCMemDeSerStream = newpCMemDeSerStream(pCPMGetRowsIn_eType_30,

        *pCPMGetRowsIn_eType_cbseek);

 

        CRowSeekMethod* pCRowSeekMethod=0;

        UnmarshallRowSeekDescription(pCMemDeSerStream,&pCRowSeekMethod,0); 

        inta2=0;

        if(pCPMGetRowsIn->cbReadBuffer_24>0x1300)                                                        pXArray_6c->init(pCPMGetRowsIn->cbReadBuffer_24);

        char *pArray = pXArray_6c->pArray_0;

        if(pArray){

            *(DWORD*)pArray = 0xcc;

            *(DWORD*)(pArray + 4) = 0;

            *(DWORD*)(pArray + 8) = 0;

            *(DWORD*)(pArray + c) = 0;

        }

        pCMPGetRowsOut =pXArray_6c->pArray_0;

        CFixedVarBufferAllocator cCFixedVarBufferAllocator(

            pCMPGetRowsOut,

            a2,

            pCPMGetRowsIn->cbReadBuffer_24,

            pCPMGetRowsIn->cbRowWidth_18,

            pCPMGetRowsIn->cbReserved_20);

        intflag =1;

        CGetRowsParams cCGetRowsParams(

            pCPMGetRowsIn->cRowsToTransfer_14,

            flag,

            pCPMGetRowsIn->cbRowWidth_18,

            &cCFixedVarBufferAllocator);

        CRowSeekMethod *pCRowSeekMethod_new;

        pCVIQuery_5c->GetRows(

            pCPMGetRowsIn->hCursor_10,

            pCRowSeekMethod,

            &cCGetRowsParams,

            &pCRowSeekMethod_new);

}

(1)UnmarshallRowSeekDescription函数根据etype类型(eRowSeekNext,eRowSeekAt,eRowSeekAtRatio或eRowSeekByBookmark),返回SeekMethod方法对象。

(2)如果cbReadBuffer_24长度大于0x1300,分配新内存存放CMPRowsOut, pCMPGetRowsOut指向分配的地址。

(3)使用pCMPGetRowsOut指针初始化CFixedVarBufferAllocator类对象。CFixedVarBufferAllocator构造函数如下所示。其中两个关键的数据成员:RowBufferStart地址为rows数据的基地址,RowBufferEnd表示当前可用的末尾地址。

CFixedVarBufferAllocator(char *ReadBuffer,inta1,int cbReadBuffer,intcbRowWidth,int cbReserved)

    {

 

        pvatable_0= &CFixedVarBufferAllocator::`vftable'{for`PVarAllocator'};

        isequal_4= (ReadBuffer != 0);

        pvatable_8= &CFixedVarBufferAllocator::`vftable'{for`PFixedAllocator'};

        ReadBuffer_0c= ReadBuffer;

        ReadBuffer_10= ReadBuffer;

        var_14= a1;

        RowBufferStart_18 = (char *)ReadBuffer + cbReserved;

        RowBufferEnd_1c = (char *)ReadBuffer + cbReadBuffer;

        cbRowWidth_20 =cbRowWidth;

        cbReserved_24= cbReserved;

 

        while (RowBufferEnd_1c & 7 )

        {

            --RowBufferEnd_1c;

        }

    }

(4)使用对象地址&cCFixedVarBufferAllocator,cbRowWidth等参数初始化CGetRowsParams对象。最后调用CVIQuery:: GetRows函数。

int CVIQuery::GetRows(int hCursor,

        CRowSeekMethod*pCRowSeekmethod,

        CGetRowsParams*pCGetRowsParams,

        CRowSeekMethod*pCRowSeekMethod_new)

    {

        int result;

        CItemCursor*pCItemCursor = *(DWORD *)(var_68 + 4*hCursor);

        CTableCursor*pCTableCursor = pCItemCursor + 0x14;

        pCTableCursor->ValidateBindings();  //检查pCTableCursor->pCTableColumnSet_4是否为

        result = pCRowSeekmethod->GetRows(pCTableCursor,

            pCItemCursor,

            pCGetRowsParams,

            pCRowSeekMethod_new);

        returnresult;

        //.................

    } 

假设etype=eRowSeekAt,则pCRowSeekmethod 指针CRowSeekAt类指针。此时函数调用序列:

CVIQuery::GetRows->CRowSeekAt:: GetRows->CVICursor:: GetRowsAt

CVICursor:: GetRowsAt函数实现如下所示。其中,参数pCTableColumnSet是由前面的DoSetBindings函数构造。在while循环中:

  • 调用CFixedVarBufferAllocator::AllocFixed获取当前行(row)存放的基地址RowBufferBase。

  • 调用CItemCursor::GetRow依次获取每一行(row)数据。

int CVICursor::GetRowsAt(int hRegion,

        int bmkOffset,

        int chapt,

        int cskip,

        CTableColumnSet*pCTableColumnSet,

        CGetRowsParams*pCGetRowsParams,

        int *pbmkOffset)

    {

        int result;

        int fBwdFetch = pCGetRowsParams->fBwdFetch_14;

        //this=pCItemCursor

        while(pCGetRowsParams->cRowsToTransfer_0!=pCGetRowsParams->cRowsAlreadyGet_4&&!result)

        {

            char *RowBufferBase= pCGetRowsParams->pCFixedVarBufferAllocator_8->AllocFixed();

            int index=0;

            result = ((CItemCursor*)this)->GetRow(index,pCTableColumnSet, pCGetRowsParams,                     RowBufferBase);

            if(!result)

            {

                pCGetRowsParams->cRowsAlreadyGet_4++;

                pCGetRowsParams->var_10= 0;

                *pbmkOffset= index + 1;

                if(fBwdFetch)

                    index++;

                else

                    index--;

            }

           

        }

    }

    --------------------------------------------------------------------------------------------

    char* CFixedVarBufferAllocator::AllocFixed()

    {

        char *result = RowBufferStart_18;

        try

        {

            if(RowBufferEnd_1c - RowBufferStart_18 <cbRowWidth_20)

                throw 0xC0000023;

            RowBufferStart_18+= cbRowWidth_20;

        }

        return result;

    }

CItemCursor::GetRow调用CWIDToOffset:: GetItemRow,代码如下所示。CWIDToOffset:: GetItemRow函数循环写入column数据。在while循环中:

  • 首先,从CTableColumnSet数组中取出CTableColumn;

  • 然后,计算Column存放地址pCTableVariant,pCTableVariant地址等于行基址RowBufferBase加上该column的偏移ValueOffset。

  • 最后,调用CTableVariant::CopyOrCoerce,将Column数据写入到pCTableVariant地址中。

int CItemCursor::GetRow(int index, CTableColumnSet *pCTableColumnSet,CGetRowsParams    *pCGetRowsParams, char*   RowBufferBase)

    {

        int value = psegvec_34->Get(index); //1=get(0);

        CWIDToOffset*pCWIDToOffset = *(DWORD*)(pCVIQuery_10->var_7c);

        return pCWIDToOffset->GetItemRow(index,value,pCTableColumnSet,pCGetRowsParams,     RowBufferBase);

 

}

------------------------------------------------------------------------------------------

    int CWIDToOffset::GetItemRow(intindex, int value,CTableColumnSet *pCTableColumnSet,   CGetRowsParams *pCGetRowsParams, char* RowBufferBase)

    {

        //...........

        int index=0;

        CTableVariant*pCTableVariant;

        while(index<pCTableColumnSet->len_0)

        {

 

            //............

            CTableColumn* pCTableColumn = pCTableColumnSet->Get(index_column);

            int var5;

            pCTableVariant = (CTableVariant*)(RowBufferBase +pCTableColumn->ValueOffset_04);

 

            CTableVariant::CopyOrCoerce(pCTableVariant,

                pCTableColumn->ValueSize_06,

                pCTableColumn->Vtype_0E,

                &var5,

                pCGetRowsParams->pCFixedVarBufferAllocator_8);//写入列属性数据

        }

}

在CTableVariant::CopyOrCoerce函数中,当vtype=0x0c,首先调用VarDataSize函数,返回变长数据大小size。

  • 如果column为定长数据,size=0,直接填充pCTableVariant指针数据。

void CTableVariant::CopyOrCoerce(CTableVariant*pCTableVariant,int ValueSize,int Vtype,int     *var5,CFixedVarBufferAllocator*pCFixedVarBufferAllocator)

    {

        //..........

        if(Vtype==0x0c)

        {

            int size = VarDataSize();

            Copy(pCTableVariant,pCFixedVarBufferAllocator, size, 0);

        }

        //.........

    }

    void CTableVariant::Copy(CTableVariant *pCTableVariant,CFixedVarBufferAllocator*pCFixedVarBufferAllocator,int   size,int a4)

    {

        //............

        if(size)

            CTableVariant::CopyData(pCFixedVarBufferAllocator, size,a4);

        pCTableVariant->vtype=vtype;

        pCTableVariant->reserved1=reserved1;

        pCTableVariant->reserved2=reserved2;

        pCTableVariant->offset=offset;

    }

  • 如果column为变长数据,size>0。函数调用序列如下:

CTableVariant::CopyData-> PVarAllocator::CopyTo->CFixedVarBufferAllocator::Allocate

调用CFixedVarBufferAllocator::Allocate获取字符串存放地址:首先计算是否存在足够的存储空间,从RowBufferEnd_1c位置向前寻找存储空间存放字符串:RowBufferEnd_1c =RowBufferEnd_1c-size;然后调用memcpy拷贝字符串。

void * CopyTo(intsize, char *src)

    {

        char *buffer = Allocate(size);

        memcpy(buffer, Src,Size);

        return buffer;

    }

 

    void* CFixedVarBufferAllocator::Allocate(int size)

    {

        try

        {

            if(RowBufferEnd_1c-RowBufferStart_18<size)

                throw 0xC0000023;

 

        }

        RowBufferEnd_1c= RowBufferEnd_1c-size;

        return RowBufferEnd_1c;

    }

查询结果数据CPMGetRowsOut在内存中的状态如下图所示。可以看出,rows中的变长数据存放在Buffer末尾位置,且以地址递减的方式进行存放。



POC与漏洞分析





实验环境如下表:


操作系统

备注

server

Win7 sp1 x86

  • 选择目标文件夹作为共享文件夹

  • 将共享文件夹添加到索引

client

Win7 sp1 x64


在client端,附件->运行,输入“\\servername”,回车,即可看到共享文件夹。打开文件夹,在搜索框里输入关键字进行搜索,这个搜索过程会产生一系列的WSP消息交互序列。

可以通过中间人的方式,修改数据包来重现这个漏洞。修改CPMSetBindingsIn和CPMGetRows消息,如下所示。

cbReadBuffer=0x4000

RowBufferBase = ReadBuffer + _cbReserved= ReadBuffer + 0x38ee

CTableVariant *pCTableVariant =RowBase + valueoffset = ReadBuffer+0x38ee+0x760 = ReadBuffer + 404e

而ReadBuffer大小为0x4000,因此向column中写入数据时,将发生地址越界。

其实,在前面获取RowBufferBase的CFixedVarBufferAllocator::AllocFixed函数中,是进行了合法检查的。

char* CFixedVarBufferAllocator::AllocFixed()

    {

        char *result = RowBufferStart_18;

        try

        {

            if(RowBufferEnd_1c - RowBufferStart_18 <cbRowWidth_20)

                throw 0xC0000023;

            RowBufferStart_18+= cbRowWidth_20;

        }

        return result;

    }

但是由于GetRowsIn中的cbRowWidth本身是不可信的,可以任意赋值,因此可以绕过检查触发漏洞。



补丁分析





补丁对CVIQuery::GetRows函数代码进行修改。在调用pCRowSeekmethod->GetRows函数前,对cbRowWidth的合法性进行判断。其中,pCTableCursor->cbRow_2值为CPMSetBindingsIn消息中的cbRow。

int CVIQuery::GetRows(int hCursor,

        CRowSeekMethod*pCRowSeekmethod,

        CGetRowsParams*pCGetRowsParams,

        CRowSeekMethod*pCRowSeekMethod_new)

    {

        int result;

        CItemCursor*pCItemCursor = *(DWORD *)(var_68 + 4*hCursor);

        CTableCursor*pCTableCursor = pCItemCursor + 0x14;

        pCTableCursor->ValidateBindings();

 

     if(pCTableCursor->cbRow_2 !=pCGetRowsParams->cbRowWidth_c)

         return0x80070057;

        result= pCRowSeekmethod->GetRows(pCTableCursor,

            pCItemCursor,

            pCGetRowsParams,

            pCRowSeekMethod_new);

        returnresult;

        //.................





启明星辰积极防御实验室(ADLab)




ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员。截止目前,ADLab通过CVE发布Windows、Linux、Unix等操作系统安全或软件漏洞近300个,持续保持亚洲领先并确立了其在国际网络安全领域的核心地位。实验室研究方向涵盖操作系统与应用系统安全研究、移动智能终端安全研究、物联网智能设备安全研究、Web安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。