pwn 0.5.621 → 0.5.626
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/.rubocop.yml +2 -2
- data/Gemfile +1 -1
- data/README.md +7 -1
- data/bin/pwn_gqrx_scanner +78 -9
- data/documentation/Agent-Tool-Registry.md +3 -3
- data/documentation/Configuration.md +301 -47
- data/documentation/Cron.md +5 -3
- data/documentation/Diagrams.md +1 -1
- data/documentation/Extrospection.md +472 -26
- data/documentation/FFI.md +72 -8
- data/documentation/Home.md +4 -4
- data/documentation/How-PWN-Works.md +7 -3
- data/documentation/Mistakes.md +9 -0
- data/documentation/SDR.md +83 -10
- data/documentation/Skills-Memory-Learning.md +101 -11
- data/documentation/diagrams/agent-tool-registry.svg +56 -55
- 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 +33 -6
- data/documentation/diagrams/dot/hardware-hacking.dot +3 -1
- data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -2
- data/documentation/diagrams/dot/mistakes-negative-feedback.dot +4 -2
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +3 -3
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
- data/documentation/diagrams/dot/sdr-radio-flow.dot +39 -16
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +383 -177
- data/documentation/diagrams/fuzzing-workflow.svg +24 -24
- data/documentation/diagrams/hardware-hacking.svg +84 -69
- data/documentation/diagrams/history-to-drivers.svg +18 -18
- data/documentation/diagrams/memory-skills-detailed.svg +137 -120
- data/documentation/diagrams/mistakes-negative-feedback.svg +152 -137
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +97 -97
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +100 -94
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +254 -207
- 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 +223 -91
- 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 +16 -10
- data/documentation/pwn_silent_help_learn_demo.gif +0 -0
- 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 +2562 -57
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/learning.rb +16 -16
- data/lib/pwn/ai/agent/loop.rb +1 -1
- data/lib/pwn/ai/agent/{introspection.rb → reflect.rb} +12 -12
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/tools/extrospection.rb +311 -8
- data/lib/pwn/ai/agent/tools/learning.rb +7 -7
- 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 +1 -1
- data/lib/pwn/ai/open_ai.rb +10 -6
- data/lib/pwn/config.rb +5 -4
- data/lib/pwn/ffi/adalm_pluto.rb +498 -0
- data/lib/pwn/ffi/fftw.rb +192 -0
- data/lib/pwn/ffi/hack_rf.rb +193 -0
- data/lib/pwn/ffi/liquid.rb +244 -0
- data/lib/pwn/ffi/rtl_sdr.rb +210 -0
- data/lib/pwn/ffi/soapy_sdr.rb +177 -0
- data/lib/pwn/ffi/stdio.rb +9 -1
- data/lib/pwn/ffi/volk.rb +241 -0
- data/lib/pwn/ffi.rb +45 -2
- data/lib/pwn/plugins/burp_suite.rb +2 -2
- data/lib/pwn/sast/pom_version.rb +2 -2
- data/lib/pwn/sast/test_case_engine.rb +1 -1
- data/lib/pwn/sdr/decoder/adsb.rb +201 -12
- data/lib/pwn/sdr/decoder/apt.rb +19 -5
- data/lib/pwn/sdr/decoder/base.rb +314 -1
- data/lib/pwn/sdr/decoder/bluetooth.rb +91 -18
- data/lib/pwn/sdr/decoder/dect.rb +81 -23
- data/lib/pwn/sdr/decoder/dsp.rb +229 -15
- data/lib/pwn/sdr/decoder/flex.rb +23 -6
- data/lib/pwn/sdr/decoder/gps.rb +80 -18
- data/lib/pwn/sdr/decoder/gsm.rb +83 -28
- data/lib/pwn/sdr/decoder/iridium.rb +80 -18
- data/lib/pwn/sdr/decoder/lora.rb +84 -28
- data/lib/pwn/sdr/decoder/lte.rb +80 -18
- data/lib/pwn/sdr/decoder/morse.rb +23 -6
- data/lib/pwn/sdr/decoder/p25.rb +90 -31
- data/lib/pwn/sdr/decoder/pager.rb +23 -6
- data/lib/pwn/sdr/decoder/pocsag.rb +23 -6
- data/lib/pwn/sdr/decoder/rds.rb +219 -52
- data/lib/pwn/sdr/decoder/rfid.rb +81 -27
- data/lib/pwn/sdr/decoder/rtl433.rb +85 -27
- data/lib/pwn/sdr/decoder/rtty.rb +23 -6
- data/lib/pwn/sdr/decoder/wifi.rb +86 -32
- data/lib/pwn/sdr/decoder/zigbee.rb +88 -35
- data/lib/pwn/sdr/decoder.rb +6 -5
- data/lib/pwn/sdr/frequency_allocation.rb +124 -81
- data/lib/pwn/sdr/gqrx.rb +1513 -165
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +96 -0
- data/spec/lib/pwn/ai/agent/{introspection_spec.rb → reflect_spec.rb} +3 -3
- data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +8 -24
- data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +37 -0
- data/spec/lib/pwn/ffi/fftw_spec.rb +26 -0
- data/spec/lib/pwn/ffi/hack_rf_spec.rb +25 -0
- data/spec/lib/pwn/ffi/liquid_spec.rb +42 -0
- data/spec/lib/pwn/ffi/rtl_sdr_spec.rb +24 -0
- data/spec/lib/pwn/ffi/soapy_sdr_spec.rb +25 -0
- data/spec/lib/pwn/ffi/volk_spec.rb +36 -0
- data/spec/lib/pwn/sdr/decoder/base_iq_spec.rb +65 -0
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +50 -4
- data/spec/lib/pwn/sdr/decoder/rds_spec.rb +45 -4
- data/third_party/pwn_rdoc.jsonl +279 -30
- metadata +21 -5
|
@@ -3,13 +3,69 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# 1616–1626.5 MHz. Native mode reports burst count/duration/energy
|
|
10
|
-
# per channel. `parse_line` retained for offline iridium-toolkit
|
|
11
|
-
# text analysis.
|
|
6
|
+
# True-air + detector-fallback decoder for Iridium.
|
|
7
|
+
# Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
|
|
8
|
+
# via Base.run_iq; degrades to Base.run_detector with no hardware.
|
|
12
9
|
module Iridium
|
|
10
|
+
# Streaming I/Q energy/burst demod for Base.run_iq.
|
|
11
|
+
class DemodIQ
|
|
12
|
+
def initialize(rate:, protocol:, modulation:, extra: {})
|
|
13
|
+
@rate = rate.to_f
|
|
14
|
+
@protocol = protocol
|
|
15
|
+
@modulation = modulation
|
|
16
|
+
@extra = extra
|
|
17
|
+
@floor = nil
|
|
18
|
+
@in_burst = false
|
|
19
|
+
@burst_t0 = nil
|
|
20
|
+
@peak = -200.0
|
|
21
|
+
@burst_n = 0
|
|
22
|
+
@threshold = (extra[:threshold] || 8.0).to_f
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def feed_iq(samples, rate: nil, &)
|
|
26
|
+
@rate = rate.to_f if rate
|
|
27
|
+
m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
|
|
28
|
+
hop = [(@rate / 1000.0).round, 1].max
|
|
29
|
+
i = 0
|
|
30
|
+
while i < m2.length
|
|
31
|
+
win = m2[i, hop]
|
|
32
|
+
break if win.nil? || win.empty?
|
|
33
|
+
|
|
34
|
+
ms = win.sum / win.length
|
|
35
|
+
lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
|
|
36
|
+
@floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
|
|
37
|
+
delta = lvl - @floor
|
|
38
|
+
if delta >= @threshold
|
|
39
|
+
unless @in_burst
|
|
40
|
+
@in_burst = true
|
|
41
|
+
@burst_t0 = Time.now
|
|
42
|
+
@peak = lvl
|
|
43
|
+
end
|
|
44
|
+
@peak = lvl if lvl > @peak
|
|
45
|
+
elsif @in_burst
|
|
46
|
+
@in_burst = false
|
|
47
|
+
@burst_n += 1
|
|
48
|
+
dur_ms = ((Time.now - @burst_t0) * 1000).round
|
|
49
|
+
msg = {
|
|
50
|
+
protocol: @protocol, event: 'burst', source: 'iq',
|
|
51
|
+
burst_no: @burst_n, peak_dbfs: @peak.round(1),
|
|
52
|
+
floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
|
|
53
|
+
duration_ms: dur_ms, modulation: @modulation,
|
|
54
|
+
sample_rate: @rate.to_i
|
|
55
|
+
}.merge(@extra.except(:threshold))
|
|
56
|
+
# protocol-specific enrichment
|
|
57
|
+
msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
|
|
58
|
+
msg[:summary] = format(
|
|
59
|
+
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
60
|
+
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
61
|
+
)
|
|
62
|
+
yield msg if block_given?
|
|
63
|
+
end
|
|
64
|
+
i += hop
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
13
69
|
# Supported Method Parameters::
|
|
14
70
|
# PWN::SDR::Decoder::Iridium.decode(
|
|
15
71
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -17,18 +73,26 @@ module PWN
|
|
|
17
73
|
|
|
18
74
|
public_class_method def self.decode(opts = {})
|
|
19
75
|
freq_obj = opts[:freq_obj]
|
|
20
|
-
|
|
76
|
+
|
|
77
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
|
|
78
|
+
proto = 'IRIDIUM'
|
|
79
|
+
demod = DemodIQ.new(
|
|
80
|
+
rate: rate, protocol: proto, modulation: 'DE-QPSK',
|
|
81
|
+
extra: { threshold: 7.0 }
|
|
82
|
+
)
|
|
83
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
21
84
|
freq_obj: freq_obj,
|
|
22
|
-
protocol:
|
|
23
|
-
|
|
85
|
+
protocol: proto,
|
|
86
|
+
sample_rate: rate,
|
|
87
|
+
source: opts[:source],
|
|
88
|
+
file: opts[:file],
|
|
89
|
+
demod: demod,
|
|
24
90
|
threshold: 7.0,
|
|
91
|
+
note: '25 kbit/s DE-QPSK — true-air I/Q path reports burst timing/energy.',
|
|
25
92
|
describe: proc { |b| { modulation: 'DE-QPSK', symbol_rate: 25_000, classification: b[:duration_ms] < 10 ? 'simplex-burst' : 'duplex-frame' } }
|
|
26
93
|
)
|
|
27
94
|
end
|
|
28
95
|
|
|
29
|
-
# Supported Method Parameters::
|
|
30
|
-
# PWN::SDR::Decoder::Iridium.parse_line(line: 'IRA: sat:23 beam:14 ...')
|
|
31
|
-
|
|
32
96
|
public_class_method def self.parse_line(opts = {})
|
|
33
97
|
line = opts[:line].to_s
|
|
34
98
|
out = { protocol: 'IRIDIUM' }
|
|
@@ -47,16 +111,14 @@ module PWN
|
|
|
47
111
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
48
112
|
end
|
|
49
113
|
|
|
50
|
-
# Display Usage for this Module
|
|
51
|
-
|
|
52
114
|
public_class_method def self.help
|
|
53
|
-
puts "USAGE (
|
|
115
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
54
116
|
#{self}.decode(
|
|
55
|
-
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
117
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
118
|
+
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
119
|
+
file: 'optional - .cu8/.cs16 capture'
|
|
56
120
|
)
|
|
57
121
|
|
|
58
|
-
#{self}.parse_line(line: 'IRA: sat:23 beam:14 pos=(+32.1/-097.0) ...')
|
|
59
|
-
|
|
60
122
|
#{self}.authors
|
|
61
123
|
"
|
|
62
124
|
end
|
data/lib/pwn/sdr/decoder/lora.rb
CHANGED
|
@@ -5,13 +5,69 @@ require 'json'
|
|
|
5
5
|
module PWN
|
|
6
6
|
module SDR
|
|
7
7
|
module Decoder
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# only from raw I/Q at ≥250 ksps, not from a 48 kHz audio tap.
|
|
12
|
-
# Native mode reports chirp-burst duration/energy (from which SF
|
|
13
|
-
# can be estimated). `parse_line` retained for offline JSON analysis.
|
|
8
|
+
# True-air + detector-fallback decoder for LoRa.
|
|
9
|
+
# Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
|
|
10
|
+
# via Base.run_iq; degrades to Base.run_detector with no hardware.
|
|
14
11
|
module LoRa
|
|
12
|
+
# Streaming I/Q energy/burst demod for Base.run_iq.
|
|
13
|
+
class DemodIQ
|
|
14
|
+
def initialize(rate:, protocol:, modulation:, extra: {})
|
|
15
|
+
@rate = rate.to_f
|
|
16
|
+
@protocol = protocol
|
|
17
|
+
@modulation = modulation
|
|
18
|
+
@extra = extra
|
|
19
|
+
@floor = nil
|
|
20
|
+
@in_burst = false
|
|
21
|
+
@burst_t0 = nil
|
|
22
|
+
@peak = -200.0
|
|
23
|
+
@burst_n = 0
|
|
24
|
+
@threshold = (extra[:threshold] || 8.0).to_f
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def feed_iq(samples, rate: nil, &)
|
|
28
|
+
@rate = rate.to_f if rate
|
|
29
|
+
m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
|
|
30
|
+
hop = [(@rate / 1000.0).round, 1].max
|
|
31
|
+
i = 0
|
|
32
|
+
while i < m2.length
|
|
33
|
+
win = m2[i, hop]
|
|
34
|
+
break if win.nil? || win.empty?
|
|
35
|
+
|
|
36
|
+
ms = win.sum / win.length
|
|
37
|
+
lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
|
|
38
|
+
@floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
|
|
39
|
+
delta = lvl - @floor
|
|
40
|
+
if delta >= @threshold
|
|
41
|
+
unless @in_burst
|
|
42
|
+
@in_burst = true
|
|
43
|
+
@burst_t0 = Time.now
|
|
44
|
+
@peak = lvl
|
|
45
|
+
end
|
|
46
|
+
@peak = lvl if lvl > @peak
|
|
47
|
+
elsif @in_burst
|
|
48
|
+
@in_burst = false
|
|
49
|
+
@burst_n += 1
|
|
50
|
+
dur_ms = ((Time.now - @burst_t0) * 1000).round
|
|
51
|
+
msg = {
|
|
52
|
+
protocol: @protocol, event: 'burst', source: 'iq',
|
|
53
|
+
burst_no: @burst_n, peak_dbfs: @peak.round(1),
|
|
54
|
+
floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
|
|
55
|
+
duration_ms: dur_ms, modulation: @modulation,
|
|
56
|
+
sample_rate: @rate.to_i
|
|
57
|
+
}.merge(@extra.except(:threshold))
|
|
58
|
+
# protocol-specific enrichment
|
|
59
|
+
msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
|
|
60
|
+
msg[:summary] = format(
|
|
61
|
+
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
62
|
+
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
63
|
+
)
|
|
64
|
+
yield msg if block_given?
|
|
65
|
+
end
|
|
66
|
+
i += hop
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
15
71
|
# Supported Method Parameters::
|
|
16
72
|
# PWN::SDR::Decoder::LoRa.decode(
|
|
17
73
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -19,23 +75,26 @@ module PWN
|
|
|
19
75
|
|
|
20
76
|
public_class_method def self.decode(opts = {})
|
|
21
77
|
freq_obj = opts[:freq_obj]
|
|
22
|
-
|
|
78
|
+
|
|
79
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 1_000_000).to_i
|
|
80
|
+
proto = 'LoRa'
|
|
81
|
+
demod = DemodIQ.new(
|
|
82
|
+
rate: rate, protocol: proto, modulation: 'CSS',
|
|
83
|
+
extra: { threshold: 8.0 }
|
|
84
|
+
)
|
|
85
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
23
86
|
freq_obj: freq_obj,
|
|
24
|
-
protocol:
|
|
25
|
-
|
|
87
|
+
protocol: proto,
|
|
88
|
+
sample_rate: rate,
|
|
89
|
+
source: opts[:source],
|
|
90
|
+
file: opts[:file],
|
|
91
|
+
demod: demod,
|
|
26
92
|
threshold: 8.0,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
t_sym_ms = b[:duration_ms] / 20.0
|
|
30
|
-
sf_est = t_sym_ms.positive? ? Math.log2(t_sym_ms * 125).round.clamp(6, 12) : nil
|
|
31
|
-
{ modulation: 'CSS', bw_khz_assumed: 125, sf_estimate: sf_est }.compact
|
|
32
|
-
}
|
|
93
|
+
note: 'CSS over 125–500 kHz — true-air I/Q path estimates SF from chirp duration; full dechirp via FFTW is layered when available.',
|
|
94
|
+
describe: proc { |b| { modulation: 'CSS', bw_khz_assumed: 125, sf_estimate: (begin; t = b[:duration_ms] / 20.0; t.positive? ? Math.log2(t * 125).round.clamp(6, 12) : nil; end) }.compact }
|
|
33
95
|
)
|
|
34
96
|
end
|
|
35
97
|
|
|
36
|
-
# Supported Method Parameters::
|
|
37
|
-
# PWN::SDR::Decoder::LoRa.parse_line(line: '{"model":"LoRa",...}')
|
|
38
|
-
|
|
39
98
|
public_class_method def self.parse_line(opts = {})
|
|
40
99
|
line = opts[:line].to_s
|
|
41
100
|
h = begin
|
|
@@ -48,10 +107,9 @@ module PWN
|
|
|
48
107
|
bits << "SF#{out[:sf]}" if out[:sf]
|
|
49
108
|
bits << "BW#{out[:bw]}" if out[:bw]
|
|
50
109
|
bits << "CR#{out[:cr]}" if out[:cr]
|
|
51
|
-
bits << "
|
|
52
|
-
bits <<
|
|
53
|
-
|
|
54
|
-
out[:summary] = "LoRa #{bits.join(' ')}".strip
|
|
110
|
+
bits << "RSSI=#{out[:rssi]}" if out[:rssi]
|
|
111
|
+
bits << out[:payload].to_s[0, 40] if out[:payload]
|
|
112
|
+
out[:summary] = bits.empty? ? line[0, 120] : bits.join(' ')
|
|
55
113
|
out
|
|
56
114
|
end
|
|
57
115
|
|
|
@@ -61,16 +119,14 @@ module PWN
|
|
|
61
119
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
62
120
|
end
|
|
63
121
|
|
|
64
|
-
# Display Usage for this Module
|
|
65
|
-
|
|
66
122
|
public_class_method def self.help
|
|
67
|
-
puts "USAGE (
|
|
123
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
68
124
|
#{self}.decode(
|
|
69
|
-
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
125
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
126
|
+
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
127
|
+
file: 'optional - .cu8/.cs16 capture'
|
|
70
128
|
)
|
|
71
129
|
|
|
72
|
-
#{self}.parse_line(line: '{\"model\":\"LoRa\",\"sf\":7,\"bw\":125,...}')
|
|
73
|
-
|
|
74
130
|
#{self}.authors
|
|
75
131
|
"
|
|
76
132
|
end
|
data/lib/pwn/sdr/decoder/lte.rb
CHANGED
|
@@ -3,13 +3,69 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# interpreted Ruby's real-time I/Q throughput. Native mode reports
|
|
10
|
-
# channel occupancy / power only. `parse_line` retained for offline
|
|
11
|
-
# cell_search / CellSearch text analysis.
|
|
6
|
+
# True-air + detector-fallback decoder for LTE.
|
|
7
|
+
# Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
|
|
8
|
+
# via Base.run_iq; degrades to Base.run_detector with no hardware.
|
|
12
9
|
module LTE
|
|
10
|
+
# Streaming I/Q energy/burst demod for Base.run_iq.
|
|
11
|
+
class DemodIQ
|
|
12
|
+
def initialize(rate:, protocol:, modulation:, extra: {})
|
|
13
|
+
@rate = rate.to_f
|
|
14
|
+
@protocol = protocol
|
|
15
|
+
@modulation = modulation
|
|
16
|
+
@extra = extra
|
|
17
|
+
@floor = nil
|
|
18
|
+
@in_burst = false
|
|
19
|
+
@burst_t0 = nil
|
|
20
|
+
@peak = -200.0
|
|
21
|
+
@burst_n = 0
|
|
22
|
+
@threshold = (extra[:threshold] || 8.0).to_f
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def feed_iq(samples, rate: nil, &)
|
|
26
|
+
@rate = rate.to_f if rate
|
|
27
|
+
m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
|
|
28
|
+
hop = [(@rate / 1000.0).round, 1].max
|
|
29
|
+
i = 0
|
|
30
|
+
while i < m2.length
|
|
31
|
+
win = m2[i, hop]
|
|
32
|
+
break if win.nil? || win.empty?
|
|
33
|
+
|
|
34
|
+
ms = win.sum / win.length
|
|
35
|
+
lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
|
|
36
|
+
@floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
|
|
37
|
+
delta = lvl - @floor
|
|
38
|
+
if delta >= @threshold
|
|
39
|
+
unless @in_burst
|
|
40
|
+
@in_burst = true
|
|
41
|
+
@burst_t0 = Time.now
|
|
42
|
+
@peak = lvl
|
|
43
|
+
end
|
|
44
|
+
@peak = lvl if lvl > @peak
|
|
45
|
+
elsif @in_burst
|
|
46
|
+
@in_burst = false
|
|
47
|
+
@burst_n += 1
|
|
48
|
+
dur_ms = ((Time.now - @burst_t0) * 1000).round
|
|
49
|
+
msg = {
|
|
50
|
+
protocol: @protocol, event: 'burst', source: 'iq',
|
|
51
|
+
burst_no: @burst_n, peak_dbfs: @peak.round(1),
|
|
52
|
+
floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
|
|
53
|
+
duration_ms: dur_ms, modulation: @modulation,
|
|
54
|
+
sample_rate: @rate.to_i
|
|
55
|
+
}.merge(@extra.except(:threshold))
|
|
56
|
+
# protocol-specific enrichment
|
|
57
|
+
msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
|
|
58
|
+
msg[:summary] = format(
|
|
59
|
+
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
60
|
+
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
61
|
+
)
|
|
62
|
+
yield msg if block_given?
|
|
63
|
+
end
|
|
64
|
+
i += hop
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
13
69
|
# Supported Method Parameters::
|
|
14
70
|
# PWN::SDR::Decoder::LTE.decode(
|
|
15
71
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -17,18 +73,26 @@ module PWN
|
|
|
17
73
|
|
|
18
74
|
public_class_method def self.decode(opts = {})
|
|
19
75
|
freq_obj = opts[:freq_obj]
|
|
20
|
-
|
|
76
|
+
|
|
77
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
|
|
78
|
+
proto = 'LTE'
|
|
79
|
+
demod = DemodIQ.new(
|
|
80
|
+
rate: rate, protocol: proto, modulation: 'OFDMA',
|
|
81
|
+
extra: { threshold: 4.0 }
|
|
82
|
+
)
|
|
83
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
21
84
|
freq_obj: freq_obj,
|
|
22
|
-
protocol:
|
|
23
|
-
|
|
85
|
+
protocol: proto,
|
|
86
|
+
sample_rate: rate,
|
|
87
|
+
source: opts[:source],
|
|
88
|
+
file: opts[:file],
|
|
89
|
+
demod: demod,
|
|
24
90
|
threshold: 4.0,
|
|
91
|
+
note: 'OFDMA 1.4–20 MHz — true-air I/Q path reports channel occupancy; PSS/SSS cell search layered on FFTW when available.',
|
|
25
92
|
describe: proc { |_b| { modulation: 'OFDMA', subcarrier_khz: 15 } }
|
|
26
93
|
)
|
|
27
94
|
end
|
|
28
95
|
|
|
29
|
-
# Supported Method Parameters::
|
|
30
|
-
# PWN::SDR::Decoder::LTE.parse_line(line: 'Found CELL ... PCI: 123 ...')
|
|
31
|
-
|
|
32
96
|
public_class_method def self.parse_line(opts = {})
|
|
33
97
|
line = opts[:line].to_s
|
|
34
98
|
out = { protocol: 'LTE' }
|
|
@@ -48,16 +112,14 @@ module PWN
|
|
|
48
112
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
49
113
|
end
|
|
50
114
|
|
|
51
|
-
# Display Usage for this Module
|
|
52
|
-
|
|
53
115
|
public_class_method def self.help
|
|
54
|
-
puts "USAGE (
|
|
116
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
55
117
|
#{self}.decode(
|
|
56
|
-
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
118
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
119
|
+
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
120
|
+
file: 'optional - .cu8/.cs16 capture'
|
|
57
121
|
)
|
|
58
122
|
|
|
59
|
-
#{self}.parse_line(line: 'Found CELL 739.0 MHz, EARFCN=5110, PCI=123, 50 PRB, -85.2 dBm')
|
|
60
|
-
|
|
61
123
|
#{self}.authors
|
|
62
124
|
"
|
|
63
125
|
end
|
|
@@ -97,11 +97,28 @@ module PWN
|
|
|
97
97
|
|
|
98
98
|
public_class_method def self.decode(opts = {})
|
|
99
99
|
freq_obj = opts[:freq_obj]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
# Prefer true-air I/Q (FM-demod → existing audio demod) when the
|
|
101
|
+
# operator asks for a source/file or sets freq_obj[:iq_source].
|
|
102
|
+
# Otherwise keep the GQRX 48 kHz UDP audio path (run_native).
|
|
103
|
+
want_iq = opts[:source] || opts[:file] || freq_obj[:iq_source] || freq_obj[:iq_file]
|
|
104
|
+
if want_iq
|
|
105
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
106
|
+
freq_obj: freq_obj,
|
|
107
|
+
protocol: 'MORSE-CW',
|
|
108
|
+
demod: Demod.new,
|
|
109
|
+
sample_rate: (opts[:sample_rate] || freq_obj[:iq_rate] || 48_000).to_i,
|
|
110
|
+
source: opts[:source],
|
|
111
|
+
file: opts[:file],
|
|
112
|
+
fm_demod: true,
|
|
113
|
+
note: 'MORSE-CW true-air: FM-demod I/Q then native bit recovery; falls back to detector without SDR hardware.'
|
|
114
|
+
)
|
|
115
|
+
else
|
|
116
|
+
PWN::SDR::Decoder::Base.run_native(
|
|
117
|
+
freq_obj: freq_obj,
|
|
118
|
+
protocol: 'MORSE-CW',
|
|
119
|
+
demod: Demod.new
|
|
120
|
+
)
|
|
121
|
+
end
|
|
105
122
|
end
|
|
106
123
|
|
|
107
124
|
# Supported Method Parameters::
|
|
@@ -122,7 +139,7 @@ module PWN
|
|
|
122
139
|
# Display Usage for this Module
|
|
123
140
|
|
|
124
141
|
public_class_method def self.help
|
|
125
|
-
puts "USAGE (
|
|
142
|
+
puts "USAGE (true-air I/Q + GQRX-audio native paths, no external binaries):
|
|
126
143
|
#{self}.decode(
|
|
127
144
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
128
145
|
)
|
data/lib/pwn/sdr/decoder/p25.rb
CHANGED
|
@@ -3,15 +3,69 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# vocoder. Full symbol recovery, deinterleave, FEC and voice decode
|
|
10
|
-
# is out of scope for a portable pure-Ruby implementation; this
|
|
11
|
-
# module instead characterises C4FM keying activity (talkgroup key-
|
|
12
|
-
# ups, control-channel duty cycle) natively via Base.run_detector.
|
|
13
|
-
# `parse_line` is retained for offline dsd-format log analysis.
|
|
6
|
+
# True-air + detector-fallback decoder for P25.
|
|
7
|
+
# Prefers PWN::FFI I/Q (RTL-SDR / ADALM-Pluto / HackRF / capture file)
|
|
8
|
+
# via Base.run_iq; degrades to Base.run_detector with no hardware.
|
|
14
9
|
module P25
|
|
10
|
+
# Streaming I/Q energy/burst demod for Base.run_iq.
|
|
11
|
+
class DemodIQ
|
|
12
|
+
def initialize(rate:, protocol:, modulation:, extra: {})
|
|
13
|
+
@rate = rate.to_f
|
|
14
|
+
@protocol = protocol
|
|
15
|
+
@modulation = modulation
|
|
16
|
+
@extra = extra
|
|
17
|
+
@floor = nil
|
|
18
|
+
@in_burst = false
|
|
19
|
+
@burst_t0 = nil
|
|
20
|
+
@peak = -200.0
|
|
21
|
+
@burst_n = 0
|
|
22
|
+
@threshold = (extra[:threshold] || 8.0).to_f
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def feed_iq(samples, rate: nil, &)
|
|
26
|
+
@rate = rate.to_f if rate
|
|
27
|
+
m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
|
|
28
|
+
hop = [(@rate / 1000.0).round, 1].max
|
|
29
|
+
i = 0
|
|
30
|
+
while i < m2.length
|
|
31
|
+
win = m2[i, hop]
|
|
32
|
+
break if win.nil? || win.empty?
|
|
33
|
+
|
|
34
|
+
ms = win.sum / win.length
|
|
35
|
+
lvl = ms.positive? ? (10.0 * Math.log10(ms)) : -120.0
|
|
36
|
+
@floor = @floor.nil? ? lvl : ((@floor * 0.98) + (lvl * 0.02))
|
|
37
|
+
delta = lvl - @floor
|
|
38
|
+
if delta >= @threshold
|
|
39
|
+
unless @in_burst
|
|
40
|
+
@in_burst = true
|
|
41
|
+
@burst_t0 = Time.now
|
|
42
|
+
@peak = lvl
|
|
43
|
+
end
|
|
44
|
+
@peak = lvl if lvl > @peak
|
|
45
|
+
elsif @in_burst
|
|
46
|
+
@in_burst = false
|
|
47
|
+
@burst_n += 1
|
|
48
|
+
dur_ms = ((Time.now - @burst_t0) * 1000).round
|
|
49
|
+
msg = {
|
|
50
|
+
protocol: @protocol, event: 'burst', source: 'iq',
|
|
51
|
+
burst_no: @burst_n, peak_dbfs: @peak.round(1),
|
|
52
|
+
floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
|
|
53
|
+
duration_ms: dur_ms, modulation: @modulation,
|
|
54
|
+
sample_rate: @rate.to_i
|
|
55
|
+
}.merge(@extra.except(:threshold))
|
|
56
|
+
# protocol-specific enrichment
|
|
57
|
+
msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
|
|
58
|
+
msg[:summary] = format(
|
|
59
|
+
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
60
|
+
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
61
|
+
)
|
|
62
|
+
yield msg if block_given?
|
|
63
|
+
end
|
|
64
|
+
i += hop
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
15
69
|
# Supported Method Parameters::
|
|
16
70
|
# PWN::SDR::Decoder::P25.decode(
|
|
17
71
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -19,33 +73,40 @@ module PWN
|
|
|
19
73
|
|
|
20
74
|
public_class_method def self.decode(opts = {})
|
|
21
75
|
freq_obj = opts[:freq_obj]
|
|
22
|
-
|
|
76
|
+
|
|
77
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 480_000).to_i
|
|
78
|
+
proto = 'P25'
|
|
79
|
+
demod = DemodIQ.new(
|
|
80
|
+
rate: rate, protocol: proto, modulation: 'C4FM',
|
|
81
|
+
extra: { threshold: 6.0 }
|
|
82
|
+
)
|
|
83
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
23
84
|
freq_obj: freq_obj,
|
|
24
|
-
protocol:
|
|
25
|
-
|
|
85
|
+
protocol: proto,
|
|
86
|
+
sample_rate: rate,
|
|
87
|
+
source: opts[:source],
|
|
88
|
+
file: opts[:file],
|
|
89
|
+
demod: demod,
|
|
26
90
|
threshold: 6.0,
|
|
91
|
+
note: 'C4FM 4-FSK + trellis — true-air I/Q path reports key-up bursts; full NAC/TG decode layered on Liquid when available.',
|
|
27
92
|
describe: proc { |b|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
93
|
+
{ modulation: 'C4FM', classification: (if b[:duration_ms] > 1500
|
|
94
|
+
'voice-superframe'
|
|
95
|
+
else
|
|
96
|
+
(b[:duration_ms] > 150 ? 'LDU/HDR' : 'TSBK/control')
|
|
97
|
+
end) }
|
|
33
98
|
}
|
|
34
99
|
)
|
|
35
100
|
end
|
|
36
101
|
|
|
37
|
-
# Supported Method Parameters::
|
|
38
|
-
# PWN::SDR::Decoder::P25.parse_line(line: 'Sync: +P25p1 NAC: 293 src: 1234 tg: 5678')
|
|
39
|
-
|
|
40
102
|
public_class_method def self.parse_line(opts = {})
|
|
41
103
|
line = opts[:line].to_s
|
|
42
104
|
out = { protocol: 'P25' }
|
|
43
|
-
out[:nac]
|
|
44
|
-
out[:
|
|
45
|
-
out[:
|
|
46
|
-
out[:
|
|
47
|
-
out[:
|
|
48
|
-
out[:summary] = "P25 NAC=#{out[:nac]} TG=#{out[:talkgroup]} RID=#{out[:radio_id]}".squeeze(' ')
|
|
105
|
+
out[:nac] = ::Regexp.last_match(1) if line =~ /NAC[:= ]+([0-9A-Fa-f]+)/
|
|
106
|
+
out[:tg] = ::Regexp.last_match(1) if line =~ /(?:TG|talkgroup)[:= ]+(\d+)/i
|
|
107
|
+
out[:rid] = ::Regexp.last_match(1) if line =~ /(?:RID|src|source)[:= ]+(\d+)/i
|
|
108
|
+
out[:sys] = ::Regexp.last_match(1) if line =~ /sys(?:tem)?[:= ]+(\w+)/i
|
|
109
|
+
out[:summary] = "P25 NAC=#{out[:nac]} TG=#{out[:tg]} RID=#{out[:rid]}"
|
|
49
110
|
out.compact
|
|
50
111
|
end
|
|
51
112
|
|
|
@@ -55,16 +116,14 @@ module PWN
|
|
|
55
116
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
56
117
|
end
|
|
57
118
|
|
|
58
|
-
# Display Usage for this Module
|
|
59
|
-
|
|
60
119
|
public_class_method def self.help
|
|
61
|
-
puts "USAGE (
|
|
120
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
62
121
|
#{self}.decode(
|
|
63
|
-
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
122
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
123
|
+
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
124
|
+
file: 'optional - .cu8/.cs16 capture'
|
|
64
125
|
)
|
|
65
126
|
|
|
66
|
-
#{self}.parse_line(line: 'Sync: +P25p1 NAC: 293 src: 1234 tg: 5678')
|
|
67
|
-
|
|
68
127
|
#{self}.authors
|
|
69
128
|
"
|
|
70
129
|
end
|
|
@@ -28,11 +28,28 @@ module PWN
|
|
|
28
28
|
|
|
29
29
|
public_class_method def self.decode(opts = {})
|
|
30
30
|
freq_obj = opts[:freq_obj]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
# Prefer true-air I/Q (FM-demod → existing audio demod) when the
|
|
32
|
+
# operator asks for a source/file or sets freq_obj[:iq_source].
|
|
33
|
+
# Otherwise keep the GQRX 48 kHz UDP audio path (run_native).
|
|
34
|
+
want_iq = opts[:source] || opts[:file] || freq_obj[:iq_source] || freq_obj[:iq_file]
|
|
35
|
+
if want_iq
|
|
36
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
37
|
+
freq_obj: freq_obj,
|
|
38
|
+
protocol: 'PAGER',
|
|
39
|
+
demod: Demod.new,
|
|
40
|
+
sample_rate: (opts[:sample_rate] || freq_obj[:iq_rate] || 240_000).to_i,
|
|
41
|
+
source: opts[:source],
|
|
42
|
+
file: opts[:file],
|
|
43
|
+
fm_demod: true,
|
|
44
|
+
note: 'PAGER true-air: FM-demod I/Q then native bit recovery; falls back to detector without SDR hardware.'
|
|
45
|
+
)
|
|
46
|
+
else
|
|
47
|
+
PWN::SDR::Decoder::Base.run_native(
|
|
48
|
+
freq_obj: freq_obj,
|
|
49
|
+
protocol: 'PAGER',
|
|
50
|
+
demod: Demod.new
|
|
51
|
+
)
|
|
52
|
+
end
|
|
36
53
|
end
|
|
37
54
|
|
|
38
55
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
@@ -44,7 +61,7 @@ module PWN
|
|
|
44
61
|
# Display Usage for this Module
|
|
45
62
|
|
|
46
63
|
public_class_method def self.help
|
|
47
|
-
puts "USAGE (
|
|
64
|
+
puts "USAGE (true-air I/Q + GQRX-audio native paths, no external binaries):
|
|
48
65
|
#{self}.decode(
|
|
49
66
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
50
67
|
)
|