Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .config/codespell_ignore.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ negociate
optiona
ot
potatoe
recal
referer
requestor
ro
Expand Down
160 changes: 160 additions & 0 deletions scapy/contrib/bluetooth_vsc_csr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# SPDX-License-Identifier: GPL-2.0-only
# This file is part of Scapy
# See https://scapy.net/ for more information
#
# scapy.contrib.description = CSR/BlueCore Bluetooth HCI Vendor-Specific Commands
# scapy.contrib.status = loads
#
# Information sources:
# - BlueZ ``tools/csr.h`` / ``tools/csr.c`` (BCCMD framing + varid list)
# - BlueZ ``tools/parser/csr.c`` (BCCMD PDU dissection)

from scapy.packet import Packet, bind_layers
from scapy.fields import (
ByteField,
LEShortField,
LEShortEnumField,
XStrField,
)

from scapy.layers.bluetooth import (
HCI_Command_Hdr,
HCI_Event_Vendor,
)

# BCCMD PDU type (first header word).
_csr_bccmd_pdu_type = {
0x0000: "getreq",
0x0001: "getresp",
0x0002: "setreq",
}

# BCCMD varids (BlueZ ``csr.h``).
# High nibble is an operation class: 0x2xxx read-only info, 0x3xxx iterators
# and parameterised gets, 0x4xxx valueless actions (resets/halts/radio),
# 0x5xxx test, 0x6xxx config, 0x7003 PS-key door.
_csr_varid = {
0x000b: "ps_clr_all",
0x000c: "ps_factory_set",
0x082d: "ps_clr_all_stores",
0x2801: "bc01_status",
0x2819: "buildid",
0x281a: "chipver",
0x281b: "chiprev",
0x2825: "interface_version",
0x282a: "rand",
0x282c: "max_crypt_key_length",
0x2836: "chipanarev",
0x2838: "buildid_loader",
0x2c00: "bt_clock",
0x3005: "ps_next",
0x3006: "ps_size",
0x3008: "crypt_key_length",
0x3009: "piconet_instance",
0x300a: "get_clr_evt",
0x300b: "get_next_builddef",
0x3012: "ps_memory_type",
0x301c: "read_build_name",
0x4001: "cold_reset",
0x4002: "warm_reset",
0x4003: "cold_halt",
0x4004: "warm_halt",
0x4005: "init_bt_stack",
0x4006: "activate_bt_stack",
0x4007: "enable_tx",
0x4008: "disable_tx",
0x4009: "recal",
0x400d: "ps_factory_restore",
0x400e: "ps_factory_restore_all",
0x400f: "ps_defrag_reset",
0x4010: "kill_vm_application",
0x4011: "hopping_on",
0x4012: "cancel_page",
0x4818: "ps_clr",
0x481c: "map_sco_pcm",
0x482e: "single_chan",
0x5004: "radiotest",
0x500c: "ps_clr_stores",
0x6000: "no_variable",
0x6802: "config_uart",
0x6805: "panic_arg",
0x6806: "fault_arg",
0x6827: "max_tx_power",
0x682b: "default_tx_power",
0x7003: "ps",
}


def _bccmd_set_length(p):
"""
Fill in the BCCMD ``length`` word (total PDU size in 16-bit words: all
bytes after the ``channel`` byte, divided by 2).
"""
total_words = (len(p) - 1) // 2
return p[:3] + total_words.to_bytes(2, "little") + p[5:]


class HCI_Cmd_VSC_CSR_BCCMD(Packet):
"""
CSR BCCMD command (opcode 0xFC00).
"""
name = "CSR BCCMD"
fields_desc = [
ByteField("channel", 0xC2),
LEShortEnumField("pdu_type", 0x0000, _csr_bccmd_pdu_type),
LEShortField("length", None),
LEShortField("seqno", 0),
LEShortEnumField("varid", 0, _csr_varid),
LEShortField("status", 0),
XStrField("value", b"\x00" * 8),
]

def post_build(self, p, pay):
p += pay
if self.length is None:
p = _bccmd_set_length(p)
return p


class HCI_Event_VSC_CSR_BCCMD(HCI_Event_Vendor):
"""
CSR BCCMD response, carried in the HCI vendor-specific event (code 0xFF).

Registered as an ``HCI_Event_Vendor`` handler, so it replaces the generic
vendor event whenever the body looks like a BCCMD PDU (see ``check``).
"""
name = "CSR BCCMD response"
match_subclass = True
fields_desc = [
ByteField("channel", 0xC2),
LEShortEnumField("pdu_type", 0x0001, _csr_bccmd_pdu_type),
LEShortField("length", None),
LEShortField("seqno", 0),
LEShortEnumField("varid", 0, _csr_varid),
LEShortField("status", 0),
XStrField("value", b""),
]

@classmethod
def check(cls, body):
"""
Checks if the given 0xFF vendor-event body is a BCCMD PDU: a 0xC2
channel byte followed by the 5-word (10-byte) header.
"""
return len(body) >= 11 and body[0] == 0xC2

def post_build(self, p, pay):
p += pay
if self.length is None:
p = _bccmd_set_length(p)
return p


bind_layers(HCI_Command_Hdr, HCI_Cmd_VSC_CSR_BCCMD, ogf=0x3F, ocf=0x000)

# The BCCMD reply rides the generic 0xFF vendor event, which is shared with
# other vendors. Rather than rebinding that event code (split_layers), register
# the response as a handler so it is only used when the body looks like a BCCMD
# PDU (see its ``check``). This lets the CSR contrib coexist with other vendor
# contribs that use the same event code.
HCI_Event_Vendor.register_handler(HCI_Event_VSC_CSR_BCCMD)
118 changes: 118 additions & 0 deletions test/contrib/bluetooth_vsc_csr.uts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
% CSR/BlueCore Bluetooth Vendor-Specific Command (VSC) layer tests

+ Load the CSR VSC contrib module

= Import the module (opt-in layer)
from scapy.layers.bluetooth import *
load_contrib("bluetooth_vsc_csr")
from scapy.contrib.bluetooth_vsc_csr import *


+ CSR BCCMD command (opcode 0xFC00)

= BCCMD shares OGF 0x3F / OCF 0x000, i.e. opcode 0xFC00
cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_CSR_BCCMD(pdu_type="getreq", seqno=1, varid=0x2819)
assert cmd.ogf == 0x3f
assert cmd.ocf == 0x000
assert cmd.opcode == 0xfc00

= GETREQ build with auto-computed length word
# The 8-byte default value area makes a 19-byte PDU: all bytes after the channel
# byte are 18, i.e. 9 little-endian words, so length is auto-filled to 0x0009.
cmd = HCI_Command_Hdr() / HCI_Cmd_VSC_CSR_BCCMD(pdu_type="getreq", seqno=1, varid=0x2819)
r = raw(cmd)
# 00fc(op) 13(plen) c2(channel) 0000(getreq) 0900(length) 0100(seqno)
# 1928(varid buildid) 0000(status) 0000000000000000(value)
assert r == bytes.fromhex("00fc13c2000009000100192800000000000000000000")
p = HCI_Command_Hdr(r)
assert HCI_Cmd_VSC_CSR_BCCMD in p
b = p[HCI_Cmd_VSC_CSR_BCCMD]
assert b.channel == 0xc2
assert b.pdu_type == 0x0000
assert b.length == 9
assert b.seqno == 1
assert b.varid == 0x2819
assert b.status == 0
assert b.value == b"\x00" * 8

= The length word counts every 16-bit word after the channel byte
# Drop the value area entirely: 11-byte PDU -> (11 - 1) / 2 = 5 words.
cmd = HCI_Cmd_VSC_CSR_BCCMD(pdu_type="getreq", seqno=1, varid=0x2819, value=b"")
r = raw(cmd)
assert r == bytes.fromhex("c200000500010019280000")
assert HCI_Cmd_VSC_CSR_BCCMD(r).length == 5

= An explicit length is preserved, not recomputed
cmd = HCI_Cmd_VSC_CSR_BCCMD(pdu_type="getreq", seqno=1, varid=0x2819, length=0x1234)
assert HCI_Cmd_VSC_CSR_BCCMD(raw(cmd)).length == 0x1234

= The pdu_type and varid enums resolve to names
cmd = HCI_Cmd_VSC_CSR_BCCMD(pdu_type="setreq", varid=0x7003)
assert cmd.pdu_type == 0x0002
assert cmd.varid == 0x7003
assert cmd.sprintf("%pdu_type%") == "setreq"
assert cmd.sprintf("%varid%") == "ps"

= A GETREQ carrying a non-default value payload (PS-key door) round-trips
# The PS-key door (varid 0x7003) puts [pskey, nwords, stores] at the head of the
# value area; the contrib carries it as opaque bytes.
val = bytes.fromhex("010004000000") + b"\x00" * 8
cmd = HCI_Cmd_VSC_CSR_BCCMD(pdu_type="getreq", seqno=0x42, varid=0x7003, value=val)
p = HCI_Cmd_VSC_CSR_BCCMD(raw(cmd))
assert p.varid == 0x7003
assert p.seqno == 0x42
assert p.value == val
# length = (1 channel excluded) -> 10 header bytes + 14 value bytes = 24 -> 12 words
assert p.length == 12


+ CSR BCCMD response (HCI vendor event, code 0xFF)

= The response is registered as an HCI_Event_Vendor handler
# The 0xFF event code is shared across vendors, so the CSR response is registered
# as a handler (its check() claims only BCCMD-shaped bodies) instead of rebound.
assert HCI_Event_VSC_CSR_BCCMD in HCI_Event_Vendor.registered_handlers
assert issubclass(HCI_Event_VSC_CSR_BCCMD, HCI_Event_Vendor)

= check() claims a 0xC2-led PDU of at least 11 bytes and nothing else
assert HCI_Event_VSC_CSR_BCCMD.check(b"\xc2" + b"\x00" * 10)
assert not HCI_Event_VSC_CSR_BCCMD.check(b"\xc2\x00") # too short
assert not HCI_Event_VSC_CSR_BCCMD.check(b"\x99" + b"\x00" * 10) # wrong channel
assert not HCI_Event_VSC_CSR_BCCMD.check(b"")

= A GETRESP vendor event dissects through the handler (buildid = 0x22bb)
# 04(evt) ff(vendor) 13(plen) c2(channel) 0100(getresp) 0900(length) 0100(seqno)
# 1928(varid buildid) 0000(status) bb22000000000000(value)
raw_evt = bytes.fromhex("04ff13c201000900010019280000bb22000000000000")
evt = HCI_Hdr(raw_evt)
assert HCI_Event_VSC_CSR_BCCMD in evt
r = evt[HCI_Event_VSC_CSR_BCCMD]
assert r.channel == 0xc2
assert r.pdu_type == 0x0001
assert r.get_field("pdu_type").i2repr(r, r.pdu_type) == "getresp"
assert r.length == 9
assert r.seqno == 1
assert r.varid == 0x2819
assert r.status == 0
assert r.value == bytes.fromhex("bb22000000000000")
# match_subclass keeps the handler reachable as the generic vendor event
assert HCI_Event_Vendor in evt
assert evt[HCI_Event_Vendor] is r

= Dissection is idempotent and rebuild reproduces the wire bytes
assert raw(HCI_Hdr(raw_evt)) == raw_evt
# rebuild from parsed fields with auto-length reproduces the PDU body
body = raw_evt[3:]
r2 = HCI_Event_VSC_CSR_BCCMD(channel=r.channel, pdu_type=r.pdu_type,
seqno=r.seqno, varid=r.varid, status=r.status,
value=r.value)
assert raw(r2) == body

= A vendor event that is not a BCCMD PDU falls back to the generic event
# No registered check claims it (channel byte is not 0xC2), so the body stays in
# HCI_Event_Vendor.data and other vendor contribs remain free to claim it.
p = HCI_Event_Hdr(b"\xff\x02\x99\xaa")
assert type(p[HCI_Event_Vendor]) is HCI_Event_Vendor
assert HCI_Event_VSC_CSR_BCCMD not in p
assert p[HCI_Event_Vendor].data == b"\x99\xaa"
assert raw(p) == b"\xff\x02\x99\xaa"
Loading