By Eneko Cruz Elejalde
Overview
This post analyzes a heap-buffer overflow in Microsoft Windows Address Book. Microsoft released an advisory for this vulnerability for the 2021 February patch Tuesday. This post will go into detail about what Microsoft Windows Address Book is, the vulnerability itself, and the steps to craft a proof-of-concept exploit that crashes the vulnerable application.
Windows Address Book
Windows Address Book is a part of the Microsoft Windows operating system and is a service that provides users with a centralized list of contacts that can be accessed and modified by both Microsoft and third party applications. The Windows Address Book maintains a local database and interface for finding and editing information about contacts, and can query network directory servers using Lightweight Directory Access Protocol (LDAP). The Windows Address Book was introduced in 1996 and was later replaced by Windows Contacts in Windows Vista and subsequently by the People App in Windows 10.
The Windows Address Book provides an API that enables other applications to directly use its database and user interface services to enable services to access and modify contact information. While Microsoft has replaced the application providing the Address Book functionality, newer replacements make use of old functionality and ensure backwards compatibility. The Windows Address Book functionality is present in several Windows Libraries that are used by Windows 10 applications, including Outlook and Windows Mail. In this way, modern applications make use of the Windows Address Book and can even import address books from older versions of Windows.
CVE-2021-24083
A heap-buffer overflow vulnerability exists within the SecurityCheckPropArrayBuffer() function within wab32.dll when processing nested properties of a contact. The network-based attack vector involves enticing a user to open a crafted .wab file containing a malicious composite property in a WAB record.
Vulnerability
The vulnerability analysis that follows is based on Windows Address Book Contacts DLL (wab32.dll) version 10.0.19041.388 running on Windows 10 x64.
The Windows Address Book Contacts DLL (i.e. wab32.dll) provides access to the Address Book API and it is used by multiple applications to interact with the Windows Address Book. The Contacts DLL handles operations related to contact and identity management. Among others, the Contacts DLL is able to import an address book (i.e, a WAB file) exported from an earlier version of the Windows Address Book.
Earlier versions of the Windows Address Book maintained a database of identities and contacts in the form of a .wab file. While current versions of Windows do not use a .wab file by default anymore, they allow importing a WAB file from an earlier installation of the Windows Address Book.
There are multiple ways of importing a WAB file into the Windows Address Book, but it was observed that applications rely on the Windows Contacts Import Tool (i.e, C:\Program Files\Windows Mail\wabmig.exe) to import an address book. The Import Tool loads wab32.dll to handle loading a WAB file, extracting relevant contacts, and importing them into the Windows Address Book.
WAB File Format
The WAB file format (commonly known as Windows Address Book or Outlook Address Book) is an undocumented and proprietary file format that contains personal identities. Identities may in turn contain contacts, and each contact might contain one or more properties.
Although the format is undocumented, the file-format has been partially reverse-engineered by a third party. The following structures were obtained from a combination of a publicly available third-party application and the disassembly of wab32.dll. Consequently, there may be inaccuracies in structure definitions, field names, and field types.
The WAB file has the following structure:
Offset Length (bytes) Field Description
--------- -------------- -------------------- -------------------
0x0 16 Magic Number Sixteen magic bytes
0x10 4 Count 1 Unknown Integer
0x14 4 Count 2 Unknown Integer
0x18 16 Table Descriptor 1 Table descriptor
0x28 16 Table Descriptor 2 Table descriptor
0x38 16 Table Descriptor 3 Table descriptor
0x48 16 Table Descriptor 4 Table descriptor
0x58 16 Table Descriptor 5 Table descriptor
0x68 16 Table Descriptor 6 Table descriptor
All multi-byte fields are represented in little-endian byte order unless otherwise specified. All string fields are in Unicode, encoded in the UTF16-LE format.
The Magic Number field contains the following sixteen bytes: 9c cb cb 8d 13 75 d2 11 91 58 00 c0 4f 79 56 a4. While some sources list the sequence of bytes 81 32 84 C1 85 05 D0 11 B2 90 00 AA 00 3C F6 76 as a valid magic number for a WAB file, it was found experimentally that replacing the sequence of bytes prevents the Windows Address Book from processing the file.
Each of the six Table Descriptor fields numbered 1 through 6 has the following structure:
Offset Length Field Description
(bytes)
------- -------- ------- -------------------
0x0 4 Type Type of table descriptor
0x4 4 Size Size of the record described
0x8 4 Offset Offset of the record described relative to the beginning of file
0xC 4 Count Number of records present at offset
The following are examples of some known types of table descriptor:
- Text Record (Type: 0x84d0): A record containing a Unicode string.
- Index Record (Type: 0xFA0): A record that may contain several descriptors to WAB records.
Each text record has the following structure:
Offset Length (bytes) Field Description
------ -------------- ------------ -------------------
0x0 N Content Text content of the record; a null terminated UNICODE string
0x0+N 0x4 RecordId A record identifier for the text record
Similarly, each index record has the following structure
Offset Length (bytes) Field Description
--------- -------------- ---------- -------------------
0x0 4 RecordId A record identifier for the index record
0x4 4 Offset Offset of the record relative to the beginning of the file
Each entry in the index record (i.e, each index record structure in succession) has an offset that points to a WAB record.
WAB Records
A WAB record is used to describe a contact. It contains fields such as email addresses and phone numbers stored in properties, which may be of various types such as string, integer, GUID, and timestamp. Each WAB record has the following structure:
Offset Length Field Description
--------- ------ --------------- -------------------
0x0 4 Unknown1 Unknown field
0x4 4 Unknown2 Unknown field
0x8 4 RecordId A record identifier for the WAB record
0xC 4 PropertyCount The number of properties contained in RecordProperties
0x10 4 Unknown3 Unknown field
0x14 4 Unknown4 Unknown field
0x18 4 Unknown5 Unknown field
0x1C 4 DataLen The length of the RecordProperties field (M)
0x20 M RecordProperties Succession of subproperties belonging to the WAB record
The following fields are relevant:
- The RecordProperties field is a succession of record property structures.
- The PropertyCount field indicates the number of properties within the RecordProperties field.
Record properties can be either simple or composite.
Simple Properties
Simple properties have the following structure:
Offset Length (bytes) Field Description
--------- -------------- --------- -------------------
0x0 0x2 Tag A property tag describing the type of the contents
0x2 0x2 Unknown Unknown field
0x4 0x4 Size Size in bytes of Value member (X)
0x8 X Value Property value or content
Tags of simple properties are smaller than 0x1000, and include the following:
Tag Name Tag Value Length Description
(bytes)
--------- ----------- --------- -------------------
PtypInteger16 0x00000002 2 A 16-bit integer
PtypInteger32 0x00000003 4 A 32-bit integer
PtypFloating32 0x00000004 4 A 32-bit floating point number
PtypFloating64 0x00000005 8 A 64-bit floating point number
PtypBoolean 0x0000000B 2 Boolean, restricted to 1 or 0
PtypString8 0x0000001E Variable A string of multibyte characters in externally specified
encoding with terminating null character (single 0 byte)
PtypBinary 0x00000102 Variable A COUNT field followed by that many bytes
PtypString 0x0000001F Variable A string of Unicode characters in UTF-16LE format encoding
with terminating null character (0x0000).
PtypGuid 0x00000048 16 A GUID with Data1, Data2, and Data3 filds in little-endian
PtypTime 0x00000040 8 A 64-bit integer representing the number of 100-nanosecond
intervals since January 1, 1601
PtypErrorCode 0x0000000A 4 A 32-bit integer encoding error information
Note the following:
- The aforementioned list is not exhaustive. For more property tag definitions, see this.
- The value of PtypBinary is prefixed by a COUNT field, which counts 16-bit words.
- In addition to the above, the following properties also exist; their usage in WAB is unknown.
- PtypEmbeddedTable (0x0000000D): The property value is a Component Object Model (COM) object.
- PtypNull (0x00000001): None: This property is a placeholder.
- PtypUnspecified (0x00000000): Any: this property type value matches any type;
Composite Properties
Composite properties have the following structure:
Offset Length Field Description
(bytes)
------ --------- ----------------- -------------------
0x0 0x2 Tag A property tag describing the type of the contents
0x2 0x2 Unknown Unknown field
0x4 0x4 NestedPropCount Number of nested properties contained in the current WAB property
0x8 0x4 Size Size in bytes of Value member (X)
0xC X Value Property value or content
Tags of composite properties are greater than or equal to 0x1000, and include the following:
Tag Name Tag Value
--------- ----------
PtypMultipleInteger16 0x00001002
PtypMultipleInteger32 0x00001003
PtypMultipleString8 0x0000101E
PtypMultipleBinary 0x00001102
PtypMultipleString 0x0000101F
PtypMultipleGuid 0x00001048
PtypMultipleTime 0x00001040
The Value field of each composite property contains NestedPropCount number of Simple properties of the corresponding type.
In case of fixed-sized properties (PtypMultipleInteger16, PtypMultipleInteger32, PtypMultipleGuid, and PtypMultipleTime), the Value field of a composite property contains NestedPropCount number of the Value field of the corresponding Simple property.
For example, in a PtypMultipleInteger32 structure with NestedPropCount of 4:
- The Size is always 16.
- The Value contains four 32-bit integers.
In case of variable-sized properties (PtypMultipleString8, PtypMultipleBinary, and PtypMultipleString), the Value field of the composite property contains NestedPropCount number of Size and Value fields of the corresponding Simple property.
For example, in a PtypMultipleString structure with NestedPropCount of 2 containing the strings “foo” and “bar” in Unicode:
- The Size is 14 00 00 00.
- The Value field contains a concatenation of the following two byte-strings:
- “foo” encoded with a four-byte length: 06 00 00 00 66 00 6f 00 6f 00.
- “bar” encoded with a four-byte length: 06 00 00 00 62 00 61 00 72 00.
Technical Details
The vulnerability in question occurs when a malformed Windows Address Book in the form of a WAB file is imported. When a user attempts to import a WAB file into the Windows Address Book, the method WABObjectInternal::Import() is called, which in turn calls ImportWABFile(). For each contact inside the WAB file, ImportWABFile() performs the following nested calls: ImportContact(), CWABStorage::ReadRecord(), ReadRecordWithoutLocking(), and finally HrGetPropArrayFromFileRecord(). This latter function receives a pointer to a file as an argument and reads the contact header and extracts PropertyCount and DataLen. The function HrGetPropArrayFromFileRecord() in turn calls SecurityCheckPropArrayBuffer() to perform security checks upon the imported file and HrGetPropArrayFromBuffer() to read the contact properties into a property array.
The function HrGetPropArrayFromBuffer() relies heavily on the correctness of the checks performed by SecurityCheckPropArrayBuffer(). However, the function fails to implement security checks upon certain property types. Specifically, SecurityCheckPropArrayBuffer() may skip checking the contents of nested properties where the property tag is unknown, while the function HrGetPropArrayFromBuffer() continues to process all nested properties regardless of the security check. As a result, it is possible to trick the function HrGetPropArrayFromBuffer() into parsing an unchecked contact property. As a result of parsing such a property, the function HrGetPropArrayFromBuffer() can be tricked into overflowing a heap buffer.
Code Analysis
The following code blocks show the affected parts of methods relevant to this vulnerability. Code snippets are demarcated by reference markers denoted by [N]. Lines not relevant to this vulnerability are replaced by a [Truncated] marker.
The following is the pseudocode of the function HrGetPropArrayFromFileRecord:
[1]
if ( !(unsigned int)SecurityCheckPropArrayBuffer(wab_buffer_full, HIDWORD(uBytes[1]), wab_buffer[3]) )
{
[2]
result = 0x8004011b; // Error
goto LABEL_25; // Return prematurely
}
[3]
result = HrGetPropArrayFromBuffer(wab_buffer_full, HIDWORD(uBytes[1]), wab_buffer[3], 0, a7);
At [1] the function SecurityCheckPropArrayBuffer() is called to perform a series of security checks upon the buffer received and the properties contained within. If the check is positive, then the input is trusted and processed by calling HrGetPropArrayFromBuffer() at [3]. Otherwise, the function returns with an error at [2].
The following is the pseudocode of the function SecurityCheckPropArrayBuffer():
__int64 __fastcall SecurityCheckPropArrayBuffer(unsigned __int8 *buffer_ptr, unsigned int buffer_length, int header_dword_3)
{
unsigned int security_check_result; // ebx
unsigned int remaining_buffer_bytes; // edi
int l_header_dword_3; // er15
unsigned __int8 *ptr_to_buffer; // r9
int current_property_tag; // ecx
__int64 c_dword_2; // r8
unsigned int v9; // edi
int VA; // ecx
int VB; // ecx
int VC; // ecx
int VD; // ecx
int VE; // ecx
int VF; // ecx
int VG; // ecx
int VH; // ecx
signed __int64 res; // rax
_DWORD *ptr_to_dword_1; // rbp
unsigned __int8 *ptr_to_dword_0; // r14
unsigned int dword_2; // eax
unsigned int v22; // edi
int v23; // esi
int v24; // ecx
unsigned __int8 *c_ptr_to_property_value; // [rsp+60h] [rbp+8h]
unsigned int v27; // [rsp+68h] [rbp+10h]
unsigned int copy_dword_2; // [rsp+70h] [rbp+18h]
security_check_result = 0;
remaining_buffer_bytes = buffer_length;
l_header_dword_3 = header_dword_3;
ptr_to_buffer = buffer_ptr;
if ( header_dword_3 )
{
while ( remaining_buffer_bytes > 4 )
{
[4]
if ( *(_DWORD *)ptr_to_buffer & 0x1000 )
{
[5]
current_property_tag = *(unsigned __int16 *)ptr_to_buffer;
if ( current_property_tag == 0x1102 ||
(unsigned int)(current_property_tag - 0x101E) <= 1 )
{
[6]
ptr_to_dword_1 = ptr_to_buffer + 4;
ptr_to_dword_0 = ptr_to_buffer;
if ( remaining_buffer_bytes < 0xC )
return security_check_result;
dword_2 = *((_DWORD *)ptr_to_buffer + 2);
v22 = remaining_buffer_bytes - 0xC;
if ( dword_2 > v22 )
return security_check_result;
ptr_to_buffer += 12;
copy_dword_2 = dword_2;
remaining_buffer_bytes = v22 - dword_2;
c_ptr_to_property_value = ptr_to_buffer;
v23 = 0;
if ( *ptr_to_dword_1 > 0u )
{
while ( (unsigned int)SecurityCheckSingleValue(
*(_DWORD *)ptr_to_dword_0,
&c_ptr_to_property_value,
©_dword_2) )
{
if ( (unsigned int)++v23 >= *ptr_to_dword_1 )
{
ptr_to_buffer = c_ptr_to_property_value;
goto LABEL_33;
}
}
return security_check_result;
}
}
else
{
[7]
if ( remaining_buffer_bytes < 0xC )
return security_check_result;
c_dword_2 = *((unsigned int *)ptr_to_buffer + 2);
v9 = remaining_buffer_bytes - 12;
if ( (unsigned int)c_dword_2 > v9 )
return security_check_result;
remaining_buffer_bytes = v9 - c_dword_2;
VA = current_property_tag - 0x1002;
if ( VA )
{
VB = VA - 1;
if ( VB && (VC = VB - 1) != 0 )
{
VD = VC - 1;
if ( VD && (VE = VD - 1) != 0 && (VF = VE - 1) != 0 && (VG = VF - 13) != 0 && (VH = VG - 44) != 0 )
res = VH == 8 ? 16i64 : 0i64;
else
res = 8i64;
}
else
{
res = 4i64;
}
}
else
{
res = 2i64;
}
if ( (unsigned int)c_dword_2 / *((_DWORD *)ptr_to_buffer + 1) != res )
return security_check_result;
ptr_to_buffer += c_dword_2 + 12;
}
}
else
{
[8]
if ( remaining_buffer_bytes < 4 )
return security_check_result;
v24 = *(_DWORD *)ptr_to_buffer;
c_ptr_to_property_value = ptr_to_buffer + 4;// new exe: v13 = buffer_ptr + 4;
v27 = remaining_buffer_bytes - 4;
if ( !(unsigned int)SecurityCheckSingleValue(v24, &c_ptr_to_property_value, &v27) )
return security_check_result;
remaining_buffer_bytes = v27;
ptr_to_buffer = c_ptr_to_property_value;
}
LABEL_33:
if ( !--l_header_dword_3 )
break;
}
}
if ( !l_header_dword_3 )
security_check_result = 1;
return security_check_result;
}
At [4] the tag of the property being processed is checked. The checks performed depend on whether the property processed in each iteration is a simple or a composite property. For simple properties (i.e, properties with tag lower than 0x1000), execution continues at [8]. The following checks are done for simple properties:
- If the remaining number of bytes in the buffer is fewer than 4, the function returns with an error.
- A pointer to the property value is obtained and SecurityCheckSingleValue() is called to perform a security check upon the simple property and its value. SecurityCheckSingleValue() performs a security check and increments the pointer to point at the next property in the buffer, so that SecurityCheckPropArrayBuffer() can check the next property on the next iteration.
- The number of total properties is decremented and compared to zero. If equal to zero, then the function returns successfully. If different, the next iteration of the loop checks the next property.
Similarly, for composite properties (i.e, properties with tag equal or higher than 0x1000) execution continues at [5] and the following is done.
For variable length composite properties (if the property tag is equal to 0x1102 (PtypMultipleBinary) or equal or smaller than 0x101f (PtypMultipleString)), the code at [6] does the following:
- The number of bytes left to read in the buffer is compared with 0xC to avoid overrunning the buffer.
- The Size field of the property is compared to the remaining buffer length to avoid overrunning the buffer.
- For each nested property, the function SecurityCheckSingleValue() is called. It:
- Performs a security check on the nested property.
- Advances the pointer to the buffer held by the caller, in order to point to the next nested property.
- The loop runs until the number of total properties in the contact (decremented in each iteration) is zero.
For fixed-length composite properties (if the property tag in question is different from 0x1102 (PtypMultipleBinary) and larger than 0x101f (PtypMultipleString)), the following happens starting at [7]:
- The number of bytes left to read in the buffer is compared with 0xC to avoid overrunning the buffer.
- The Size is compared to the remaining buffer length to avoid overrunning the buffer.
- The size of each nested property, which depends only on the property tag, is calculated from the parent property tag.
- The Size is divided by NestedPropCount to obtain the size of each nested property.
- The function returns with an error if the calculated subproperty size is different from the property size deduced from parent property tag.
- The buffer pointer is incremented by the size of the parent property value to point to the next property.
Unknown or non-processable property types are assigned the nested property size 0x0.
It was observed that if the calculated property size is zero, the buffer pointer is advanced by the size of the property value, as described by the header. The buffer is advanced regardless of the property size and by advancing the buffer, the security check permits the value of the parent property (which may include subproperties) to stay unchecked. For the security check to pass the result of the division performed on Step 4 for fixed-length composite properties must be zero. Therefore for an unknown or non-processable property to pass the security check, NestedPropCount must be larger than Size. Note that since the size of any property in bytes is at least two, NestedPropCount must always be no larger than half of Size, and therefore, the aforementioned division must never be zero in benign cases.
After the checks have concluded, the function returns zero for a failed check and one for a passed check.
Subsequently, the function HrGetPropArrayFromFileRecord() calls HrGetPropArrayFromBuffer(), which aims to collect the properties into an array of _SPropValue structs and return it to the caller. The _SPropValue array has a length equal of the number of properties (as given by the contact header) and is allocated in the heap through a call to LocalAlloc(). The number of properties is multiplied by sizeof(_SPropValue) to yield the total buffer size. The following fragment shows the allocation taking place:
if ( !property_array_r )
{
ret = -2147024809;
goto LABEL_71;
}
*property_array_r = 0i64;
header_dword_3_1 = set_to_zero + header_dword_3;
[9]
if ( (unsigned int)header_dword_3_1 < header_dword_3
|| (unsigned int)header_dword_3_1 > 0xAAAAAAA
|| (v10 = (unsigned int)header_dword_3_1,
property_array = (struct _SPropValue *)LocalAlloc(
0x40u,
0x18 * header_dword_3_1),
// sizeof(_SPropValue) * n_properties_in_binary
(*property_array_r = property_array) == 0i64) )
{
ERROR_INSUFICIENT_MEMORY:
ret = 0x8007000E;
goto LABEL_71;
}
An allocation of sizeof(_SPropValue) * n_properties_in_binary can be observed at [9]. Immediately after, each of the property structures are initialized and their property tag member is set to 1. After initialization, the buffer, on which security checks have already been performed, is processed property by property, advancing the property a pointer to the next property with the property header and value sizes provided by the property in question.
If the property processed by the specific loop iteration is a simple property, the following code is executed:
if ( !_bittest((const signed int *)¤t_property_tag, 0xCu) )
{
if ( v16 < 4 )
break;
dword_1 = wab_ulong_buffer_full[1];
ptr_to_dword_2 = (char *)(wab_ulong_buffer_full + 2);
v38 = v16 - 4;
if ( (unsigned int)dword_1 > v38 )
break;
current_property_tag = (unsigned __int16)current_property_tag;
if ( (unsigned __int16)current_property_tag > 0xBu )
{
[10]
v39 = current_property_tag - 0x1E;
if ( !v39 )
goto LABEL_79;
v40 = v39 - 1;
if ( !v40 )
goto LABEL_79;
v41 = v40 - 0x21;
if ( !v41 )
goto LABEL_56;
v42 = v41 - 8;
if ( v42 )
{
if ( v42 != 0xBA )
goto LABEL_56;
v43 = dword_1;
(*property_array_r)[p_idx].Value.bin.lpb = (LPBYTE)LocalAlloc(0x40u, dword_1);
if ( !(*property_array_r)[p_idx].Value.bin.lpb )
goto ERROR_INSUFICIENT_MEMORY;
(*property_array_r)[p_idx].Value.l = dword_1;
v44 = *(&(*property_array_r)[p_idx].Value.at + 1);
}
else
{
LABEL_79:
[11]
v43 = dword_1;
(*property_array_r)[p_idx].Value.cur.int64 = (LONGLONG)LocalAlloc(0x40u, dword_1);
v44 = (*property_array_r)[p_idx].Value.dbl;
if ( v44 == 0.0 )
goto ERROR_INSUFICIENT_MEMORY;
}
memcpy_0(*(void **)&v44, ptr_to_dword_2, v43);
wab_ulong_buffer_full = (ULONG *)&ptr_to_dword_2[v43];
}
else
{
LABEL_56:
[12]
memcpy_0(&(*property_array_r)[v15].Value, ptr_to_dword_2, dword_1);
wab_ulong_buffer_full = (ULONG *)&ptr_to_dword_2[dword_1];
}
remaining_bytes_to_process = v38 - dword_1;
goto NEXT_PROPERTY;
}
[Truncated]
NEXT_PROPERTY:
++p_idx;
processed_property_count = (unsigned int)(processed_property_count_1 + 1);
processed_property_count_1 = processed_property_count;
if ( (unsigned int)processed_property_count >= c_header_dword_3 )
return 0;
}
At [10] the property tag is extracted and compared with several constants. If the property tag is 0x1e (PtypString8), 0x1f (PtypString), or 0x48 (PtypGuid), then execution continues at [11]. If the property tag is 0x40 (PtypTime) or is not recognized, execution continues at [12]. The memcpy call at [12] is prone to a heap overflow.
Conversely, if the property being processed in the specific loop iteration is not a simple property, the following code is executed. Notably, when the following code is executed, the pointer DWORD* wab_ulong_buffer_full points to the property tag of the property being processed. Regardless of which composite property is being processed, before the property tag is identified the buffer is advanced to point to the beginning of the property value, which is at the 4th 32-bit integer.
[13]
if ( v16 < 4 )
break;
c_dword_1 = wab_ulong_buffer_full[1];
v19 = v16 - 4;
if ( v19 < 4 )
break;
dword_2 = wab_ulong_buffer_full[2];
wab_ulong_buffer_full += 3;
remaining_bytes_to_process = v19 - 4;
[14]
if ( (unsigned __int16)current_property_tag >= 0x1002u )
{
if ( (unsigned __int16)current_property_tag <= 0x1007u || (unsigned __int16)current_property_tag == 0x1014 )
goto LABEL_80;
if ( (unsigned __int16)current_property_tag == 0x101E )
{
[Truncated]
}
if ( (unsigned __int16)current_property_tag == 0x101F )
{
[Truncated]
}
if ( ((unsigned __int16)current_property_tag - 0x1040) & 0xFFFFFFF7 )
{
if ( (unsigned __int16)current_property_tag == 0x1102 )
{
[Truncated]
}
}
else
{
LABEL_80:
[15]
(*property_array_r)[p_idx].Value.bin.lpb = (LPBYTE)LocalAlloc(0x40u, dword_2);
if ( !(*property_array_r)[p_idx].Value.bin.lpb )
goto ERROR_INSUFICIENT_MEMORY;
(*property_array_r)[p_idx].Value.l = c_dword_1;
if ( (unsigned int)dword_2 > remaining_bytes_to_process )
break;
memcpy_0((*property_array_r)[p_idx].Value.bin.lpb, wab_ulong_buffer_full, dword_2);
wab_ulong_buffer_full = (ULONG *)((char *)wab_ulong_buffer_full + dword_2);
remaining_bytes_to_process -= dword_2;
}
}
NEXT_PROPERTY:
++p_idx;
processed_property_count = (unsigned int)(processed_property_count_1 + 1);
processed_property_count_1 = processed_property_count;
if ( (unsigned int)processed_property_count >= c_header_dword_3 )
return 0;
}
After the buffer has been advanced at [13], the property tag is compared with several constants starting at [14]. Finally, the code fragment at [15] attempts to process a composite property (i.e. >= 0x1000) with a tag not contemplated by the previous constants.
Although the processing logic of each type of property is irrelevant, an interesting fact is that if the property tag is not recognized, the buffer pointer has still been advanced to the end of the end of its header, and it’s never retracted. This happens if all of the following conditions are met:
- The property tag is larger or equal than 0x1002.
- The property tag is larger than 0x1007.
- The property tag is different from 0x1014.
- The property tag is different from 0x101e.
- The property tag is different from 0x101f.
- The property tag is different from 0x1102.
- The result of subtracting 0x1040 from the property tag, and performing a bitwise AND of the result with 0xFFFFFFF7 is nonzero.
Interestingly, if all of the above conditions are met, the property header of the composite property is skipped, and the next loop iteration will interpret its property body as a different property.
Therefore, it is possible to overflow the _SPropValue array allocated in the heap by HrGetPropArrayFromBuffer() by using the following observations:
- A specially crafted composite unknown or non-processable property can be made to bypass security checks if NestedPropCount is larger than Size.
- HrGetPropArrayFromBuffer() can be made to interpret the Value of a specially crafted property as a separate property.
Proof-of-Concept
In order to create a malicious WAB file from a benign WAB file, export a valid WAB file from an instance of the Windows Address Book. It is noted that Outlook Express on Windows XP includes the ability to export contacts as a WAB file.
The benign WAB file can be modified to make it malicious by altering a contact inside it to have the following characteristics:
- A nested property containing the following:
- A tag of an unknown or unprocessable type, for example the tag 0x1058, with the following conditions:
- Must be larger or equal than 0x1002.
- Must be larger than 0x1007.
- Must be different from 0x1014, 0x101e, 0x101f, and 0x1102.
- The result of subtracting 0x1040 from the property tag, and performing a bitwise AND of the result with 0xFFFFFFF7 is non-zero.
- Must be different from 0x1002, 0x1003, 0x1004, 0x1005, 0x1006, 0x1007, 0x1014, 0x1040, and 0x1048.
- NestedPropCount is larger than Size.
- The Value of the composite property is empty.
- A malicious simple property containing the following:
- A property tag different from 0x1e, 0x1f, 0x40 and 0x48. For example, the tag 0x0.
- The Size value is larger than 0x18 x NestedPropCount in order to overflow the _SPropValue array buffer.
- An unspecified number of trailing bytes, that will overflow the _SPropValue array buffer.
Finally, when an attacker tricks an unsuspecting user into importing the specially crafted WAB file, the vulnerability is triggered and code execution could be achieved. Failed exploitation attempts will most likely result in a crash of the Windows Address Book Import Tool.
Due to the presence of ASLR and a lack of a scripting engine, we were unable to obtain arbitrary code execution in Windows 10 from this vulnerability.
Conclusion
Hopefully you enjoyed this dive into CVE-2021-24083, and if you did, go ahead and check out our other blog post on a use-after-free vulnerability in Adobe Acrobat Reader DC. If you haven’t already, make sure to follow us on Twitter to keep up to date with our work. Happy hacking!