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.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Gemfile +1 -1
  4. data/README.md +7 -1
  5. data/bin/pwn_gqrx_scanner +78 -9
  6. data/documentation/Agent-Tool-Registry.md +3 -3
  7. data/documentation/Configuration.md +301 -47
  8. data/documentation/Cron.md +5 -3
  9. data/documentation/Diagrams.md +1 -1
  10. data/documentation/Extrospection.md +472 -26
  11. data/documentation/FFI.md +72 -8
  12. data/documentation/Home.md +4 -4
  13. data/documentation/How-PWN-Works.md +7 -3
  14. data/documentation/Mistakes.md +9 -0
  15. data/documentation/SDR.md +83 -10
  16. data/documentation/Skills-Memory-Learning.md +101 -11
  17. data/documentation/diagrams/agent-tool-registry.svg +56 -55
  18. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  19. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  20. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  21. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  22. data/documentation/diagrams/cron-scheduling.svg +23 -23
  23. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  24. data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -6
  25. data/documentation/diagrams/dot/hardware-hacking.dot +3 -1
  26. data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -2
  27. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +4 -2
  28. data/documentation/diagrams/dot/overall-pwn-architecture.dot +3 -3
  29. data/documentation/diagrams/dot/persistence-filesystem.dot +3 -3
  30. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
  31. data/documentation/diagrams/dot/sdr-radio-flow.dot +39 -16
  32. data/documentation/diagrams/driver-framework.svg +13 -13
  33. data/documentation/diagrams/extrospection-world-awareness.svg +383 -177
  34. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  35. data/documentation/diagrams/hardware-hacking.svg +84 -69
  36. data/documentation/diagrams/history-to-drivers.svg +18 -18
  37. data/documentation/diagrams/memory-skills-detailed.svg +137 -120
  38. data/documentation/diagrams/mistakes-negative-feedback.svg +152 -137
  39. data/documentation/diagrams/network-infra-testing.svg +27 -27
  40. data/documentation/diagrams/overall-pwn-architecture.svg +97 -97
  41. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  42. data/documentation/diagrams/persistence-filesystem.svg +100 -94
  43. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  44. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +254 -207
  45. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  46. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  47. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  48. data/documentation/diagrams/sdr-radio-flow.svg +223 -91
  49. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  50. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  51. data/documentation/diagrams/web-application-testing.svg +26 -26
  52. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  53. data/documentation/pwn-ai-Agent.md +16 -10
  54. data/documentation/pwn_silent_help_learn_demo.gif +0 -0
  55. data/lib/pwn/ai/agent/assembly.rb +1 -1
  56. data/lib/pwn/ai/agent/btc.rb +1 -1
  57. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  58. data/lib/pwn/ai/agent/extrospection.rb +2562 -57
  59. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  60. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  61. data/lib/pwn/ai/agent/learning.rb +16 -16
  62. data/lib/pwn/ai/agent/loop.rb +1 -1
  63. data/lib/pwn/ai/agent/{introspection.rb → reflect.rb} +12 -12
  64. data/lib/pwn/ai/agent/sast.rb +1 -1
  65. data/lib/pwn/ai/agent/tools/extrospection.rb +311 -8
  66. data/lib/pwn/ai/agent/tools/learning.rb +7 -7
  67. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  68. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  69. data/lib/pwn/ai/agent.rb +1 -1
  70. data/lib/pwn/ai/open_ai.rb +10 -6
  71. data/lib/pwn/config.rb +5 -4
  72. data/lib/pwn/ffi/adalm_pluto.rb +498 -0
  73. data/lib/pwn/ffi/fftw.rb +192 -0
  74. data/lib/pwn/ffi/hack_rf.rb +193 -0
  75. data/lib/pwn/ffi/liquid.rb +244 -0
  76. data/lib/pwn/ffi/rtl_sdr.rb +210 -0
  77. data/lib/pwn/ffi/soapy_sdr.rb +177 -0
  78. data/lib/pwn/ffi/stdio.rb +9 -1
  79. data/lib/pwn/ffi/volk.rb +241 -0
  80. data/lib/pwn/ffi.rb +45 -2
  81. data/lib/pwn/plugins/burp_suite.rb +2 -2
  82. data/lib/pwn/sast/pom_version.rb +2 -2
  83. data/lib/pwn/sast/test_case_engine.rb +1 -1
  84. data/lib/pwn/sdr/decoder/adsb.rb +201 -12
  85. data/lib/pwn/sdr/decoder/apt.rb +19 -5
  86. data/lib/pwn/sdr/decoder/base.rb +314 -1
  87. data/lib/pwn/sdr/decoder/bluetooth.rb +91 -18
  88. data/lib/pwn/sdr/decoder/dect.rb +81 -23
  89. data/lib/pwn/sdr/decoder/dsp.rb +229 -15
  90. data/lib/pwn/sdr/decoder/flex.rb +23 -6
  91. data/lib/pwn/sdr/decoder/gps.rb +80 -18
  92. data/lib/pwn/sdr/decoder/gsm.rb +83 -28
  93. data/lib/pwn/sdr/decoder/iridium.rb +80 -18
  94. data/lib/pwn/sdr/decoder/lora.rb +84 -28
  95. data/lib/pwn/sdr/decoder/lte.rb +80 -18
  96. data/lib/pwn/sdr/decoder/morse.rb +23 -6
  97. data/lib/pwn/sdr/decoder/p25.rb +90 -31
  98. data/lib/pwn/sdr/decoder/pager.rb +23 -6
  99. data/lib/pwn/sdr/decoder/pocsag.rb +23 -6
  100. data/lib/pwn/sdr/decoder/rds.rb +219 -52
  101. data/lib/pwn/sdr/decoder/rfid.rb +81 -27
  102. data/lib/pwn/sdr/decoder/rtl433.rb +85 -27
  103. data/lib/pwn/sdr/decoder/rtty.rb +23 -6
  104. data/lib/pwn/sdr/decoder/wifi.rb +86 -32
  105. data/lib/pwn/sdr/decoder/zigbee.rb +88 -35
  106. data/lib/pwn/sdr/decoder.rb +6 -5
  107. data/lib/pwn/sdr/frequency_allocation.rb +124 -81
  108. data/lib/pwn/sdr/gqrx.rb +1513 -165
  109. data/lib/pwn/version.rb +1 -1
  110. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +96 -0
  111. data/spec/lib/pwn/ai/agent/{introspection_spec.rb → reflect_spec.rb} +3 -3
  112. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +8 -24
  113. data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +37 -0
  114. data/spec/lib/pwn/ffi/fftw_spec.rb +26 -0
  115. data/spec/lib/pwn/ffi/hack_rf_spec.rb +25 -0
  116. data/spec/lib/pwn/ffi/liquid_spec.rb +42 -0
  117. data/spec/lib/pwn/ffi/rtl_sdr_spec.rb +24 -0
  118. data/spec/lib/pwn/ffi/soapy_sdr_spec.rb +25 -0
  119. data/spec/lib/pwn/ffi/volk_spec.rb +36 -0
  120. data/spec/lib/pwn/sdr/decoder/base_iq_spec.rb +65 -0
  121. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +50 -4
  122. data/spec/lib/pwn/sdr/decoder/rds_spec.rb +45 -4
  123. data/third_party/pwn_rdoc.jsonl +279 -30
  124. metadata +21 -5
@@ -3,18 +3,33 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # Pure-Ruby DSP primitives shared by every PWN::SDR::Decoder::* module.
6
+ # DSP primitives shared by every PWN::SDR::Decoder::* module.
7
7
  #
8
- # Nothing in here shells out. Everything operates on plain Ruby Arrays
9
- # of Float samples (normalised -1.0..1.0), so decoders can consume the
10
- # 48 kHz s16le mono audio that PWN::SDR::GQRX streams over UDP without
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
- # These are intentionally simple, readable, allocation-heavy reference
14
- # implementations good enough for ≤48 kHz audio-rate work on a modern
15
- # CPU. For MHz-rate raw I/Q you would want SIMD/native code, which is
16
- # out of scope for a pure-Ruby decoder namespace.
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
- alpha = (opts[:alpha] || 0.995).to_f
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 primitives no external binaries):
373
- #{self}.unpack_s16le(data: raw_bytes)
374
- #{self}.resample(samples:, src_rate:, dst_rate:)
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
  "
@@ -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
- PWN::SDR::Decoder::Base.run_native(
191
- freq_obj: freq_obj,
192
- protocol: 'FLEX',
193
- demod: Demod.new
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 (ruby-native, no external binaries):
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
  )
@@ -3,13 +3,69 @@
3
3
  module PWN
4
4
  module SDR
5
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.
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
- PWN::SDR::Decoder::Base.run_detector(
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: 'GPS',
23
- note: 'BPSK/DSSS 1.023 Mcps below thermal noise — native mode reports composite L1 energy only.',
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 (ruby-native detector, no external binaries):
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
@@ -3,20 +3,66 @@
3
3
  module PWN
4
4
  module SDR
5
5
  module Decoder
6
- # Pure-Ruby GSM (2G) BCCH activity detector.
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
- 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
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
- PWN::SDR::Decoder::Base.run_detector(
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: 'GSM',
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.',
84
+ protocol: proto,
85
+ sample_rate: rate,
86
+ source: opts[:source],
87
+ file: opts[:file],
88
+ demod: demod,
32
89
  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
- }
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
- # Supported Method Parameters::
41
- # PWN::SDR::Decoder::GSM.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
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 (ruby-native detector, no external binaries):
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