pwn 0.5.618 → 0.5.621

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/README.md +14 -6
  4. data/bin/pwn_gqrx_scanner +25 -5
  5. data/documentation/Agent-Tool-Registry.md +4 -4
  6. data/documentation/Cron.md +13 -0
  7. data/documentation/Diagrams.md +6 -2
  8. data/documentation/Extrospection.md +96 -11
  9. data/documentation/Home.md +5 -4
  10. data/documentation/How-PWN-Works.md +7 -4
  11. data/documentation/Mistakes.md +97 -0
  12. data/documentation/Persistence.md +4 -1
  13. data/documentation/SDR.md +23 -5
  14. data/documentation/Skills-Memory-Learning.md +16 -5
  15. data/documentation/Transparent-Browser.md +18 -1
  16. data/documentation/diagrams/agent-tool-registry.svg +103 -100
  17. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  18. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  19. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  20. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  21. data/documentation/diagrams/cron-scheduling.svg +23 -23
  22. data/documentation/diagrams/dot/agent-tool-registry.dot +3 -3
  23. data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -10
  24. data/documentation/diagrams/dot/memory-skills-detailed.dot +16 -5
  25. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
  26. data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
  27. data/documentation/diagrams/dot/persistence-filesystem.dot +4 -2
  28. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +25 -7
  29. data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
  30. data/documentation/diagrams/driver-framework.svg +13 -13
  31. data/documentation/diagrams/extrospection-world-awareness.svg +241 -93
  32. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  33. data/documentation/diagrams/hardware-hacking.svg +18 -18
  34. data/documentation/diagrams/history-to-drivers.svg +18 -18
  35. data/documentation/diagrams/memory-skills-detailed.svg +169 -97
  36. data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
  37. data/documentation/diagrams/network-infra-testing.svg +27 -27
  38. data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
  39. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  40. data/documentation/diagrams/persistence-filesystem.svg +111 -87
  41. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  42. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +263 -149
  43. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  44. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  45. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  46. data/documentation/diagrams/sdr-radio-flow.svg +76 -56
  47. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  48. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  49. data/documentation/diagrams/web-application-testing.svg +26 -26
  50. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  51. data/documentation/pwn-ai-Agent.md +28 -15
  52. data/lib/pwn/ai/agent/extrospection.rb +494 -6
  53. data/lib/pwn/ai/agent/learning.rb +67 -7
  54. data/lib/pwn/ai/agent/loop.rb +72 -6
  55. data/lib/pwn/ai/agent/mistakes.rb +369 -0
  56. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  57. data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
  58. data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
  59. data/lib/pwn/ai/agent.rb +1 -0
  60. data/lib/pwn/plugins/jira_data_center.rb +2 -2
  61. data/lib/pwn/sdr/decoder/adsb.rb +82 -0
  62. data/lib/pwn/sdr/decoder/apt.rb +136 -0
  63. data/lib/pwn/sdr/decoder/base.rb +258 -171
  64. data/lib/pwn/sdr/decoder/bluetooth.rb +71 -0
  65. data/lib/pwn/sdr/decoder/dect.rb +70 -0
  66. data/lib/pwn/sdr/decoder/dsp.rb +396 -0
  67. data/lib/pwn/sdr/decoder/flex.rb +177 -210
  68. data/lib/pwn/sdr/decoder/gps.rb +74 -0
  69. data/lib/pwn/sdr/decoder/gsm.rb +31 -62
  70. data/lib/pwn/sdr/decoder/iridium.rb +66 -0
  71. data/lib/pwn/sdr/decoder/lora.rb +80 -0
  72. data/lib/pwn/sdr/decoder/lte.rb +67 -0
  73. data/lib/pwn/sdr/decoder/morse.rb +138 -0
  74. data/lib/pwn/sdr/decoder/p25.rb +74 -0
  75. data/lib/pwn/sdr/decoder/pager.rb +61 -0
  76. data/lib/pwn/sdr/decoder/pocsag.rb +176 -54
  77. data/lib/pwn/sdr/decoder/rfid.rb +79 -0
  78. data/lib/pwn/sdr/decoder/rtl433.rb +86 -0
  79. data/lib/pwn/sdr/decoder/rtty.rb +139 -0
  80. data/lib/pwn/sdr/decoder/wifi.rb +77 -0
  81. data/lib/pwn/sdr/decoder/zigbee.rb +74 -0
  82. data/lib/pwn/sdr/decoder.rb +75 -7
  83. data/lib/pwn/sdr/frequency_allocation.rb +82 -41
  84. data/lib/pwn/sdr/gqrx.rb +186 -95
  85. data/lib/pwn/version.rb +1 -1
  86. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
  87. data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
  88. data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
  89. data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
  90. data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
  91. data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
  92. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
  93. data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
  94. data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
  95. data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
  96. data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
  97. data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
  98. data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
  99. data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
  100. data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
  101. data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
  102. data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
  103. data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
  104. data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
  105. data/third_party/pwn_rdoc.jsonl +115 -5
  106. metadata +46 -6
  107. data/README.md.bak +0 -200
@@ -1,253 +1,220 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'open3'
5
- require 'tty-spinner'
6
- require 'io/wait'
7
-
8
3
  module PWN
9
4
  module SDR
10
5
  module Decoder
11
- # Flex Decoder Module for Pagers
6
+ # Pure-Ruby FLEX™ pager decoder.
7
+ #
8
+ # FLEX is Motorola's synchronous paging protocol running at 1600,
9
+ # 3200 or 6400 bps in 2- or 4-level FSK. GQRX's NBFM discriminator
10
+ # audio (48 kHz UDP tap) is NRZ-sliced at 1600 baud (2-FSK mode),
11
+ # locked onto the Sync-1 A-word (bit-sync 0xAAAA + A1 = 0x78F3), then
12
+ # each 1.875 s frame's 88 × 32-bit BCH(31,21) codewords are walked to
13
+ # recover Frame-Info, Address and Vector/Message words. Alphanumeric
14
+ # (7-bit ASCII, 3 chars per 21-bit payload) and numeric (BCD) message
15
+ # bodies are extracted. No `multimon-ng`, no `sox` — 100 % Ruby.
16
+ #
17
+ # Limitation: 3200/6400 bps 4-FSK modes require raw discriminator
18
+ # amplitude quantisation into four levels; only frame/cycle/capcode
19
+ # metadata (not message body) is emitted for those speeds.
12
20
  module Flex
13
- # Supported Method Parameters::
14
- # pocsag_resp = PWN::SDR::Decoder::Flex.decode(
15
- # freq_obj: 'required - GQRX socket object returned from #connect method'
16
- # )
17
-
18
- public_class_method def self.decode(opts = {})
19
- freq_obj = opts[:freq_obj]
20
- gqrx_sock = freq_obj[:gqrx_sock]
21
- udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
22
- udp_port = freq_obj[:udp_port] || 7355
23
-
24
- freq_obj.delete(:gqrx_sock)
25
-
26
- skip_freq_char = "\n"
27
-
28
- puts JSON.pretty_generate(freq_obj)
29
- puts 'Press [ENTER] to Continue to next frequency...'
30
-
31
- # Spinner setup with dynamic terminal width awareness
32
- spinner = TTY::Spinner.new(
33
- '[:spinner] :status',
34
- format: :arrow_pulse,
35
- clear: true,
36
- hide_cursor: true
37
- )
38
-
39
- spinner_overhead = 12
40
- max_title_length = [TTY::Screen.width - spinner_overhead, 50].max
41
-
42
- initial_title = "INFO: Decoding #{self.to_s.split('::').last} on udp://#{udp_ip}:#{udp_port} ..."
43
- initial_title = initial_title[0...max_title_length] if initial_title.length > max_title_length
44
- spinner.update(status: initial_title)
45
- spinner.auto_spin
46
-
47
- skip_freq = false
48
-
49
- # === Replace netcat with PWN::Plugins::Sock.listen ===
50
- udp_listener = PWN::SDR::GQRX.listen_udp(
51
- udp_ip: udp_ip,
52
- udp_port: udp_port
53
- )
21
+ BS1 = 0xAAAA
22
+ A1 = 0x78F3 # 1600 / 2-FSK Sync-1 A-word
23
+ A_TABLE = {
24
+ 0x870C => [1600, 2], 0x78F3 => [1600, 2],
25
+ 0xB068 => [3200, 2], 0x4F97 => [3200, 2],
26
+ 0x7B18 => [3200, 4], 0x84E7 => [3200, 4],
27
+ 0xDEA0 => [6400, 4], 0x215F => [6400, 4]
28
+ }.freeze
29
+ NUM_TABLE = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '/', 'U', ' ', '-', ']', '['].freeze
30
+
31
+ # Streaming FLEX demodulator fed by Base.run_native.
32
+ class Demod
33
+ def initialize(rate: 48_000)
34
+ @rate = rate
35
+ @buf = []
36
+ @inv = nil
37
+ @carry = []
38
+ end
54
39
 
55
- # Combined processing pipeline: sox → multimon-ng
56
- decode_cmd = '
57
- sox -t raw -e signed-integer -b 16 -r 48000 -c 1 - \
58
- -t raw -e signed-integer -b 16 -r 22050 -c 1 - | \
59
- multimon-ng -t raw \
60
- -a FLEX \
61
- -a FLEX_NEXT \
62
- -
63
- '
40
+ def feed(samples, &)
41
+ @buf.concat(samples)
42
+ max = (@rate * 4).to_i
43
+ @buf.shift(@buf.length - max) if @buf.length > max
44
+ return if @buf.length < (@rate * 2) # need ≥1 frame
64
45
 
65
- mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(decode_cmd)
46
+ try_lock if @inv.nil?
47
+ return if @inv.nil?
66
48
 
67
- current_title = 'Waiting for data frames...'
49
+ bits = PWN::SDR::Decoder::DSP.nrz_slice(samples: @buf, rate: @rate, baud: 1600, invert: @inv)
50
+ @buf.clear
51
+ bits = @carry + bits
52
+ @carry = Flex.decode_bits(bits: bits, &)
53
+ end
68
54
 
69
- # Thread: read from UDP listener and feed to sox|multimon pipeline
70
- receiver_thread = Thread.new do
71
- begin
72
- loop do
73
- data, _sender = udp_listener.recv(4096)
74
- next unless data.to_s.bytesize.positive?
55
+ private
75
56
 
76
- mm_stdin.write(data)
77
- mm_stdin.flush rescue nil
78
- end
79
- rescue IOError, Errno::EPIPE, EOFError, Errno::ECONNRESET
80
- # normal exit path when shutting down
57
+ def try_lock
58
+ @inv = [false, true].find do |inv|
59
+ bits = PWN::SDR::Decoder::DSP.nrz_slice(samples: @buf, rate: @rate, baud: 1600, invert: inv)
60
+ PWN::SDR::Decoder::DSP.find_sync(bits: bits, pattern: (BS1 << 16) | A1, width: 32, max_err: 3)
81
61
  end
82
62
  end
63
+ end
83
64
 
84
- # Thread: read decoded output from multimon-ng and display it
85
- decoder_thread = Thread.new do
86
- # buffer = +''
87
- buffer = ''
88
-
89
- valid_types = %w[ALN BIN HEX NUM TON TONE UNK]
90
- loop do
91
- begin
92
- chunk = mm_stdout.readpartial(4096)
93
- # buffer << chunk
94
- buffer = "#{buffer}#{chunk}"
95
-
96
- while (line = buffer.slice!(/^.*\n/))
97
- line = line.chomp
98
- next if line.empty? || !line.start_with?('FLEX')
99
-
100
- decoded_at = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
101
- dec_msg = { decoded_at: decoded_at }
102
- dec_msg[:raw_inspected] = line.inspect
103
-
104
- protocol = line[0..8]
105
- protocol = 'FLEX' unless protocol == 'FLEX_NEXT'
106
- dec_msg[:protocol] = protocol
107
-
108
- # ────────────────────────────── Detect format ──────────────────────────────
109
- # Sometimes Flex is space delimited, sometimes pipe delimited
110
- # FLEX_NEXT appears to always be pipe delimited
111
-
112
- delimiter = '|'
113
- space_delim = false
114
- if line.start_with?('FLEX: ')
115
- delimiter = ' '
116
- space_delim = true
117
- end
118
-
119
- flex_pipe_delim = false
120
- flex_pipe_delim = true if line.start_with?('FLEX|')
121
-
122
- parts = line.split(delimiter)
123
-
124
- # protocol index already used
125
- idx_already_used = [0]
126
- target_parts_idx = 1
127
- target_parts_idx += 1 if flex_pipe_delim
128
- target_parts_idx += 2 if space_delim
129
- dec_msg[:speed] = parts[target_parts_idx] if parts[target_parts_idx]
130
- idx_already_used.push(target_parts_idx)
131
-
132
- target_parts_idx += 2
133
- dec_msg[:capcode] = parts[target_parts_idx] if parts[target_parts_idx]
134
- idx_already_used.push(target_parts_idx)
135
-
136
- target_parts_idx -= 1
137
- dec_msg[:capcode_loc] = parts[target_parts_idx] if parts[target_parts_idx]
138
- idx_already_used.push(target_parts_idx)
139
-
140
- while target_parts_idx < parts.size
141
- if idx_already_used.include?(target_parts_idx)
142
- target_parts_idx += 1
143
- next
144
- end
145
-
146
- key = parts[target_parts_idx]
147
- key = 'long_sequence_number' if key == 'LS'
148
-
149
- if key && valid_types.include?(key)
150
- dec_msg[:type] = key
151
-
152
- dec_msg[:type_desc] = case key
153
- when 'ALN'
154
- 'Human-readable text'
155
- when 'BIN'
156
- 'Binary / data payload (typically 32 bit words)'
157
- when 'HEX'
158
- 'Raw hex representation of data'
159
- when 'NUM'
160
- 'Numbers only'
161
- when 'TON', 'TONE'
162
- 'Just alert tone, no message'
163
- when 'UNK'
164
- 'Decoded but type unknown / unsupported format'
165
- end
166
-
167
- target_parts_idx += 1
168
- payload_parts = parts[target_parts_idx..]
169
- dec_msg[:type_payload] = payload_parts.join(delimiter)
170
-
171
- break
172
- else
173
- target_parts_idx += 1
174
- dec_msg[key] = parts[target_parts_idx] if parts[target_parts_idx]
175
- end
176
-
177
- idx_already_used.push(target_parts_idx)
178
- target_parts_idx += 1
179
- end
65
+ # Supported Method Parameters::
66
+ # carry = PWN::SDR::Decoder::Flex.decode_bits(bits: [...]) { |msg| ... }
180
67
 
181
- final_msg = freq_obj.merge(dec_msg)
182
- puts JSON.pretty_generate(final_msg)
183
- # TODO: Append dec_msg to a log file in a better way
184
- flex_log_file = "/tmp/flex_decoder_#{Time.now.strftime('%Y%m%d')}.log"
185
- File.open(flex_log_file, 'a') do |f|
186
- f.puts("#{JSON.generate(final_msg)},")
187
- end
188
- end
189
- rescue EOFError, IOError
190
- break
191
- end
68
+ public_class_method def self.decode_bits(opts = {})
69
+ bits = opts[:bits] || []
70
+ i = 0
71
+ loop do
72
+ # Sync-1 = 16-bit BS1 + 16-bit A + 16-bit B (=~A) + 16-bit ~A
73
+ idx = PWN::SDR::Decoder::DSP.find_sync(bits: bits, pattern: BS1, width: 16, max_err: 1, from: i)
74
+ break unless idx
75
+ break if idx + 144 > bits.length
76
+
77
+ a_word = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[idx + 16, 16])
78
+ speed = A_TABLE[a_word]
79
+ unless speed
80
+ i = idx + 1
81
+ next
82
+ end
83
+ # Frame Info Word follows Sync-1 (32-bit BCH codeword)
84
+ fiw = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[idx + 64, 32])
85
+ cycle = (fiw >> 4) & 0x0F
86
+ frame = (fiw >> 8) & 0x7F
87
+ block0 = idx + 64 + 32 + 16 # after FIW + Sync-2 marker
88
+ frame_bits = 1600 * 2 # ≈ one FLEX frame @ 1600 bps (upper bound)
89
+ avail = [bits.length - block0, frame_bits].min
90
+ words = []
91
+ (avail / 32).times do |w|
92
+ words << PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[block0 + (w * 32), 32])
192
93
  end
94
+ emit_frame(words: words, cycle: cycle, frame: frame, speed: speed) { |m| yield m if block_given? }
95
+ i = block0 + avail
193
96
  end
97
+ tail_from = [bits.length - 3200, 0].max
98
+ bits[tail_from..] || []
99
+ end
194
100
 
195
- loop do
196
- spinner.update(status: current_title)
197
-
198
- # Non-blocking ENTER check to exit gracefully
199
- next unless $stdin.wait_readable(0)
200
-
201
- begin
202
- char = $stdin.read_nonblock(1)
203
- next unless char == skip_freq_char
204
-
205
- skip_freq = true
206
- puts "\n[!] ENTER pressed → stopping decoder..."
101
+ # Supported Method Parameters::
102
+ # PWN::SDR::Decoder::Flex.emit_frame(words:, cycle:, frame:, speed:) { |msg| ... }
103
+
104
+ public_class_method def self.emit_frame(opts = {})
105
+ words = opts[:words] || []
106
+ cycle = opts[:cycle]
107
+ frame = opts[:frame]
108
+ speed = opts[:speed] || [1600, 2]
109
+ return if words.empty?
110
+
111
+ # Block Info Word (word 0): a = address-start, v = vector-start
112
+ biw = words[0]
113
+ a_start = (biw >> 8) & 0x3F
114
+ v_start = (biw >> 2) & 0x3F
115
+ v_start = a_start + 1 if v_start <= a_start
116
+ addr_words = words[a_start...v_start] || []
117
+ addr_words.each_with_index do |aw, k|
118
+ next if aw.nil? || aw.zero?
119
+
120
+ capcode = (aw >> 11) & 0x1FFFFF
121
+ vec = words[v_start + k]
122
+ type = vec ? ((vec >> 4) & 0x7) : nil
123
+ mstart = vec ? ((vec >> 7) & 0x7F) : nil
124
+ mlen = vec ? ((vec >> 14) & 0x7F) : 0
125
+ mwords = mstart && mlen.positive? ? (words[mstart, mlen] || []) : []
126
+ body, tname =
127
+ case type
128
+ when 5 then [alpha_decode(words: mwords), 'ALN']
129
+ when 3 then [numeric_decode(words: mwords), 'NUM']
130
+ when 0, nil then [nil, 'TON']
131
+ else [hex_decode(words: mwords), 'BIN']
132
+ end
133
+ out = {
134
+ protocol: 'FLEX', speed: "#{speed[0]}/#{speed[1]}",
135
+ cycle: cycle, frame: frame,
136
+ capcode: capcode.to_s.rjust(9, '0'),
137
+ type: tname, type_payload: body
138
+ }.compact
139
+ out[:summary] = "FLEX RIC=#{out[:capcode]} #{tname}: #{body}"[0, 160]
140
+ yield out if block_given?
141
+ end
142
+ end
207
143
 
208
- break
209
- rescue IO::WaitReadable, EOFError
210
- # ignore
144
+ # Supported Method Parameters::
145
+ # str = PWN::SDR::Decoder::Flex.alpha_decode(words: [Integer, ...])
146
+
147
+ public_class_method def self.alpha_decode(opts = {})
148
+ words = opts[:words] || []
149
+ out = +''
150
+ words.each do |w|
151
+ payload = (w >> 11) & 0x1FFFFF
152
+ 3.times do |c|
153
+ ch = (payload >> (c * 7)) & 0x7F
154
+ next if ch < 0x20 || ch == 0x7F
155
+
156
+ out << ch.chr
211
157
  end
212
-
213
- break if skip_freq
214
158
  end
159
+ out.strip
160
+ end
215
161
 
216
- spinner.success('Decoding stopped')
217
- rescue StandardError => e
218
- spinner.error("Decoding failed: #{e.message}") if defined?(spinner)
219
- raise
220
- ensure
221
- # Cleanup
222
- [receiver_thread, decoder_thread].each do |thread|
223
- thread.kill if thread.alive?
162
+ # Supported Method Parameters::
163
+ # str = PWN::SDR::Decoder::Flex.numeric_decode(words: [Integer, ...])
164
+
165
+ public_class_method def self.numeric_decode(opts = {})
166
+ words = opts[:words] || []
167
+ out = +''
168
+ words.each do |w|
169
+ payload = (w >> 11) & 0x1FFFFF
170
+ 5.times { |d| out << NUM_TABLE[(payload >> (d * 4)) & 0xF] }
224
171
  end
172
+ out.strip
173
+ end
225
174
 
226
- [mm_stdin, mm_stdout, mm_stderr].each { |io| io.close rescue nil }
175
+ # Supported Method Parameters::
176
+ # str = PWN::SDR::Decoder::Flex.hex_decode(words: [Integer, ...])
227
177
 
228
- mm_wait_thr&.value rescue nil
178
+ public_class_method def self.hex_decode(opts = {})
179
+ words = opts[:words] || []
180
+ words.map { |w| format('%08X', w & 0xFFFFFFFF) }.join(' ')
181
+ end
229
182
 
230
- PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if defined?(udp_listener) && udp_listener
183
+ # Supported Method Parameters::
184
+ # PWN::SDR::Decoder::Flex.decode(
185
+ # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
186
+ # )
231
187
 
232
- spinner.stop if defined?(spinner) && spinner
188
+ public_class_method def self.decode(opts = {})
189
+ freq_obj = opts[:freq_obj]
190
+ PWN::SDR::Decoder::Base.run_native(
191
+ freq_obj: freq_obj,
192
+ protocol: 'FLEX',
193
+ demod: Demod.new
194
+ )
233
195
  end
234
196
 
235
197
  # Author(s):: 0day Inc. <support@0dayinc.com>
236
198
 
237
199
  public_class_method def self.authors
238
- "AUTHOR(S):
239
- 0day Inc. <support@0dayinc.com>
240
- "
200
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
241
201
  end
242
202
 
243
203
  # Display Usage for this Module
244
204
 
245
205
  public_class_method def self.help
246
- puts "USAGE:
206
+ puts "USAGE (ruby-native, no external binaries):
247
207
  #{self}.decode(
248
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq method'
208
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
249
209
  )
250
210
 
211
+ #{self}.decode_bits(bits: [0,1,...]) { |msg| ... }
212
+ #{self}.alpha_decode(words: [Integer, ...])
213
+ #{self}.numeric_decode(words: [Integer, ...])
214
+
215
+ NOTE: Set GQRX to Narrow FM (~15 kHz). 1600 bps 2-FSK is fully
216
+ decoded; 3200/6400 4-FSK emit frame/capcode metadata only.
217
+
251
218
  #{self}.authors
252
219
  "
253
220
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PWN
4
+ module SDR
5
+ module Decoder
6
+ # Pure-Ruby GNSS / GPS L1-C/A activity detector.
7
+ #
8
+ # 1.023 Mcps BPSK/DSSS spread across 2 MHz — acquisition/tracking of
9
+ # 32 PRNs in real time is not feasible in interpreted Ruby. Native
10
+ # mode reports L1 carrier presence / C/N₀ proxy only. `parse_line`
11
+ # retained for offline gnss-sdr / NMEA text analysis.
12
+ module GPS
13
+ # Supported Method Parameters::
14
+ # PWN::SDR::Decoder::GPS.decode(
15
+ # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
16
+ # )
17
+
18
+ public_class_method def self.decode(opts = {})
19
+ freq_obj = opts[:freq_obj]
20
+ PWN::SDR::Decoder::Base.run_detector(
21
+ freq_obj: freq_obj,
22
+ protocol: 'GPS',
23
+ note: 'BPSK/DSSS 1.023 Mcps below thermal noise — native mode reports composite L1 energy only.',
24
+ threshold: 3.0,
25
+ describe: proc { |_b| { modulation: 'BPSK/DSSS', chip_rate: 1_023_000 } }
26
+ )
27
+ end
28
+
29
+ # Supported Method Parameters::
30
+ # PWN::SDR::Decoder::GPS.parse_line(line: 'Position at ... Lat = 32.7 [deg] ...')
31
+
32
+ public_class_method def self.parse_line(opts = {})
33
+ line = opts[:line].to_s
34
+ out = { protocol: 'GPS' }
35
+ out[:prn] = ::Regexp.last_match(1) if line =~ /PRN[ =]?(\d{1,2})/
36
+ out[:cn0] = ::Regexp.last_match(1) if line =~ /CN0[ =]?([\d.]+)/i
37
+ out[:lat] = ::Regexp.last_match(1) if line =~ /Lat(?:itude)?\s*=\s*(-?[\d.]+)/i
38
+ out[:lon] = ::Regexp.last_match(1) if line =~ /Long(?:itude)?\s*=\s*(-?[\d.]+)/i
39
+ out[:alt] = ::Regexp.last_match(1) if line =~ /Height\s*=\s*(-?[\d.]+)/i
40
+ out[:utc] = ::Regexp.last_match(1) if line =~ /UTC\s*=?\s*([\d:.-]+T?[\d:.]*)/
41
+ out[:nmea] = line if line.start_with?('$G')
42
+ out[:summary] = if out[:lat]
43
+ "GPS FIX #{out[:lat]},#{out[:lon]} alt=#{out[:alt]}m"
44
+ elsif out[:prn]
45
+ "GPS PRN#{out[:prn]} CN0=#{out[:cn0]}"
46
+ else
47
+ "GPS #{line[0, 100]}"
48
+ end
49
+ out.compact
50
+ end
51
+
52
+ # Author(s):: 0day Inc. <support@0dayinc.com>
53
+
54
+ public_class_method def self.authors
55
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
56
+ end
57
+
58
+ # Display Usage for this Module
59
+
60
+ public_class_method def self.help
61
+ puts "USAGE (ruby-native detector, no external binaries):
62
+ #{self}.decode(
63
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
64
+ )
65
+
66
+ #{self}.parse_line(line: 'Position ... Lat = 32.7 Long = -97.1 Height = 210')
67
+
68
+ #{self}.authors
69
+ "
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -3,18 +3,21 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # GSM (2G) broadcast-channel decoder.
6
+ # Pure-Ruby GSM (2G) BCCH activity detector.
7
7
  #
8
- # GSM is 270.833 kbit/s GMSK it CANNOT be recovered from GQRX's 48 kHz
9
- # demodulated-audio UDP tap. This module therefore drives the SDR
10
- # directly via `grgsm_livemon_headless` (from gr-gsm) which publishes
11
- # decoded Um bursts as GSMTAP on udp/4729, then reads them back with
12
- # `tshark` for structured field extraction (MCC/MNC/LAC/CI/ARFCN, paging
13
- # IMSIs/TMSIs, System Information messages, etc.).
14
- #
15
- # Interface matches PWN::SDR::Decoder::Flex / ::RDS so the GQRX
16
- # dispatcher (`decoder: :gsm`) works uniformly.
8
+ # GSM is 270.833 kbit/s GMSK across a 200 kHz channel physically
9
+ # unrecoverable from a 48 kHz demodulated-audio tap and too fast for
10
+ # interpreted Ruby to demodulate from raw I/Q in real time. Rather
11
+ # than shell out to `grgsm_livemon_headless` + `tshark`, this module
12
+ # runs Base.run_detector to characterise BCCH/CCCH burst structure
13
+ # (577 μs slots × 8 = 4.615 ms TDMA frame) natively. `parse_line` is
14
+ # retained for offline GSMTAP-tshark pipe-delimited analysis.
17
15
  module GSM
16
+ TSHARK_FIELDS = %w[
17
+ frame.time gsmtap.arfcn gsmtap.chan_type gsm_a.imsi gsm_a.tmsi
18
+ e212.mcc e212.mnc gsm_a.lac gsm_a.bssmap.cell_ci gsm_a.dtap.msg_rr_type
19
+ ].freeze
20
+
18
21
  # Supported Method Parameters::
19
22
  # PWN::SDR::Decoder::GSM.decode(
20
23
  # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
@@ -22,36 +25,15 @@ module PWN
22
25
 
23
26
  public_class_method def self.decode(opts = {})
24
27
  freq_obj = opts[:freq_obj]
25
- raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
26
-
27
- hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
28
- gain = (freq_obj[:rf_gain] || 40).to_s.to_f
29
-
30
- tshark_fields = %w[
31
- frame.time
32
- gsmtap.arfcn
33
- gsmtap.chan_type
34
- gsm_a.imsi
35
- gsm_a.tmsi
36
- e212.mcc
37
- e212.mnc
38
- gsm_a.lac
39
- gsm_a.bssmap.cell_ci
40
- gsm_a.dtap.msg_rr_type
41
- ]
42
-
43
- direct_cmd = "sh -c 'grgsm_livemon_headless -f #{hz} -g #{gain} >/dev/null 2>&1 & " \
44
- 'LMPID=$!; trap "kill $LMPID 2>/dev/null" EXIT; sleep 2; ' \
45
- 'tshark -i lo -l -Y gsmtap -T fields -E separator=\| ' \
46
- "#{tshark_fields.map { |f| "-e #{f}" }.join(' ')}'"
47
-
48
- PWN::SDR::Decoder::Base.run_pipeline(
28
+ PWN::SDR::Decoder::Base.run_detector(
49
29
  freq_obj: freq_obj,
50
30
  protocol: 'GSM',
51
- required_bins: %w[grgsm_livemon_headless tshark],
52
- direct_cmd: direct_cmd,
53
- line_match: /\S/,
54
- parser: proc { |line| parse_line(line: line) }
31
+ note: '270.833 kbit/s GMSK exceeds audio-tap Nyquist; native mode reports TDMA burst duty/energy only. Feed captured GSMTAP-tshark fields to .parse_line for MCC/MNC/LAC/CI/IMSI.',
32
+ threshold: 5.0,
33
+ describe: proc { |b|
34
+ frames = (b[:duration_ms] / 4.615).round
35
+ { modulation: 'GMSK', symbol_rate: 270_833, tdma_frames: frames, classification: frames > 10 ? 'BCCH/CCCH-continuous' : 'RACH/paging-burst' }
36
+ }
55
37
  )
56
38
  end
57
39
 
@@ -62,31 +44,22 @@ module PWN
62
44
  line = opts[:line].to_s
63
45
  f = line.split('|', -1)
64
46
  out = {
65
- protocol: 'GSM',
66
- arfcn: f[1],
67
- chan_type: f[2],
68
- imsi: f[3],
69
- tmsi: f[4],
70
- mcc: f[5],
71
- mnc: f[6],
72
- lac: f[7],
73
- cell_id: f[8],
74
- rr_msg_type: f[9]
47
+ protocol: 'GSM', frame_time: f[0], arfcn: f[1], chan_type: f[2],
48
+ imsi: f[3], tmsi: f[4], mcc: f[5], mnc: f[6], lac: f[7],
49
+ cell_id: f[8], rr_msg_type: f[9]
75
50
  }.reject { |_, v| v.to_s.empty? }
76
-
77
51
  if out[:imsi].to_s.length.between?(14, 16)
78
52
  out[:imsi_mcc] = out[:imsi][0, 3]
79
53
  out[:imsi_mnc] = out[:imsi][3, 3]
80
54
  out[:imsi_msin] = out[:imsi][6..]
81
55
  end
82
-
83
- summary_bits = []
84
- summary_bits << "ARFCN=#{out[:arfcn]}" if out[:arfcn]
85
- summary_bits << "MCC/MNC=#{out[:mcc]}/#{out[:mnc]}" if out[:mcc]
86
- summary_bits << "LAC=#{out[:lac]} CI=#{out[:cell_id]}" if out[:lac]
87
- summary_bits << "IMSI=#{out[:imsi]}" if out[:imsi]
88
- summary_bits << "TMSI=#{out[:tmsi]}" if out[:tmsi]
89
- out[:summary] = "GSM #{summary_bits.join(' ')}".strip
56
+ bits = []
57
+ bits << "ARFCN=#{out[:arfcn]}" if out[:arfcn]
58
+ bits << "MCC/MNC=#{out[:mcc]}/#{out[:mnc]}" if out[:mcc]
59
+ bits << "LAC=#{out[:lac]} CI=#{out[:cell_id]}" if out[:lac]
60
+ bits << "IMSI=#{out[:imsi]}" if out[:imsi]
61
+ bits << "TMSI=#{out[:tmsi]}" if out[:tmsi]
62
+ out[:summary] = "GSM #{bits.join(' ')}".strip
90
63
  out
91
64
  end
92
65
 
@@ -99,15 +72,11 @@ module PWN
99
72
  # Display Usage for this Module
100
73
 
101
74
  public_class_method def self.help
102
- puts "USAGE:
75
+ puts "USAGE (ruby-native detector, no external binaries):
103
76
  #{self}.decode(
104
77
  freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
105
78
  )
106
79
 
107
- NOTE: Requires `grgsm_livemon_headless` (gr-gsm) and `tshark`.
108
- GSM cannot be decoded from GQRX's 48 kHz audio tap; this
109
- module drives the SDR directly and reads GSMTAP on lo:4729.
110
-
111
80
  #{self}.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
112
81
 
113
82
  #{self}.authors