Module: PWN::SDR::Decoder::GSM

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

Overview

GSM (2G) true-air FCCH/SCH decoder.

270.833 kbit/s GMSK. FCCH burst = 148 all-zero bits → a pure +67.708 kHz tone for ~547 μs. Detect via variance-dip on the FM discriminator (PWN::FFI::Liquid.freq_demod), estimate carrier offset from mean deviation, then correlate the SCH 64-bit extended training sequence 8 timeslots later and recover the 6-bit BSIC (NCC/BCC) + 19-bit reduced frame number (T1/T2/T3'). Emits freq_offset_hz:, bsic:, ncc:, bcc:, rfn:.

Defined Under Namespace

Classes: DemodIQ

Constant Summary collapse

SYMBOL_RATE =
270_833.0
FCCH_TONE =

67.708 kHz above carrier

SYMBOL_RATE / 4.0
FCCH_BITS =
148
SCH_ETSC =

SCH extended training sequence (64 bits, TS 45.002 Table 5.2.5)

[
  1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0,
  0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
  0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1,
  0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1
].freeze
TSHARK_FIELDS =
%w[
  frame.time gsmtap.arfcn gsmtap.chan_type gsm_a.imsi gsm_a.tmsi
  e212.mcc e212.mnc gsm_a.lac gsm_a.bssmap.cell_ci gsm_a.dtap.msg_rr_type
].freeze

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



227
228
229
# File 'lib/pwn/sdr/decoder/gsm.rb', line 227

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

.decode(opts = {}) ⇒ Object

Supported Method Parameters

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



183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/pwn/sdr/decoder/gsm.rb', line 183

public_class_method def self.decode(opts = {})
  freq_obj = opts[:freq_obj]
  rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 1_083_333).to_i
  PWN::SDR::Decoder::Base.run_iq(
    freq_obj: freq_obj,
    protocol: 'GSM',
    sample_rate: rate,
    source: opts[:source],
    file: opts[:file],
    demod: DemodIQ.new(rate: rate),
    note: '270.833 kbit/s GMSK — I/Q→freqdem→FCCH tone lock→SCH TS correlate→Viterbi→BSIC/RFN.',
    describe: proc { |b| { modulation: 'GMSK', tdma_frames: (b[:duration_ms] / 4.615).round } }
  )
end

.helpObject



231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/pwn/sdr/decoder/gsm.rb', line 231

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 (≥1.0833 Msps)'
    )

    #{self}.viterbi_decode(bits:, k: 5, g0: 0o23, g1: 0o33)

    #{self}.authors
  "
end

.parity(opts = {}) ⇒ Object



168
169
170
171
172
173
174
175
176
# File 'lib/pwn/sdr/decoder/gsm.rb', line 168

public_class_method def self.parity(opts = {})
  parity = opts[:parity].to_i
  p = 0
  while parity.positive?
    p ^= 1
    parity &= parity - 1
  end
  p
end

.parse_line(opts = {}) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/pwn/sdr/decoder/gsm.rb', line 203

public_class_method def self.parse_line(opts = {})
  line = opts[:line].to_s
  f = line.split('|', -1)
  out = {
    protocol: 'GSM', frame_time: f[0], arfcn: f[1], chan_type: f[2],
    imsi: f[3], tmsi: f[4], mcc: f[5], mnc: f[6], lac: f[7],
    cell_id: f[8], rr_msg_type: f[9]
  }.reject { |_, v| v.to_s.empty? }
  if out[:imsi].to_s.length.between?(14, 16)
    out[:imsi_mcc]  = out[:imsi][0, 3]
    out[:imsi_mnc]  = out[:imsi][3, 3]
    out[:imsi_msin] = out[:imsi][6..]
  end
  bits = []
  bits << "ARFCN=#{out[:arfcn]}" if out[:arfcn]
  bits << "MCC/MNC=#{out[:mcc]}/#{out[:mnc]}" if out[:mcc]
  bits << "LAC=#{out[:lac]} CI=#{out[:cell_id]}" if out[:lac]
  bits << "IMSI=#{out[:imsi]}" if out[:imsi]
  out[:summary] = "GSM #{bits.join(' ')}".strip
  out
end

.viterbi_decode(opts = {}) ⇒ Object

Supported Method Parameters

bits = PWN::SDR::Decoder::GSM.viterbi_decode( bits: 'required - Array<0|1> soft/hard coded bits (rate-1/2)', k: 5, g0: 0o23, g1: 0o33 ) Minimal hard-decision K=5 rate-½ Viterbi (GSM 05.03 CC(2,1,5)).



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/pwn/sdr/decoder/gsm.rb', line 128

public_class_method def self.viterbi_decode(opts = {})
  bits = opts[:bits]
  k    = (opts[:k] || 5).to_i
  g0   = (opts[:g0] || 0o23).to_i
  g1   = (opts[:g1] || 0o33).to_i
  nstates = 1 << (k - 1)
  npairs  = bits.length / 2
  pm = Array.new(nstates, 1 << 30)
  pm[0] = 0
  bp = Array.new(npairs) { Array.new(nstates, 0) }
  npairs.times do |t|
    r0 = bits[t * 2]
    r1 = bits[(t * 2) + 1]
    npm = Array.new(nstates, 1 << 30)
    nstates.times do |s|
      [0, 1].each do |u|
        reg = (u << (k - 1)) | s
        o0 = parity(reg & g0)
        o1 = parity(reg & g1)
        m  = pm[s] + (o0 == r0 ? 0 : 1) + (o1 == r1 ? 0 : 1)
        ns = reg >> 1
        if m < npm[ns]
          npm[ns] = m
          bp[t][ns] = (s << 1) | u
        end
      end
    end
    pm = npm
  end
  # traceback from best final state
  s = pm.each_with_index.min_by(&:first).last
  out = Array.new(npairs)
  (npairs - 1).downto(0) do |t|
    v = bp[t][s]
    out[t] = v & 1
    s = v >> 1
  end
  out
end