Module: PWN::SDR::Decoder::Bluetooth
- Defined in:
- lib/pwn/sdr/decoder/bluetooth.rb
Overview
Bluetooth LE 1M single-channel advertising/connected PDU decoder.
I/Q → continuous Ruby FM/NRZ fallback at 1 Mbit/s → hunt LSB-first Access Address (adv = 0x8E89BED6) → dewhiten (7-bit LFSR seeded ch|0x40) → PDU header (type/len) → AdvA (6 bytes) → CRC-24 (poly 0x65B, init 0x555555). Emits per-PDU pdu_type:, adv_addr:, crc_ok:. CRC failures are rejected. Connected packets require explicit AA, CRCInit and encryption state. CTE header is parsed; no direction finding, hopping, L2CAP assembly, 2M/coded PHY or decryption.
Defined Under Namespace
Classes: DemodIQ, ExtendedHeader, LinkLayer, SymbolStream
Constant Summary collapse
- BLE_ADV_AA =
0x8E89BED6- BLE_CRC_POLY =
0x65B- BLE_CRC_INIT =
0x555555- BLE_PDU_TYPE =
{ 0 => 'ADV_IND', 1 => 'ADV_DIRECT_IND', 2 => 'ADV_NONCONN_IND', 3 => 'SCAN_REQ', 4 => 'SCAN_RSP', 5 => 'CONNECT_IND', 6 => 'ADV_SCAN_IND', 7 => 'ADV_EXT_IND' }.freeze
- BLE_ADV_CHANNELS =
{ 37 => 2_402_000_000, 38 => 2_426_000_000, 39 => 2_480_000_000 }.freeze
- GIAC_LAP =
BR/EDR: 64-bit sync word derived from LAP; general-inquiry LAP=0x9E8B33.
0x9E8B33
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.ble_crc24(opts = {}) ⇒ Object
- Supported Method Parameters
crc = PWN::SDR::Decoder::Bluetooth.ble_crc24(bytes: Array
) BLE CRC-24 (LSB-first LFSR, poly 0x65B, init 0x555555).
-
.decode(opts = {}) ⇒ Object
Realtime options forwarded to Base: on_frame (Hash callback), output (writable IO), interactive (default true), duration (seconds), stop (callable), queue_size (bounded chunks), log_file (path or false).
-
.detect(opts = {}) ⇒ Object
Energy observations only; never substituted for protocol decoding.
- .help ⇒ Object
- .parse_line(opts = {}) ⇒ Object
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. [email protected]
394 395 396 |
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 394 public_class_method def self. "AUTHOR(S):\n 0day Inc. <[email protected]>\n" end |
.ble_crc24(opts = {}) ⇒ Object
- Supported Method Parameters
crc = PWN::SDR::Decoder::Bluetooth.ble_crc24(bytes: Array
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 322 public_class_method def self.ble_crc24(opts = {}) bytes = opts[:bytes] || [] # Reflect the specification's MSB-oriented CRCInit for a right-shift LFSR. reg = opts.fetch(:init, BLE_CRC_INIT).to_s(2).rjust(24, '0').reverse.to_i(2) bytes.each do |byte| 8.times do |i| b = (byte >> i) & 1 fb = (reg ^ b) & 1 reg >>= 1 reg ^= 0xDA6000 if fb == 1 # reflected x^24+x^10+x^9+x^6+x^4+x^3+x+1 reg &= 0xFFFFFF end end # Register holds CRC LSB-first — return as-is (matched LSB-first on air) reg end |
.decode(opts = {}) ⇒ Object
Realtime options forwarded to Base: on_frame (Hash callback), output (writable IO), interactive (default true), duration (seconds), stop (callable), queue_size (bounded chunks), log_file (path or false).
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 347 public_class_method def self.decode(opts = {}) freq_obj = opts[:freq_obj] hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq]) raise ArgumentError, 'only BLE 1M PHY is supported' unless opts.fetch(:phy, :le1m).to_sym == :le1m ble = opts.fetch(:ble, freq_obj.fetch(:ble, true)) # Nearest BLE advertising channel unless caller forces one. ch = opts[:channel] || BLE_ADV_CHANNELS.min_by { |_, f| (f - hz).abs }&.first || 37 rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 4_000_000).to_i PWN::SDR::Decoder::Base.run_iq( **opts, fallback: :raise, freq_obj: freq_obj, protocol: ble ? 'BLE' : 'BT-BR/EDR', sample_rate: rate, source: opts[:source], file: opts[:file], demod: DemodIQ.new(rate: rate, channel: ch, ble: ble, access_address: opts.fetch(:access_address, BLE_ADV_AA), crc_init: opts[:crc_init], encrypted: opts[:encrypted], extended: opts.fetch(:extended, false)), note: 'LE 1M single-channel GFSK: AA, dewhitening, PDU fields and CRC-24.', describe: proc { |b| { modulation: 'GFSK', channel: ch, hop_slots: (b[:duration_ms] / 0.625).round } } ) end |
.detect(opts = {}) ⇒ Object
Energy observations only; never substituted for protocol decoding.
374 375 376 377 |
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 374 public_class_method def self.detect(opts = {}) Base.run_detector(opts.merge(protocol: 'Bluetooth', note: 'Energy detector only; use .decode for supported protocol frames.')) end |
.help ⇒ Object
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 398 public_class_method def self.help puts "USAGE: # Supported: BLE 1M legacy advertising; extended: true permits type 7 # ADV_EXT_IND/AUX_ADV_IND on an explicitly selected channel 0..39. # Connected: access_address:, crc_init:, encrypted: true/false required. # Encrypted bytes remain ciphertext; no key-based decryption implemented. # CTEInfo and LL_CHANNEL_MAP_IND fields are parsed (no direction finding). # No BR/EDR, coded/2M PHY, hopping, AUX chain or L2CAP assembly. # Invalid lengths/CRC are rejected. Repeated valid packets are preserved. # phy: :le1m; ble: true. No silent energy fallback from .decode. #{self}.detect(freq_obj: 'required', on_frame: 'optional callback') # Run ble crc24 and return its result #{self}.ble_crc24( bytes: 'optional - bytes value consumed by #ble_crc24 (defaults to [])' ) # Run decode and return its result #{self}.decode( freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq', on_frame: 'optional - callback receiving each emitted Hash', output: 'optional - writable IO (default stdout)', interactive: 'optional - false disables ENTER input', duration: 'optional - finite seconds to run', stop: 'optional - callable returning true to stop', queue_size: 'optional - bounded pending chunks (default 8)', log_file: 'optional - JSONL path or false to disable logging', channel: 'optional - channel value consumed by #decode', access_address: 'optional - Integer connection access address; default advertising AA', crc_init: 'optional - Integer 24-bit CRCInit; required for connected decoding', encrypted: 'optional - required for connected decoding: true yields ciphertext only; false yields plaintext', extended: 'optional - enable extended advertising PDUs, default false', sample_rate: 'optional - sample rate value consumed by #decode', source: 'optional - source value consumed by #decode', file: 'optional - filesystem path' ) # Parse CRC-verified connected header and payload bytes. #{self}::LinkLayer.parse( header: 'required - Array of two or three header bytes', payload: 'required - Array of payload bytes', encrypted: 'required - Boolean indicating ciphertext rather than plaintext' ) # Parse length-bounded extended advertising header bytes. #{self}::ExtendedHeader.parse(payload: 'required - Array of extended advertising payload bytes') # Run parse line and return its result #{self}.parse_line( line: 'optional - line value consumed by #parse_line' ) # Print the AUTHOR(S) string for this module. #{self}.authors " constants.sort end |
.parse_line(opts = {}) ⇒ Object
379 380 381 382 383 384 385 386 387 388 389 390 |
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 379 public_class_method def self.parse_line(opts = {}) line = opts[:line].to_s out = { protocol: 'Bluetooth' } out[:lap] = ::Regexp.last_match(1) if line =~ /LAP[=: ]([0-9a-fA-F]{6})/ out[:uap] = ::Regexp.last_match(1) if line =~ /UAP[=: ]([0-9a-fA-F]{2})/ out[:bd_addr] = ::Regexp.last_match(1) if line =~ /(?:AdvA|BD_ADDR)[=: ]([0-9a-fA-F:]{12,17})/ out[:pdu_type] = ::Regexp.last_match(1) if line =~ /\b(ADV_\w+|SCAN_\w+|CONNECT_REQ)\b/ out[:channel] = ::Regexp.last_match(1) if line =~ /ch[=: ]?(\d{1,2})\b/i out[:rssi] = ::Regexp.last_match(1) if line =~ /rssi[=: ]?(-?\d+)/i out[:summary] = "BT #{out.values_at(:pdu_type, :bd_addr, :lap).compact.join(' ')}".strip out.compact end |