pwn 0.5.615 → 0.5.618

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +109 -85
  3. data/README.md.bak +200 -0
  4. data/bin/pwn +47 -2
  5. data/bin/pwn_gqrx_scanner +75 -18
  6. data/documentation/AI-Integration.md +42 -26
  7. data/documentation/AWS.md +57 -0
  8. data/documentation/Agent-Tool-Registry.md +56 -0
  9. data/documentation/Banner.md +17 -0
  10. data/documentation/Blockchain.md +18 -0
  11. data/documentation/Bounty.md +21 -0
  12. data/documentation/BurpSuite.md +41 -16
  13. data/documentation/CLI-Drivers.md +58 -0
  14. data/documentation/Configuration.md +66 -0
  15. data/documentation/Contributing.md +33 -19
  16. data/documentation/Cron.md +47 -0
  17. data/documentation/Diagrams.md +123 -40
  18. data/documentation/Drivers.md +43 -16
  19. data/documentation/Extrospection.md +54 -0
  20. data/documentation/FFI.md +14 -0
  21. data/documentation/Fuzzing.md +36 -0
  22. data/documentation/General-PWN-Usage.md +50 -30
  23. data/documentation/Hardware.md +40 -0
  24. data/documentation/Home.md +78 -51
  25. data/documentation/How-PWN-Works.md +59 -57
  26. data/documentation/Installation.md +53 -28
  27. data/documentation/Metasploit.md +34 -0
  28. data/documentation/NmapIt.md +18 -12
  29. data/documentation/PWN.png +0 -0
  30. data/documentation/PWN_Contributors_and_Users.png +0 -0
  31. data/documentation/Persistence.md +38 -0
  32. data/documentation/Plugins.md +97 -47
  33. data/documentation/Reporting.md +25 -18
  34. data/documentation/SAST.md +39 -22
  35. data/documentation/SDR.md +40 -0
  36. data/documentation/Sessions.md +39 -0
  37. data/documentation/Skills-Memory-Learning.md +49 -29
  38. data/documentation/Swarm.md +71 -0
  39. data/documentation/Transparent-Browser.md +26 -22
  40. data/documentation/Troubleshooting.md +44 -25
  41. data/documentation/WWW.md +32 -0
  42. data/documentation/What-is-PWN.md +51 -32
  43. data/documentation/Why-PWN.md +41 -11
  44. data/documentation/diagrams/agent-tool-registry.svg +284 -0
  45. data/documentation/diagrams/ai-integration-tool-calling.svg +141 -103
  46. data/documentation/diagrams/aws-cloud-security.svg +166 -0
  47. data/documentation/diagrams/build.sh +20 -0
  48. data/documentation/diagrams/burp-vs-zap-preference.svg +47 -62
  49. data/documentation/diagrams/code-scanning-sast.svg +139 -79
  50. data/documentation/diagrams/cron-scheduling.svg +148 -0
  51. data/documentation/diagrams/dot/_THEME.md +27 -0
  52. data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
  53. data/documentation/diagrams/dot/ai-integration-tool-calling.dot +47 -20
  54. data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
  55. data/documentation/diagrams/dot/burp-vs-zap-preference.dot +23 -12
  56. data/documentation/diagrams/dot/code-scanning-sast.dot +46 -18
  57. data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
  58. data/documentation/diagrams/dot/driver-framework.dot +36 -14
  59. data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
  60. data/documentation/diagrams/dot/fuzzing-workflow.dot +44 -24
  61. data/documentation/diagrams/dot/hardware-hacking.dot +48 -0
  62. data/documentation/diagrams/dot/history-to-drivers.dot +31 -17
  63. data/documentation/diagrams/dot/memory-skills-detailed.dot +57 -25
  64. data/documentation/diagrams/dot/network-infra-testing.dot +51 -19
  65. data/documentation/diagrams/dot/overall-pwn-architecture.dot +110 -44
  66. data/documentation/diagrams/dot/penetration-testing-workflow.dot +56 -25
  67. data/documentation/diagrams/dot/persistence-filesystem.dot +29 -0
  68. data/documentation/diagrams/dot/plugin-ecosystem.dot +41 -27
  69. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +87 -46
  70. data/documentation/diagrams/dot/pwn-repl-prototyping.dot +49 -30
  71. data/documentation/diagrams/dot/reporting-pipeline.dot +43 -14
  72. data/documentation/diagrams/dot/reverse-engineering-flow.dot +40 -18
  73. data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
  74. data/documentation/diagrams/dot/sessions-cron-automation.dot +41 -14
  75. data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
  76. data/documentation/diagrams/dot/web-application-testing.dot +45 -23
  77. data/documentation/diagrams/dot/zero-day-research-flow.dot +46 -24
  78. data/documentation/diagrams/driver-framework.svg +87 -66
  79. data/documentation/diagrams/extrospection-world-awareness.svg +185 -0
  80. data/documentation/diagrams/fuzzing-workflow.svg +128 -115
  81. data/documentation/diagrams/hardware-hacking.svg +163 -0
  82. data/documentation/diagrams/history-to-drivers.svg +84 -82
  83. data/documentation/diagrams/memory-skills-detailed.svg +167 -119
  84. data/documentation/diagrams/network-infra-testing.svg +146 -84
  85. data/documentation/diagrams/overall-pwn-architecture.svg +356 -185
  86. data/documentation/diagrams/penetration-testing-workflow.svg +161 -108
  87. data/documentation/diagrams/persistence-filesystem.svg +174 -0
  88. data/documentation/diagrams/plugin-ecosystem.svg +225 -121
  89. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +257 -185
  90. data/documentation/diagrams/pwn-repl-prototyping.svg +126 -137
  91. data/documentation/diagrams/reporting-pipeline.svg +128 -68
  92. data/documentation/diagrams/reverse-engineering-flow.svg +100 -80
  93. data/documentation/diagrams/sdr-radio-flow.svg +146 -0
  94. data/documentation/diagrams/sessions-cron-automation.svg +112 -68
  95. data/documentation/diagrams/swarm-multi-agent.svg +225 -0
  96. data/documentation/diagrams/web-application-testing.svg +136 -100
  97. data/documentation/diagrams/zero-day-research-flow.svg +116 -112
  98. data/documentation/fax-spectrogram.png +0 -0
  99. data/documentation/fax-waveform.png +0 -0
  100. data/documentation/pwn-REPL.md +40 -24
  101. data/documentation/pwn-ai-Agent.md +59 -30
  102. data/documentation/pwn_android_war_dialer_session.png +0 -0
  103. data/documentation/pwn_install.png +0 -0
  104. data/documentation/pwn_wallpaper.jpg +0 -0
  105. data/documentation/ringing-spectrogram.png +0 -0
  106. data/documentation/ringing-waveform.png +0 -0
  107. data/git_commit.sh +1 -1
  108. data/lib/pwn/ai/agent/assembly.rb +1 -1
  109. data/lib/pwn/ai/agent/btc.rb +1 -1
  110. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  111. data/lib/pwn/ai/agent/extrospection.rb +618 -0
  112. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  113. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  114. data/lib/pwn/ai/agent/introspection.rb +91 -0
  115. data/lib/pwn/ai/agent/learning.rb +6 -4
  116. data/lib/pwn/ai/agent/loop.rb +15 -0
  117. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  118. data/lib/pwn/ai/agent/sast.rb +1 -1
  119. data/lib/pwn/ai/agent/swarm.rb +437 -0
  120. data/lib/pwn/ai/agent/tools/cron.rb +163 -0
  121. data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
  122. data/lib/pwn/ai/agent/tools/learning.rb +108 -0
  123. data/lib/pwn/ai/agent/tools/memory.rb +27 -0
  124. data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
  125. data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
  126. data/lib/pwn/ai/agent/tools/skills.rb +30 -0
  127. data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
  128. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  129. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  130. data/lib/pwn/ai/agent.rb +3 -0
  131. data/lib/pwn/ai/anthropic.rb +19 -4
  132. data/lib/pwn/ai.rb +0 -1
  133. data/lib/pwn/config.rb +10 -23
  134. data/lib/pwn/cron.rb +16 -7
  135. data/lib/pwn/plugins/repl.rb +90 -281
  136. data/lib/pwn/sdr/decoder/base.rb +251 -0
  137. data/lib/pwn/sdr/decoder/gsm.rb +84 -185
  138. data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
  139. data/lib/pwn/sdr/decoder.rb +1 -0
  140. data/lib/pwn/sdr/gqrx.rb +446 -65
  141. data/lib/pwn/version.rb +1 -1
  142. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
  143. data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
  144. data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
  145. data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
  146. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
  147. data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
  148. data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
  149. data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
  150. data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
  151. data/third_party/pwn_rdoc.jsonl +199 -37
  152. metadata +65 -3
  153. data/lib/pwn/ai/introspection.rb +0 -76
@@ -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
  "
@@ -1,244 +1,87 @@
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
- # POCSAG Decoder Module for Pagers
6
+ # POCSAG (CCIR Radiopaging Code No. 1) decoder for pager networks.
7
+ #
8
+ # Pipeline: GQRX UDP audio → sox 48k→22.05k → multimon-ng POCSAG512/1200/2400.
9
+ #
10
+ # multimon-ng emits lines of the form:
11
+ # POCSAG1200: Address: 123456 Function: 3 Alpha: FIRE ALARM ZONE 4
12
+ # POCSAG512: Address: 000042 Function: 0 Numeric: 5551234
13
+ # POCSAG2400: Address: 987654 Function: 1 Skyper: ...
14
+ # POCSAG1200: Address: 123456 Function: 2 Alpha: <partial><EOT>
15
+ #
16
+ # This module parses those into { protocol, baud, address, function,
17
+ # function_desc, type, type_desc, message } and JSON-logs each page.
12
18
  module POCSAG
19
+ FUNCTION_DESC = {
20
+ 0 => 'Numeric (Tone/A)',
21
+ 1 => 'Tone only (B)',
22
+ 2 => 'Tone only (C)',
23
+ 3 => 'Alphanumeric (D)'
24
+ }.freeze
25
+
26
+ TYPE_DESC = {
27
+ 'Alpha' => 'Alphanumeric text (7-bit ASCII)',
28
+ 'Numeric' => 'Numeric-only (BCD)',
29
+ 'Skyper' => 'Skyper network encoded',
30
+ 'Tone' => 'Tone-only alert (no message body)'
31
+ }.freeze
32
+
13
33
  # Supported Method Parameters::
14
- # pocsag_resp = PWN::SDR::Decoder::POCSAG.decode(
15
- # freq_obj: 'required - GQRX socket object returned from #connect method'
34
+ # PWN::SDR::Decoder::POCSAG.decode(
35
+ # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
16
36
  # )
17
37
 
18
38
  public_class_method def self.decode(opts = {})
19
39
  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
40
 
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
41
+ PWN::SDR::Decoder::Base.run_pipeline(
42
+ freq_obj: freq_obj,
43
+ protocol: 'POCSAG',
44
+ required_bins: %w[sox multimon-ng],
45
+ decode_cmd: 'multimon-ng -q -t raw -e -u -p ' \
46
+ '-a POCSAG512 -a POCSAG1200 -a POCSAG2400 -f auto -',
47
+ line_match: /^POCSAG(?:512|1200|2400):/,
48
+ parser: proc { |line| parse_line(line: line) }
53
49
  )
50
+ end
54
51
 
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 POCSAG512 \
61
- -a POCSAG1200 \
62
- -a POCSAG2400 \
63
- -
64
- '
65
-
66
- mm_stdin, mm_stdout, mm_stderr, mm_wait_thr = Open3.popen3(decode_cmd)
67
-
68
- current_title = 'Waiting for data frames...'
52
+ # Supported Method Parameters::
53
+ # PWN::SDR::Decoder::POCSAG.parse_line(line: 'POCSAG1200: Address: ...')
69
54
 
70
- # Thread: read from UDP listener and feed to sox|multimon pipeline
71
- receiver_thread = Thread.new do
72
- begin
73
- loop do
74
- data, _sender = udp_listener.recv(4096)
75
- next unless data.to_s.bytesize.positive?
55
+ public_class_method def self.parse_line(opts = {})
56
+ line = opts[:line].to_s
57
+ out = { protocol: 'POCSAG' }
76
58
 
77
- mm_stdin.write(data)
78
- mm_stdin.flush rescue nil
79
- end
80
- rescue IOError, Errno::EPIPE, EOFError, Errno::ECONNRESET
81
- # normal exit path when shutting down
82
- end
59
+ if (m = line.match(/^POCSAG(\d+):/))
60
+ out[:baud] = m[1].to_i
83
61
  end
84
-
85
- # Thread: read decoded output from multimon-ng and display it
86
- decoder_thread = Thread.new do
87
- # buffer = +''
88
- buffer = ''
89
-
90
- valid_types = %w[ALN BIN HEX NUM TON TONE UNK]
91
- loop do
92
- begin
93
- chunk = mm_stdout.readpartial(4096)
94
- # buffer << chunk
95
- buffer = "#{buffer}#{chunk}"
96
-
97
- while (line = buffer.slice!(/^.*\n/))
98
- line = line.chomp
99
- next if line.empty? || !line.start_with?('POCSAG')
100
-
101
- decoded_at = Time.now.strftime('%Y-%m-%d %H:%M:%S%z')
102
- dec_msg = { decoded_at: decoded_at }
103
- dec_msg[:raw_inspected] = line.inspect
104
-
105
- protocol = line[0..8]
106
- protocol = line[0..9] unless protocol == 'POCSAG512'
107
- dec_msg[:protocol] = protocol
108
-
109
- # ────────────────────────────── Detect format ──────────────────────────────
110
- # Sometimes POCSAG is space delimited, sometimes pipe delimited
111
- # FLEX_NEXT appears to always be pipe delimited
112
-
113
- delimiter = '|'
114
- space_delim = false
115
- if line.start_with?('FLEX: ')
116
- delimiter = ' '
117
- space_delim = true
118
- end
119
-
120
- flex_pipe_delim = false
121
- flex_pipe_delim = true if line.start_with?('FLEX|')
122
-
123
- parts = line.split(delimiter)
124
-
125
- # protocol index already used
126
- idx_already_used = [0]
127
- target_parts_idx = 1
128
- target_parts_idx += 1 if flex_pipe_delim
129
- target_parts_idx += 2 if space_delim
130
- dec_msg[:speed] = parts[target_parts_idx] if parts[target_parts_idx]
131
- idx_already_used.push(target_parts_idx)
132
-
133
- target_parts_idx += 2
134
- dec_msg[:capcode] = parts[target_parts_idx] if parts[target_parts_idx]
135
- idx_already_used.push(target_parts_idx)
136
-
137
- target_parts_idx -= 1
138
- dec_msg[:capcode_loc] = parts[target_parts_idx] if parts[target_parts_idx]
139
- idx_already_used.push(target_parts_idx)
140
-
141
- while target_parts_idx < parts.size
142
- if idx_already_used.include?(target_parts_idx)
143
- target_parts_idx += 1
144
- next
145
- end
146
-
147
- key = parts[target_parts_idx]
148
- key = 'long_sequence_number' if key == 'LS'
149
-
150
- if key && valid_types.include?(key)
151
- dec_msg[:type] = key
152
-
153
- dec_msg[:type_desc] = case key
154
- when 'ALN'
155
- 'Human-readable text'
156
- when 'BIN'
157
- 'Binary / data payload (typically 32 bit words)'
158
- when 'HEX'
159
- 'Raw hex representation of data'
160
- when 'NUM'
161
- 'Numbers only'
162
- when 'TON', 'TONE'
163
- 'Just alert tone, no message'
164
- when 'UNK'
165
- 'Decoded but type unknown / unsupported format'
166
- end
167
-
168
- target_parts_idx += 1
169
- payload_parts = parts[target_parts_idx..]
170
- dec_msg[:type_payload] = payload_parts.join(delimiter)
171
-
172
- break
173
- else
174
- target_parts_idx += 1
175
- dec_msg[key] = parts[target_parts_idx] if parts[target_parts_idx]
176
- end
177
-
178
- idx_already_used.push(target_parts_idx)
179
- target_parts_idx += 1
180
- end
181
-
182
- final_msg = freq_obj.merge(dec_msg)
183
- puts JSON.pretty_generate(final_msg)
184
- # TODO: Append dec_msg to a log file in a better way
185
- flex_log_file = "/tmp/flex_decoder_#{Time.now.strftime('%Y%m%d')}.log"
186
- File.open(flex_log_file, 'a') do |f|
187
- f.puts("#{JSON.generate(final_msg)},")
188
- end
189
- end
190
- rescue EOFError, IOError
191
- break
192
- end
193
- end
62
+ if (m = line.match(/Address:\s*(\d+)/i))
63
+ out[:address] = m[1].to_i
64
+ out[:capcode] = m[1]
194
65
  end
195
-
196
- loop do
197
- spinner.update(status: current_title)
198
-
199
- # Non-blocking ENTER check to exit gracefully
200
- next unless $stdin.wait_readable(0)
201
-
202
- begin
203
- char = $stdin.read_nonblock(1)
204
- next unless char == skip_freq_char
205
-
206
- skip_freq = true
207
- puts "\n[!] ENTER pressed → stopping decoder..."
208
-
209
- break
210
- rescue IO::WaitReadable, EOFError
211
- # ignore
212
- end
213
-
214
- break if skip_freq
66
+ if (m = line.match(/Function:\s*(\d+)/i))
67
+ fn = m[1].to_i
68
+ out[:function] = fn
69
+ out[:function_desc] = FUNCTION_DESC[fn] || 'Unknown'
215
70
  end
216
-
217
- spinner.success('Decoding stopped')
218
- rescue StandardError => e
219
- spinner.error("Decoding failed: #{e.message}") if defined?(spinner)
220
- raise
221
- ensure
222
- # Cleanup
223
- [receiver_thread, decoder_thread].each do |thread|
224
- thread.kill if thread.alive?
71
+ if (m = line.match(/\b(Alpha|Numeric|Skyper|Tone):\s*(.*)$/i))
72
+ type = m[1].capitalize
73
+ out[:type] = type
74
+ out[:type_desc] = TYPE_DESC[type] || 'Unknown'
75
+ out[:message] = m[2].to_s.gsub(/<[A-Z]{2,3}>/, '').strip
225
76
  end
226
-
227
- [mm_stdin, mm_stdout, mm_stderr].each { |io| io.close rescue nil }
228
-
229
- mm_wait_thr&.value rescue nil
230
-
231
- PWN::SDR::GQRX.disconnect_udp(udp_listener: udp_listener) if defined?(udp_listener) && udp_listener
232
-
233
- spinner.stop if defined?(spinner) && spinner
77
+ out[:summary] = "POCSAG#{out[:baud]} RIC=#{out[:capcode]} F#{out[:function]} #{out[:type]}: #{out[:message]}"
78
+ out
234
79
  end
235
80
 
236
81
  # Author(s):: 0day Inc. <support@0dayinc.com>
237
82
 
238
83
  public_class_method def self.authors
239
- "AUTHOR(S):
240
- 0day Inc. <support@0dayinc.com>
241
- "
84
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
242
85
  end
243
86
 
244
87
  # Display Usage for this Module
@@ -246,9 +89,11 @@ module PWN
246
89
  public_class_method def self.help
247
90
  puts "USAGE:
248
91
  #{self}.decode(
249
- freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq method'
92
+ freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
250
93
  )
251
94
 
95
+ #{self}.parse_line(line: 'POCSAG1200: Address: 123456 Function: 3 Alpha: HELLO')
96
+
252
97
  #{self}.authors
253
98
  "
254
99
  end
@@ -7,6 +7,7 @@ module PWN
7
7
  module SDR
8
8
  # Deocder Module for SDR signals.
9
9
  module Decoder
10
+ autoload :Base, 'pwn/sdr/decoder/base'
10
11
  autoload :Flex, 'pwn/sdr/decoder/flex'
11
12
  autoload :GSM, 'pwn/sdr/decoder/gsm'
12
13
  autoload :POCSAG, 'pwn/sdr/decoder/pocsag'