In the June update, Microsoft fixes multiple remote execution vulnerabilities, including the CVE-2017-8543（CNVD-2017-09381，CNNVD-201706-556） Windows Search Service vulnerability, which affects almost all Windows operating systems. For Windows XP and Windows Server 2003 that have stopped updating, Microsoft also provide corresponding patch, the user can manually download the patch to install.
The Windows Search Service (WSS) is a default enabled basic service for Windows, responsible for creating and maintaining file system indexes. The vulnerability in WSS may allow a remote unauthenticated attacker trigger the vulnerability through an SMB connection and then take control of a target computer.
When the client issue queries to a remote server, Windows Search Protocol (WSP) is used for data interaction. The sequence of interactive messages is shown below. the CPMConnectIn message begins a session between the client and server, including the name of the server and the index name (default Windows\SYSTEMINDEX). The CPMCreateQueryIn message creates a new query, including file directory range and keyword information; CMPSetBindingsIn message is used to set the contents of the returned query result, such as file name and file type infomation; CPMGetRowsIn message is used to request query results.
All Windows Search Protocol messages have a 16-byte header. The following table shows the Windows Search Protocol message header format.
_msg is a 32-bit integer that identifies the type of message following the header. The following table lists the Windows Search Protocol messages and the integer values specified for each message.
CPMSetBindingsIn and CPMGetRowsIn are very important related to the cause of the vulnerability. First we introduce the CPMSetBindingsIn message, the message format is as follows.
struct Column aColumns[SIZE];
The first 0x10 bytes are the message header; hCursor is the handle returned by the CPMCreateQueryOut message; cbRow represents the length of the row, in bytes; aColumns is the Column struct array; cColumns is the length of the array. Here, each row represents a query result, and each column represents the query attribute, such as the file name, file type, and so on.
first query result :row
second query result: row
The Column structure in CPMSetBindingsIn is defined as follows:
The CFullPropSpec structure contains a property set GUID and a property identifier to uniquely identify a property. For example, guidFilename=E05ACF41-5AF70648-BD8759C7-D9248EB9 identify file name.
Vtype is a 32-bit unsigned integer that specifies the type of data value contained in the column. See the vType field in table 2 for the list of values for this field. In the CPMSetBindingsIn message, Vtype is typically set to 0x0c.
A 4-byte signed integer.
A 4-byte unsigned integer.
A null-terminated string using the system code page.
A null-terminated, 16-bit Unicode string. See [UNICODE].
Note The protocol uses UTF-16 LE encoding.
ValueOffset specifying the offset of the column value in the row.
ValueSize specifying the size of the column value in bytes.
The CRequestServer class is responsible for handling CMP messages. Member functions such as DoSetBindings, DoCreateQuery and DoGetRows functions are used to parse the corresponding message. CCProxyMessage_c0 is an important data member that represents the received data Buffer.
DoSetBindings function implementation is shown below.
(1) First, DoSetBindings function initializes the pCPMSetBindingsIn pointer, making it point to the received CPMSetBindingsIn data, and then initializes the CMemDeSerStream class with the pCPMSetBindingsIn pointer. The CMemDeSerStream class is used to read each field.
(2) Then, DoSetBindings function Contruct CTableColumnSet class with pCMemDeSerStream pointer as parameter. The CTableColumnSet class and the CPidMapper class are derived classes of the CCountedDynArray class. CCountedDynArray is an array class, Data member Array_4 is an array of pointers. The CTableColumnSet constructor first calls GetULong to get the array length cColumns as the number of loops, and then parse the aColumns array element in the while loop:
Parse CFullPropSpec struct in column, add pointer &CFullPropSpec to CPidMappe:
Parse other items of column, save them to CTableColumn class，and then add pCTableColumn pointer to CTableColumnSet:
(3) Finally, DoSetBindings function call CVIQuery::SetBindings with pCPidMapper and pCTableColumnset as parameter. CVIQuery:: SetBindings call CTableCursor::CheckBindings to validate legality of CTableColumn.
CTableCursor::checkBinding call CTableColumn::Validate, In CTableColumn::Validate function, if ValueSize + ValueOffset>cbRow, throw exception to prevent buffer overflow.
Next we introduce the CPMGetRowsIn message, the CPMGetRowsIn message format is as follows:
cbRowWidth indicate the length of a row in bytes, with same meaning as cbRow in CPMSetBindingsIn.
cbReserved indicate the offset, in bytes, of the Rows field in the CPMGetRowsOut response message. This offset begins from the first byte of the message header.
eType contain one of the following values indicating the type of operation to perform.
The message format for CPMGetRowsOut is as follows:
In the CPMGetRowsOut message, Row data is formatted as prescribed by column information in the most recent CPMSetBindingsIn message. CTableVariant class is used to represent column data in each row. The CTableVariant structure is defined as follows.
Fixed-sized columns MUST be stored at the offsets specified by the most recent CPMSetBindingsIn message.
Variable-sized columns, the variable data itself (for example, the string) is stored near the end of the buffer in descending order, The fixed-sized area MUST contain a CTableVariant for each column, stored at the offset specified in the most recent CPMSetBindingsIn message.
DoGetRows function is called to parse CPMGetRowsIn, the function implementation is shown below.
(1) First, DoGetRows call UnmarshallRowSeekDescription function to get a SeekMethod object according to etype value. if cbReadBuffer_24>0x1300, Allocate new memory for CMPRowsOut.
(2) Then, DoGetRows construct CFixedVarBufferAllocator with pCMPGetRowsOut as first parameter, and construct CGetRowsParams with &cCFixedVarBufferAllocator and cbRowWidth.
(3) Finally, DoGetRows call CVIQuery:: GetRows.
Assuming etype = eRowSeekAt, then the pCRowSeekmethod pointer is CRowSeekAt class pointer. The function call sequence is:
CVIQuery::GetRows->CRowSeekAt:: GetRows->CVICursor:: GetRowsAt
CVICursor::GetRowsAt is shown as follows, pCTableColumnSet is constuct in DoSetBindings. In while Loop, first call CFixedVarBufferAllocator::AllocFixed to get RowBufferBase, then call CItemCursor::GetRow get each row.
CItemCursor::GetRow call CWIDToOffset:: GetItemRow.
CWIDToOffset::GetItemRow function is to write each column data. In while loop, first get CTableColumn from CTableColumnSet Array, then calculate pCTableVariant value as address of Column, finally call CTableVariant::CopyOrCoerce, writing Column data into pCTableVariant Address.
In CTableVariant::CopyOrCoerce function, when vtype=0x0c, call VarDataSize fucntion, return Variable Data size.
For Variable-sized columns, returned size=0, just write to pCTableVariant address.
For Variable-sized columns, returned size>0. The function call sequence is:
CFixedVarBufferAllocator::Allocate is called to get address to store variable data. First calculate whether there is enough space for variable data, then find the storage space from the RowBufferEnd_1c position in descending order, finally call memcpy copy string.
we can see the buffer is filled in from both ends. CTableVariant structures, one for each row, are stored at the beginning of the buffer. Each of these structures points to the row data which is stored starting at the end of the buffer.
Test environment is shown below.
Win7 sp1 x86
choose a directory as search target and add the directory to indexed scope;
set the directory as a share directory.
Win7 sp1 x64
On the client, press the Windows Logo key + R，Type the Universal Naming Convention path to the server (\\SERVERNAME) and press Enter. Opens the share directory, and type keywords in the Search box, we can see WSP message interaction sequence.
we reproduce the vulnerability by doing MITM modification. the construct CPMSetBindingsIn and CPMGetRows is shown as below.
RowBufferBase = ReadBuffer + _cbReserved= ReadBuffer + 0x38ee
CTableVariant*pCTableVariant=RowBase+valueoffset = ReadBuffer+0x38ee+0x760 = ReadBuffer + 404e
cbReadBuffer is size of ReadBuffer, so buffersize =0x4000, when writing column data to pCTableVariant(ReadBuffer +404e) will result in writing out of border.
Is there a border check? Yes. CFixedVarBufferAllocator::AllocFixed check if there is enough space for row data with size cbRowWidth.
However, cbRowWidth itself (in GetRowsIn) is lack of legitimacy check. So you can arbitrarily assign value, bypass the check, trigger the vulnerability.
The Patch modifies the CVIQuery :: GetRows function code.
Legitimacy Check of cbRowWidth is added Before calling the GetRows function. The check code throw exception if pCGetRowsParams->cbRowWidth_c is not equal to pCTableCursor-> cbRow_2, where cbRow_2 is cbRow in the CPMSetBindingsIn message and cbRowWidth_c is cbRowWidth in CPMGetRowsIn.
In CTableCursor::checkBinding, CTableColumn::Validate is called to Check the legitimacy of cbRow in CPMSetBindingsIn. So above comparisoncan ensure the legitimacy of cbRowWidth in CPMGetRowsIn.