Overview
CVE-2024-38812 is a critical heap-overflow vulnerability identified in VMware vCenter Server’s implementation of the DCERPC (Distributed Computing Environment/Remote Procedure Call) protocol. This flaw allows a malicious actor with network access to the vCenter Server to send specially crafted packets, potentially leading to remote code execution (RCE). The vulnerability, classified under CWE-122 (Heap-based Buffer Overflow), arises when memory allocated in the heap is improperly overwritten, leading to unpredictable behavior that could be exploited. Published in September 2024, CVE-2024-38812 carries a CVSS score of 9.8, highlighting its severity and high risk of exploitation. VMware vCenter Server version 8.0U3a is vulnerable, while version 8.0U3b contains the necessary patches to mitigate this issue. This vulnerability affects not only vCenter Server but also VMware Cloud Foundation, as detailed in VMware’s security advisory VMSA-2024-0019.
To fully grasp the exploitation potential of CVE-2024-38812, it is crucial to understand key concepts that govern the DCERPC protocol and how it handles memory and communication within vCenter Server. Critical components of the protocol play a vital role in mapping operations and data types, facilitating client-server communication. These technical elements help explain how a heap overflow can be triggered. In the following sections, we will dive into the vulnerability details, illustrating how these components interact to enable potential remote code execution, while also highlighting the patches and updates available in VMware vCenter Server 8.0U3b.
Reaching The Vulnerable Function
Figure 1 – Operation #, Unmarshalling Param Count, and Type Index #
In the libdcerpc.so library, the rpc_ss_ndr_unmar_interp function plays a key role in the marshalling (serialization) and unmarshalling (deserialization) processes of the RPC framework, the parameters such as parameter count and type index are crucial in controlling the program flow during the marshalling (serialization) and unmarshalling (deserialization) processes of the RPC system. The parameter count (e.g., 1LL, 2LL, 10LL) specifies how many parameters or arguments the function expects for a specific remote operation. The type index (e.g., 0x1E8LL, 0xB8LL, 0x2F0LL) provides the starting position in the IDL_type_vec table, which holds information about the types of parameters for that particular operation. These elements work together to ensure that the appropriate data types are processed in the correct order. As rpc_ss_ndr_unmar_interp() proceeds, it uses the parameter count to loop through the parameters and the type index to retrieve information about each parameter’s type from the type vector table.
To exploit a vulnerability in the DCERPC operations (Figure 1), such as a heap-overflow vulnerability, an attacker must carefully select a function (Figure 2) with the appropriate parameter count and type index to trigger the vulnerable code. The specific function chosen controls which parameters are processed and how memory is allocated and accessed. For instance, if an operation with a higher parameter count and a particular type index leads to a vulnerable memory allocation, the attacker can craft an input that exploits that allocation process. To reach the vulnerable function, the attacker would need to understand which specific operation (identified by opX_ssr) calls rpc_ss_ndr_unmar_interp() with the right combination of parameter count and type index to navigate to the memory management code that contains the flaw. Only by selecting the correct function with the corresponding type index can the attacker reach the point in the code where the vulnerability can be exploited.
Figure 2- Operation # and Function Signatures
Root Cause Analysis
Figure 3 – rpc_ss_ndr_contiguous_elt()
The function rpc_ss_ndr_contiguous_elt() (Figure 3) is vulnerable due to its handling of the range_list->lower value, which is controlled by user input. Specifically, the vulnerability (Figure 4) resides in the following line:
Figure 4 – User Controlled, range_product and range_list->lower
This line modifies the base address of p_array_addr by adding an offset derived from several parameters: the range_list->lower value, element_size, and the multiplier range_product. Since the range_list->lower value is sourced from input provided to the function, an attacker can manipulate this value to control the offset, directly impacting the pointer arithmetic that recalculates the base address of p_array_addr. If the offset is large enough, it can push the array’s address into memory regions that the attacker can control. Once an attacker controls the range_list->lower value, they effectively control the memory address to which p_array_addr points. By carefully manipulating range_list->lower and influencing how much the base pointer is incremented, the attacker can shift the pointer into arbitrary memory locations. This control allows the attacker to potentially read or write to critical areas of memory, leading to severe consequences such as modifying control structures or executing arbitrary code.
The use of element_size and range_product as multipliers further amplifies the risk. Larger values for these parameters increase the effect of the manipulated range_list->lower, making it easier for an attacker to shift the pointer into dangerous memory regions.
In the function rpc_ss_ndr_contiguous_elt()shown in Figure 3, the pointers can be manipulated to reach beyond a 4GB memory boundary due to the calculation involving range_product, range_list->lower, and element_size. In this case, range_product is derived from potentially large values in range_list and range_difference. When these large values are multiplied together, the result is an offset added to p_array_addr. These calculations have been broken out in Figure 5 for reference.
Figure 5 – Output Operations on p_array_addr
If the resulting offset is large enough, it can push the address from the base into much higher memory regions, potentially reaching into the 500 GB range or higher. Since range_list->lower is attacker-controlled, this enables crafted inputs to manipulate the offset and effectively cause pointer arithmetic to exceed normal bounds (Figure 6), accessing unintended memory locations well beyond the 4GB range, leading to a potential heap overflow or arbitrary memory access.
Figure 6 – Calculations for Z_values Array
Triggering the Vulnerability
Figure 7 – Stub Data
With the root cause of the vulnerability understood, python can be used to create a proof-of-concept packet in order to trigger a crash. “stub_data,” as shown in Figure 7, represents the key portion of the network packet that is needed in order to trigger the vulnerability. After four bytes of padding, the Z_values represent the conformance information for an array. Specifically, Z_values indicate the expected size or bounds of each dimension of the array being processed. These values allow the marshalling/unmarshalling functions to correctly allocate memory and process array elements dynamically. Figure 8 below shows “stub_data” expanded, as it would be seen in a malicious network packet sent over the network.
Figure 8 – Full Protocol Packet Sent to a vulnerable Server
The Z_values are fed into the function rpc_ss_ndr_contiguous_elt to help determine:
- The total number of elements in the array.
- Whether the array is contiguous in memory.
- How to calculate the address of each element in memory.
When the code checks the range difference and compares it to the Z_values, it verifies whether the array conforms to the expected size for each dimension. If the difference matches the Z_values, the array is considered contiguous, meaning that the elements are stored sequentially in memory. This helps the unmarshalling process allocate the right amount of memory and correctly interpret the array’s structure.
Each byte in the Z_values array, as shown in (Figure 7), serves a distinct role in establishing conditions that could lead to exploitation of the vulnerability.
Z_values[0] = 0x63:
- Z_values[0] represents the first dimension of the array. The range_product is 52067 (in decimal), which equals 0xCB63 in hex.
- Since Z_values are only 1 byte each, we take the least significant byte (LSB) of 0xCB63, which is 0x63. This byte represents the conformance for the first dimension, fitting it into the byte-size constraint.
Z_values[1] = 0x66:
- Z_values[1] corresponds to the second dimension. The second dimension has a range_difference of 52070 (in decimal), which is 0xCB66 in hex.
- Again, since Z_values are 1 byte, we use the LSB of 0xCB66, which is 0x66, to represent the conformance information for the second dimension.
Z_values[2] = 0x03:
- Z_values[2] represents the third dimension. Based on our research, it is assumed to be a smaller dimension, like the range_list->lower value, which is 3. This could represent a dimension with only a few elements.
- Thus, Z_values[2] is set to 0x03, indicating the small size for the third dimension.
Z_values[3] = 0x02:
- Z_values[3] is for the fourth dimension and is set to 0x02, which implies that the fourth dimension contains only a single element. This is a typical case for higher-dimensional arrays when certain dimensions may have fewer elements.
Leveraging the vulnerability
Figure 9 – unmar_by_copying function signature
In the function rpc_ss_ndr_unmar_by_copying(Figure 9), memory copying is performed using the memcpy function (Figure 10), where p_array_addr acts as the destination pointer and data_addr as the source. This memcpy can be leveraged to exploit the vulnerability. The length of the data to be copied is controlled by IDL_left_in_buff, which is calculated based on the buffer’s remaining size and the value of copy_length, a variable influenced by attacker-controlled input. Since both p_array_addr and the length (IDL_left_in_buff) depend on external input, an attacker can control the memory destination and the amount of memory being copied.
Figure 10 – memcpy inside unmar_by_copying
By sending the packet outlined in (Figure 8) a segmentation fault will occur at this memcpy. The video in (Figure 11) uses gdb to demonstrate the crash and the potential impact of this heap overflow.
Figure 11 – Proof of Concept
Patch Impact Analysis
Figure 12 – Vulnerable section of code patched
The patch (Figure 12) introduces a significant fix focusing on how the calculation involving the range list is handled, particularly in the verification of range_list->lower. The original code had less control over ensuring that the range_list->lower and range_difference values were within expected boundaries, allowing an attacker to manipulate them. The updated version 8.0U3b makes sure that checks are properly applied to the range_list->lower, and it eliminates vulnerabilities related to unbounded pointer arithmetic by using stronger conditions, this involves making sure that the difference between the two values (range_difference) matches the specific value found at a particular position in the Z_values array, which is determined by dividing the range_index by 4 and then moving one spot further in the array and controlling how the range_product is calculated across iterations. These changes reduce the risk of unintentional memory manipulation, effectively preventing the overflow condition that could lead to remote code execution by preventing memory from being moved to unintended regions.
Summary
The exploitation hinges on the ability to manipulate memory pointers using elements like range_list->lower, which can cause pointer arithmetic to push memory addresses into unintended regions, leading to arbitrary memory writes. Moreover, in functions such as rpc_ss_ndr_unmar_by_copying(), attacker-controlled variables like copy_length allow the attacker to control both the destination and the amount of data being copied, heightening the risk of memory corruption. VMware’s patch introduces additional checks on memory-boundary calculations, preventing unbounded pointer arithmetic and reducing the potential for remote exploitation.