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.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/README.md +14 -6
- data/bin/pwn_gqrx_scanner +25 -5
- data/documentation/Agent-Tool-Registry.md +4 -4
- data/documentation/Cron.md +13 -0
- data/documentation/Diagrams.md +6 -2
- data/documentation/Extrospection.md +96 -11
- data/documentation/Home.md +5 -4
- data/documentation/How-PWN-Works.md +7 -4
- data/documentation/Mistakes.md +97 -0
- data/documentation/Persistence.md +4 -1
- data/documentation/SDR.md +23 -5
- data/documentation/Skills-Memory-Learning.md +16 -5
- data/documentation/Transparent-Browser.md +18 -1
- data/documentation/diagrams/agent-tool-registry.svg +103 -100
- data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
- data/documentation/diagrams/aws-cloud-security.svg +25 -25
- data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
- data/documentation/diagrams/code-scanning-sast.svg +25 -25
- data/documentation/diagrams/cron-scheduling.svg +23 -23
- data/documentation/diagrams/dot/agent-tool-registry.dot +3 -3
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -10
- data/documentation/diagrams/dot/memory-skills-detailed.dot +16 -5
- data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +4 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +25 -7
- data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +241 -93
- data/documentation/diagrams/fuzzing-workflow.svg +24 -24
- data/documentation/diagrams/hardware-hacking.svg +18 -18
- data/documentation/diagrams/history-to-drivers.svg +18 -18
- data/documentation/diagrams/memory-skills-detailed.svg +169 -97
- data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +111 -87
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +263 -149
- data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
- data/documentation/diagrams/reporting-pipeline.svg +18 -18
- data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
- data/documentation/diagrams/sdr-radio-flow.svg +76 -56
- data/documentation/diagrams/sessions-cron-automation.svg +18 -18
- data/documentation/diagrams/swarm-multi-agent.svg +39 -39
- data/documentation/diagrams/web-application-testing.svg +26 -26
- data/documentation/diagrams/zero-day-research-flow.svg +25 -25
- data/documentation/pwn-ai-Agent.md +28 -15
- data/lib/pwn/ai/agent/extrospection.rb +494 -6
- data/lib/pwn/ai/agent/learning.rb +67 -7
- data/lib/pwn/ai/agent/loop.rb +72 -6
- data/lib/pwn/ai/agent/mistakes.rb +369 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
- data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
- data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/plugins/jira_data_center.rb +2 -2
- data/lib/pwn/sdr/decoder/adsb.rb +82 -0
- data/lib/pwn/sdr/decoder/apt.rb +136 -0
- data/lib/pwn/sdr/decoder/base.rb +258 -171
- data/lib/pwn/sdr/decoder/bluetooth.rb +71 -0
- data/lib/pwn/sdr/decoder/dect.rb +70 -0
- data/lib/pwn/sdr/decoder/dsp.rb +396 -0
- data/lib/pwn/sdr/decoder/flex.rb +177 -210
- data/lib/pwn/sdr/decoder/gps.rb +74 -0
- data/lib/pwn/sdr/decoder/gsm.rb +31 -62
- data/lib/pwn/sdr/decoder/iridium.rb +66 -0
- data/lib/pwn/sdr/decoder/lora.rb +80 -0
- data/lib/pwn/sdr/decoder/lte.rb +67 -0
- data/lib/pwn/sdr/decoder/morse.rb +138 -0
- data/lib/pwn/sdr/decoder/p25.rb +74 -0
- data/lib/pwn/sdr/decoder/pager.rb +61 -0
- data/lib/pwn/sdr/decoder/pocsag.rb +176 -54
- data/lib/pwn/sdr/decoder/rfid.rb +79 -0
- data/lib/pwn/sdr/decoder/rtl433.rb +86 -0
- data/lib/pwn/sdr/decoder/rtty.rb +139 -0
- data/lib/pwn/sdr/decoder/wifi.rb +77 -0
- data/lib/pwn/sdr/decoder/zigbee.rb +74 -0
- data/lib/pwn/sdr/decoder.rb +75 -7
- data/lib/pwn/sdr/frequency_allocation.rb +82 -41
- data/lib/pwn/sdr/gqrx.rb +186 -95
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
- data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +115 -5
- metadata +46 -6
- data/README.md.bak +0 -200
data/lib/pwn/sdr/decoder/flex.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
46
|
+
try_lock if @inv.nil?
|
|
47
|
+
return if @inv.nil?
|
|
66
48
|
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
175
|
+
# Supported Method Parameters::
|
|
176
|
+
# str = PWN::SDR::Decoder::Flex.hex_decode(words: [Integer, ...])
|
|
227
177
|
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/pwn/sdr/decoder/gsm.rb
CHANGED
|
@@ -3,18 +3,21 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
# GSM (2G)
|
|
6
|
+
# Pure-Ruby GSM (2G) BCCH activity detector.
|
|
7
7
|
#
|
|
8
|
-
# GSM is 270.833 kbit/s GMSK
|
|
9
|
-
# demodulated-audio
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|