标题 | 简介 | 类型 | 公开时间 | |||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
详情 | ||||||||||||||||||||||||||||||||||||||||
[SAFE-ID: JIWO-2024-94] 作者: 闲云野鸡 发表于: [2017-07-19]
本文共 [454] 位读者顶过
漏洞描述 [出自:jiwo.org]
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)代表查询结果属性,例如文件名称、文件类型等。
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。
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 = newpCMemDeSerStream((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循环中:
pCPidMapper->array_4[CurrentIndex]= &cCFullPropSpec
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,intcbReserved) {
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循环中:
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循环中:
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。
void CTableVariant::CopyOrCoerce(CTableVariant*pCTableVariant,int ValueSize,intVtype,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; }
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与漏洞分析
实验环境如下表:
在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; //................. } |