CVE-2024-26230
is a critical vulnerability found in the Windows Telephony Service (TapiSrv), which can lead to an elevation of privilege on affected systems. The exploit leverages a use-after-free in FreeDialogInstance
. By manipulating the registry, an attacker controls memory allocation to create a fake object, triggering the UAF in TUISPIDLLCallback
to gain code execution. This is further chained with techniques to bypass mitigations like CFG and ultimately load a malicious DLL, escalating privileges to SYSTEM via PrintSpoofer. In this blog post, we will take an in-depth look at how this vulnerability works, how it can be exploited, and the mitigation strategies that can help defend against it.
Product | Windows Telephony Service (TapiSrv) |
---|---|
Vendor | Microsoft |
Security Impact | Elevation of Privilege |
CVE ID | CVE-2024-26230 |
CWE(s) | CWE-416 - Use After Free |
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Microsoft Windows 10 Version 22H2 Build 19045.3803 AMD64
The vulnerability exists within the FreeDialogInstance
function in C:\Windows\System32\tapisrv.dll
. Specifically, it occurs when the function deallocates the GOLD
object while the original context handle retains a pointer to it, leading to a use-after-free condition.
Here are the primary reasons for the occurrence of this bug and how it can be exploited:
FreeDialogInstance
function deallocates the GOLD
object (identified by the magic number 0x474F4C44
) without ensuring that the context handle object no longer references it. This issue is evident at both Line 1 and Line 2.
TUISPIDLLCallBack
function attempts to use the freed GOLD
object, which is now a dangling pointer. It accesses a virtual function at v12 + 0x20
and subsequently invokes it, triggering the use-after-free vulnerability.
In typical use-after-free exploitation scenarios, we seek a suitable object to serve as an exploit primitive. In kernel mode, a variety of objects can be leveraged for this purpose. However, in user mode—where TAPISrv
operates as an RPC service—the vulnerability can only be exploited with objects within the same process. As such, without an appropriate object already present in the target process, direct exploitation of the vulnerability is not possible.
In this case, we need to think outside the box. Instead of searching for a suitable object to serve as an exploit primitive, the goal is to find a primitive that allows us to create a memory allocation where both the size and content can be fully controlled. With this approach, we can craft a fake object that can be used to exploit the vulnerability.
One such primitive is a dispatch function named TRequestMakeCall
, which opens the registry key Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities
and allocates memory to store the key values.
By inspecting the registry with Registry Editor
, we can confirm that this registry key is configured with Full Control
permissions for the user.
Since this key is fully user-controlled, it allows us to manipulate the value of the RequestMakeCall
key. This gives us control over both the allocated heap size and the content of the memory.
The Windows Telephony Service is a component of Windows RPC (Remote Procedure Call), allowing us to interact with it as we would with any standard RPC service.
To utilize RPC, some knowledge with Interface Definition Language (IDL) is required.
IDL stands for Interface Definition Language, which is used to define the interfaces of an RPC server. It is an attributed programming language and is similar to a C header file. Understanding IDL is important because it helps you know the interfaces that Telephony
provides and understand how we can communicate with Telephony
services. But don’t worry—it’s quite simple. Let’s take a deep dive together!
The telephony service provides three interfaces, which can be defined as follows:
interface tapsrv {
typedef [context_handle] void* PCONTEXT_HANDLE_TYPE;
long ClientAttach(
[out][context_handle] PCONTEXT_HANDLE_TYPE* pphContext,
[in] long lProcessID,
[out]long* phAsyncEventsEvent,
[in, string] const wchar_t* pszDomainUser,
[in, string] wchar_t* pszMachine
);
void ClientRequest(
[in] PCONTEXT_HANDLE_TYPE phContext,
[in, out, length_is(*plUsedSize), size_is(lNeededSize)] unsigned char* pBuffer,
[in] long lNeededSize,
[in, out] long* plUsedSize
);
void ClientDetach(
[in, out] PCONTEXT_HANDLE_TYPE* pphContext
);
};
The most important interface here, is the ClientRequest
. It allows you to communicate, or in other terms, use various functions that Telephony offers in gaFuncs
table.
gaFuncs
in IDA.
As you can see, the ClientRequest
function takes a buffer, pBuffer
, as its second argument. The pBuffer
structure is defined as follows:
The first element (at index 0) is always the offset to the functions in the gaFuncs
table. For example:
GetUIDllName
, the offset is 0x1.TUISPIDLLCallback
, the offset is 0x2, and so on.The second element in the buffer represents the first argument for the invoked function. The third element corresponds to the second argument, and so forth.
Each element in the buffer occupies 4 bytes, which is equivalent to the size of an integer.
To invoke the GetUIDllName
, your pseudo-code would be:
write_func_offset((void*)buffer, GaFuncs::GetUIDllName); // first one at 0th is always offset of the dispatch functions in gaFuncs table
write_param((void*)buffer, 1, 1); // param1_GetUIDllName
write_param((void*)buffer, 2, 3); // param2_GetUIDllName
write_param((void*)buffer, 5, -1); // param5_GetUIDllName
LONG byteWritten = (sizeof(buffer) / sizeof(*buffer));
ClientRequest(context, buffer, 1024, &byteWritten);
Using the exploit primitive described above, the use-after-free vulnerability can be exploited with the following setup:
GetUIDllName
to create a GOLD
object.FreeDialogInstance
to free the GOLD
object, thereby creating a dangling pointer.TRequestMakeCall
to craft a fake object for the use-after-free scenario.TUISPIDLLCallback
to trigger the use-after-free on the fake object, taking control of the program’s execution flow.The visualization of these steps is shown below.
At this stage, we are able to control the RIP (Return Instruction Pointer) to point to any desired location. However, the Control Flow Guard mitigation prevents the creation of a ROP (Return-Oriented Programming) chain in this exploitation attempt. Further information on Control Flow Guard can be found in the following references:
Nevertheless, it remains possible to invoke Win32 functions imported by the binary. For example, I could invoke VirtualAlloc
as below.
The exploitation steps to achieve LoadLibraryW
to load our controlled DLL file are as follows:
malloc
function. If malloc
is executed successfully, it will return a pointer to the allocated address. In TUISPIDLLCallback
, the lower 32 bits of the returned value are written into the RPC input buffer and returned to the client. Consequently, this results in a useful information leak.
We successfully leaked lower 32 bit.
Step 2: Utilize the leaked lower 32 bits above to invoke a call to VirtualAlloc
, enabling the creation of a Read-Write-Execute (RWX) memory region.
Step 3: Copy the DLL path to the previously created RWX memory region using memcpy_s
. The copy operation is limited to three characters at a time so we have to repeat this step for a few times.
LoadLibrary
with the path to our hack.dll
(a reverse shell) that was copied into the RWX memory region in Step 3. This will result a shell with NT Authority\Network Service
privileges.
NT Authority/Network Service
have SeImpersonate
privilege. The SeImpersonate
is a Windows privilege that grants a user or process the ability to impersonate the security context of another user or account. This privilege allows a process to assume the identity of a different user, enabling it to perform actions or access resources as if it were that user. However, if not properly managed or granted to unauthorized users or processes, the SeImpersonate
can pose a significant security risk. In this case, we can achieve higher privilege by using PrintSpoofer
exploit to leverage to NT Authority/System
. For further details on PrintSpoofer exploit, see PrintSpoofer
While conducting further research into this n-day vulnerability, my colleague Chen Le Qi discovered an interesting bug in one of the functions used during the exploitation process. Special thanks to him for his invaluable help and for sharing insights during the writing of this blog post.
This bug is located in the GetPriorityList
function within tapisrv.dll
and allows for an Out-of-Bound Write primitives, and later on, an information leak in SetPriorityLeak
. It allows an attacker to leak the address of GOLD
objects.
The GetPriorityList
function was used to spray the heap during earlier exploitation steps. Below are more details about what it does:
The function invokes RegQueryValueExW
to retrieve length of the value from a registry key.
Upon success, it allocates memory on the heap based on the length of the registry key’s value.
It invokes RegQueryValueEx
again, gets the value and converts the registry key’s value to uppercase using _wcsupr
, and stores the result ins *a3
(the third argument).
The issue occurs in the final step when the function invokes _wcsupr
to convert the registry key’s value to uppercase. The _wcsupr
function and the GetPriorityList
does not verify whether the input string is null-terminated. Consequently, if the registry key’s value is not null-terminated, _wcsupr
will read beyond the buffer until it encounters a null byte (\0
). This can result in an information leak later on, as the extra data read is stored in *a3
and returned to the caller.
After the buffer *a3
is returned, it is subsequently used in the SetPriorityList
function.
As shown in the image above, the lstrlenW
function is called to calculate the length of the lpString
that is saved to the registry key. Unfortunately, lstrlenW
assumes that the input string is null-terminated. If the string becomes corrupted in GetPriorityList
and is then passed to SetPriorityList
, it will be saved to the registry key. This behavior can result in an information leak.
There are two code paths that could be used to exploit this vulnerability:
ClientAttach
-> ClientDetach
with RequestMediaCall
key.LSetAppPriority
with RequestMakeCall
key.
In recent updates, Microsoft addressed this bug by explicitly null terminating the string, preventing _wcsupr
from reading beyond the allocated buffer. This single patch should also address two bugs in both GetPriorityList
and SetPriorityList
, as it is GetPriorityList
that accepts a string without null-termination.
This bug has been assigned the name CVE-2024-43626 and got patched in November 12, 2024 Updates. Interestingly, this is not the only issue Microsoft addressed in the patch. If we take a closer look, another patch can be observed at Line 20
, where an integer overflow check is implemented on cbData
. The cbData
variable represents the length of the registry key’s value, as obtained during the initial invocation of RegQueryValueExW
within GetPriorityList
.
After the cbData
value is retrieved, it is used to allocate memory for the registry key’s value by invoking HeapAlloc
with a size of cbData + 2
. If cbData
reach the maximum value of a DWORD, this could result in an integer overflow.
However, I was unable to create a registry key value of such a large size during testing. This limitation exists because the length of a registry key’s value is limited. If anyone has insights into creating large registry key values, please comment down below, I would greatly appreciate your knowledge. Thank you in advance!
Telephony
service with command sc start Tapisrv
. Telephony service is not running by default, but you could start it with normal user privilege. After started, it will run under NT Authority/Network Services
privilege.
sc query Tapisrv
Copy the hack.dll
to Desktop under path C:\Users\user01\Desktop\hack.dll
. It’s a simple reverse shell, source code could be found in attached Proof of Concept
.
Run Set_Null_DACL.exe
to set DACL of hack.dll
to NULL, which means everyone could access. Source code for this program could be found in attached Proof of Concept
.
nc -nlvp 13338
.
whoami /priv
to verify that NT Authority/Network Service
have SeImpersonate
privilege.
PrintSpoofer
exploit to achieve NT Authority/System
. For further details, see PrintSpoofer
Due to the nature of the vulnerability, an attacker needs to start the Telephony service as a normal user and the service could crash during exploitation. Consequently, users should implement measures to prevent normal users from starting the Telephony service or to monitor and log any instances when the service crashes.
PoC_CVE_2024_26230.exe (exploit)
CVE-2024-26230
highlights a significant vulnerability within the Windows Telephony Service, presenting a risk for privilege escalation on affected systems. While this vulnerability can be exploited through a series of carefully orchestrated steps, the key to mitigating such risks lies in proactive system hardening. By restricting access to the Telephony Service, monitoring unusual service activity, and ensuring proper security configurations, administrators can significantly reduce the attack surface. As with any critical vulnerability, staying updated with the latest patches and applying security best practices are essential in protecting systems from potential exploits.
Thanks for reading!