pwn 0.5.613 → 0.5.617

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -10
  3. data/README.md +117 -17
  4. data/bin/pwn +47 -2
  5. data/bin/pwn_gqrx_scanner +75 -18
  6. data/documentation/AI-Integration.md +36 -0
  7. data/documentation/BurpSuite.md +26 -0
  8. data/documentation/Contributing.md +31 -0
  9. data/documentation/Diagrams.md +140 -0
  10. data/documentation/Drivers.md +25 -0
  11. data/documentation/General-PWN-Usage.md +60 -0
  12. data/documentation/Home.md +78 -0
  13. data/documentation/How-PWN-Works.md +62 -0
  14. data/documentation/Installation.md +75 -0
  15. data/documentation/NmapIt.md +18 -0
  16. data/documentation/Plugins.md +59 -0
  17. data/documentation/Reporting.md +28 -0
  18. data/documentation/SAST.md +32 -0
  19. data/documentation/Skills-Memory-Learning.md +44 -0
  20. data/documentation/Transparent-Browser.md +32 -0
  21. data/documentation/Troubleshooting.md +38 -0
  22. data/documentation/What-is-PWN.md +53 -0
  23. data/documentation/Why-PWN.md +48 -0
  24. data/documentation/diagrams/ai-integration-tool-calling.svg +172 -0
  25. data/documentation/diagrams/build.sh +20 -0
  26. data/documentation/diagrams/burp-vs-zap-preference.svg +69 -0
  27. data/documentation/diagrams/code-scanning-sast.svg +172 -0
  28. data/documentation/diagrams/dot/_THEME.md +27 -0
  29. data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
  30. data/documentation/diagrams/dot/ai-integration-tool-calling.dot +50 -0
  31. data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
  32. data/documentation/diagrams/dot/burp-vs-zap-preference.dot +26 -0
  33. data/documentation/diagrams/dot/code-scanning-sast.dot +49 -0
  34. data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
  35. data/documentation/diagrams/dot/driver-framework.dot +39 -0
  36. data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
  37. data/documentation/diagrams/dot/fuzzing-workflow.dot +47 -0
  38. data/documentation/diagrams/dot/history-to-drivers.dot +34 -0
  39. data/documentation/diagrams/dot/memory-skills-detailed.dot +60 -0
  40. data/documentation/diagrams/dot/network-infra-testing.dot +54 -0
  41. data/documentation/diagrams/dot/overall-pwn-architecture.dot +115 -0
  42. data/documentation/diagrams/dot/penetration-testing-workflow.dot +60 -0
  43. data/documentation/diagrams/dot/plugin-ecosystem.dot +45 -0
  44. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +98 -0
  45. data/documentation/diagrams/dot/pwn-repl-prototyping.dot +56 -0
  46. data/documentation/diagrams/dot/reporting-pipeline.dot +46 -0
  47. data/documentation/diagrams/dot/reverse-engineering-flow.dot +43 -0
  48. data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
  49. data/documentation/diagrams/dot/sessions-cron-automation.dot +44 -0
  50. data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
  51. data/documentation/diagrams/dot/web-application-testing.dot +49 -0
  52. data/documentation/diagrams/dot/zero-day-research-flow.dot +49 -0
  53. data/documentation/diagrams/driver-framework.svg +111 -0
  54. data/documentation/diagrams/fuzzing-workflow.svg +160 -0
  55. data/documentation/diagrams/history-to-drivers.svg +119 -0
  56. data/documentation/diagrams/memory-skills-detailed.svg +202 -0
  57. data/documentation/diagrams/network-infra-testing.svg +183 -0
  58. data/documentation/diagrams/overall-pwn-architecture.svg +405 -0
  59. data/documentation/diagrams/penetration-testing-workflow.svg +201 -0
  60. data/documentation/diagrams/plugin-ecosystem.svg +276 -0
  61. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +306 -0
  62. data/documentation/diagrams/pwn-repl-prototyping.svg +168 -0
  63. data/documentation/diagrams/reporting-pipeline.svg +151 -0
  64. data/documentation/diagrams/reverse-engineering-flow.svg +134 -0
  65. data/documentation/diagrams/sessions-cron-automation.svg +136 -0
  66. data/documentation/diagrams/web-application-testing.svg +173 -0
  67. data/documentation/diagrams/zero-day-research-flow.svg +153 -0
  68. data/documentation/pwn-REPL.md +33 -0
  69. data/documentation/pwn-ai-Agent.md +43 -0
  70. data/git_commit.sh +1 -1
  71. data/lib/pwn/ai/agent/assembly.rb +1 -1
  72. data/lib/pwn/ai/agent/btc.rb +1 -1
  73. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  74. data/lib/pwn/ai/agent/extrospection.rb +618 -0
  75. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  76. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  77. data/lib/pwn/ai/agent/introspection.rb +91 -0
  78. data/lib/pwn/ai/agent/learning.rb +379 -0
  79. data/lib/pwn/ai/agent/loop.rb +33 -3
  80. data/lib/pwn/ai/agent/metrics.rb +155 -0
  81. data/lib/pwn/ai/agent/prompt_builder.rb +32 -3
  82. data/lib/pwn/ai/agent/sast.rb +1 -1
  83. data/lib/pwn/ai/agent/swarm.rb +437 -0
  84. data/lib/pwn/ai/agent/tools/cron.rb +163 -0
  85. data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
  86. data/lib/pwn/ai/agent/tools/learning.rb +221 -0
  87. data/lib/pwn/ai/agent/tools/memory.rb +27 -0
  88. data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
  89. data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
  90. data/lib/pwn/ai/agent/tools/skills.rb +124 -8
  91. data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
  92. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  93. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  94. data/lib/pwn/ai/agent.rb +5 -0
  95. data/lib/pwn/ai/anthropic.rb +19 -4
  96. data/lib/pwn/ai/grok.rb +249 -106
  97. data/lib/pwn/ai.rb +0 -1
  98. data/lib/pwn/config.rb +96 -36
  99. data/lib/pwn/cron.rb +16 -7
  100. data/lib/pwn/plugins/monkey_patch.rb +2 -2
  101. data/lib/pwn/plugins/repl.rb +158 -284
  102. data/lib/pwn/sdr/decoder/base.rb +251 -0
  103. data/lib/pwn/sdr/decoder/gsm.rb +84 -185
  104. data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
  105. data/lib/pwn/sdr/decoder.rb +1 -0
  106. data/lib/pwn/sdr/gqrx.rb +446 -65
  107. data/lib/pwn/version.rb +1 -1
  108. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
  109. data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
  110. data/spec/lib/pwn/ai/agent/learning_spec.rb +68 -0
  111. data/spec/lib/pwn/ai/agent/metrics_spec.rb +27 -0
  112. data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
  113. data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
  114. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
  115. data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +30 -0
  116. data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
  117. data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
  118. data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
  119. data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
  120. data/third_party/pwn_rdoc.jsonl +178 -33
  121. metadata +110 -36
  122. data/documentation/PWN.png +0 -0
  123. data/documentation/PWN_Contributors_and_Users.png +0 -0
  124. data/documentation/fax-spectrogram.png +0 -0
  125. data/documentation/fax-waveform.png +0 -0
  126. data/documentation/frozen_string_literal_files_changed_from_false_to_true.txt +0 -54
  127. data/documentation/lifecycle_authz_replay.example.yaml +0 -27
  128. data/documentation/pwn_android_war_dialer_session.png +0 -0
  129. data/documentation/pwn_install.png +0 -0
  130. data/documentation/pwn_wallpaper.jpg +0 -0
  131. data/documentation/ringing-spectrogram.png +0 -0
  132. data/documentation/ringing-waveform.png +0 -0
  133. data/documentation/vulnerability_report_template.md +0 -37
  134. data/lib/pwn/ai/introspection.rb +0 -76
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require 'tty-spinner'
6
+ require 'tty-screen'
7
+ require 'io/wait'
8
+
9
+ module PWN
10
+ module SDR
11
+ module Decoder
12
+ # Shared pipeline plumbing for every PWN::SDR::Decoder::* module.
13
+ #
14
+ # Encapsulates the pattern established by PWN::SDR::Decoder::Flex and
15
+ # PWN::SDR::Decoder::RDS:
16
+ #
17
+ # 1. Bind to the GQRX UDP audio stream (48 kHz s16le mono).
18
+ # 2. Feed it through `sox` (resample to 22 050 Hz) into an external
19
+ # demodulator (multimon-ng, rtl_433, acarsdec, dsd, ...).
20
+ # 3. Read decoded lines back on a background thread, hand each to a
21
+ # caller-supplied parser Proc, merge with freq_obj, JSON-log it.
22
+ # 4. Show a TTY::Spinner status line and exit cleanly on [ENTER].
23
+ #
24
+ # Decoders whose external tool needs raw I/Q direct from the SDR (ADSB,
25
+ # GSM, LoRa, WiFi, ZigBee, ...) can instead pass `direct_cmd:` — Base
26
+ # will spawn it stand-alone (no UDP → sox bridge) and still handle the
27
+ # spinner / stdout parser / logging / [ENTER]-to-skip loop uniformly.
28
+ module Base
29
+ # Supported Method Parameters::
30
+ # PWN::SDR::Decoder::Base.run_pipeline(
31
+ # freq_obj: 'required - freq_obj Hash from PWN::SDR::GQRX.init_freq',
32
+ # protocol: 'required - short name used in banner / log filename',
33
+ # decode_cmd: 'optional - shell pipeline reading 22 050 Hz s16le on stdin',
34
+ # direct_cmd: 'optional - stand-alone shell cmd (owns the SDR itself)',
35
+ # line_match: 'optional - Regexp/String a stdout line must match',
36
+ # parser: 'optional - Proc.new { |line| Hash } to structure a line',
37
+ # required_bins: 'optional - Array of executables that must exist on PATH',
38
+ # resample_hz: 'optional - sox output rate for decode_cmd (default 22050)'
39
+ # )
40
+
41
+ public_class_method def self.run_pipeline(opts = {})
42
+ freq_obj = opts[:freq_obj]
43
+ protocol = opts[:protocol] || 'SIGNAL'
44
+ decode_cmd = opts[:decode_cmd]
45
+ direct_cmd = opts[:direct_cmd]
46
+ line_match = opts[:line_match]
47
+ parser = opts[:parser]
48
+ required_bins = Array(opts[:required_bins])
49
+ resample_hz = opts[:resample_hz] || 22_050
50
+
51
+ raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
52
+
53
+ gqrx_sock = freq_obj[:gqrx_sock]
54
+ udp_ip = freq_obj[:udp_ip] || '127.0.0.1'
55
+ udp_port = freq_obj[:udp_port] || 7355
56
+ freq_obj = freq_obj.dup
57
+ freq_obj.delete(:gqrx_sock)
58
+ freq_obj.delete(:decoder_module)
59
+
60
+ skip_freq_char = "\n"
61
+
62
+ puts JSON.pretty_generate(freq_obj)
63
+ puts "\n*** #{protocol} Decoder ***"
64
+ puts 'Press [ENTER] to continue to next frequency...'
65
+
66
+ missing = required_bins.reject { |b| bin_available?(bin: b) }
67
+ unless missing.empty?
68
+ puts "[!] Missing required executable(s): #{missing.join(', ')}"
69
+ puts ' Install them and re-run, or press [ENTER] to skip.'
70
+ end
71
+
72
+ spinner = TTY::Spinner.new(
73
+ '[:spinner] :status',
74
+ format: :arrow_pulse,
75
+ clear: true,
76
+ hide_cursor: true
77
+ )
78
+ spinner_overhead = 12
79
+ max_title_length = [TTY::Screen.width - spinner_overhead, 50].max
80
+ banner = "INFO: Decoding #{protocol} on udp://#{udp_ip}:#{udp_port} ..."
81
+ banner = "INFO: Decoding #{protocol} via `#{direct_cmd.to_s.split.first}` ..." if direct_cmd
82
+ banner = banner[0...max_title_length] if banner.length > max_title_length
83
+ spinner.update(status: banner)
84
+ spinner.auto_spin
85
+
86
+ log_file = "/tmp/#{protocol.downcase.gsub(/[^a-z0-9]+/, '_')}_decoder_#{Time.now.strftime('%Y%m%d')}.log"
87
+
88
+ udp_listener = nil
89
+ receiver_thread = nil
90
+ mm_stdin = mm_stdout = mm_stderr = mm_wait_thr = nil
91
+
92
+ if direct_cmd && missing.empty?
93
+ mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(direct_cmd)
94
+ elsif decode_cmd && missing.empty?
95
+ udp_listener = PWN::SDR::GQRX.listen_udp(udp_ip: udp_ip, udp_port: udp_port)
96
+
97
+ full_cmd = 'sox -t raw -e signed-integer -b 16 -r 48000 -c 1 - ' \
98
+ "-t raw -e signed-integer -b 16 -r #{resample_hz} -c 1 - | #{decode_cmd}"
99
+ mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(full_cmd)
100
+
101
+ receiver_thread = Thread.new do
102
+ loop do
103
+ data, = udp_listener.recv(4096)
104
+ next unless data.to_s.bytesize.positive?
105
+
106
+ mm_stdin.write(data)
107
+ begin
108
+ mm_stdin.flush
109
+ rescue StandardError
110
+ nil
111
+ end
112
+ end
113
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
114
+ nil
115
+ end
116
+ end
117
+
118
+ current_title = 'Waiting for data frames...'
119
+ decoder_thread = nil
120
+ if mm_stdout
121
+ decoder_thread = Thread.new do
122
+ buffer = ''
123
+ loop do
124
+ chunk = mm_stdout.readpartial(4096)
125
+ buffer = "#{buffer}#{chunk}"
126
+ while (line = buffer.slice!(/^.*\n/))
127
+ line = line.chomp
128
+ next if line.empty?
129
+ next if line_match && !match_line?(line: line, matcher: line_match)
130
+
131
+ dec_msg = { decoded_at: Time.now.strftime('%Y-%m-%d %H:%M:%S%z'), raw: line }
132
+ dec_msg.merge!(parser.call(line)) if parser.respond_to?(:call)
133
+ final_msg = freq_obj.merge(dec_msg)
134
+
135
+ spinner.stop
136
+ puts JSON.pretty_generate(final_msg)
137
+ spinner.auto_spin
138
+
139
+ File.open(log_file, 'a') { |f| f.puts("#{JSON.generate(final_msg)},") }
140
+ disp = dec_msg[:summary] || line
141
+ current_title = disp[0...max_title_length]
142
+ end
143
+ rescue IOError
144
+ break
145
+ end
146
+ end
147
+ elsif gqrx_sock
148
+ # No external decoder — fall back to live signal-strength telemetry
149
+ decoder_thread = Thread.new do
150
+ loop do
151
+ lvl = PWN::SDR::GQRX.cmd(gqrx_sock: gqrx_sock, cmd: 'l STRENGTH').to_f
152
+ current_title = "signal #{format('%+.1f', lvl)} dBFS (analog / no external demod)"
153
+ sleep 0.3
154
+ end
155
+ rescue StandardError
156
+ nil
157
+ end
158
+ end
159
+
160
+ loop do
161
+ spinner.update(status: current_title)
162
+ next unless $stdin.wait_readable(0)
163
+
164
+ begin
165
+ char = $stdin.read_nonblock(1)
166
+ next unless char == skip_freq_char
167
+
168
+ puts "\n[!] ENTER pressed → stopping #{protocol} decoder..."
169
+ break
170
+ rescue IO::WaitReadable, EOFError
171
+ nil
172
+ end
173
+ end
174
+
175
+ spinner.success('Decoding stopped')
176
+ rescue StandardError => e
177
+ spinner.error("Decoding failed: #{e.message}") if defined?(spinner) && spinner
178
+ raise
179
+ ensure
180
+ [receiver_thread, decoder_thread].compact.each { |t| t.kill if t.alive? }
181
+ [mm_stdin, mm_stdout, mm_stderr].compact.each do |io|
182
+ io.close
183
+ rescue StandardError
184
+ nil
185
+ end
186
+ begin
187
+ mm_wait_thr&.value
188
+ rescue StandardError
189
+ nil
190
+ end
191
+ PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if udp_listener
192
+ spinner.stop if defined?(spinner) && spinner
193
+ end
194
+
195
+ # Supported Method Parameters::
196
+ # PWN::SDR::Decoder::Base.bin_available?(bin: 'multimon-ng')
197
+
198
+ public_class_method def self.bin_available?(opts = {})
199
+ bin = opts[:bin].to_s
200
+ return false if bin.empty?
201
+
202
+ ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
203
+ File.executable?(File.join(dir, bin))
204
+ end
205
+ end
206
+
207
+ # Supported Method Parameters::
208
+ # PWN::SDR::Decoder::Base.match_line?(line: str, matcher: Regexp|String|Array)
209
+
210
+ public_class_method def self.match_line?(opts = {})
211
+ line = opts[:line].to_s
212
+ matcher = opts[:matcher]
213
+ case matcher
214
+ when Regexp then line.match?(matcher)
215
+ when String then line.start_with?(matcher)
216
+ when Array then matcher.any? { |m| match_line?(line: line, matcher: m) }
217
+ else true
218
+ end
219
+ end
220
+
221
+ # Author(s):: 0day Inc. <support@0dayinc.com>
222
+
223
+ public_class_method def self.authors
224
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
225
+ end
226
+
227
+ # Display Usage for this Module
228
+
229
+ public_class_method def self.help
230
+ puts "USAGE:
231
+ #{self}.run_pipeline(
232
+ freq_obj: 'required - freq_obj from PWN::SDR::GQRX.init_freq',
233
+ protocol: 'required - short protocol name',
234
+ decode_cmd: 'optional - stdin pipeline (sox-resampled 22 050 Hz s16le)',
235
+ direct_cmd: 'optional - stand-alone cmd that owns the SDR',
236
+ line_match: 'optional - Regexp/String/Array line filter',
237
+ parser: 'optional - Proc { |line| Hash } structured extractor',
238
+ required_bins: 'optional - Array of executables that must be on PATH',
239
+ resample_hz: 'optional - sox output rate (default 22050)'
240
+ )
241
+
242
+ #{self}.bin_available?(bin: 'multimon-ng')
243
+ #{self}.match_line?(line: str, matcher: Regexp|String|Array)
244
+
245
+ #{self}.authors
246
+ "
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -3,213 +3,112 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # SDR Decoder for GSM signals.
6
+ # GSM (2G) broadcast-channel decoder.
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.
7
17
  module GSM
8
- POWER_THRESHOLD = 0.1
9
- SLEEP_INTERVAL = 0.1
10
- HEADER_SIZE = 44
11
- BURST_DURATION_SEC = 0.000577
18
+ # Supported Method Parameters::
19
+ # PWN::SDR::Decoder::GSM.decode(
20
+ # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
21
+ # )
12
22
 
13
- # TSC 0 binary sequence (26 bits): 00100101110000101010011011
14
- TSC_0 = [0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1].freeze
15
-
16
- # Starts the live decoding thread.
17
- public_class_method def self.start(opts = {})
18
- freq_obj = opts[:freq_obj]
19
- raise ':ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
20
-
21
- gqrx_sock = freq_obj[:gqrx_sock]
22
- freq = freq_obj[:freq]
23
- bandwidth = freq_obj[:bandwidth].to_i
24
- record_path = freq_obj[:record_path]
25
-
26
- sleep 0.1 until File.exist?(record_path)
27
-
28
- header = File.binread(record_path, HEADER_SIZE)
29
- raise 'Invalid WAV header' unless header.start_with?('RIFF') && header.include?('WAVE')
30
-
31
- bytes_read = HEADER_SIZE
32
-
33
- puts "GSM Decoder started for freq: #{freq}, bandwidth: #{bandwidth}"
34
-
35
- Thread.new do
36
- loop do
37
- current_size = File.size(record_path)
38
- if current_size > bytes_read
39
- new_bytes = current_size - bytes_read
40
- # Ensure full I/Q pairs (8 bytes)
41
- new_bytes -= new_bytes % 8
42
- data = File.binread(record_path, new_bytes, bytes_read)
43
- process_chunk(
44
- data: data,
45
- bandwidth: bandwidth,
46
- freq: freq
47
- )
48
- bytes_read = current_size
49
- end
50
-
51
- sleep SLEEP_INTERVAL
52
- end
53
- rescue StandardError => e
54
- puts "Decoder error: #{e.message}"
55
- ensure
56
- cleanup(record_path: record_path)
57
- end
58
- end
59
-
60
- # Stops the decoding thread.
61
- public_class_method def self.stop(opts = {})
23
+ public_class_method def self.decode(opts = {})
62
24
  freq_obj = opts[:freq_obj]
63
25
  raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
64
26
 
65
- decoder_thread = freq_obj[:decoder_thread]
66
- decoder_thread.kill if decoder_thread.is_a?(Thread)
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(
49
+ freq_obj: freq_obj,
50
+ 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) }
55
+ )
67
56
  end
68
57
 
69
- class << self
70
- private
71
-
72
- def process_chunk(opts = {})
73
- data = opts[:data]
74
- bandwidth = opts[:bandwidth]
75
- freq = opts[:freq]
76
- raise ':ERROR: :data, :bandwidth, and :freq are required' unless data && bandwidth && freq
77
-
78
- samples = data.unpack('f< *')
79
- return if samples.length.odd? # Skip incomplete
80
-
81
- complex_samples = []
82
- (0...samples.length).step(2) do |i|
83
- complex_samples << Complex(samples[i], samples[i + 1])
84
- end
85
-
86
- window_size = [(bandwidth * BURST_DURATION_SEC).round, complex_samples.length].min
87
- return if window_size <= 0
88
-
89
- # Simplified power on sliding windows
90
- powers = []
91
- complex_samples.each_cons(window_size) do |window|
92
- power = window.map { |c| c.abs**2 }.sum / window_size
93
- powers << power
94
- end
95
-
96
- max_power = powers.max
97
- return unless max_power > POWER_THRESHOLD
98
-
99
- # Demod the entire chunk (assume burst-aligned roughly)
100
- bits = demod_gmsk(complex_samples)
101
- # Synchronize via TSC correlation
102
- sync_offset = find_tsc_offset(bits, TSC_0)
103
- return unless sync_offset >= 0
104
-
105
- # Extract data bits from normal burst structure
106
- burst_start = sync_offset - 58 # TSC starts at symbol 58 (0-index)
107
- return unless burst_start >= 0 && burst_start + 148 <= bits.length
108
-
109
- data_bits = extract_data_bits(bits, burst_start)
110
- puts "Burst synchronized at offset #{sync_offset} for #{freq} Hz (power: #{max_power.round(4)})"
111
- decode_imsi(
112
- data_bits: data_bits,
113
- freq: freq
114
- )
115
- end
116
-
117
- def demod_gmsk(complex_samples)
118
- return [] if complex_samples.length < 2
119
-
120
- bits = []
121
- (1...complex_samples.length).each do |i|
122
- prod = complex_samples[i] * complex_samples[i - 1].conj
123
- # Sign of imaginary part for quadrature differential
124
- bit = (prod.imag >= 0 ? 0 : 1) # Or adjust polarity
125
- bits << bit
126
- end
127
- bits
128
- end
129
-
130
- def find_tsc_offset(bits, tsc)
131
- max_corr = -1
132
- best_offset = -1
133
- tsc_length = tsc.length # 26
134
- (0...(bits.length - tsc_length + 1)).each do |offset|
135
- window = bits[offset, tsc_length]
136
- corr = window.zip(tsc).count { |b1, b2| b1 == b2 }
137
- if corr > max_corr
138
- max_corr = corr
139
- best_offset = offset
140
- end
141
- end
142
- # Threshold: e.g., >20 matches for good sync
143
- max_corr > 20 ? best_offset : -1
58
+ # Supported Method Parameters::
59
+ # PWN::SDR::Decoder::GSM.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
60
+
61
+ public_class_method def self.parse_line(opts = {})
62
+ line = opts[:line].to_s
63
+ f = line.split('|', -1)
64
+ 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]
75
+ }.reject { |_, v| v.to_s.empty? }
76
+
77
+ if out[:imsi].to_s.length.between?(14, 16)
78
+ out[:imsi_mcc] = out[:imsi][0, 3]
79
+ out[:imsi_mnc] = out[:imsi][3, 3]
80
+ out[:imsi_msin] = out[:imsi][6..]
144
81
  end
145
82
 
146
- # Extract 114 data bits from normal burst (ignoring tails/guard)
147
- def extract_data_bits(bits, burst_start)
148
- data1_start = burst_start + 2
149
- data2_start = burst_start + 88 # After TSC 26 + data1 57 = 85, +3? Wait, structure: tail2(0-1), data(2-58), tsc(59-84), data(85-141), tail(142-143)
150
- data1 = bits[data1_start, 57]
151
- data2 = bits[data2_start, 57]
152
- data1 + data2
153
- end
154
-
155
- def decode_imsi(opts = {})
156
- data_bits = opts[:data_bits]
157
- freq = opts[:freq]
158
- raise ':ERROR: :data_bits and :freq are required' unless data_bits && freq
159
-
160
- # Simplified "IMSI extraction": Interpret first ~60 bits as packed digits (4 bits per digit, BCD-like).
161
- # In reality: Deinterleave (over bursts), Viterbi decode convolutional code (polys G0=10011b, G1=11011b),
162
- # CRC check, parse L3 message (e.g., Paging Req Type 1 has IMSI IE at specific offset, packed BCD).
163
- # Here: Raw data bits to 15-digit IMSI (first 60 bits -> 15 nibbles).
164
- return unless data_bits.length >= 60
165
-
166
- imsi_digits = []
167
- data_bits[0, 60].each_slice(4) do |nibble|
168
- digit = nibble.join.to_i(2)
169
- imsi_digits << (digit % 10) # Mod 10 for digit-like, or keep as is for hex
170
- end
171
-
172
- # Format as 3(MCC)+3(MNC)+9(MSIN)
173
- mcc = imsi_digits[0, 3].join
174
- mnc = imsi_digits[3, 3].join
175
- msin = imsi_digits[6, 9].join
176
- imsi = "#{mcc.ljust(3, '0')}#{mnc.ljust(3, '0')}#{msin.ljust(9, '0')}"
177
-
178
- puts "Decoded IMSI: #{imsi} at #{freq} Hz"
179
- # TODO: Integrate full L3 parser (e.g., from ruby-gsm gem or custom).
180
- end
181
-
182
- def cleanup(opts = {})
183
- record_path = opts[:record_path]
184
- raise ':ERROR: :record_path is required' unless record_path
185
-
186
- return unless File.exist?(record_path)
187
-
188
- File.delete(record_path)
189
- puts "Cleaned up recording: #{record_path}"
190
- end
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
90
+ out
191
91
  end
192
92
 
193
93
  # Author(s):: 0day Inc. <support@0dayinc.com>
194
94
 
195
95
  public_class_method def self.authors
196
- "AUTHOR(S):
197
- 0day Inc. <support@0dayinc.com>
198
- "
96
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
199
97
  end
200
98
 
201
99
  # Display Usage for this Module
202
100
 
203
101
  public_class_method def self.help
204
102
  puts "USAGE:
205
- gsm_decoder_thread = PWN::SDR::Decoder::GSM.start(
206
- freq_obj: 'required - freq_obj returned from PWN::SDR::Receiver::GQRX.init_freq method'
103
+ #{self}.decode(
104
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
207
105
  )
208
106
 
209
- # To stop the decoder thread:
210
- PWN::SDR::Decoder::GSM.stop(
211
- freq_obj: 'required - freq_obj returned from PWN::SDR::Receiver::GQRX.init_freq method'
212
- )
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
+ #{self}.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
213
112
 
214
113
  #{self}.authors
215
114
  "