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/base.rb
CHANGED
|
@@ -11,7 +11,7 @@ module PWN
|
|
|
11
11
|
# Shared, 100 % Ruby-native pipeline plumbing for every
|
|
12
12
|
# PWN::SDR::Decoder::* module.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# Three entry points, none of which shell out to any external binary:
|
|
15
15
|
#
|
|
16
16
|
# run_native — Bind the GQRX 48 kHz s16le mono UDP audio tap, unpack
|
|
17
17
|
# the samples with PWN::SDR::Decoder::DSP, hand each
|
|
@@ -21,6 +21,14 @@ module PWN
|
|
|
21
21
|
# pretty-printed, JSONL-logged, and shown on the
|
|
22
22
|
# spinner. [ENTER] stops cleanly.
|
|
23
23
|
#
|
|
24
|
+
# run_iq — True-air path. Opens a real SDR front-end via
|
|
25
|
+
# PWN::FFI::{RTLSdr,HackRF,AdalmPluto,SoapySDR} (or
|
|
26
|
+
# reads a capture file), streams interleaved I/Q into
|
|
27
|
+
# a demod that responds to `#feed_iq(iq, rate:, &emit)`
|
|
28
|
+
# (or `#feed` after optional FM-demod). Falls back to
|
|
29
|
+
# run_detector when no hardware/file source is present
|
|
30
|
+
# so the operator still gets structured output.
|
|
31
|
+
#
|
|
24
32
|
# run_detector — For protocols whose bit-rate/bandwidth cannot be
|
|
25
33
|
# recovered from a 48 kHz demodulated-audio tap (GSM,
|
|
26
34
|
# LTE, ADS-B, WiFi, LoRa, GPS, DECT, ZigBee, Bluetooth,
|
|
@@ -302,6 +310,299 @@ module PWN
|
|
|
302
310
|
end
|
|
303
311
|
end
|
|
304
312
|
|
|
313
|
+
# Supported Method Parameters::
|
|
314
|
+
# src = PWN::SDR::Decoder::Base.resolve_iq_source(
|
|
315
|
+
# freq_obj: 'required',
|
|
316
|
+
# source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:soapy|:file',
|
|
317
|
+
# sample_rate: 'optional - desired rate Hz',
|
|
318
|
+
# file: 'optional - path to .cu8/.cs16/.iq capture'
|
|
319
|
+
# )
|
|
320
|
+
# Returns { kind:, rate_hz:, ... } handle, or nil if nothing available.
|
|
321
|
+
|
|
322
|
+
public_class_method def self.resolve_iq_source(opts = {})
|
|
323
|
+
freq_obj = opts[:freq_obj] || {}
|
|
324
|
+
want = (opts[:source] || freq_obj[:iq_source] || :auto).to_s.downcase.to_sym
|
|
325
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_048_000).to_i
|
|
326
|
+
file = opts[:file] || freq_obj[:iq_file]
|
|
327
|
+
freq_hz = begin
|
|
328
|
+
PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
329
|
+
rescue StandardError
|
|
330
|
+
freq_obj[:freq].to_i
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
try = lambda do |kind|
|
|
334
|
+
case kind
|
|
335
|
+
when :file
|
|
336
|
+
return nil if file.to_s.empty? || !File.file?(file.to_s)
|
|
337
|
+
|
|
338
|
+
fmt = opts[:iq_format] || freq_obj[:iq_format]
|
|
339
|
+
fmt ||= file.to_s.end_with?('.cs16', '.sc16') ? :cs16 : :cu8
|
|
340
|
+
{ kind: :file, path: file.to_s, format: fmt.to_sym, rate_hz: rate, freq_hz: freq_hz }
|
|
341
|
+
when :rtlsdr
|
|
342
|
+
return nil unless PWN::FFI.available?(mod: :RTLSdr)
|
|
343
|
+
return nil if PWN::FFI::RTLSdr.list_devices.empty?
|
|
344
|
+
|
|
345
|
+
dev = PWN::FFI::RTLSdr.open(index: (opts[:index] || 0).to_i)
|
|
346
|
+
PWN::FFI::RTLSdr.configure(
|
|
347
|
+
device: dev, freq_hz: freq_hz, rate_hz: rate,
|
|
348
|
+
gain_db: opts[:gain_db] || freq_obj[:gain_db],
|
|
349
|
+
ppm: opts[:ppm] || freq_obj[:ppm] || 0
|
|
350
|
+
)
|
|
351
|
+
{ kind: :rtlsdr, device: dev, rate_hz: rate, freq_hz: freq_hz, format: :cu8 }
|
|
352
|
+
when :hackrf
|
|
353
|
+
return nil unless PWN::FFI.available?(mod: :HackRF)
|
|
354
|
+
|
|
355
|
+
dev = PWN::FFI::HackRF.open(serial: opts[:serial])
|
|
356
|
+
PWN::FFI::HackRF.configure(
|
|
357
|
+
device: dev, freq_hz: freq_hz, rate_hz: rate,
|
|
358
|
+
lna_gain: opts[:lna_gain] || 16,
|
|
359
|
+
vga_gain: opts[:vga_gain] || 20,
|
|
360
|
+
amp: opts[:amp]
|
|
361
|
+
)
|
|
362
|
+
{ kind: :hackrf, device: dev, rate_hz: rate, freq_hz: freq_hz, format: :cs8 }
|
|
363
|
+
when :adalm_pluto, :pluto
|
|
364
|
+
return nil unless PWN::FFI.available?(mod: :AdalmPluto)
|
|
365
|
+
|
|
366
|
+
ctx = PWN::FFI::AdalmPluto.open(uri: opts[:uri] || freq_obj[:pluto_uri])
|
|
367
|
+
PWN::FFI::AdalmPluto.configure(
|
|
368
|
+
context: ctx, freq_hz: freq_hz, rate_hz: rate,
|
|
369
|
+
gain_db: opts[:gain_db] || freq_obj[:gain_db]
|
|
370
|
+
)
|
|
371
|
+
handle = PWN::FFI::AdalmPluto.start_rx(
|
|
372
|
+
context: ctx,
|
|
373
|
+
samples: (opts[:chunk_samples] || 262_144).to_i
|
|
374
|
+
)
|
|
375
|
+
{ kind: :adalm_pluto, context: ctx, handle: handle, rate_hz: rate, freq_hz: freq_hz, format: :cs16 }
|
|
376
|
+
when :soapy
|
|
377
|
+
return nil unless PWN::FFI.available?(mod: :SoapySDR)
|
|
378
|
+
|
|
379
|
+
# Discovery only today — Soapy streaming is layered later.
|
|
380
|
+
devs = PWN::FFI::SoapySDR.list_devices
|
|
381
|
+
return nil if devs.empty?
|
|
382
|
+
|
|
383
|
+
{ kind: :soapy, devices: devs, rate_hz: rate, freq_hz: freq_hz, format: :cf32, streaming: false }
|
|
384
|
+
end
|
|
385
|
+
rescue StandardError
|
|
386
|
+
nil
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
order =
|
|
390
|
+
case want
|
|
391
|
+
when :auto
|
|
392
|
+
%i[file rtlsdr adalm_pluto hackrf soapy]
|
|
393
|
+
else
|
|
394
|
+
[want]
|
|
395
|
+
end
|
|
396
|
+
order.each do |k|
|
|
397
|
+
h = try.call(k)
|
|
398
|
+
return h if h
|
|
399
|
+
end
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Supported Method Parameters::
|
|
404
|
+
# chunk = PWN::SDR::Decoder::Base.read_iq_chunk(source: handle, bytes: 262_144)
|
|
405
|
+
# Returns raw binary String in the source's native format, or nil on EOF.
|
|
406
|
+
|
|
407
|
+
public_class_method def self.read_iq_chunk(opts = {})
|
|
408
|
+
src = opts[:source]
|
|
409
|
+
bytes = (opts[:bytes] || 262_144).to_i
|
|
410
|
+
raise 'ERROR: :source required' unless src.is_a?(Hash)
|
|
411
|
+
|
|
412
|
+
case src[:kind]
|
|
413
|
+
when :file
|
|
414
|
+
src[:io] ||= File.open(src[:path], 'rb')
|
|
415
|
+
data = src[:io].read(bytes)
|
|
416
|
+
data.to_s.empty? ? nil : data
|
|
417
|
+
when :rtlsdr
|
|
418
|
+
PWN::FFI::RTLSdr.read_sync(device: src[:device], bytes: bytes)
|
|
419
|
+
when :adalm_pluto
|
|
420
|
+
PWN::FFI::AdalmPluto.read_sync(handle: src[:handle])
|
|
421
|
+
when :hackrf, :soapy
|
|
422
|
+
# HackRF is callback-based; Soapy streaming layered later.
|
|
423
|
+
nil
|
|
424
|
+
end
|
|
425
|
+
rescue StandardError
|
|
426
|
+
nil
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Supported Method Parameters::
|
|
430
|
+
# iq = PWN::SDR::Decoder::Base.unpack_iq(source: handle, data: raw_string)
|
|
431
|
+
# → interleaved Array<Float> [I0,Q0,…]
|
|
432
|
+
|
|
433
|
+
public_class_method def self.unpack_iq(opts = {})
|
|
434
|
+
src = opts[:source] || {}
|
|
435
|
+
data = opts[:data].to_s
|
|
436
|
+
case (src[:format] || :cu8).to_sym
|
|
437
|
+
when :cs16 then PWN::SDR::Decoder::DSP.unpack_cs16le(data: data)
|
|
438
|
+
when :cs8
|
|
439
|
+
# HackRF signed 8-bit interleaved I/Q
|
|
440
|
+
data.unpack('c*').map { |v| v / 128.0 }
|
|
441
|
+
else # :cu8 / unknown
|
|
442
|
+
PWN::SDR::Decoder::DSP.unpack_cu8(data: data)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Supported Method Parameters::
|
|
447
|
+
# PWN::SDR::Decoder::Base.close_iq_source(source: handle)
|
|
448
|
+
|
|
449
|
+
public_class_method def self.close_iq_source(opts = {})
|
|
450
|
+
src = opts[:source]
|
|
451
|
+
return unless src.is_a?(Hash)
|
|
452
|
+
|
|
453
|
+
case src[:kind]
|
|
454
|
+
when :file
|
|
455
|
+
src[:io]&.close
|
|
456
|
+
when :rtlsdr
|
|
457
|
+
PWN::FFI::RTLSdr.close(device: src[:device]) if src[:device]
|
|
458
|
+
when :adalm_pluto
|
|
459
|
+
PWN::FFI::AdalmPluto.stop_rx(handle: src[:handle]) if src[:handle]
|
|
460
|
+
PWN::FFI::AdalmPluto.close(context: src[:context]) if src[:context]
|
|
461
|
+
when :hackrf
|
|
462
|
+
PWN::FFI::HackRF.close(device: src[:device]) if src[:device]
|
|
463
|
+
end
|
|
464
|
+
rescue StandardError
|
|
465
|
+
nil
|
|
466
|
+
ensure
|
|
467
|
+
src[:io] = nil if src.is_a?(Hash)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Supported Method Parameters::
|
|
471
|
+
# PWN::SDR::Decoder::Base.run_iq(
|
|
472
|
+
# freq_obj: 'required - freq_obj Hash',
|
|
473
|
+
# protocol: 'required - short name',
|
|
474
|
+
# demod: 'required - object with #feed_iq(iq, rate:, &emit)
|
|
475
|
+
# OR #feed(samples, &emit) when fm_demod:true',
|
|
476
|
+
# sample_rate: 'optional - Hz (default 2_048_000)',
|
|
477
|
+
# source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:file',
|
|
478
|
+
# file: 'optional - path to capture',
|
|
479
|
+
# fm_demod: 'optional - FM-demod I/Q→audio then #feed (default false)',
|
|
480
|
+
# chunk_bytes: 'optional - bytes per read (default 262144)',
|
|
481
|
+
# fallback: 'optional - :detector|:raise|:silent (default :detector)',
|
|
482
|
+
# note: 'optional - shown once when falling back',
|
|
483
|
+
# describe: 'optional - Proc for detector fallback'
|
|
484
|
+
# )
|
|
485
|
+
|
|
486
|
+
public_class_method def self.run_iq(opts = {})
|
|
487
|
+
freq_obj = opts[:freq_obj]
|
|
488
|
+
protocol = opts[:protocol] || 'SIGNAL'
|
|
489
|
+
demod = opts[:demod]
|
|
490
|
+
rate = (opts[:sample_rate] || 2_048_000).to_i
|
|
491
|
+
fm_demod = opts[:fm_demod] ? true : false
|
|
492
|
+
chunk_b = (opts[:chunk_bytes] || 262_144).to_i
|
|
493
|
+
fallback = (opts[:fallback] || :detector).to_sym
|
|
494
|
+
|
|
495
|
+
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
496
|
+
raise 'ERROR: :demod required' if demod.nil?
|
|
497
|
+
|
|
498
|
+
src = resolve_iq_source(
|
|
499
|
+
freq_obj: freq_obj,
|
|
500
|
+
source: opts[:source],
|
|
501
|
+
sample_rate: rate,
|
|
502
|
+
file: opts[:file],
|
|
503
|
+
gain_db: opts[:gain_db],
|
|
504
|
+
uri: opts[:uri],
|
|
505
|
+
index: opts[:index],
|
|
506
|
+
chunk_samples: chunk_b / 4
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
unless src && (src[:kind] != :soapy || src[:streaming] != false)
|
|
510
|
+
case fallback
|
|
511
|
+
when :raise
|
|
512
|
+
raise 'ERROR: no I/Q source available (RTL-SDR / ADALM-Pluto / HackRF / file)'
|
|
513
|
+
when :silent
|
|
514
|
+
return nil
|
|
515
|
+
else
|
|
516
|
+
return run_detector(
|
|
517
|
+
freq_obj: freq_obj,
|
|
518
|
+
protocol: protocol,
|
|
519
|
+
note: opts[:note] || "No I/Q source — falling back to energy detector. Plug in RTL-SDR/Pluto or pass iq_file: for true-air decode of #{protocol}.",
|
|
520
|
+
threshold: opts[:threshold],
|
|
521
|
+
describe: opts[:describe]
|
|
522
|
+
)
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
rate = src[:rate_hz] if src[:rate_hz]
|
|
527
|
+
log_obj = strip_freq_obj(freq_obj: freq_obj).merge(
|
|
528
|
+
iq_source: src[:kind], iq_rate: rate, iq_format: src[:format]
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
puts JSON.pretty_generate(log_obj)
|
|
532
|
+
puts "\n*** #{protocol} Decoder (true-air I/Q via #{src[:kind]}) ***"
|
|
533
|
+
puts 'Press [ENTER] to continue to next frequency...'
|
|
534
|
+
|
|
535
|
+
spinner, max_len = build_spinner(
|
|
536
|
+
banner: "INFO: Air-decoding #{protocol} @ #{rate} Hz from #{src[:kind]}"
|
|
537
|
+
)
|
|
538
|
+
log_file = log_path(protocol: protocol)
|
|
539
|
+
current_line = "Streaming I/Q from #{src[:kind]}..."
|
|
540
|
+
iq_q = Queue.new
|
|
541
|
+
stop = false
|
|
542
|
+
|
|
543
|
+
reader_thread = Thread.new do
|
|
544
|
+
loop do
|
|
545
|
+
break if stop
|
|
546
|
+
|
|
547
|
+
raw = read_iq_chunk(source: src, bytes: chunk_b)
|
|
548
|
+
break if raw.nil?
|
|
549
|
+
|
|
550
|
+
iq_q.push(raw) if raw.bytesize.positive?
|
|
551
|
+
end
|
|
552
|
+
rescue StandardError => e
|
|
553
|
+
current_line = "iq-reader: #{e.class}: #{e.message}"
|
|
554
|
+
ensure
|
|
555
|
+
iq_q.push(:eof)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
decoder_thread = Thread.new do
|
|
559
|
+
emit = proc do |msg|
|
|
560
|
+
next unless msg.is_a?(Hash)
|
|
561
|
+
|
|
562
|
+
final = log_obj.merge(decoded_at: Time.now.strftime('%Y-%m-%d %H:%M:%S%z')).merge(msg)
|
|
563
|
+
spinner.stop
|
|
564
|
+
puts JSON.pretty_generate(final)
|
|
565
|
+
spinner.auto_spin
|
|
566
|
+
File.open(log_file, 'a') { |f| f.puts("#{JSON.generate(final)},") }
|
|
567
|
+
disp = (msg[:summary] || msg[:raw] || msg.values.compact.first).to_s
|
|
568
|
+
current_line = disp[0...max_len]
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
loop do
|
|
572
|
+
raw = iq_q.pop
|
|
573
|
+
break if raw == :eof || stop
|
|
574
|
+
next unless raw.is_a?(String)
|
|
575
|
+
|
|
576
|
+
iq = unpack_iq(source: src, data: raw)
|
|
577
|
+
if fm_demod && demod.respond_to?(:feed)
|
|
578
|
+
audio = PWN::SDR::Decoder::DSP.fm_demod_iq(iq: iq)
|
|
579
|
+
demod.feed(audio, &emit)
|
|
580
|
+
elsif demod.respond_to?(:feed_iq)
|
|
581
|
+
demod.feed_iq(iq, rate: rate, &emit)
|
|
582
|
+
elsif demod.respond_to?(:feed)
|
|
583
|
+
# magnitude envelope as last-resort real signal
|
|
584
|
+
demod.feed(PWN::SDR::Decoder::DSP.mag_sq(iq: iq).map { |v| Math.sqrt(v) }, &emit)
|
|
585
|
+
else
|
|
586
|
+
raise 'ERROR: demod must respond to #feed_iq or #feed'
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
rescue StandardError => e
|
|
590
|
+
current_line = "iq-demod: #{e.class}: #{e.message}"
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
wait_for_enter(spinner: spinner, title_ref: -> { current_line })
|
|
594
|
+
stop = true
|
|
595
|
+
spinner.success('Air-decode stopped')
|
|
596
|
+
rescue StandardError => e
|
|
597
|
+
spinner&.error("Air-decode failed: #{e.message}") if defined?(spinner)
|
|
598
|
+
raise
|
|
599
|
+
ensure
|
|
600
|
+
stop = true if defined?(stop)
|
|
601
|
+
[reader_thread, decoder_thread].compact.each { |th| th.kill if th&.alive? }
|
|
602
|
+
close_iq_source(source: src) if defined?(src) && src
|
|
603
|
+
spinner&.stop if defined?(spinner)
|
|
604
|
+
end
|
|
605
|
+
|
|
305
606
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
306
607
|
|
|
307
608
|
public_class_method def self.authors
|
|
@@ -319,6 +620,17 @@ module PWN
|
|
|
319
620
|
rate: 'optional - UDP sample rate (default 48000)'
|
|
320
621
|
)
|
|
321
622
|
|
|
623
|
+
#{self}.run_iq(
|
|
624
|
+
freq_obj: 'required - freq_obj Hash',
|
|
625
|
+
protocol: 'required - short protocol name',
|
|
626
|
+
demod: 'required - #feed_iq(iq, rate:, &emit) or #feed',
|
|
627
|
+
sample_rate: 'optional - Hz (default 2_048_000)',
|
|
628
|
+
source: 'optional - :auto|:rtlsdr|:hackrf|:adalm_pluto|:file',
|
|
629
|
+
file: 'optional - .cu8/.cs16 capture path',
|
|
630
|
+
fm_demod: 'optional - FM-demod then #feed (default false)',
|
|
631
|
+
fallback: 'optional - :detector|:raise|:silent (default :detector)'
|
|
632
|
+
)
|
|
633
|
+
|
|
322
634
|
#{self}.run_detector(
|
|
323
635
|
freq_obj: 'required - freq_obj from PWN::SDR::GQRX.init_freq',
|
|
324
636
|
protocol: 'required - short protocol name',
|
|
@@ -327,6 +639,7 @@ module PWN
|
|
|
327
639
|
describe: 'optional - Proc { |burst| Hash } extra fields'
|
|
328
640
|
)
|
|
329
641
|
|
|
642
|
+
#{self}.resolve_iq_source(freq_obj:, source: :auto, sample_rate:, file:)
|
|
330
643
|
#{self}.match_line?(line: str, matcher: Regexp|String|Array)
|
|
331
644
|
|
|
332
645
|
#{self}.authors
|
|
@@ -3,12 +3,75 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# mode reports per-channel hop bursts and derives channel index from
|
|
10
|
-
# freq_obj. `parse_line` retained for offline text analysis.
|
|
6
|
+
# True-air + detector-fallback decoder for Bluetooth.
|
|
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.
|
|
11
9
|
module Bluetooth
|
|
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: 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
|
+
|
|
68
|
+
# Protocol-specific enrichment of an IQ-burst message (channel TBD).
|
|
69
|
+
public_class_method def self.enrich(opts = {})
|
|
70
|
+
msg = opts[:msg] || {}
|
|
71
|
+
msg.merge(channel: nil)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
12
75
|
# Supported Method Parameters::
|
|
13
76
|
# PWN::SDR::Decoder::Bluetooth.decode(
|
|
14
77
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -19,20 +82,32 @@ module PWN
|
|
|
19
82
|
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
20
83
|
ble = freq_obj[:ble] || freq_obj[:mode].to_s.casecmp('ble').zero?
|
|
21
84
|
ch = ble ? ((hz - 2_402_000_000) / 2_000_000).clamp(0, 39) : ((hz - 2_402_000_000) / 1_000_000).clamp(0, 78)
|
|
22
|
-
|
|
85
|
+
|
|
86
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
|
|
87
|
+
proto = ble ? 'BLE' : 'BT-BR/EDR'
|
|
88
|
+
demod = DemodIQ.new(
|
|
89
|
+
rate: rate, protocol: proto, modulation: 'GFSK',
|
|
90
|
+
extra: { threshold: 9.0 }
|
|
91
|
+
)
|
|
92
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
23
93
|
freq_obj: freq_obj,
|
|
24
|
-
protocol:
|
|
25
|
-
|
|
94
|
+
protocol: proto,
|
|
95
|
+
sample_rate: rate,
|
|
96
|
+
source: opts[:source],
|
|
97
|
+
file: opts[:file],
|
|
98
|
+
demod: demod,
|
|
26
99
|
threshold: 9.0,
|
|
100
|
+
note: '1 Mbit/s GFSK FHSS — true-air I/Q path reports hop-burst density per tuned channel.',
|
|
27
101
|
describe: proc { |b|
|
|
28
|
-
{ modulation: 'GFSK', channel:
|
|
102
|
+
{ modulation: 'GFSK', channel: begin
|
|
103
|
+
b[:channel]
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end, hop_slots: (b[:duration_ms] / 0.625).round }
|
|
29
107
|
}
|
|
30
108
|
)
|
|
31
109
|
end
|
|
32
110
|
|
|
33
|
-
# Supported Method Parameters::
|
|
34
|
-
# PWN::SDR::Decoder::Bluetooth.parse_line(line: 'systime=... LAP=9e8b33 ...')
|
|
35
|
-
|
|
36
111
|
public_class_method def self.parse_line(opts = {})
|
|
37
112
|
line = opts[:line].to_s
|
|
38
113
|
out = { protocol: 'Bluetooth' }
|
|
@@ -52,16 +127,14 @@ module PWN
|
|
|
52
127
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
53
128
|
end
|
|
54
129
|
|
|
55
|
-
# Display Usage for this Module
|
|
56
|
-
|
|
57
130
|
public_class_method def self.help
|
|
58
|
-
puts "USAGE (
|
|
131
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
59
132
|
#{self}.decode(
|
|
60
|
-
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
133
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
134
|
+
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
135
|
+
file: 'optional - .cu8/.cs16 capture'
|
|
61
136
|
)
|
|
62
137
|
|
|
63
|
-
#{self}.parse_line(line: 'systime=... ch=37 LAP=9e8b33 ...')
|
|
64
|
-
|
|
65
138
|
#{self}.authors
|
|
66
139
|
"
|
|
67
140
|
end
|
data/lib/pwn/sdr/decoder/dect.rb
CHANGED
|
@@ -3,12 +3,69 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# slot bursts (417 μs) and derives carrier index from freq_obj.
|
|
10
|
-
# `parse_line` retained for offline text analysis.
|
|
6
|
+
# True-air + detector-fallback decoder for DECT.
|
|
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.
|
|
11
9
|
module DECT
|
|
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
|
+
|
|
12
69
|
# Supported Method Parameters::
|
|
13
70
|
# PWN::SDR::Decoder::DECT.decode(
|
|
14
71
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -16,23 +73,26 @@ module PWN
|
|
|
16
73
|
|
|
17
74
|
public_class_method def self.decode(opts = {})
|
|
18
75
|
freq_obj = opts[:freq_obj]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
76
|
+
|
|
77
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
|
|
78
|
+
proto = 'DECT'
|
|
79
|
+
demod = DemodIQ.new(
|
|
80
|
+
rate: rate, protocol: proto, modulation: 'GFSK',
|
|
81
|
+
extra: { threshold: 7.0 }
|
|
82
|
+
)
|
|
83
|
+
PWN::SDR::Decoder::Base.run_iq(
|
|
22
84
|
freq_obj: freq_obj,
|
|
23
|
-
protocol:
|
|
24
|
-
|
|
85
|
+
protocol: proto,
|
|
86
|
+
sample_rate: rate,
|
|
87
|
+
source: opts[:source],
|
|
88
|
+
file: opts[:file],
|
|
89
|
+
demod: demod,
|
|
25
90
|
threshold: 7.0,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ modulation: 'GFSK', carrier: ch, tdma_slots: slots, classification: slots >= 24 ? 'FP-beacon-frame' : 'PP-burst' }
|
|
29
|
-
}
|
|
91
|
+
note: '1.152 Mbit/s GFSK 24-slot TDMA — true-air I/Q path reports slot bursts.',
|
|
92
|
+
describe: proc { |b| { modulation: 'GFSK', tdma_slots: (b[:duration_ms] / 0.417).round, classification: (b[:duration_ms] / 0.417).round >= 24 ? 'FP-beacon-frame' : 'PP-burst' } }
|
|
30
93
|
)
|
|
31
94
|
end
|
|
32
95
|
|
|
33
|
-
# Supported Method Parameters::
|
|
34
|
-
# PWN::SDR::Decoder::DECT.parse_line(line: 'RFPI: 01 23 45 67 89 slot 4 carrier 3 RSSI -55')
|
|
35
|
-
|
|
36
96
|
public_class_method def self.parse_line(opts = {})
|
|
37
97
|
line = opts[:line].to_s
|
|
38
98
|
out = { protocol: 'DECT' }
|
|
@@ -51,16 +111,14 @@ module PWN
|
|
|
51
111
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
52
112
|
end
|
|
53
113
|
|
|
54
|
-
# Display Usage for this Module
|
|
55
|
-
|
|
56
114
|
public_class_method def self.help
|
|
57
|
-
puts "USAGE (
|
|
115
|
+
puts "USAGE (true-air I/Q via PWN::FFI + detector fallback):
|
|
58
116
|
#{self}.decode(
|
|
59
|
-
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'
|
|
60
120
|
)
|
|
61
121
|
|
|
62
|
-
#{self}.parse_line(line: 'RFPI: 01 23 45 67 89 slot 4 carrier 3')
|
|
63
|
-
|
|
64
122
|
#{self}.authors
|
|
65
123
|
"
|
|
66
124
|
end
|