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.
- checksums.yaml +4 -4
- data/README.md +109 -85
- data/README.md.bak +200 -0
- data/bin/pwn +47 -2
- data/bin/pwn_gqrx_scanner +75 -18
- data/documentation/AI-Integration.md +42 -26
- data/documentation/AWS.md +57 -0
- data/documentation/Agent-Tool-Registry.md +56 -0
- data/documentation/Banner.md +17 -0
- data/documentation/Blockchain.md +18 -0
- data/documentation/Bounty.md +21 -0
- data/documentation/BurpSuite.md +41 -16
- data/documentation/CLI-Drivers.md +58 -0
- data/documentation/Configuration.md +66 -0
- data/documentation/Contributing.md +33 -19
- data/documentation/Cron.md +47 -0
- data/documentation/Diagrams.md +123 -40
- data/documentation/Drivers.md +43 -16
- data/documentation/Extrospection.md +54 -0
- data/documentation/FFI.md +14 -0
- data/documentation/Fuzzing.md +36 -0
- data/documentation/General-PWN-Usage.md +50 -30
- data/documentation/Hardware.md +40 -0
- data/documentation/Home.md +78 -51
- data/documentation/How-PWN-Works.md +59 -57
- data/documentation/Installation.md +53 -28
- data/documentation/Metasploit.md +34 -0
- data/documentation/NmapIt.md +18 -12
- data/documentation/PWN.png +0 -0
- data/documentation/PWN_Contributors_and_Users.png +0 -0
- data/documentation/Persistence.md +38 -0
- data/documentation/Plugins.md +97 -47
- data/documentation/Reporting.md +25 -18
- data/documentation/SAST.md +39 -22
- data/documentation/SDR.md +40 -0
- data/documentation/Sessions.md +39 -0
- data/documentation/Skills-Memory-Learning.md +49 -29
- data/documentation/Swarm.md +71 -0
- data/documentation/Transparent-Browser.md +26 -22
- data/documentation/Troubleshooting.md +44 -25
- data/documentation/WWW.md +32 -0
- data/documentation/What-is-PWN.md +51 -32
- data/documentation/Why-PWN.md +41 -11
- data/documentation/diagrams/agent-tool-registry.svg +284 -0
- data/documentation/diagrams/ai-integration-tool-calling.svg +141 -103
- data/documentation/diagrams/aws-cloud-security.svg +166 -0
- data/documentation/diagrams/build.sh +20 -0
- data/documentation/diagrams/burp-vs-zap-preference.svg +47 -62
- data/documentation/diagrams/code-scanning-sast.svg +139 -79
- data/documentation/diagrams/cron-scheduling.svg +148 -0
- data/documentation/diagrams/dot/_THEME.md +27 -0
- data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
- data/documentation/diagrams/dot/ai-integration-tool-calling.dot +47 -20
- data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
- data/documentation/diagrams/dot/burp-vs-zap-preference.dot +23 -12
- data/documentation/diagrams/dot/code-scanning-sast.dot +46 -18
- data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
- data/documentation/diagrams/dot/driver-framework.dot +36 -14
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
- data/documentation/diagrams/dot/fuzzing-workflow.dot +44 -24
- data/documentation/diagrams/dot/hardware-hacking.dot +48 -0
- data/documentation/diagrams/dot/history-to-drivers.dot +31 -17
- data/documentation/diagrams/dot/memory-skills-detailed.dot +57 -25
- data/documentation/diagrams/dot/network-infra-testing.dot +51 -19
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +110 -44
- data/documentation/diagrams/dot/penetration-testing-workflow.dot +56 -25
- data/documentation/diagrams/dot/persistence-filesystem.dot +29 -0
- data/documentation/diagrams/dot/plugin-ecosystem.dot +41 -27
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +87 -46
- data/documentation/diagrams/dot/pwn-repl-prototyping.dot +49 -30
- data/documentation/diagrams/dot/reporting-pipeline.dot +43 -14
- data/documentation/diagrams/dot/reverse-engineering-flow.dot +40 -18
- data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
- data/documentation/diagrams/dot/sessions-cron-automation.dot +41 -14
- data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
- data/documentation/diagrams/dot/web-application-testing.dot +45 -23
- data/documentation/diagrams/dot/zero-day-research-flow.dot +46 -24
- data/documentation/diagrams/driver-framework.svg +87 -66
- data/documentation/diagrams/extrospection-world-awareness.svg +185 -0
- data/documentation/diagrams/fuzzing-workflow.svg +128 -115
- data/documentation/diagrams/hardware-hacking.svg +163 -0
- data/documentation/diagrams/history-to-drivers.svg +84 -82
- data/documentation/diagrams/memory-skills-detailed.svg +167 -119
- data/documentation/diagrams/network-infra-testing.svg +146 -84
- data/documentation/diagrams/overall-pwn-architecture.svg +356 -185
- data/documentation/diagrams/penetration-testing-workflow.svg +161 -108
- data/documentation/diagrams/persistence-filesystem.svg +174 -0
- data/documentation/diagrams/plugin-ecosystem.svg +225 -121
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +257 -185
- data/documentation/diagrams/pwn-repl-prototyping.svg +126 -137
- data/documentation/diagrams/reporting-pipeline.svg +128 -68
- data/documentation/diagrams/reverse-engineering-flow.svg +100 -80
- data/documentation/diagrams/sdr-radio-flow.svg +146 -0
- data/documentation/diagrams/sessions-cron-automation.svg +112 -68
- data/documentation/diagrams/swarm-multi-agent.svg +225 -0
- data/documentation/diagrams/web-application-testing.svg +136 -100
- data/documentation/diagrams/zero-day-research-flow.svg +116 -112
- data/documentation/fax-spectrogram.png +0 -0
- data/documentation/fax-waveform.png +0 -0
- data/documentation/pwn-REPL.md +40 -24
- data/documentation/pwn-ai-Agent.md +59 -30
- data/documentation/pwn_android_war_dialer_session.png +0 -0
- data/documentation/pwn_install.png +0 -0
- data/documentation/pwn_wallpaper.jpg +0 -0
- data/documentation/ringing-spectrogram.png +0 -0
- data/documentation/ringing-waveform.png +0 -0
- data/git_commit.sh +1 -1
- data/lib/pwn/ai/agent/assembly.rb +1 -1
- data/lib/pwn/ai/agent/btc.rb +1 -1
- data/lib/pwn/ai/agent/burp_suite.rb +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +618 -0
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/introspection.rb +91 -0
- data/lib/pwn/ai/agent/learning.rb +6 -4
- data/lib/pwn/ai/agent/loop.rb +15 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/swarm.rb +437 -0
- data/lib/pwn/ai/agent/tools/cron.rb +163 -0
- data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
- data/lib/pwn/ai/agent/tools/learning.rb +108 -0
- data/lib/pwn/ai/agent/tools/memory.rb +27 -0
- data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
- data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
- data/lib/pwn/ai/agent/tools/skills.rb +30 -0
- data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
- data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
- data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
- data/lib/pwn/ai/agent.rb +3 -0
- data/lib/pwn/ai/anthropic.rb +19 -4
- data/lib/pwn/ai.rb +0 -1
- data/lib/pwn/config.rb +10 -23
- data/lib/pwn/cron.rb +16 -7
- data/lib/pwn/plugins/repl.rb +90 -281
- data/lib/pwn/sdr/decoder/base.rb +251 -0
- data/lib/pwn/sdr/decoder/gsm.rb +84 -185
- data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/gqrx.rb +446 -65
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
- data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
- data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
- data/third_party/pwn_rdoc.jsonl +199 -37
- metadata +65 -3
- data/lib/pwn/ai/introspection.rb +0 -76
data/lib/pwn/sdr/decoder/gsm.rb
CHANGED
|
@@ -3,213 +3,112 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
206
|
-
freq_obj: 'required - freq_obj returned from PWN::SDR::
|
|
103
|
+
#{self}.decode(
|
|
104
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
207
105
|
)
|
|
208
106
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
-
#
|
|
15
|
-
# freq_obj: 'required -
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/pwn/sdr/decoder.rb
CHANGED