pwn 0.5.620 → 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/README.md +1 -1
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Cron.md +13 -0
- data/documentation/Diagrams.md +1 -1
- data/documentation/Extrospection.md +71 -5
- data/documentation/Home.md +2 -2
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Mistakes.md +2 -1
- data/documentation/Persistence.md +2 -1
- data/documentation/Skills-Memory-Learning.md +1 -0
- data/documentation/Transparent-Browser.md +18 -1
- data/documentation/diagrams/agent-tool-registry.svg +55 -54
- 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 +2 -2
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +29 -8
- data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +2 -2
- data/documentation/diagrams/dot/persistence-filesystem.dot +2 -1
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +229 -99
- 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 +124 -101
- data/documentation/diagrams/mistakes-negative-feedback.svg +55 -55
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +85 -85
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +94 -85
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +226 -180
- 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 +31 -31
- 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 +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +433 -4
- data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
- data/lib/pwn/sdr/decoder/adsb.rb +14 -33
- data/lib/pwn/sdr/decoder/apt.rb +97 -36
- data/lib/pwn/sdr/decoder/base.rb +258 -171
- data/lib/pwn/sdr/decoder/bluetooth.rb +13 -20
- data/lib/pwn/sdr/decoder/dect.rb +13 -27
- 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 +12 -21
- data/lib/pwn/sdr/decoder/gsm.rb +21 -73
- data/lib/pwn/sdr/decoder/iridium.rb +11 -26
- data/lib/pwn/sdr/decoder/lora.rb +16 -23
- data/lib/pwn/sdr/decoder/lte.rb +11 -32
- data/lib/pwn/sdr/decoder/morse.rb +95 -21
- data/lib/pwn/sdr/decoder/p25.rb +19 -16
- data/lib/pwn/sdr/decoder/pager.rb +22 -38
- data/lib/pwn/sdr/decoder/pocsag.rb +175 -68
- data/lib/pwn/sdr/decoder/rfid.rb +24 -33
- data/lib/pwn/sdr/decoder/rtl433.rb +21 -30
- data/lib/pwn/sdr/decoder/rtty.rb +100 -27
- data/lib/pwn/sdr/decoder/wifi.rb +21 -42
- data/lib/pwn/sdr/decoder/zigbee.rb +18 -40
- data/lib/pwn/sdr/decoder.rb +8 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +24 -3
- metadata +3 -1
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
|
data/lib/pwn/sdr/decoder/gps.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
module PWN
|
|
6
4
|
module SDR
|
|
7
5
|
module Decoder
|
|
8
|
-
# GNSS / GPS L1-C/A
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
12
|
module GPS
|
|
13
13
|
# Supported Method Parameters::
|
|
14
14
|
# PWN::SDR::Decoder::GPS.decode(
|
|
@@ -17,23 +17,17 @@ module PWN
|
|
|
17
17
|
|
|
18
18
|
public_class_method def self.decode(opts = {})
|
|
19
19
|
freq_obj = opts[:freq_obj]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
conf = (freq_obj[:gnss_conf] || '/usr/share/gnss-sdr/conf/gnss-sdr_GPS_L1_rtlsdr_realtime.conf').to_s
|
|
23
|
-
direct_cmd = "gnss-sdr --config_file=#{Shellwords.escape(conf)}"
|
|
24
|
-
|
|
25
|
-
PWN::SDR::Decoder::Base.run_pipeline(
|
|
20
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
26
21
|
freq_obj: freq_obj,
|
|
27
22
|
protocol: 'GPS',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
parser: proc { |line| parse_line(line: line) }
|
|
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 } }
|
|
32
26
|
)
|
|
33
27
|
end
|
|
34
28
|
|
|
35
29
|
# Supported Method Parameters::
|
|
36
|
-
# PWN::SDR::Decoder::GPS.parse_line(line: 'Position at ... Lat = 32.7 [deg]
|
|
30
|
+
# PWN::SDR::Decoder::GPS.parse_line(line: 'Position at ... Lat = 32.7 [deg] ...')
|
|
37
31
|
|
|
38
32
|
public_class_method def self.parse_line(opts = {})
|
|
39
33
|
line = opts[:line].to_s
|
|
@@ -64,14 +58,11 @@ module PWN
|
|
|
64
58
|
# Display Usage for this Module
|
|
65
59
|
|
|
66
60
|
public_class_method def self.help
|
|
67
|
-
puts "USAGE:
|
|
61
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
68
62
|
#{self}.decode(
|
|
69
63
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
70
64
|
)
|
|
71
65
|
|
|
72
|
-
NOTE: Requires `gnss-sdr` and an active-antenna-biased front end.
|
|
73
|
-
Override the receiver config via freq_obj[:gnss_conf].
|
|
74
|
-
|
|
75
66
|
#{self}.parse_line(line: 'Position ... Lat = 32.7 Long = -97.1 Height = 210')
|
|
76
67
|
|
|
77
68
|
#{self}.authors
|
data/lib/pwn/sdr/decoder/gsm.rb
CHANGED
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
module PWN
|
|
6
4
|
module SDR
|
|
7
5
|
module Decoder
|
|
8
|
-
# GSM (2G)
|
|
9
|
-
#
|
|
10
|
-
# GSM is 270.833 kbit/s GMSK — it CANNOT be recovered from GQRX's 48 kHz
|
|
11
|
-
# demodulated-audio UDP tap. This module therefore drives the SDR
|
|
12
|
-
# directly via `grgsm_livemon_headless` (from gr-gsm), which publishes
|
|
13
|
-
# decoded Um bursts as GSMTAP on udp/4729, and reads them back with
|
|
14
|
-
# `tshark` for structured field extraction (MCC/MNC/LAC/CI/ARFCN, paging
|
|
15
|
-
# IMSIs/TMSIs, System Information messages, etc.).
|
|
16
|
-
#
|
|
17
|
-
# Interface matches PWN::SDR::Decoder::Flex / ::RDS so the GQRX
|
|
18
|
-
# dispatcher (`decoder: :gsm`) works uniformly.
|
|
6
|
+
# Pure-Ruby GSM (2G) BCCH activity detector.
|
|
19
7
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
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.
|
|
24
15
|
module GSM
|
|
25
16
|
TSHARK_FIELDS = %w[
|
|
26
|
-
frame.time
|
|
27
|
-
|
|
28
|
-
gsmtap.chan_type
|
|
29
|
-
gsm_a.imsi
|
|
30
|
-
gsm_a.tmsi
|
|
31
|
-
e212.mcc
|
|
32
|
-
e212.mnc
|
|
33
|
-
gsm_a.lac
|
|
34
|
-
gsm_a.bssmap.cell_ci
|
|
35
|
-
gsm_a.dtap.msg_rr_type
|
|
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
|
|
36
19
|
].freeze
|
|
37
20
|
|
|
38
21
|
# Supported Method Parameters::
|
|
@@ -42,35 +25,15 @@ module PWN
|
|
|
42
25
|
|
|
43
26
|
public_class_method def self.decode(opts = {})
|
|
44
27
|
freq_obj = opts[:freq_obj]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
48
|
-
gain = (freq_obj[:rf_gain] || 40).to_s.to_f
|
|
49
|
-
sdr_args = freq_obj[:sdr_args].to_s
|
|
50
|
-
gsmtap = (freq_obj[:gsmtap_port] || 4729).to_i
|
|
51
|
-
|
|
52
|
-
grgsm = ['grgsm_livemon_headless', '-f', hz.to_s, '-g', gain.to_s]
|
|
53
|
-
grgsm.push('--args', sdr_args) unless sdr_args.empty?
|
|
54
|
-
|
|
55
|
-
tshark = ['tshark', '-i', 'lo', '-l', '-n',
|
|
56
|
-
'-f', "udp port #{gsmtap}",
|
|
57
|
-
'-Y', 'gsmtap',
|
|
58
|
-
'-T', 'fields', '-E', 'separator=|']
|
|
59
|
-
TSHARK_FIELDS.each { |f| tshark.push('-e', f) }
|
|
60
|
-
|
|
61
|
-
# bash -c '<grgsm> >/dev/null 2>&1 & pid=$!; trap ... ; <tshark>'
|
|
62
|
-
inner = "#{Shellwords.join(grgsm)} >/dev/null 2>&1 & " \
|
|
63
|
-
'LMPID=$!; trap "kill $LMPID 2>/dev/null" EXIT INT TERM; ' \
|
|
64
|
-
"sleep 2; exec #{Shellwords.join(tshark)}"
|
|
65
|
-
direct_cmd = "bash -c #{Shellwords.escape(inner)}"
|
|
66
|
-
|
|
67
|
-
PWN::SDR::Decoder::Base.run_pipeline(
|
|
28
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
68
29
|
freq_obj: freq_obj,
|
|
69
30
|
protocol: 'GSM',
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
37
|
)
|
|
75
38
|
end
|
|
76
39
|
|
|
@@ -81,25 +44,15 @@ module PWN
|
|
|
81
44
|
line = opts[:line].to_s
|
|
82
45
|
f = line.split('|', -1)
|
|
83
46
|
out = {
|
|
84
|
-
protocol: 'GSM',
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
chan_type: f[2],
|
|
88
|
-
imsi: f[3],
|
|
89
|
-
tmsi: f[4],
|
|
90
|
-
mcc: f[5],
|
|
91
|
-
mnc: f[6],
|
|
92
|
-
lac: f[7],
|
|
93
|
-
cell_id: f[8],
|
|
94
|
-
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]
|
|
95
50
|
}.reject { |_, v| v.to_s.empty? }
|
|
96
|
-
|
|
97
51
|
if out[:imsi].to_s.length.between?(14, 16)
|
|
98
52
|
out[:imsi_mcc] = out[:imsi][0, 3]
|
|
99
53
|
out[:imsi_mnc] = out[:imsi][3, 3]
|
|
100
54
|
out[:imsi_msin] = out[:imsi][6..]
|
|
101
55
|
end
|
|
102
|
-
|
|
103
56
|
bits = []
|
|
104
57
|
bits << "ARFCN=#{out[:arfcn]}" if out[:arfcn]
|
|
105
58
|
bits << "MCC/MNC=#{out[:mcc]}/#{out[:mnc]}" if out[:mcc]
|
|
@@ -119,16 +72,11 @@ module PWN
|
|
|
119
72
|
# Display Usage for this Module
|
|
120
73
|
|
|
121
74
|
public_class_method def self.help
|
|
122
|
-
puts "USAGE:
|
|
75
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
123
76
|
#{self}.decode(
|
|
124
77
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
125
78
|
)
|
|
126
79
|
|
|
127
|
-
NOTE: Requires `grgsm_livemon_headless` (gr-gsm) and `tshark`.
|
|
128
|
-
GSM cannot be decoded from GQRX's 48 kHz audio tap; this
|
|
129
|
-
module drives the SDR directly and reads GSMTAP on lo:4729.
|
|
130
|
-
Set freq_obj[:sdr_args] (e.g. 'rtl=1') if GQRX owns device 0.
|
|
131
|
-
|
|
132
80
|
#{self}.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
|
|
133
81
|
|
|
134
82
|
#{self}.authors
|