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
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.decode(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::SDR::Decoder::GSM.decode( freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq' ).
- .help ⇒ Object
- .parity(opts = {}) ⇒ Object
- .parse_line(opts = {}) ⇒ Object
-
.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)).
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. support@0dayinc.com
227 228 229 |
# File 'lib/pwn/sdr/decoder/gsm.rb', line 227 public_class_method def self. "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 |
.help ⇒ Object
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 |