Module: PWN::SDR::Decoder::Bluetooth

Defined in:
lib/pwn/sdr/decoder/bluetooth.rb

Overview

Bluetooth LE (& BR/EDR sync-trailer) true-air decoder.

I/Q → PWN::FFI::Liquid.gmsk_demod (or DSP.fm_demod_iq→NRZ) 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: — ubertooth-parity advertising sniff with no external binary.

Defined Under Namespace

Classes: DemodIQ

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

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



189
190
191
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 189

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <support@0dayinc.com>\n"
end

.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).



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 133

public_class_method def self.ble_crc24(opts = {})
  bytes = opts[:bytes] || []
  reg = BLE_CRC_INIT
  bytes.each do |byte|
    8.times do |i|
      b = (byte >> i) & 1
      fb = (reg ^ b) & 1
      reg >>= 1
      reg ^= (BLE_CRC_POLY << 0) | 0xB4C000 if fb == 1
      reg &= 0xFFFFFF
    end
  end
  # Register holds CRC LSB-first — return as-is (matched LSB-first on air)
  reg
end

.decode(opts = {}) ⇒ Object

Supported Method Parameters

PWN::SDR::Decoder::Bluetooth.decode( freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq' )



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 154

public_class_method def self.decode(opts = {})
  freq_obj = opts[:freq_obj]
  hz  = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
  ble = freq_obj[:ble] || freq_obj[:mode].to_s.casecmp('ble').zero? || 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(
    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),
    note: '1 Mbit/s GFSK — I/Q→gmskdem→AA 0x8E89BED6→dewhiten→PDU/AdvA/CRC-24.',
    describe: proc { |b| { modulation: 'GFSK', channel: ch, hop_slots: (b[:duration_ms] / 0.625).round } }
  )
end

.helpObject



193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 193

public_class_method def self.help
  puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
    #{self}.decode(
      freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
      source:   'optional - :auto|:rtlsdr|:adalm_pluto|:file',
      file:     'optional - .cu8/.cs16 capture (≥2 Msps)',
      channel:  'optional - BLE adv channel 37|38|39 (default nearest to freq)'
    )

    #{self}.ble_crc24(bytes: [..])

    #{self}.authors
  "
end

.parse_line(opts = {}) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/pwn/sdr/decoder/bluetooth.rb', line 174

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