White House issues call to action in light of new intelligence on Russian cyberthreat
The Biden administration renewed calls Monday for the private sector to address known vulnerabilities and shore up cyberdefenses in light of a looming possibility of a cyberattack from Russia on U.S. infrastructure. “The most troubling piece,” Anne Neubeger, the White House’s deputy national security adviser for cyber and emerging technology, said, is that “we continue to see known vulnerabilities for which we have patches available” used by cyberattackers to compromise U.S. companies. The administration has repeatedly warned the critical infrastructure sector about the potential for Russia to engage in malicious cyber activity against the United States in response to the recently imposed economic sanctions.
Exodus Intelligence is answering the call
In response to the renewed calls for the private sector to address known vulnerabilities, Exodus Intelligence is offering their N-Day vulnerability subscription for FREEfrom April 1st through July 1st.
The N-Day Vulnerability subscription provides customers with intelligence about critically exploitable, publicly disclosed vulnerabilities on widely used software, hardware, embedded devices, and industrial control systems. Every vulnerability is analyzed, documented, and enriched with high-impact intelligence derived by some of the best reverse engineers in the world. At times, vendor patches fail to properly secure the underlying vulnerability. Exodus Intelligence’s proprietary research enhances patch management efforts. Subscribed customers have access to an arsenal of more than 1200 vulnerability intelligence packages to ensure defensive measures are properly implemented.
For those that are concerned about Zero-day vulnerabilities, Exodus is also offering the benefit of our Zero-day vulnerability subscription for up to 50% off for new registrations from April 1st through July 1st. Exodus’ Zero-day Subscription provides customers with critically exploitable vulnerability reports, unknown to the public, aﬀecting widely used and relied upon software, hardware, embedded devices, and industrial control systems. Customers will gain access to a proprietary library of over 200 Zero-day vulnerability reports in addition to proof of concept exploits and highly enriched vulnerability intelligence packages. These Zero-day Vulnerability Intelligence packages, unavailable anywhere else, enable customers to reduce their mean time to detect and mitigate critically exploitable vulnerabilities.
These offerings are available to the United States (and allied countries) Private and Public Sectors to gain the immediate benefit of advanced vulnerability analysis, mitigation guidance/signatures, and proof-of-concepts to test against current defenses.
To register for FREEN-day Intelligence, please fill out the webformhere
This post analyzes a use-after-free vulnerability in clfs.sys, the kernel driver that implements the Common Logging File System, a general-purpose logging service that can be used by user-space and kernel-space processes in Windows. A method to exploit this vulnerability to achieve privilege escalation in Windows is also outlined.
Along with two other similar vulnerabilities, Microsoft patched this vulnerability in September 2021 and assigned the CVEs CVE-2021-36955, CVE-2021-36963, and CVE-2021-38633 to them. In the absence of any public information separating the three CVEs, we’ve decided to use CVE-2021-36955 to refer to the vulnerability described herein.
The Preliminaries section describes CLFS structures, Code Analysis explains the vulnerability with the help of code snippets, and the Exploitation section outlines the steps that lead to a functional exploit.
Common Log File System (CLFS) provides a high-performance, general-purpose log file subsystem that dedicated client applications can use and multiple clients can share to optimize log access. Any user-mode application that needs logging or recovery support can use CLFS. The following structures are taken from both the official documentation and a third-party’s unofficial documentation.
Every Base Log File is made up various records. These records are stored in sectors, which are written to in units of I/O called log blocks. These log blocks are always read and written in an atomic fashion to guarantee consistency.
Every Base Log File is made up of various records. These records are stored in sectors, which are written to in units of I/O called log blocks. The Base Log File is composed of 6 different metadata blocks (3 of which are shadows), which are all examples of log blocks.
The three types of records that exist in such blocks are:
Control Record that contains info about layout, extend area and truncate area.
Base Record that contains symbol tables and info about the client, container and security contexts.
Truncate Record that contains info on every client that needs to have sectors changed as a result of a truncate operation.
Three metadata records were defined above, yet six metadata blocks exist (and each metadata block only contains one record). This is due to shadow blocks, which are yet another technique used for consistency. Shadow blocks contain the previous copy of the metadata that was written, and by using the dump count in the record header, can be used to restore previously known good data in case of torn writes.
The following enumeration describes the six types of metadata blocks.
After Version, the next set of fields are all related to CLFS Log Extension. This data could potentially be non-zero in memory, but for a stable Base Log File on disk, all of these fields are expected to be zero. This does not, of course, imply the CLFS driver or code necessarily makes this assumption.
The first CLFS Log Extension field, eExtendState, identifies the current extend state for the file using the enumeration below:
The next two values iExtendBlock and iFlushBlock identify the index of the block being extended, followed by the block being flushed, the latter of which will normally be the shadow block. Next, the sector size of the new block is stored in cNewBlockSectors and the original sector size before the extend operation is stored in cExtendStartSectors. Finally, the number of sectors that were added is saved in cExtendSectors.
Block Context: The control record ends with the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File. Although this is expected to be 6, there could potentially exist additional metadata blocks, and so for forward support, the cBlocks field indicates the number of blocks in the array.
Each array entry is identified by the CLFS_METADATA_BLOCK structure, shown below:
typedef struct _CLFS_METADATA_BLOCK
} CLFS_METADATA_BLOCK, *PCLFS_METADATA_BLOCK;
On disk, the cbOffset field indicates the offset, starting from the control metadata block (i.e.: the first sector in the Base Log File). Of where the metadata block can be found. The cbImage field, on the other hand, contains the size of the corresponding block, while the eBlockType corresponds to the previously shown enumeration of possible metadata block types.
In memory, an additional field, pbImage, is used to store a pointer to the data in kernel-mode memory.
CLFS In-Memory Class
Once in memory, a CLFS Base Log File is represented by a CClfsBaseFile class, which can be further extended by a CClfsBaseFilePersisted. The definition for the former can be found in public symbols and is shown below:
These fields mainly represent data seen earlier, such as the size of the container, the sector size, the array of metadata blocks and their number, as well as the size of the whole Base Log File and its location in kernel mode memory. Additionally, the class is reference counted, and almost any access to any of its fields is protected by the m_presImage lock, which is an executive resource accessed in either shared or exclusive mode. Finally, each block itself is also referenced in the m_rgcBlockReferences array, noting there’s a limit of 65535 references. When the general block has been referenced at least once, the m_fGeneralBlockReferenced boolean is used to indicate the fact.
All code listings show decompiled C code; source code is not available in the affected product. Structure definitions are obtained by reverse engineering and may not accurately reflect structures defined in the source code.
Opening a Log File
The CreateLogFile() function in the Win32 API can be used to open an existing log. This function triggers a call to CClfsBaseFilePersisted::OpenImage() in clfs.sys. The pseudocode of CClfsBaseFilePersisted::OpenImage() is listed below:
After initializing some in-memory data structures, CClfsContainer:Open() is called to open the existing Base Log File at . ReadImage() is then called to read the Base Log File at . If the current extend state in the Extend Context is not ClfsExtendStateNone(0) at , the possibility to expand the Base Log File is explored.
If the original sector size before the previous extension (ControlRecord->cExtendStartSectors) is less than or equal to the current sector size of the Base Log File (m_cbContainer), and the sector size of the Block (to be expanded) after the previous extension (ControlRecord->cNewBlockSectors) is less than or equal to the latest required sector size (current sector size of the Block to be expanded this->m_rgBlocks[ControlRecord->iExtendBlock].cbImage >> 9 plus the number of sectors previously added cExtendSectors >> 1), the Base Log File needs expansion. ExtendMetadataBlock() is duly called at .
In all non-malicious cases, the current extend state is expected to be ClfsExtendStateNone(0) when the log file is written to disk.
Since the Extend Context is under attacker control (described below), all the fields discussed above can be set by the attacker.
Reading Base Log File
The CClfsBaseFilePersisted::ReadImage() function called at  is responsible for reading the Base Log File from disk. The pseudocode of this function is listed below:
The in-memory buffer of the rgBlocks array, which defines the set of metadata blocks that exist in the Base Log File, is allocated (m_rgBlocks) at . Each array entry is identified by the CLFS_METADATA_BLOCK structure, which is of size 0x18. The cBlocks field, which indicates the number of blocks in the array, is set to the default value 6 (Hence, the size of allocation for m_rgBlocks is 0x18 * 6 = 0x90).
The content in m_rgBlocks is initialized to 0 at . The first two entries in m_rgBlocks are for the Control Record and its shadow, both of which have a fixed size of 0x400. The sizes and offsets for these blocks are duly set at .
At this stage, m_rgBlocks looks like the following in memory:
CClfsBaseFile::GetControlRecord() is called to retrieve the Control Record from the Base Log File at . The pbImage field in the first two entries in m_rgBlocks are duly populated. More on this below.
At this stage, m_rgBlocks contains the following values:
AcquireMetadataBlock() is called with the second parameter set to ClfsMetaBlockGeneral to read in the General Metadata Block from the Base Log File at . The pbImage field for the corresponding entry and its shadow in m_rgBlocks are duly populated. More on this below.
At this stage, m_rgBlocks looks like the following in memory:
AcquireMetadataBlock() is called with the second parameter of type _CLFS_METADATA_BLOCK_TYPE set to ClfsMetaBlockControl (0) to acquire the Control MetaData Block at . The record offset is retrieved and used to calculate the address of the Control Record, which is saved at .
Acquiring Metadata Block
The CClfsBaseFile::AcquireMetadataBlock() function is used to acquire a metadata block. The pseudocode of this function is listed below:
The m_rgcBlockReferences entry for the Control Metadata Block is increased by 1 to signal its usage at .
If the reference count is 1, it is clear the Control Metadata Block was not being actively used (prior to this). In this case, it needs to be read from disk. The second entry in the virtual function table is set to CClfsBaseFilePersisted::ReadMetadataBlock(), which is duly called at .
Read Metadata Block
The CClfsBaseFilePersisted::ReadMetadataBlock() function is used to read a metadata block from disk. The pseudocode of this function is listed below:
The size of the Metadata block to be read is retrieved and saved in cbImage. Note that these sizes are stored in the Control Record of the Base Log File.
To read the Control Record, the hardcoded value is taken at , as the Control Record is of a fixed size. Memory (ClfsMetadataBlock) is allocated to read the metadata block from disk at . The corresponding pbImage entry in m_rgBlocks is filled in and ClfsMetadataBlock is initialized to 0 at . The function CClfsContainer::ReadSector() is called to read the specified number of sectors from disk at .
ClfsMetadataBlock now contains the exact contents of the metadata Block as present in the file. It is important to note that the Control Metadata Block contains the Control Context as described earlier. Thus, the contents of the Control Context are fully controlled by the attacker. ClfsMetadataBlock is decoded via a call to ClfsDecodeBlock at . It is also important to note that in the case of the Control Metadata Block, this does not modify any field in the Control Context. The corresponding shadow pbImage entry in m_rgBlocks is also set to ClfsMetadataBlock at .
Extending Metadata Block
The call to ExtendMetadataBlock() at  is used to extend the size of a particular metadata block in the Base Log File. The pseudocode of this function pertaining to when the current extend state is ClfsExtendStateFlushingBlock(2) is listed below:
The index of the block being extended (iFlushBlock) and the block being flushed (iExtendBlock) are extracted from the Extend Context in the Control Record of the Base Log File at . With specially crafted values of the above fields and cNewBlockSectors at , code execution reaches ProcessCurrentBlockForExtend() at . ProcessCurrentBlockForExtend() internally calls ExtendMetadataBlockDescriptor(), whose pseudocode is listed below:
It is important to note that the pbImage field for the General Metadata Block and its shadow point to the same memory (refer to  and ). The pbImage field of the iFlushBlock index in m_rgBlocks is freed, and the corresponding entry is cleared at . For example, if iFlushBlock is set to 2, m_rgBlocks looks like the following in memory:
Since the original entry and the shadow index entry pointed to the same memory, the repopulation leaves a reference to freed memory. Any use of the General Metadata Block will refer to this freed memory, resulting in a Use After Free.
The vulnerability can be converted to a double free by closing the handle to the Base Log File. This will trigger a call to FreeMetadataBlock, which will free all pbImage entries in m_rgBlocks.
A basic understanding of the segment heap in the windows kernel introduced since the 19H1 update is required to understand the exploit mechanism. The paper titled “Scoop the Windows 10 pool!” from SSTIC 2020 describes this mechanism in detail.
Windows Notification Facility
Objects from Windows Notification Facility (WNF) are used to groom the heap and convert the use after free into a full exploit. A good understanding of WNF is thus required to understand the exploit. The details about WNF described below are taken from the following sources:
When a WNF State Name is created via a call to NtCreateWnfStateName(), ExpWnfCreateNameInstance() is called internally to create a name instance. The pseudocode of ExpWnfCreateNameInstance() is listed below:
A chunk of size 0xa8 (as seen at ) is allocated from the Paged Pool at  as a structure of type _WNF_NAME_INSTANCE. This results in an allocation of size 0xc0 from the LFH. This structure is listed below:
The StateData pointer is referred to when the WNF State Data is updated and queried. The variable-size data immediately follows the _WNF_STATE_DATA structure.
Updating WNF State Data
When the WNF State Data is updated via a call to NtUpdateWnfStateData(), ExpWnfWriteStateData() is called internally to write to the StateData pointer. The pseudocode of ExpWnfWriteStateData() is listed below.
The InputBuffer and Length parameters to the function contain the contents and size of the data. It is important to note that these can be controlled by a user.
The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at . If the StateData pointer is NULL (as is the case initially) at , or if the current size is lesser than the size of the new data at , memory is allocated from the Paged Pool for the new StateData pointer at . It important to note that the size of allocation is the size of the new data (Length) plus 0x10, to account for the _WNF_STATE_DATA header. The Header and AllocateSize fields shown at  and  of the _WNF_STATE_DATA header are then initialized at .
Note that if the current StateData pointer is large enough for the new data, code execution from  jumps directly to . Length bytes from the InputBuffer parameter are then copied into the StateData pointer at . The DataSize field in the _WNF_STATE_DATA header is also filled at .
Deleting WNF State Name
A WNF State Name can be deleted via a call to NtDeleteWnfStateName(). Among other things, this function frees the associated name instance and StateData buffers described above.
Querying WNF State Data
When WNF State Data is queried via a call to NtQueryWnfStateData(), ExpWnfReadStateData() is called internally to read from the StateData pointer. The pseudocode of ExpWnfReadStateData() is listed below.
The OutBuf and OutBufSize parameters to the function are provided by the user to store the queried data. The StateData pointer is first retrieved from the related name instance of type _WNF_NAME_INSTANCE at . If the output buffer is large enough to store the data (which is checked at ), StateData->DataSize bytes starting right after the StateData header are copied into the output buffer at .
After the creation of a pipe, a user has the ability to add attributes to the pipe. The attributes are a key-value pair, and are stored into a linked list. The PipeAttribute object is allocated in the PagedPool, and has the following structure:
The size of the allocation and the data is fully controlled by an attacker. The AttributeName and AttributeValue are pointers pointing at different offsets of the data field. A pipe attribute can be created on a pipe using the NtFsControlFile syscall, and the 0x11003C control code.
The attribute’s value can then be read using the 0x110038 control code. The AttributeValue pointer and the AttributeValueSize will be used to read the attribute value and return it to the user.
Steps for Exploitation
Exploitation of the vulnerability involves the following steps:
Spray large number of Pipe Attributes of size 0x7a00 to use up all fragmented chunks in VS backend and allocate new ones. The last few will each be allocated on separate segments of size 0x11000, with the last (0x11000-0x7a00) bytes of each segment unused.
Delete one of the later Pipe Attributes. This will consolidate the first 0x7a00 bytes with the remaining bytes in the rest of the segment, and put the entire segment back in the VS backend.
Allocate the vulnerable chunk of size 0x7a00 by opening the malicious Base Log File. This will get allocated from the freed segment in Step 2. Similar to Step 1, the last (0x11000-0x7a00) bytes will be unused. The vulnerable chunk will be freed for the first time shortly afterwards. Similar to Step 2, the entire segment will be back in the VS backend.
Spray large number of WNF_STATE_DATA objects of size 0x1000. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 3. Note that no size lesser than 0x1000 (and maximum is 0x1000 for WNF_STATE_DATA objects) can be used because that will have an additional header that will corrupt the header in the vulnerable chunk, blocking a double free.
Free the vulnerable chunk for the second time. This will end up freeing the memory of one of the WNF_STATE_DATA objects allocated in Step 4, without actually releasing the object.
Allocate a WNF_STATE_DATA object of size 0x1000 over the freed chunk in Step 5. This will create 2 entirely overlapping WNF_STATE_DATA objects of size 0x1000.
Free all the WNF_STATE_DATA objects allocated in Step 4. This will once again put the entire vulnerable segment (of size 0x11000) back in the VS backend.
Spray large number of WNF_STATE_DATA objects of size 0x700, each with unique data. This will first use up fragmented chunks in VS backend and then the entire freed segment in Step 7. Each page in the freed segment is now split as (0x700,0x700,0x1d0 (remaining)). Note, here size 0x700 can be used because the rest of the exploit doesn’t require any more freeing of the vulnerable chunk. This now creates 2 overlapping WNF_STATE_DATA objects, one of size 0x1000 (allocated in Step 6) and other of size 0x700 (allocated here). Size 0x700 is specifically chosen for 2 reasons. The first reason being that the additional chunk header (of size 0x10) in the 0x700-sized object means that the StateData header of the 0x1000-sized object is 0x10 bytes before the StateData header of the 0x700-sized object. Thus, the StateData header of the 0x700-sized object overlaps with the StateData data of the 0x1000-sized object.
Update the StateData of the 0x1000-sized object to corrupt the StateData header of the 0x700-sized object such that the AllocatedSize and DataSize fields of the 0x700-sized object is increased from 0x6c0 (0x700-0x40) to 0x7000 each. Now, querying or updating the 0x700-sized object will result in an out-of-bounds read/write into adjacent 0x700-sized WNF_STATE_DATA objects allocated in Step 8.
Identify the corrupted 0x700-sized WNF_STATE_DATA object by querying all of them with a Buffer size of 0x700. All will return successfully except for the corrupted one, which will return with an error indicating that the buffer size is too small. This is because the DataSize field was increased (refer to Step 9).
Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to further identify the next 2 adjacent WNF_STATE_DATA objects using the OOB read. The first of these will be at offset 0x710 and the second will be at offset 0x1000 from the corrupted 0x700-sized WNF_STATE_DATA object.
Free the second newly identified WNF_STATE_DATA object of size 0x700.
Create a new process, which will run with the same privileges as the exploit process. The token of this new process is allocated over the freed WNF_STATE_DATA object in Step 12. This is the second reason for choosing size 0x700, as the size of the token object is also 0x700.
Query the corrupted 0x700-sized WNF_STATE_DATA object (identified in Step 10) to identify the contents of the token allocated in Step 13 using the OOB read. Calculate the offset to the Privileges.Enabled and Privileges.Present fields in the token*object.
Update the corrupted 0x700-sized WNF_STATE_DATA object to corrupt the first adjacent object (identified in Step 11) using the OOB write. Increase the AllocatedSize and DataSize fields in the StateData pointer (refer to Step 9).
Update the most recent corrupted WNF_STATE_DATA object (Step 15) to corrupt the adjacent token object using the OOB write. Overwrite the Privileges.Enabled and Privileges.Presentfields in the token object to 0xffffffffffffffff, thereby setting all the privileges. This completes the LPE.
We hope you enjoyed reading the deep dive into a use-after-free in CLFS, and if you did, go ahead and check out our other blog posts on vulnerability analysis and exploitation. If you haven’t already, make sure to follow us on Twitter to keep up to date with our work. Happy hacking!
A vulnerability exists within Advantech iView SNMP management tool that allows for remote attackers to bypass authentication checks and reach a SQL injection vulnerability within the ‘ztp_search_value’ parameter to the ‘NetworkServlet’ endpoint. Successful exploitation allows for remote code execution with administrator privileges.
A vulnerability exists within Advantech iView SNMP management tool that allows for remote attackers to bypass authentication checks and reach a SQL injection vulnerability within the ‘data’ parameter to the ‘NetworkServlet’ endpoint. Successful exploitation allows for the exfiltration of user data, included clear text passwords.
A vulnerability exists within Advantech iView SNMP management tool that allows for remote attackers to bypass authentication checks and reach a SQL injection vulnerability within the ‘search_term’ parameter to the ‘NetworkServlet’ endpoint. Successful exploitation allows for remote code execution with administrator privileges.
A vulnerability exists within Advantech iView SNMP management tool that allows for remote attackers to bypass authentication checks and reach a SQL injection vulnerability within the ‘getInventoryReportData’ parameter to the ‘NetworkServlet’ endpoint. Successful exploitation allows for remote code execution with administrator privileges.
A vulnerability exists within Advantech iView SNMP management tool that allows for remote attackers to bypass authentication checks and reach a SQL injection vulnerability within the ‘ztp_config_id’ parameter to the ‘NetworkServlet’ endpoint. Successful exploitation allows for the exfiltration of user data, included clear text passwords.