Tuesday 7 September 2021

Behind BTHPORT.SYS

Paving the way for 0days

Dedicated to:
Zix, who spent precious-time guiding me within the last two-months.
Mohammed, Jalil, Zakariae, Yasser, Jeff keep fighting!
Hakirat, knchofkom.

The registered handler for IRP_MJ_DEVICE_CONTROL (BthHandleControlDispatch) supports a multitude of control-codes to permit applications to interact with SDP, HCI and L2CAP protocols. However, most operations require local Radio to be active and [&H411180] can be issued to query its state.

enum _BTH_RADIO_STATE
{
	RADIO_STATE_UNKNOWN,
	RADIO_ON_PENDING,
	RADIO_ON,
	RADIO_OFF_PENDING,
	RADIO_OFF,
	RADIO_DISABLED_BY_POLICY
}; 
BLUETOOTH_FIND_RADIO_PARAMS params;
HANDLE hRadio;
DWORD  state, returned;
params.dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS);
if (BluetoothFindFirstRadio(&params, &hRadio))
{
    if (DeviceIoControl(hRadio,
                        0x411180,
                        NULL, 0,
                        &state, sizeof(state),
                        &returned, NULL))
        printf("Radio state: %d\n", state);
    CloseHandle(hRadio);
}

BluetoothFindFirstRadio invokes functions from devobj.dll to acquire an Object-path using a GUID that uniquely identifies Bluetooth radios and calls CreateFileW with FILE_SHARE_READ | FILE_SHARE_WRITE to get a HANDLE;

wchar_t* GetRadioPath()
{
    unsigned int    flags[8]=
    {
        [0] = 32,
        [6] = 0
    }, size = 0;
    unsigned char*  tmp_path;
    HANDLE   hDeviceInfoList;
    wchar_t*     path = NULL;
    if ((hDeviceInfoList = (HANDLE) DevObjCreateDeviceInfoList(NULL,
                                                               NULL,
                                                               0, 0, 0))
        != INVALID_HANDLE_VALUE)
    {
        if (DevObjGetClassDevs(hDeviceInfoList,
                               &GUID_BTHPORT_DEVICE_INTERFACE, NULL,
                               18, 0, 0) &&
            DevObjEnumDeviceInterfaces(hDeviceInfoList,
                               NULL, &GUID_BTHPORT_DEVICE_INTERFACE,
                               0, &flags))
        {
        while (! DevObjGetDeviceInterfaceDetail(hDeviceInfoList,
                                                &flags,
                                                tmppath,
                                                size, &size,
                                                NULL))
        {
            if (tmppath)
                break;
            tmppath = (unsigned char*) malloc(size);
            memset(tmppath, 0, size);
            *((unsigned int*) tmppath) = 8;
        }
        path = (wchar_t*) (tmppath + 4);
        }
        DevObjDestroyDeviceInfoList(hDeviceInfoList);
    }
    return path;
}

To perform device-inquiry (scan for devices in proximity), [&H411000] takes BTH_DEVICE_INQUIRY in lpInBuffer and a PDWORD to receive device-count in lpOutBuffer, it winds up in HCI_DoInquiry calling HCI_SendCommandEx:

#define GIAC    0x009E8B33
#define LIAC    0x009E8B00
typedef struct { ULONG Lap; BYTE TimeoutMultiplier; BYTE TransmitPwrLvl; } __attribute__((packed)) BTH_DEVICE_INQUIRY; DWORD Inquire(HANDLE hRadio, unsigned char multiplier, unsigned char power) { BTH_DEVICE_INQUIRY inquiry; DWORD ndevice
=0, returned; if (multiplier <= 0x30 && power <= 0x14) { inquiry.Lap = GIAC; inquiry.TimeoutMultiplier = multiplier; inquiry.TransmitPwrLvl = power; DeviceIoControl(hRadio, 0x411000, &inquiry, sizeof(inquiry), &ndevice, sizeof(ndevice), &returned, NULL); } return ndevice; }

Retrieve devices with [&H410008] which writes into a BTH_DEVICE_INFO_LIST structure, an initial call with sizeof(ULONG) in nOutBufferSize gives numOfDevices, used to allocate a sufficiently large block. Noteworthy is that if BTH_DEVICE_INFO is not a packed structure and its size is different than 272, this procedure fails.

BTH_DEVICE_INFO_LIST* GetRemoteDevice(HANDLE hRadio)
{
    BTH_DEVICE_INFO_LIST* devices;
    ULONG count;
    DWORD size, returned;
    devices = NULL;
    if (DeviceIoControl(hRadio,
                        0x410008,
                        NULL, 0,
                        &count, sizeof(count),
                        &returned, NULL))
    {
        if (count)
        {
        size = sizeof(ULONG) + sizeof(BTH_DEVICE_INFO) * (count-1);
        devices =  (BTH_DEVICE_INFO_LIST*) malloc(size);
        DeviceIoControl(hRadio,
                        0x410008,
                        NULL, 0,
                        devices, size,
                        &returned, NULL);
        }
    }
    return devices;
}

To make this device discoverable, [&H411020] will perform HCI_Write_Scan_Enable with HCI_UpdateScanEnable:

int ScanEnable(HANDLE hRadio, unsigned char bEnable)
{
    unsigned short scanFlags[2];
    DWORD returned;
    scanFlags[0] = 0x0103;
    scanFlags[1] = bEnable & 1;
    return DeviceIoControl(hRadio,
                           0x411020,
                           scanFlags, sizeof(scanFlags),
                           NULL, 0,
                           &returned, NULL);
}

A local paged SDP Database (TAG: SdpD) stores the available services, each its own set of attributes and SecurityFlags, Winsock provides functions to lookup services on remote Devices: 

  • WSALookupServiceBeginW 
  • WSALookupServiceNextW 
  • WSALookupServiceEnd

A local service with a GUID assigned can bind to BT_PORT_ANY and listen for incoming connections, but lookups won't be able to reveal the record until it's registered with WSASetService which ends up in NSPSetService from wshbth.dll.

A valid SDP Record in lpBlob can be used if supplied, otherwise an SDP Tree is constructed from WSAQUERYSET, converted to a stream and passed to SdpPublishRecord or SdpRemoveRecord depending on (essoperation).

Each SDP_NODE begins with an SDP_NODE_HEADER containing descriptors to describe data elements constant or variable in nature, a leading-byte must be calculated as (Type << 3) | ((SpecificType >> 8) & 7).

  • 0x35 (SEQUENCE, VARBYTE)    0x3A     (SIZE)
  • 0x09 (UINT, WORD)                     0x0001 (VALUE)
  • 0x1C (UUID, OWORD)                  00001101-0000-1000-8000-00805F9B34FB
  • 0x19 (UUID, WORD)                     0x0100
  • 0x08 (UINT, BYTE)                       0x04
  • 0x25 (STRING, VARBYTE)           0x0E     (LENGTH)

Attributes encoded as UINT16 quantize a stream, for better visualization, a hierarchical model can be constructed:

Channel #(0x04) which is stored as UINT8 in ProtocolDescriptorList(4) should be altered, getsockname populates a _SOCKADDR_BTH structure with socket-information including ->port.

  • [&H410214] and [&H41021c] call SdpDB_AddRecord
  • [&H410218] calls SdpDB_RemoveRecord

These IOCTLs with their respective names are given in [1] and [2], a valid BLOB must be sent in lpInBuffer and a HANDLE in lpOutBuffer  (to unlink record from _SDP_DATABASE) for this operation to be successful.

unsigned char blobSdp[] =
{
    0x35, 0x3A, 0x09, 0x00, 0x01, 0x35, 0x11, 0x1C,
    0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x10, 0x00,
    0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB,
    0x09, 0x00, 0x04, 0x35, 0x0C, 0x35, 0x03, 0x19,
    0x01, 0x00, 0x35, 0x05, 0x19, 0x00, 0x03, 0x08,
    0xFF, 0x09, 0x01, 0x00, 0x25, 0x0E, 0x53, 0x69,
    0x6D, 0x70, 0x6C, 0x65, 0x20, 0x53, 0x65, 0x72,
    0x76, 0x69, 0x63, 0x65
};

HANDLE AddRecord(HANDLE hRadio, int hSocket)
{
    SOCKADDR_BTH socketaddress;
    HANDLE       hRecord;
    DWORD        returned;
    int          socketsize = sizeof(socketaddress);
    getsockname(hSocket, (sockaddr*) &socketaddress, &socketsize);
    blobSdp[40] = socketaddress.port;
    if (DeviceIoControl(hRadio,
                        0x410214,
                        &blobSdp, sizeof(blobSdp),
                        &hRecord, sizeof(hRecord),
                        &returned, NULL))
        return hRecord;
    return INVALID_HANDLE_VALUE;
}

To send SDP requests to a remote party, an L2CAP connection needs to be established first with [&H410200] taking in the CONNECTION_REQUEST structure, writing ExAllocatePool kernel-pointer back to userspace as an identifier in L2CapConnectionList and can be used for disconnecting [&H410204].

💩

typedef struct
{
    BTH_ADDR  BthAddress;
    DWORD     ConnType;
    void*     Connection;
    BYTE      Flags;
} __attribute__((packed)) CONNECTION_REQUEST;
void* GetConnection(HANDLE hRadio, BTH_ADDR address)
{
    CONNECTION_REQUEST request;
    DWORD returned;
    memset(&request, 0, sizeof(request));
    request.ConnType   = 2;        // [Bit#0: new(0)/cached(1)]
    request.BthAddress = address;
    DeviceIoControl(hRadio,
                    0x410200,
                    &request, sizeof(request),
                    &request, sizeof(request),
                    &returned, NULL);
    return request.Connection;
}

int TerminateConnection(HANDLE hRadio, void* Connection)
{
    DWORD  returned;
    return DeviceIoControl(hRadio,
                           0x410204,
                           &Connection, sizeof(Connection),
                           NULL, 0,
                           &returned, NULL);
}

This requires an active device to be in proximity for session-establishment, service-lookup can be executed with [&H410208], UUIDs to be searched-for in lpInBuffer, record-handles will be written to lpOutBuffer;
If
UUID is 16/32bit, it will replace HIDWORD of BASE_UUID(00000000-0000-1000-8000-00805F9B34FB).

typedef struct
{
    void*         Connection;
    SdpQueryUuid  Uuids[MAX_UUIDS_IN_QUERY];
} __attribute__((packed)) SERVICE_SEARCH;

unsigned int* GetServices16(HANDLE hRadio, void* Connection, USHORT uuid)
{
    SERVICE_SEARCH  query;
    unsigned int*   services, size;
    DWORD           returned;

    size     = sizeof(int) * 32;
    services = (unsigned int*) malloc(size);
    memset(services, 0, size);
    memset(&query, 0, sizeof(query));
    query.Connection        = Connection;
    query.Uuids[0].uuidType = SDP_ST_UUID16;
    query.Uuids[0].u.uuid16 = uuid;

    if (DeviceIoControl(hRadio,
                        0x410208,
                        &query, sizeof(query),
                        services, size,
                        &returned, NULL))
        return services;
    free(services);
    return NULL;
}
// RFCOMM Protocol
if ((services = GetServices16(hRadio, request.Connection, 0x0003)))
{
    unsigned int handle, i = 0;
    while ((i < 32) && (handle = services[i++]))
        printf("Service Handle: 0x%X\n", handle);
}

Once a service-handle is obtained, attributes can be searched-for with [&H41020C] which takes attribute-ranges in input and outputs a sequence-blob:

typedef struct
{
    void*              Connection;
    DWORD              Flags;
    DWORD              ServiceHandle;
    SdpAttributeRange  Range[1];
} __attribute__((packed)) ATTRIBUTE_SEARCH;

typedef struct
{
    DWORD              BlobSize[2];
    unsigned char      Blob[0];
} __attribute__((packed)) ATTRIBUTE_RESPONSE;

ATTRIBUTE_RESPONSE* GetAttribute(HANDLE   hRadio,
                                 void* Connection,
                                 unsigned hService, 
                                 unsigned
AttributeId) { ATTRIBUTE_SEARCH request; ATTRIBUTE_RESPONSE* response; DWORD returned, size; request.Connection = Connection; request.Flags = 0; request.Range[0].minAttribute = AttributeId; request.Range[0].maxAttribute = AttributeId; request.ServiceHandle = hService; size = sizeof(ATTRIBUTE_RESPONSE) + 512; response = (ATTRIBUTE_RESPONSE*) malloc(size); if (DeviceIoControl(hRadio, 0x41020C, &request, sizeof(request), response, size, &returned, NULL)) return response; free(response); return NULL; }
// Get Service-Name
ATTRIBUTE_RESPONSE* response;
response = GetAttribute(hRadio, request.Connection, services[0], 0x100);
if (response)
{
    int i, upper;
    i       = -1;
    upper   = response->BlobSize[0] - 1;
    while (i < upper)
    {
        printf("%02X ", response->Blob[++i]);
        if ((i & 7) != 7)
            continue;
        printf("\n");
    }
    printf("\n");
    free(response);
}

HCI packets begin with an indicator to identify their type:

HCI commands are variable-length structures containing an OpCode (composed of Opcode Group Field(OGF) and Opcode Command Field(OCF)) and a finite set of Parameters, with Length specified.

#define Opcode(OGF, OCF) ((OGF << 10) | OCF)
void SplitOpcode(unsigned short opcode, unsigned char* ogf, unsigned char* ocf)
{
    *ogf = opcode >> 10;
    *ocf = opcode & 1023;
}

Vendor-specific commands have OGF &B111111, bthport.sys provides [&H410050] to call HCI_VendorCommand which requires providing the correct ManufacturerId and LmpVersion(0) in a PBTH_VENDOR_SPECIFIC_COMMAND structure, obtainable with [&H410000].

int GetLocalInfo(HANDLE hRadio, BTH_LOCAL_RADIO_INFO* pDeviceInfo)
{
    DWORD  returned;
    return DeviceIoControl(hRadio,
                           0x410000,
                           NULL, 0,
                           pDeviceInfo, sizeof(*pDeviceInfo),
                           &returned, NULL);
}

This component prohibits non-authorized processes and limited to a specific OGF, it should be patched to permit arbitrary HCI commands instead:

Make sure to backup the original Driver
To modify a file from
%WINDIR%\\System32\\drivers, transfer ownership from NT SERVICE\TrustedInstaller to current user and give Users full-control, reboot in Safe Mode (4) to replace it before rebooting with Driver Signature Enforcement (7) disabled.

Terms:

EIR      Extended Inquiry Response
SDP    Service Discovery Protocol
HCI     Host-Controller Interface
DIB     Device Information Block
bthprops.cpl
BluetoothApis.dll
bthport.sys 
devobj.dll
ws2_32.dll
wshbth.dll

No comments:

Post a Comment