(CVE-2024-43626) Windows Telephony Service Heap Out-of-Bounds Read/Write Leading to Elevation of Privilege

CVE: CVE-2024-43626

Affected Versions: Windows 10 (1507, 1607, 1809, 21H2, 22H2); Windows 11 (22H2, 23H2, 24H2); Windows Server 2008 and later

CVSS3.1: 7.8 (High) — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

Product Windows Telephony Service (tapisrv.dll)
Vendor Microsoft
Severity High — a local unprivileged attacker may exploit this to elevate privileges to SYSTEM
Affected Versions Windows 10 (1507, 1607, 1809, 21H2, 22H2); Windows 11 (22H2, 23H2, 24H2); Windows Server 2008 and later
Tested Versions Windows 11 23H2 (Build 22631.3593)
CVE Identifier CVE-2024-43626
CVE Description Improper input validation in Windows Telephony Server in Microsoft Windows may allow an unprivileged attacker to execute code as SYSTEM
CWE Classification(s) CWE-122: Heap-based Buffer Overflow

CVSS3.1 Scoring System

Base Score: 7.8 (High) Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Metric Value
Attack Vector (AV) Local
Attack Complexity (AC) Low
Privileges Required (PR) Low
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) High
Availability (A) High

Product Background

The Windows Telephony Service is a local service running on Windows by default, with its main logic implemented in tapisrv.dll inside a svchost.exe process. It exposes three RPC interfaces accessible to unprivileged clients:

[
    uuid(2f5f6520-ca46-1067-b319-00dd010662da),
    version(1.0),
]
interface DefaultIfName
{
    long Proc0_ClientAttach(
        handle_t hBindingHandle,
        [out][context_handle] void **pCtxHandle,
        [in]long lProcessID,
        [out]long *phAsyncEventsEvent,
        [in][string]wchar_t *pszDomainUser,
        [in][string]wchar_t *pszMachine);

    void Proc1_ClientRequest(
        handle_t hBindingHandle,
        [in][context_handle] void *CtxHandle,
        [in][out][size_is(BufferSize)][length_is(*InputOutputSize)]char *Buffer,
        [in] long BufferSize,
        [in][out] long *InputOutputSize);

    void Proc2_ClientDetach(
        handle_t hBindingHandle,
        [in][out][context_handle] void **pCtxHandle);
}

ClientAttach() retrieves a context handle, which is passed to ClientRequest() to perform operations. ClientRequest() is a dispatcher that invokes sub-functions based on an opnum.

Technical Details

Opnum 69 corresponds to LSetAppPriority(), which has a path into GetPriorityListTReqCall(). This function opens the HandOffPriorities registry key in the current user hive and passes it to GetPriorityList():

void __fastcall GetPriorityListTReqCall(wchar_t **pOutput)
{
    if ( !RegOpenCurrentUser(0xF003Fu, &hkCU) )
    {
        if ( !RegOpenKeyExW(hkCU,
                L"Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities",
                0, 0x20019u, &hkHandOffPriority) )
        {
            GetPriorityList(hkHandOffPriority, L"RequestMakeCall", pOutput);
            RegCloseKey(hkHandOffPriority);
        }
        RegCloseKey(hkCU);
    }
}

Inside GetPriorityList(), the registry value is read via RegQueryValueExW() — first with a null output buffer to obtain the data size, then into a heap buffer of cbData + 2 bytes. The data is passed directly to _wcsupr(), an unsafe string function that depends on a null terminator to determine the string’s length:

void __fastcall GetPriorityList(HKEY hKey, LPCWSTR RequestMediaCallName, wchar_t **pOutput)
{
    if ( RegQueryValueExW(hKey, RequestMediaCallName, 0, &Type, 0, &cbData) || !cbData )
    {
        *pOutput = 0;
    }
    else
    {
        v6 = HeapAlloc(ghTapisrvHeap, HEAP_ZERO_MEMORY, cbData + 2);
        v7 = (wchar_t *)v6;
        if ( v6 )
        {
            *(_WORD *)v6 = '"';
            if ( !RegQueryValueExW(hKey, RequestMediaCallName, 0, &Type, (LPBYTE)v6 + 2, &cbData) )
            {
                _wcsupr(v7);    /* unsafe — no null terminator validation */
                *pOutput = v7;
            }
        }
    }
}

Since an unprivileged user controls the contents of the RequestMakeCall registry value under their own hive, they can write a value without a null terminator. When _wcsupr() processes this value it continues past the end of the allocated heap chunk into the adjacent chunk, corrupting data belonging to it. By shaping the heap so that a useful structure borders the allocation, an attacker can corrupt fields such as a “buffer size used” counter or a pointer to hijack execution.

Additionally, LSetAppPriority() calls SetPriorityList() to write the processed value back to the registry:

LSTATUS __fastcall SetPriorityList(HKEY hKey, LPCWSTR lpValueName, LPCWSTR lpString)
{
    if ( !lpString )
        return RegDeleteValueW(hKey, lpValueName);
    v7 = lstrlenW(lpString);
    return RegSetValueExW(hKey, lpValueName, 0, 1u, (const BYTE *)lpString + 2, 2 * v7);
}

lstrlenW() similarly depends on the null terminator, so it reads past the end of the allocation into the adjacent chunk and writes that data — including heap pointers — into the registry value. By querying this value, an attacker can leak heap addresses from the svchost.exe process, breaking ASLR and heap randomisation.

The _wcsupr() out-of-bounds write can be avoided during the information leak phase by starting the payload with a non-ASCII character, since _wcsupr() stops when it encounters a character outside the ASCII range.

The recommended fix is to replace RegQueryValueExW() with RegGetValueW(), which enforces the data type and automatically appends null terminators for string values.

The following screenshots demonstrate the heap pointer leak in action:

Heap pointer leak — PoC output

Heap pointer leak — consistent across runs

Credit

Chen Le Qi and Nguyễn Đăng Nguyễn of STAR Labs SG Pte. Ltd.

Timeline