Request for advice for future PR: establishing conventions for Bluetooth HCI Vendor-Specific Commands (VSCs)
In Bluetooth over HCI, there is a notion of "Vendor-Specific Commands". These have the Opcode Group Field (OGF) set to 0x3F (max value of all 1s for this 6-bit field), and then have the Opcode Command Field (10 bits) set to whatever the vendor wants. Vendors use these to implement chip-specific functionality, which is not outlined in the HCI spec. E.g. common examples of this are things like setting the BDADDR, transmit power, or allowing memory read/write to a controller's memory.
I'd like to submit a future PR to include VSCs for Realtek which I REed (e.g. for use with scapy-usbbluetooth). But there are also VSCs which are publicly documented (e.g. see here for TI or here (auto-downloading PDF) for Cypress) which I could imagine adding eventually.
Before I submit the PR, I'd like to discuss how you'd like this to be handled in an extensible way. I see 4 possible options:
- Just put VSCs in bluetooth.py where all the existing HCI stuff is.
- Create a new file (bluetoothVSC.py?) which gets all VSCs for all vendors.
- Create a per-vendor file (bluetoothVSCRealtek.py, bluetoothVSCTexasInstruments.py, etc), which has only individual companies' VSCs.
How would you prefer this be handled?
An important thing to consider is that VSCs will necessarily conflict with each other between vendors. I.e. vendor 1 will tend to start with OGF=0x3F, OCF=0x001, and vendor 2 may well do the same. So I don't know whether that will cause a problem for Scapy, but I'm assuming it won't, and people would just cast the VSCs to whichever vendor interpretation they want and that it would be handled fine?
There would also be Vendor Specific Events (VSEs) which are the definitions of the data that comes back from VSCs (e.g. a read-RAM could be limited to 4 bytes response, or variable length up to 251 bytes.)
Example Realtek VSCs
| Functionality | OCF | Size, in bits, of memory to read (1 byte) | Address to read (4 bytes) |
|---|---|---|---|
| Read RAM | 0x061 | Valid values: 0x08, 0x10, 0x20 | Little-endian value. E.g. 0x12, 0x34, 0x56, 0x78 = 0x78563412 |
Scapy definition:
# Tested and known-working definitions
class HCI_Cmd_VSC_Realtek_Read_Mem(Packet):
name = "Realtek Read Memory"
fields_desc = [
ByteField("size", 0x20),
XLEIntField("address", 0x80000000)
]
bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Realtek_Read_Mem, ogf=0x3f, ocf=0x0061)
| Functionality | OCF | Size, in bits, of memory to write (1 byte) | Address to write (4 bytes) | Data to write (variable: 1, 2, 4 bytes) |
|---|---|---|---|---|
| Write RAM | 0x062 | Valid values: 0x08, 0x10, 0x20 | 4 byte, little-endian value. E.g. 0x12, 0x34, 0x56, 0x78 = 0x78563412 | Little-endian value, depending on size. E.g. 0x00, 0x11, 0x22, 0x33 = 0x33221100 |
Scapy definition:
# Tested and known-working definitions
class HCI_Cmd_VSC_Realtek_Write_Mem(Packet):
name = "Realtek Write Memory"
fields_desc = [
ByteField("size", 0x20),
XLEIntField("address", 0x80000000),
XLEIntField("data_to_write", 0x33221100)
]
bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_Realtek_Write_Mem, ogf=0x3f, ocf=0x0062)
Hi there!
Just a couple of thoughts I have given to this...
I think a possible location for this would be the contrib folder. Some of this VSCs are documented and some are reverse engineered. Some may be present in some chips or firmware versions and other not. Some of them will be supported officially, some of them are there just for devel/debug purposes. I would not trust this to be a well documented spec with a reliability that would justify this in the main folder. I think contrib is a nice place for that. What do you think?
A second idea I would bet for is to create a per vendor file. Most of vendors include similar/same commands for all their chips. Grouping per vendor will keep things tidy. Puntually, probably, it would make sense to create a file for a particular chip or lineup but I would say that would be minor situations. I would for sure avoid allocating all of the VSCs in a single file because it would lead to a lot of binding collisions. Many vendors have the same opcode mean different operations.
Would this make sense to scapy people?
Thanks!
I agree that scapy/contrib is a good solution.
VSCs can for example be loaded with load_contrib("bluetooth_vsc_realtek"), and will avoid conflicting VSCs to be loaded at the same time. How many vendors / files are we looking at?
How many vendors / files are we looking at?
Bluetooth has ~20-30 entities which might plausibly make VSCs (primarily 20+ silicon makers, but occasionally (RT)OS makers too like MS, Android, or Zephyr.) Right now I only have ~4 silicon makers' security-relevant VSCs at hand (and don't intend to document non-security-relevant ones initially, even if their documentation is public.)
I'd also ask for suggestion when there are potentially overlapping VSCs from the same company. I.e. TI has two different ranges that have the same functionality (setting a BDADDR) for different chips, but I find it likely that eventually we'll find a vendor that conflicts with itself over time (either due to it just not being a problem for the devs, or due to mergers & acquisitions). So would we name files like "bluetooth_VSC_TexasInstruments1" vs. "bluetooth_VSC_TexasInstruments2" and then it's on the user to load the right one for the chip they're dealing with?
I'd also ask for suggestion when there are potentially overlapping VSCs from the same company. I.e. TI has two different ranges that have the same functionality (setting a BDADDR) for different chips, but I find it likely that eventually we'll find a vendor that conflicts with itself over time (either due to it just not being a problem for the devs, or due to mergers & acquisitions). So would we name files like "bluetooth_VSC_TexasInstruments1" vs. "bluetooth_VSC_TexasInstruments2" and then it's on the user to load the right one for the chip they're dealing with?
For this I would suggest using lineups naming or even specific chip models if needed. I hope this will not be a huge thing to deal with... I hope this will not happen that often.
For this I would suggest using lineups naming or even specific chip models if needed. I hope this will not be a huge thing to deal with... I hope this will not happen that often.
I don't think we want to do either of those. Let's not hope, let's assume conflicts will routinely be true and handle the worst case scenario. E.g. let's say we find a conflict with Broadcom. Then we'd have e.g. bluetooth_VSC_Broadcom_433x_2070x.py vs. bluetooth_VSC_Broadcom_2034_204x.py vs. bluetooth_VSC_Broadcom_435x etc (made even worse if it was individual chip numbers, because it'd be inevitably incomplete and in need of multiple updates.)
I think it's better to just document inside bluetooth_VSC_Broadcom1, bluetooth_VSC_Broadcom2, bluetooth_VSC_Broadcom3 files (or _family1, _family2, etc if that seems more clear), which chips they apply to, as those families of VSC usage are figured out. Because another thing is that if one had to go changing names on a per chip family or individual chip basis for files as we learned they applied to new models being released, then we'd be breaking everyone's past code that loaded the old file names.
(In retrospect I kind of like the _familyX suffix and then we could name all files _family1 to start with, with the explicit statement in the documentation that it's not possible to load multiple families between chip vendors or even within a chip vendor, without there being an expectation of a fatal conflict. And if anyone knows they need VSCs from two families for whatever reason, it's their own job to hack together a merged family file that picks just the non-conflicting pieces they need.)
This look good to me. I agree that conflicting VSCs must be handled by the end users if they need a to mix and match them.
Thanks for this discussion. I faced a similar situation in automotive. Here are a few concepts, I've used. Maybe some will be applicable for your task as well.
ObservableDict:
class UDS(ISOTP):
services = ObservableDict(
{0x10: 'DiagnosticSessionControl',
0x11: 'ECUReset',
...,
...,
0x7f: 'NegativeResponse'}) # type: Dict[int, str]
name = 'UDS'
fields_desc = [
XByteEnumField('service', 0, services)
]
I defined the "default" enum values in uds.py. Once I have a proprietary extension of the protocol on a specific target, I can add the new descriptions for the enum values dynamically in contrib files. For example: contrib/automotive/bmw/definitions.py
UDS.services[0xBF] = "DevelopmentJob"
UDS.services[0xFF] = "DevelopmentJobPositiveResponse"
Dynamic PacketListFields
Suppose you have a packet that contains a list of "sub" packets. In DTC Snapshots, I have the following structure:
DTCSnapshot(number_of_subpackets=3)
CustomSnapshotA(identifier=1234, information_A=42, information_B=1337)
CustomSnapshotB(identifier=2345, mac="AA:BB:CC:DD:EE:FF")
CustomSnapshotC(identifier=4321, float_value=3.41)
The tricky part is, that each sub-packet can have a unique length, depending on the specified data. I needed to extend the DTCSnapshot class in a way, that I can add Sub-Packet definitions depending on the current Target, since every Target has different Sub-Packets.
Therefore I've used a PacketListField with a callback which I can dynamically overwrite.
uds.py
class DTCSnapshot(Packet):
identifiers = defaultdict(list) # for later extension
@staticmethod
def next_identifier_cb(pkt, lst, cur, remain):
return Raw
fields_desc = [
ByteField("record_number", 0),
ByteField("record_number_of_identifiers", 0),
PacketListField(
"snapshotData", None,
next_cls_cb=lambda pkt, lst, cur, remain: DTCSnapshot.next_identifier_cb(
pkt, lst, cur, remain))
]
def extract_padding(self, s):
return '', s
Custom extension:
class _SnapshotIdentifierPacket(Packet):
def extract_padding(self, s: bytes) -> Tuple[bytes, Optional[bytes]]:
return b'', s
class KM_STAND(_SnapshotIdentifierPacket):
description = "km_stand"
fields_desc = [
XShortField("identifier", 0),
ThreeBytesField("value", 0)
]
class ABS_ZEIT(_SnapshotIdentifierPacket):
description = "abs_zeit"
fields_desc = [
XShortField("identifier", 0),
IntField("value", 0)
]
DTCSnapshot.identifiers[b'\x17\x00'] = KM_STAND
DTCSnapshot.identifiers[b'\x17\x01'] = ABS_ZEIT
def next_identifier_cb(pkt: Packet,
lst: List[BasePacket],
cur: Optional[Packet],
remain: bytes
) -> Optional[Type[Packet]]:
if remain is None or len(remain) < 3:
logging.error("No data to parse %s", repr(remain))
return cast(Type[Packet], Raw)
if pkt.record_number_of_identifiers == len(lst) + 1:
# this Snapshot is fully parsed
return None
snapshots_left = pkt.record_number_of_identifiers - (len(lst) + 1)
current_identifier = remain[:2]
ret = None
try:
ret = DTCSnapshot.identifiers[current_identifier]
except KeyError:
pass
if ret:
return ret
logging.error("couldn't parse ...")
return cast(Type[Packet], Raw)
DTCSnapshot.next_identifier_cb = next_identifier_cb