CVE-2017-8543 Windows Search漏洞分析及POC关键部分
更多资讯和分析文章请关注启明星辰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安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。