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
data/lib/pwn/sdr/decoder/dsp.rb
CHANGED
|
@@ -3,18 +3,33 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
6
|
+
# DSP primitives shared by every PWN::SDR::Decoder::* module.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# any external `sox` / `multimon-ng` / `minimodem` / etc dependency.
|
|
8
|
+
# Default path is pure Ruby operating on Array<Float> samples
|
|
9
|
+
# normalised to -1.0..1.0 (48 kHz s16le mono from GQRX UDP — no
|
|
10
|
+
# `sox` / `multimon-ng` / `minimodem` dependency).
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
12
|
+
# When the matching system library is present the hot paths
|
|
13
|
+
# transparently accelerate via PWN::FFI::{Volk,Liquid,FFTW}:
|
|
14
|
+
#
|
|
15
|
+
# unpack_s16le → PWN::FFI::Volk (SIMD s16→f32 convert)
|
|
16
|
+
# unpack_cs16le → pure / Volk path (interleaved I/Q s16 → f32)
|
|
17
|
+
# unpack_cu8 → pure path (RTL-SDR u8 I/Q → f32)
|
|
18
|
+
# resample → PWN::FFI::Liquid (msresamp multi-stage)
|
|
19
|
+
# dc_block → PWN::FFI::Liquid (firfilt DC blocker)
|
|
20
|
+
# rms_dbfs → PWN::FFI::Volk (accumulate of squares)
|
|
21
|
+
# mag_sq / fm_demod_iq → true-air I/Q paths for Base.run_iq
|
|
22
|
+
#
|
|
23
|
+
# Each accelerated method falls back to the pure-Ruby body when
|
|
24
|
+
# the backend is missing or raises, so decoders never require a
|
|
25
|
+
# native library at install time. Force pure Ruby for testing with
|
|
26
|
+
# PWN::SDR::Decoder::DSP.native = false
|
|
17
27
|
module DSP
|
|
28
|
+
@native = true
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
attr_accessor :native
|
|
32
|
+
end
|
|
18
33
|
TWO_PI = Math::PI * 2
|
|
19
34
|
|
|
20
35
|
# ITA2 / Baudot 5-bit → ASCII (LTRS + FIGS shift tables). Index = code.
|
|
@@ -54,6 +69,13 @@ module PWN
|
|
|
54
69
|
|
|
55
70
|
public_class_method def self.unpack_s16le(opts = {})
|
|
56
71
|
data = opts[:data].to_s
|
|
72
|
+
if native && PWN::FFI.available?(mod: :Volk)
|
|
73
|
+
begin
|
|
74
|
+
return PWN::FFI::Volk.unpack_s16le(data: data)
|
|
75
|
+
rescue StandardError
|
|
76
|
+
# fall through to pure Ruby
|
|
77
|
+
end
|
|
78
|
+
end
|
|
57
79
|
norm = 1.0 / 32_768.0
|
|
58
80
|
data.unpack('s<*').map { |v| v * norm }
|
|
59
81
|
end
|
|
@@ -71,6 +93,18 @@ module PWN
|
|
|
71
93
|
dst_rate = opts[:dst_rate].to_f
|
|
72
94
|
return samples.dup if (src_rate - dst_rate).abs < 1e-6
|
|
73
95
|
|
|
96
|
+
if native && PWN::FFI.available?(mod: :Liquid)
|
|
97
|
+
begin
|
|
98
|
+
# liquid rate = output/input
|
|
99
|
+
return PWN::FFI::Liquid.resample(
|
|
100
|
+
samples: samples,
|
|
101
|
+
rate: dst_rate / src_rate
|
|
102
|
+
)
|
|
103
|
+
rescue StandardError
|
|
104
|
+
# fall through to pure Ruby
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
74
108
|
ratio = src_rate / dst_rate
|
|
75
109
|
out_len = (samples.length / ratio).floor
|
|
76
110
|
out = Array.new(out_len)
|
|
@@ -145,7 +179,23 @@ module PWN
|
|
|
145
179
|
|
|
146
180
|
public_class_method def self.dc_block(opts = {})
|
|
147
181
|
samples = opts[:samples]
|
|
148
|
-
|
|
182
|
+
# Prefer liquid FIR DC blocker when caller asks for it (m:) or when
|
|
183
|
+
# alpha is the default AND liquid is available — otherwise keep the
|
|
184
|
+
# classic single-pole IIR so call sites that pass a custom alpha
|
|
185
|
+
# stay bit-identical to pure Ruby.
|
|
186
|
+
if native && opts[:m] && PWN::FFI.available?(mod: :Liquid)
|
|
187
|
+
begin
|
|
188
|
+
return PWN::FFI::Liquid.dc_block(
|
|
189
|
+
samples: samples,
|
|
190
|
+
m: opts[:m],
|
|
191
|
+
as_db: opts[:as_db] || 60.0
|
|
192
|
+
)
|
|
193
|
+
rescue StandardError
|
|
194
|
+
# fall through
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
alpha = (opts[:alpha] || 0.995).to_f
|
|
149
199
|
y_prev = 0.0
|
|
150
200
|
x_prev = 0.0
|
|
151
201
|
samples.map do |x|
|
|
@@ -354,12 +404,169 @@ module PWN
|
|
|
354
404
|
samples = opts[:samples]
|
|
355
405
|
return -120.0 if samples.nil? || samples.empty?
|
|
356
406
|
|
|
407
|
+
if native && PWN::FFI.available?(mod: :Volk) && samples.length >= 64
|
|
408
|
+
begin
|
|
409
|
+
# Σ x² via volk: scale-square in Ruby is still the bottleneck for
|
|
410
|
+
# tiny buffers so keep the pure-Ruby path under 64 samples.
|
|
411
|
+
sq = samples.map { |v| v * v }
|
|
412
|
+
ms = PWN::FFI::Volk.accumulate(samples: sq) / samples.length
|
|
413
|
+
return -120.0 if ms <= 0
|
|
414
|
+
|
|
415
|
+
return 10.0 * Math.log10(ms)
|
|
416
|
+
rescue StandardError
|
|
417
|
+
# fall through
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
357
421
|
ms = samples.sum { |v| v * v } / samples.length
|
|
358
422
|
return -120.0 if ms <= 0
|
|
359
423
|
|
|
360
424
|
10.0 * Math.log10(ms)
|
|
361
425
|
end
|
|
362
426
|
|
|
427
|
+
# ── True-air I/Q primitives (used by Base.run_iq) ─────────────────
|
|
428
|
+
|
|
429
|
+
# Supported Method Parameters::
|
|
430
|
+
# iq = PWN::SDR::Decoder::DSP.unpack_cs16le(
|
|
431
|
+
# data: 'required - raw String of interleaved little-endian s16 I/Q'
|
|
432
|
+
# )
|
|
433
|
+
# Returns interleaved Array<Float> [I0,Q0,I1,Q1,…] normalised ±1.0.
|
|
434
|
+
|
|
435
|
+
public_class_method def self.unpack_cs16le(opts = {})
|
|
436
|
+
data = opts[:data].to_s
|
|
437
|
+
if native && PWN::FFI.available?(mod: :Volk)
|
|
438
|
+
begin
|
|
439
|
+
return PWN::FFI::Volk.unpack_s16le(data: data)
|
|
440
|
+
rescue StandardError
|
|
441
|
+
# fall through
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
norm = 1.0 / 32_768.0
|
|
445
|
+
data.unpack('s<*').map { |v| v * norm }
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Supported Method Parameters::
|
|
449
|
+
# iq = PWN::SDR::Decoder::DSP.unpack_cu8(
|
|
450
|
+
# data: 'required - raw String of interleaved u8 I/Q (RTL-SDR)'
|
|
451
|
+
# )
|
|
452
|
+
# Returns interleaved Array<Float> [I0,Q0,…] centred & normalised ±1.0.
|
|
453
|
+
|
|
454
|
+
public_class_method def self.unpack_cu8(opts = {})
|
|
455
|
+
data = opts[:data].to_s
|
|
456
|
+
norm = 1.0 / 128.0
|
|
457
|
+
data.unpack('C*').map { |v| (v - 127.5) * norm }
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Supported Method Parameters::
|
|
461
|
+
# m2 = PWN::SDR::Decoder::DSP.mag_sq(
|
|
462
|
+
# iq: 'required - interleaved Array<Float> [I0,Q0,I1,Q1,…]'
|
|
463
|
+
# )
|
|
464
|
+
# Returns Array<Float> of I²+Q² per sample (length = iq.length/2).
|
|
465
|
+
|
|
466
|
+
public_class_method def self.mag_sq(opts = {})
|
|
467
|
+
iq = opts[:iq]
|
|
468
|
+
n = iq.length / 2
|
|
469
|
+
out = Array.new(n)
|
|
470
|
+
i = 0
|
|
471
|
+
while i < n
|
|
472
|
+
re = iq[i * 2].to_f
|
|
473
|
+
im = iq[(i * 2) + 1].to_f
|
|
474
|
+
out[i] = (re * re) + (im * im)
|
|
475
|
+
i += 1
|
|
476
|
+
end
|
|
477
|
+
out
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Supported Method Parameters::
|
|
481
|
+
# audio = PWN::SDR::Decoder::DSP.fm_demod_iq(
|
|
482
|
+
# iq: 'required - interleaved Array<Float> [I0,Q0,…]',
|
|
483
|
+
# kf: 'optional - modulation index scale (default 1.0)'
|
|
484
|
+
# )
|
|
485
|
+
# Polar-discriminant FM demod. Prefers PWN::FFI::Liquid.freq_demod
|
|
486
|
+
# when available; otherwise atan2 difference of consecutive samples.
|
|
487
|
+
|
|
488
|
+
public_class_method def self.fm_demod_iq(opts = {})
|
|
489
|
+
iq = opts[:iq]
|
|
490
|
+
kf = (opts[:kf] || 1.0).to_f
|
|
491
|
+
n = iq.length / 2
|
|
492
|
+
return [] if n < 2
|
|
493
|
+
|
|
494
|
+
if native && PWN::FFI.available?(mod: :Liquid)
|
|
495
|
+
begin
|
|
496
|
+
return PWN::FFI::Liquid.freq_demod(iq: iq, kf: kf)
|
|
497
|
+
rescue StandardError
|
|
498
|
+
# fall through
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
out = Array.new(n - 1)
|
|
503
|
+
prev_re = iq[0].to_f
|
|
504
|
+
prev_im = iq[1].to_f
|
|
505
|
+
i = 1
|
|
506
|
+
while i < n
|
|
507
|
+
re = iq[i * 2].to_f
|
|
508
|
+
im = iq[(i * 2) + 1].to_f
|
|
509
|
+
# arg(z * conj(z_prev)) = atan2 cross/dot
|
|
510
|
+
dot = (re * prev_re) + (im * prev_im)
|
|
511
|
+
cross = (im * prev_re) - (re * prev_im)
|
|
512
|
+
out[i - 1] = Math.atan2(cross, dot) * kf
|
|
513
|
+
prev_re = re
|
|
514
|
+
prev_im = im
|
|
515
|
+
i += 1
|
|
516
|
+
end
|
|
517
|
+
out
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Supported Method Parameters::
|
|
521
|
+
# power = PWN::SDR::Decoder::DSP.iq_rms_dbfs(
|
|
522
|
+
# iq: 'required - interleaved Array<Float> [I0,Q0,…]'
|
|
523
|
+
# )
|
|
524
|
+
|
|
525
|
+
public_class_method def self.iq_rms_dbfs(opts = {})
|
|
526
|
+
m2 = mag_sq(iq: opts[:iq])
|
|
527
|
+
return -120.0 if m2.empty?
|
|
528
|
+
|
|
529
|
+
ms = m2.sum / m2.length
|
|
530
|
+
return -120.0 if ms <= 0
|
|
531
|
+
|
|
532
|
+
10.0 * Math.log10(ms)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Correlate a real template against magnitude-squared energy for
|
|
536
|
+
# PPM / OOK preambles. Returns best lag + score.
|
|
537
|
+
# Supported Method Parameters::
|
|
538
|
+
# hit = PWN::SDR::Decoder::DSP.correlate(
|
|
539
|
+
# samples: 'required - Array<Float>',
|
|
540
|
+
# template: 'required - Array<Float> (same units)'
|
|
541
|
+
# )
|
|
542
|
+
# → { lag:, score: } or nil
|
|
543
|
+
|
|
544
|
+
public_class_method def self.correlate(opts = {})
|
|
545
|
+
samples = opts[:samples]
|
|
546
|
+
template = opts[:template]
|
|
547
|
+
tlen = template.length
|
|
548
|
+
return nil if tlen.zero? || samples.length < tlen
|
|
549
|
+
|
|
550
|
+
best_lag = 0
|
|
551
|
+
best_sc = -Float::INFINITY
|
|
552
|
+
upto = samples.length - tlen
|
|
553
|
+
i = 0
|
|
554
|
+
while i <= upto
|
|
555
|
+
sc = 0.0
|
|
556
|
+
j = 0
|
|
557
|
+
while j < tlen
|
|
558
|
+
sc += samples[i + j] * template[j]
|
|
559
|
+
j += 1
|
|
560
|
+
end
|
|
561
|
+
if sc > best_sc
|
|
562
|
+
best_sc = sc
|
|
563
|
+
best_lag = i
|
|
564
|
+
end
|
|
565
|
+
i += 1
|
|
566
|
+
end
|
|
567
|
+
{ lag: best_lag, score: best_sc }
|
|
568
|
+
end
|
|
569
|
+
|
|
363
570
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
364
571
|
|
|
365
572
|
public_class_method def self.authors
|
|
@@ -369,13 +576,19 @@ module PWN
|
|
|
369
576
|
# Display Usage for this Module
|
|
370
577
|
|
|
371
578
|
public_class_method def self.help
|
|
372
|
-
puts "USAGE (pure-Ruby DSP
|
|
373
|
-
#{self}.
|
|
374
|
-
#{self}.
|
|
579
|
+
puts "USAGE (pure-Ruby DSP + optional PWN::FFI acceleration):
|
|
580
|
+
#{self}.native = true|false # toggle VOLK/liquid/FFTW backends
|
|
581
|
+
#{self}.unpack_s16le(data: raw_bytes) # → Volk
|
|
582
|
+
#{self}.unpack_cs16le(data: raw_cs16) # I/Q s16 → f32
|
|
583
|
+
#{self}.unpack_cu8(data: raw_u8) # RTL-SDR u8 I/Q
|
|
584
|
+
#{self}.mag_sq(iq:) / #{self}.iq_rms_dbfs(iq:)
|
|
585
|
+
#{self}.fm_demod_iq(iq:, kf: 1.0) # → Liquid
|
|
586
|
+
#{self}.correlate(samples:, template:)
|
|
587
|
+
#{self}.resample(samples:, src_rate:, dst_rate:) # → Liquid
|
|
375
588
|
#{self}.goertzel(samples:, rate:, freq:)
|
|
376
589
|
#{self}.envelope(samples:, window: 32)
|
|
377
590
|
#{self}.envelope_signed(samples:, window: 8)
|
|
378
|
-
#{self}.dc_block(samples:, alpha: 0.995)
|
|
591
|
+
#{self}.dc_block(samples:, alpha: 0.995, m: nil) # → Liquid if m:
|
|
379
592
|
#{self}.nrz_slice(samples:, rate:, baud:, invert: false)
|
|
380
593
|
#{self}.fsk_slice(samples:, rate:, baud:, mark_hz:, space_hz:)
|
|
381
594
|
#{self}.find_sync(bits:, pattern:, width:, max_err: 0, from: 0)
|
|
@@ -383,9 +596,10 @@ module PWN
|
|
|
383
596
|
#{self}.even_parity_ok?(word:, width: 32)
|
|
384
597
|
#{self}.bch_31_21_syndrome(word:)
|
|
385
598
|
#{self}.baudot_decode(bits:)
|
|
386
|
-
#{self}.rms_dbfs(samples:)
|
|
599
|
+
#{self}.rms_dbfs(samples:) # → Volk (≥64)
|
|
387
600
|
|
|
388
601
|
Constants: MORSE_TABLE, BAUDOT_LTRS, BAUDOT_FIGS
|
|
602
|
+
Backends: PWN::FFI.backends # { Volk: true, Liquid: true, AdalmPluto: true, … }
|
|
389
603
|
|
|
390
604
|
#{self}.authors
|
|
391
605
|
"
|
data/lib/pwn/sdr/decoder/flex.rb
CHANGED
|
@@ -187,11 +187,28 @@ module PWN
|
|
|
187
187
|
|
|
188
188
|
public_class_method def self.decode(opts = {})
|
|
189
189
|
freq_obj = opts[:freq_obj]
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
# Prefer true-air I/Q (FM-demod → existing audio demod) when the
|
|
191
|
+
# operator asks for a source/file or sets freq_obj[:iq_source].
|
|
192
|
+
# Otherwise keep the GQRX 48 kHz UDP audio path (run_native).
|
|
193
|
+
want_iq = opts[:source] || opts[:file] || freq_obj[:iq_source] || freq_obj[:iq_file]
|
|
194
|
+
if want_iq
|
|
195
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
196
|
+
freq_obj: freq_obj,
|
|
197
|
+
protocol: 'FLEX',
|
|
198
|
+
demod: Demod.new,
|
|
199
|
+
sample_rate: (opts[:sample_rate] || freq_obj[:iq_rate] || 240_000).to_i,
|
|
200
|
+
source: opts[:source],
|
|
201
|
+
file: opts[:file],
|
|
202
|
+
fm_demod: true,
|
|
203
|
+
note: 'FLEX true-air: FM-demod I/Q then native bit recovery; falls back to detector without SDR hardware.'
|
|
204
|
+
)
|
|
205
|
+
else
|
|
206
|
+
PWN::SDR::Decoder::Base.run_native(
|
|
207
|
+
freq_obj: freq_obj,
|
|
208
|
+
protocol: 'FLEX',
|
|
209
|
+
demod: Demod.new
|
|
210
|
+
)
|
|
211
|
+
end
|
|
195
212
|
end
|
|
196
213
|
|
|
197
214
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
@@ -203,7 +220,7 @@ module PWN
|
|
|
203
220
|
# Display Usage for this Module
|
|
204
221
|
|
|
205
222
|
public_class_method def self.help
|
|
206
|
-
puts "USAGE (
|
|
223
|
+
puts "USAGE (true-air I/Q + GQRX-audio native paths, no external binaries):
|
|
207
224
|
#{self}.decode(
|
|
208
225
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
209
226
|
)
|
data/lib/pwn/sdr/decoder/gps.rb
CHANGED
|
@@ -3,13 +3,69 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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.
|
|
6
|
+
# True-air + detector-fallback decoder for GPS.
|
|
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 GPS
|
|
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::GPS.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_048_000).to_i
|
|
78
|
+
proto = 'GPS'
|
|
79
|
+
demod = DemodIQ.new(
|
|
80
|
+
rate: rate, protocol: proto, modulation: 'BPSK/DSSS',
|
|
81
|
+
extra: { threshold: 3.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: 3.0,
|
|
91
|
+
note: 'BPSK/DSSS 1.023 Mcps — true-air I/Q path reports composite L1 energy; PRN acquisition is layered on FFTW when available.',
|
|
25
92
|
describe: proc { |_b| { modulation: 'BPSK/DSSS', chip_rate: 1_023_000 } }
|
|
26
93
|
)
|
|
27
94
|
end
|
|
28
95
|
|
|
29
|
-
# Supported Method Parameters::
|
|
30
|
-
# PWN::SDR::Decoder::GPS.parse_line(line: 'Position at ... Lat = 32.7 [deg] ...')
|
|
31
|
-
|
|
32
96
|
public_class_method def self.parse_line(opts = {})
|
|
33
97
|
line = opts[:line].to_s
|
|
34
98
|
out = { protocol: 'GPS' }
|
|
@@ -55,16 +119,14 @@ module PWN
|
|
|
55
119
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
56
120
|
end
|
|
57
121
|
|
|
58
|
-
# Display Usage for this Module
|
|
59
|
-
|
|
60
122
|
public_class_method def self.help
|
|
61
|
-
puts "USAGE (
|
|
123
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
62
124
|
#{self}.decode(
|
|
63
|
-
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'
|
|
64
128
|
)
|
|
65
129
|
|
|
66
|
-
#{self}.parse_line(line: 'Position ... Lat = 32.7 Long = -97.1 Height = 210')
|
|
67
|
-
|
|
68
130
|
#{self}.authors
|
|
69
131
|
"
|
|
70
132
|
end
|
data/lib/pwn/sdr/decoder/gsm.rb
CHANGED
|
@@ -3,20 +3,66 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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.
|
|
6
|
+
# GSM (2G) true-air BCCH/CCCH activity decoder.
|
|
15
7
|
module GSM
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
# Streaming I/Q energy/burst demod for Base.run_iq.
|
|
9
|
+
class DemodIQ
|
|
10
|
+
def initialize(rate:, protocol:, modulation:, extra: {})
|
|
11
|
+
@rate = rate.to_f
|
|
12
|
+
@protocol = protocol
|
|
13
|
+
@modulation = modulation
|
|
14
|
+
@extra = extra
|
|
15
|
+
@floor = nil
|
|
16
|
+
@in_burst = false
|
|
17
|
+
@burst_t0 = nil
|
|
18
|
+
@peak = -200.0
|
|
19
|
+
@burst_n = 0
|
|
20
|
+
@threshold = (extra[:threshold] || 8.0).to_f
|
|
21
|
+
@carry = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def feed_iq(samples, rate: nil, &)
|
|
25
|
+
@rate = rate.to_f if rate
|
|
26
|
+
m2 = PWN::SDR::Decoder::DSP.mag_sq(iq: samples)
|
|
27
|
+
# process in ~1 ms hops
|
|
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)
|
|
56
|
+
msg[:summary] = format(
|
|
57
|
+
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
58
|
+
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
59
|
+
)
|
|
60
|
+
yield msg if block_given?
|
|
61
|
+
end
|
|
62
|
+
i += hop
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
20
66
|
|
|
21
67
|
# Supported Method Parameters::
|
|
22
68
|
# PWN::SDR::Decoder::GSM.decode(
|
|
@@ -25,20 +71,31 @@ module PWN
|
|
|
25
71
|
|
|
26
72
|
public_class_method def self.decode(opts = {})
|
|
27
73
|
freq_obj = opts[:freq_obj]
|
|
28
|
-
|
|
74
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 1_000_000).to_i
|
|
75
|
+
proto = 'GSM'
|
|
76
|
+
extra = {}
|
|
77
|
+
describe = proc { |b| { modulation: 'GMSK', symbol_rate: 270_833, tdma_frames: (b[:duration_ms] / 4.615).round, classification: (b[:duration_ms] / 4.615) > 10 ? 'BCCH/CCCH-continuous' : 'RACH/paging-burst' } }
|
|
78
|
+
demod = DemodIQ.new(
|
|
79
|
+
rate: rate, protocol: proto, modulation: 'GMSK',
|
|
80
|
+
extra: { threshold: 5.0 }.merge(extra)
|
|
81
|
+
)
|
|
82
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
29
83
|
freq_obj: freq_obj,
|
|
30
|
-
protocol:
|
|
31
|
-
|
|
84
|
+
protocol: proto,
|
|
85
|
+
sample_rate: rate,
|
|
86
|
+
source: opts[:source],
|
|
87
|
+
file: opts[:file],
|
|
88
|
+
demod: demod,
|
|
32
89
|
threshold: 5.0,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{ modulation: 'GMSK', symbol_rate: 270_833, tdma_frames: frames, classification: frames > 10 ? 'BCCH/CCCH-continuous' : 'RACH/paging-burst' }
|
|
36
|
-
}
|
|
90
|
+
note: '270.833 kbit/s GMSK — true-air path streams I/Q from RTL-SDR/Pluto and characterises TDMA burst duty; full Viterbi/BCCH SI decode is layered on Liquid when available.',
|
|
91
|
+
describe: describe
|
|
37
92
|
)
|
|
38
93
|
end
|
|
39
94
|
|
|
40
|
-
|
|
41
|
-
|
|
95
|
+
TSHARK_FIELDS = %w[
|
|
96
|
+
frame.time gsmtap.arfcn gsmtap.chan_type gsm_a.imsi gsm_a.tmsi
|
|
97
|
+
e212.mcc e212.mnc gsm_a.lac gsm_a.bssmap.cell_ci gsm_a.dtap.msg_rr_type
|
|
98
|
+
].freeze
|
|
42
99
|
|
|
43
100
|
public_class_method def self.parse_line(opts = {})
|
|
44
101
|
line = opts[:line].to_s
|
|
@@ -69,16 +126,14 @@ module PWN
|
|
|
69
126
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
70
127
|
end
|
|
71
128
|
|
|
72
|
-
# Display Usage for this Module
|
|
73
|
-
|
|
74
129
|
public_class_method def self.help
|
|
75
|
-
puts "USAGE (
|
|
130
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
76
131
|
#{self}.decode(
|
|
77
|
-
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
132
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
133
|
+
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
134
|
+
file: 'optional - .cu8/.cs16 capture'
|
|
78
135
|
)
|
|
79
136
|
|
|
80
|
-
#{self}.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
|
|
81
|
-
|
|
82
137
|
#{self}.authors
|
|
83
138
|
"
|
|
84
139
|
end
|