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/ffi/fftw.rb
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module FFI
|
|
9
|
+
# Thin single-precision FFTW3 binding (`libfftw3f`).
|
|
10
|
+
#
|
|
11
|
+
# Used by PWN::SDR::* spectrum work (GQRX FFT snapshots, wideband energy
|
|
12
|
+
# detectors) when MHz-rate FFTs outgrow pure-Ruby DFT. Missing library
|
|
13
|
+
# degrades cleanly via `.available?` — callers keep pure-Ruby fallbacks.
|
|
14
|
+
#
|
|
15
|
+
# No compile step at gem install; no shells. Plans are built with
|
|
16
|
+
# FFTW_ESTIMATE so first-call latency stays acceptable for REPL use.
|
|
17
|
+
module FFTW
|
|
18
|
+
extend PubFFI::Library
|
|
19
|
+
|
|
20
|
+
FFTW_ESTIMATE = (1 << 6)
|
|
21
|
+
FFTW_MEASURE = 0
|
|
22
|
+
FFTW_FORWARD = -1
|
|
23
|
+
FFTW_BACKWARD = 1
|
|
24
|
+
|
|
25
|
+
@load_error = nil
|
|
26
|
+
begin
|
|
27
|
+
ffi_lib %w[fftw3f libfftw3f.so.3 libfftw3f.so]
|
|
28
|
+
rescue LoadError => e
|
|
29
|
+
@load_error = e
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
attr_reader :load_error
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
unless @load_error
|
|
37
|
+
# fftwf_complex is float[2] (re, im) under the C ABI we target.
|
|
38
|
+
attach_function :fftwf_malloc, [:size_t], :pointer
|
|
39
|
+
attach_function :fftwf_free, [:pointer], :void
|
|
40
|
+
attach_function :fftwf_destroy_plan, [:pointer], :void
|
|
41
|
+
attach_function :fftwf_cleanup, [], :void
|
|
42
|
+
attach_function :fftwf_execute, [:pointer], :void
|
|
43
|
+
|
|
44
|
+
# Real → complex 1-D (out has n/2+1 complex bins = 2*(n/2+1) floats)
|
|
45
|
+
attach_function :fftwf_plan_dft_r2c_1d,
|
|
46
|
+
%i[int pointer pointer uint],
|
|
47
|
+
:pointer
|
|
48
|
+
|
|
49
|
+
# Complex → real 1-D
|
|
50
|
+
attach_function :fftwf_plan_dft_c2r_1d,
|
|
51
|
+
%i[int pointer pointer uint],
|
|
52
|
+
:pointer
|
|
53
|
+
|
|
54
|
+
# Complex ↔ complex 1-D
|
|
55
|
+
attach_function :fftwf_plan_dft_1d,
|
|
56
|
+
%i[int pointer pointer int uint],
|
|
57
|
+
:pointer
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Supported Method Parameters::
|
|
61
|
+
# PWN::FFI::FFTW.available?
|
|
62
|
+
|
|
63
|
+
public_class_method def self.available?
|
|
64
|
+
!@load_error && respond_to?(:fftwf_malloc, true)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Supported Method Parameters::
|
|
70
|
+
# spectrum = PWN::FFI::FFTW.rfft(
|
|
71
|
+
# samples: 'required - Array<Float> real input (length = n)',
|
|
72
|
+
# n: 'optional - FFT size (default samples.length; zero-pads/truncates)'
|
|
73
|
+
# )
|
|
74
|
+
# Returns Array of [re, im] pairs, length n/2+1 (DC .. Nyquist).
|
|
75
|
+
|
|
76
|
+
public_class_method def self.rfft(opts = {})
|
|
77
|
+
raise 'ERROR: libfftw3f not available' unless available?
|
|
78
|
+
|
|
79
|
+
samples = opts[:samples]
|
|
80
|
+
n = (opts[:n] || samples.length).to_i
|
|
81
|
+
raise 'ERROR: n must be >= 1' if n < 1
|
|
82
|
+
|
|
83
|
+
in_ptr = fftwf_malloc(n * 4)
|
|
84
|
+
out_bins = (n / 2) + 1
|
|
85
|
+
out_ptr = fftwf_malloc(out_bins * 2 * 4)
|
|
86
|
+
raise 'ERROR: fftwf_malloc failed' if in_ptr.null? || out_ptr.null?
|
|
87
|
+
|
|
88
|
+
# zero + copy
|
|
89
|
+
in_ptr.write_array_of_float(Array.new(n, 0.0))
|
|
90
|
+
src = samples.first([n, samples.length].min).map(&:to_f)
|
|
91
|
+
in_ptr.write_array_of_float(src + Array.new(n - src.length, 0.0)) if src.length < n
|
|
92
|
+
in_ptr.write_array_of_float(src) if src.length == n
|
|
93
|
+
|
|
94
|
+
plan = fftwf_plan_dft_r2c_1d(n, in_ptr, out_ptr, FFTW_ESTIMATE)
|
|
95
|
+
raise 'ERROR: fftwf_plan_dft_r2c_1d failed' if plan.null?
|
|
96
|
+
|
|
97
|
+
fftwf_execute(plan)
|
|
98
|
+
flat = out_ptr.read_array_of_float(out_bins * 2)
|
|
99
|
+
result = Array.new(out_bins) { |i| [flat[2 * i], flat[(2 * i) + 1]] }
|
|
100
|
+
|
|
101
|
+
fftwf_destroy_plan(plan)
|
|
102
|
+
fftwf_free(in_ptr)
|
|
103
|
+
fftwf_free(out_ptr)
|
|
104
|
+
result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Supported Method Parameters::
|
|
108
|
+
# mag = PWN::FFI::FFTW.rfft_magnitude(
|
|
109
|
+
# samples: 'required - Array<Float>',
|
|
110
|
+
# n: 'optional - FFT size'
|
|
111
|
+
# )
|
|
112
|
+
# Returns Array<Float> of |X[k]| for k=0..n/2.
|
|
113
|
+
|
|
114
|
+
public_class_method def self.rfft_magnitude(opts = {})
|
|
115
|
+
rfft(opts).map { |re, im| Math.sqrt((re * re) + (im * im)) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Supported Method Parameters::
|
|
119
|
+
# power_db = PWN::FFI::FFTW.rfft_power_db(
|
|
120
|
+
# samples: 'required - Array<Float>',
|
|
121
|
+
# n: 'optional - FFT size',
|
|
122
|
+
# floor: 'optional - dB floor for zeros (default -120.0)'
|
|
123
|
+
# )
|
|
124
|
+
# Returns Array<Float> of 20*log10(|X[k]|) with a noise floor.
|
|
125
|
+
|
|
126
|
+
public_class_method def self.rfft_power_db(opts = {})
|
|
127
|
+
floor = (opts[:floor] || -120.0).to_f
|
|
128
|
+
rfft_magnitude(opts).map do |m|
|
|
129
|
+
m.positive? ? (20.0 * Math.log10(m)) : floor
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Supported Method Parameters::
|
|
134
|
+
# spectrum = PWN::FFI::FFTW.cfft(
|
|
135
|
+
# iq: 'required - interleaved Array<Float> I/Q (even length)',
|
|
136
|
+
# n: 'optional - number of complex samples (default iq.length/2)',
|
|
137
|
+
# sign: 'optional - :forward (default) or :backward'
|
|
138
|
+
# )
|
|
139
|
+
# Returns Array of [re, im] pairs length n.
|
|
140
|
+
|
|
141
|
+
public_class_method def self.cfft(opts = {})
|
|
142
|
+
raise 'ERROR: libfftw3f not available' unless available?
|
|
143
|
+
|
|
144
|
+
iq = opts[:iq]
|
|
145
|
+
n = (opts[:n] || (iq.length / 2)).to_i
|
|
146
|
+
raise 'ERROR: n must be >= 1' if n < 1
|
|
147
|
+
|
|
148
|
+
sign = opts[:sign] == :backward ? FFTW_BACKWARD : FFTW_FORWARD
|
|
149
|
+
bytes = n * 2 * 4
|
|
150
|
+
in_ptr = fftwf_malloc(bytes)
|
|
151
|
+
out_ptr = fftwf_malloc(bytes)
|
|
152
|
+
raise 'ERROR: fftwf_malloc failed' if in_ptr.null? || out_ptr.null?
|
|
153
|
+
|
|
154
|
+
src = iq.first([2 * n, iq.length].min).map(&:to_f)
|
|
155
|
+
padded = src + Array.new((2 * n) - src.length, 0.0)
|
|
156
|
+
in_ptr.write_array_of_float(padded)
|
|
157
|
+
|
|
158
|
+
plan = fftwf_plan_dft_1d(n, in_ptr, out_ptr, sign, FFTW_ESTIMATE)
|
|
159
|
+
raise 'ERROR: fftwf_plan_dft_1d failed' if plan.null?
|
|
160
|
+
|
|
161
|
+
fftwf_execute(plan)
|
|
162
|
+
flat = out_ptr.read_array_of_float(n * 2)
|
|
163
|
+
result = Array.new(n) { |i| [flat[2 * i], flat[(2 * i) + 1]] }
|
|
164
|
+
|
|
165
|
+
fftwf_destroy_plan(plan)
|
|
166
|
+
fftwf_free(in_ptr)
|
|
167
|
+
fftwf_free(out_ptr)
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
172
|
+
|
|
173
|
+
public_class_method def self.authors
|
|
174
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Display Usage for this Module
|
|
178
|
+
|
|
179
|
+
public_class_method def self.help
|
|
180
|
+
puts "USAGE:
|
|
181
|
+
#{self}.available? # => true/false
|
|
182
|
+
#{self}.rfft(samples:, n: nil) # real→complex, [[re,im],…]
|
|
183
|
+
#{self}.rfft_magnitude(samples:, n: nil) # |X[k]|
|
|
184
|
+
#{self}.rfft_power_db(samples:, n:, floor: -120.0)
|
|
185
|
+
#{self}.cfft(iq:, n: nil, sign: :forward) # complex FFT
|
|
186
|
+
|
|
187
|
+
#{self}.authors
|
|
188
|
+
"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module FFI
|
|
9
|
+
# Thin libhackrf binding for inventory / RX of raw I/Q.
|
|
10
|
+
#
|
|
11
|
+
# Intentionally control-plane first: init/open/tune/rate/gains + one-shot
|
|
12
|
+
# sync-style helpers used by Extrospection `probe_rf` and by wideband
|
|
13
|
+
# PWN::SDR::Decoder::* modules that need real I/Q (not GQRX audio).
|
|
14
|
+
# Streaming callbacks stay opt-in — Ruby GC must not run on the
|
|
15
|
+
# libusb transfer thread, so call sites that need continuous RX should
|
|
16
|
+
# buffer into a Queue from a dedicated thread.
|
|
17
|
+
module HackRF
|
|
18
|
+
extend PubFFI::Library
|
|
19
|
+
|
|
20
|
+
HACKRF_SUCCESS = 0
|
|
21
|
+
|
|
22
|
+
@load_error = nil
|
|
23
|
+
begin
|
|
24
|
+
ffi_lib %w[hackrf libhackrf.so.0 libhackrf.so]
|
|
25
|
+
rescue LoadError => e
|
|
26
|
+
@load_error = e
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
attr_reader :load_error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
unless @load_error
|
|
34
|
+
attach_function :hackrf_init, [], :int
|
|
35
|
+
attach_function :hackrf_exit, [], :int
|
|
36
|
+
attach_function :hackrf_library_version, [], :string
|
|
37
|
+
attach_function :hackrf_library_release, [], :string
|
|
38
|
+
attach_function :hackrf_error_name, [:int], :string
|
|
39
|
+
|
|
40
|
+
attach_function :hackrf_open, [:pointer], :int
|
|
41
|
+
attach_function :hackrf_open_by_serial, %i[string pointer], :int
|
|
42
|
+
attach_function :hackrf_close, [:pointer], :int
|
|
43
|
+
|
|
44
|
+
attach_function :hackrf_set_freq, %i[pointer uint64], :int
|
|
45
|
+
attach_function :hackrf_set_sample_rate, %i[pointer double], :int
|
|
46
|
+
attach_function :hackrf_set_baseband_filter_bandwidth, %i[pointer uint32], :int
|
|
47
|
+
attach_function :hackrf_set_vga_gain, %i[pointer uint32], :int
|
|
48
|
+
attach_function :hackrf_set_lna_gain, %i[pointer uint32], :int
|
|
49
|
+
attach_function :hackrf_set_amp_enable, %i[pointer uint8], :int
|
|
50
|
+
attach_function :hackrf_is_streaming, [:pointer], :int
|
|
51
|
+
attach_function :hackrf_board_id_read, %i[pointer pointer], :int
|
|
52
|
+
attach_function :hackrf_board_id_name, [:uint8], :string
|
|
53
|
+
attach_function :hackrf_version_string_read, %i[pointer pointer uint8], :int
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Supported Method Parameters::
|
|
57
|
+
# PWN::FFI::HackRF.available?
|
|
58
|
+
|
|
59
|
+
public_class_method def self.available?
|
|
60
|
+
!@load_error && respond_to?(:hackrf_init, true)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Supported Method Parameters::
|
|
66
|
+
# info = PWN::FFI::HackRF.info
|
|
67
|
+
# Returns Hash with library_version / library_release / available.
|
|
68
|
+
|
|
69
|
+
public_class_method def self.info
|
|
70
|
+
return { available: false, error: @load_error&.message } unless available?
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
available: true,
|
|
74
|
+
library_version: hackrf_library_version.to_s,
|
|
75
|
+
library_release: hackrf_library_release.to_s
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Supported Method Parameters::
|
|
80
|
+
# dev = PWN::FFI::HackRF.open(serial: nil)
|
|
81
|
+
# Returns FFI::Pointer (opaque hackrf_device*) or raises.
|
|
82
|
+
|
|
83
|
+
public_class_method def self.open(opts = {})
|
|
84
|
+
raise 'ERROR: libhackrf not available' unless available?
|
|
85
|
+
|
|
86
|
+
check!(hackrf_init)
|
|
87
|
+
dev_ptr = PubFFI::MemoryPointer.new(:pointer)
|
|
88
|
+
serial = opts[:serial]
|
|
89
|
+
rc = if serial
|
|
90
|
+
hackrf_open_by_serial(serial.to_s, dev_ptr)
|
|
91
|
+
else
|
|
92
|
+
hackrf_open(dev_ptr)
|
|
93
|
+
end
|
|
94
|
+
check!(rc)
|
|
95
|
+
dev_ptr.read_pointer
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Supported Method Parameters::
|
|
99
|
+
# PWN::FFI::HackRF.close(device: pointer)
|
|
100
|
+
|
|
101
|
+
public_class_method def self.close(opts = {})
|
|
102
|
+
dev = opts[:device]
|
|
103
|
+
return unless dev && !dev.null?
|
|
104
|
+
|
|
105
|
+
check!(hackrf_close(dev))
|
|
106
|
+
hackrf_exit
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Supported Method Parameters::
|
|
111
|
+
# PWN::FFI::HackRF.configure(
|
|
112
|
+
# device: 'required - pointer from .open',
|
|
113
|
+
# freq_hz: 'required - center frequency Hz',
|
|
114
|
+
# rate_hz: 'optional - sample rate (default 10e6)',
|
|
115
|
+
# lna_gain: 'optional - 0..40 step 8 (default 16)',
|
|
116
|
+
# vga_gain: 'optional - 0..62 step 2 (default 20)',
|
|
117
|
+
# amp: 'optional - true/false RF amp (default false)',
|
|
118
|
+
# bb_bw_hz: 'optional - baseband filter BW Hz'
|
|
119
|
+
# )
|
|
120
|
+
|
|
121
|
+
public_class_method def self.configure(opts = {}) # rubocop:disable Naming/PredicateMethod
|
|
122
|
+
dev = opts[:device]
|
|
123
|
+
raise 'ERROR: :device required' if dev.nil? || dev.null?
|
|
124
|
+
|
|
125
|
+
freq = opts[:freq_hz].to_i
|
|
126
|
+
rate = (opts[:rate_hz] || 10_000_000).to_f
|
|
127
|
+
lna = (opts[:lna_gain] || 16).to_i
|
|
128
|
+
vga = (opts[:vga_gain] || 20).to_i
|
|
129
|
+
amp = opts[:amp] ? 1 : 0
|
|
130
|
+
|
|
131
|
+
check!(hackrf_set_freq(dev, freq))
|
|
132
|
+
check!(hackrf_set_sample_rate(dev, rate))
|
|
133
|
+
check!(hackrf_set_lna_gain(dev, lna))
|
|
134
|
+
check!(hackrf_set_vga_gain(dev, vga))
|
|
135
|
+
check!(hackrf_set_amp_enable(dev, amp))
|
|
136
|
+
check!(hackrf_set_baseband_filter_bandwidth(dev, opts[:bb_bw_hz].to_i)) if opts[:bb_bw_hz]
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Supported Method Parameters::
|
|
141
|
+
# meta = PWN::FFI::HackRF.device_info(device: pointer)
|
|
142
|
+
# Returns { board_id:, board_name:, version: }
|
|
143
|
+
|
|
144
|
+
public_class_method def self.device_info(opts = {})
|
|
145
|
+
dev = opts[:device]
|
|
146
|
+
raise 'ERROR: :device required' if dev.nil? || dev.null?
|
|
147
|
+
|
|
148
|
+
bid_ptr = PubFFI::MemoryPointer.new(:uint8)
|
|
149
|
+
check!(hackrf_board_id_read(dev, bid_ptr))
|
|
150
|
+
bid = bid_ptr.read_uint8
|
|
151
|
+
ver_buf = PubFFI::MemoryPointer.new(:char, 255)
|
|
152
|
+
check!(hackrf_version_string_read(dev, ver_buf, 255))
|
|
153
|
+
{
|
|
154
|
+
board_id: bid,
|
|
155
|
+
board_name: hackrf_board_id_name(bid).to_s,
|
|
156
|
+
version: ver_buf.read_string
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
161
|
+
|
|
162
|
+
public_class_method def self.authors
|
|
163
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Display Usage for this Module
|
|
167
|
+
|
|
168
|
+
public_class_method def self.help
|
|
169
|
+
puts "USAGE:
|
|
170
|
+
#{self}.available?
|
|
171
|
+
#{self}.info
|
|
172
|
+
dev = #{self}.open(serial: nil)
|
|
173
|
+
#{self}.configure(device: dev, freq_hz:, rate_hz: 10e6, lna_gain: 16, vga_gain: 20, amp: false)
|
|
174
|
+
#{self}.device_info(device: dev)
|
|
175
|
+
#{self}.close(device: dev)
|
|
176
|
+
|
|
177
|
+
#{self}.authors
|
|
178
|
+
"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class << self
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def check!(code)
|
|
185
|
+
return if code == HACKRF_SUCCESS
|
|
186
|
+
|
|
187
|
+
name = available? ? hackrf_error_name(code).to_s : code.to_s
|
|
188
|
+
raise "ERROR: libhackrf rc=#{code} (#{name})"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module FFI
|
|
9
|
+
# Thin liquid-dsp binding.
|
|
10
|
+
#
|
|
11
|
+
# Provides the high-level DSP building blocks that PWN::SDR::Decoder::*
|
|
12
|
+
# needs beyond pure-Ruby primitives: FM / frequency demodulation,
|
|
13
|
+
# multi-stage arbitrary resampling, and Kaiser FIR filters. Opaque
|
|
14
|
+
# liquid handles are held as FFI::Pointer and always freed in an
|
|
15
|
+
# ensure — no compile step, no shells. If libliquid is absent
|
|
16
|
+
# `.available?` is false and DSP falls back to pure Ruby.
|
|
17
|
+
#
|
|
18
|
+
# Complex samples use the GCC float _Complex ABI: interleaved
|
|
19
|
+
# float[2] (re, im) per sample, which is how liquid_float_complex is
|
|
20
|
+
# laid out under LIQUID_DEFINE_COMPLEX.
|
|
21
|
+
module Liquid
|
|
22
|
+
extend PubFFI::Library
|
|
23
|
+
|
|
24
|
+
@load_error = nil
|
|
25
|
+
begin
|
|
26
|
+
ffi_lib %w[liquid libliquid.so.1 libliquid.so]
|
|
27
|
+
rescue LoadError => e
|
|
28
|
+
@load_error = e
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
attr_reader :load_error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless @load_error
|
|
36
|
+
# ── freqdem (analog FM / frequency demodulator) ────────────────
|
|
37
|
+
attach_function :freqdem_create, [:float], :pointer
|
|
38
|
+
attach_function :freqdem_destroy, [:pointer], :int
|
|
39
|
+
attach_function :freqdem_reset, [:pointer], :int
|
|
40
|
+
attach_function :freqdem_print, [:pointer], :int
|
|
41
|
+
attach_function :freqdem_demodulate_block,
|
|
42
|
+
%i[pointer pointer uint pointer],
|
|
43
|
+
:int
|
|
44
|
+
|
|
45
|
+
# ── msresamp_rrrf (multi-stage arbitrary real resampler) ───────
|
|
46
|
+
attach_function :msresamp_rrrf_create, %i[float float], :pointer
|
|
47
|
+
attach_function :msresamp_rrrf_destroy, [:pointer], :int
|
|
48
|
+
attach_function :msresamp_rrrf_reset, [:pointer], :int
|
|
49
|
+
attach_function :msresamp_rrrf_get_rate, [:pointer], :float
|
|
50
|
+
attach_function :msresamp_rrrf_get_delay, [:pointer], :float
|
|
51
|
+
attach_function :msresamp_rrrf_get_num_output, %i[pointer uint], :uint
|
|
52
|
+
attach_function :msresamp_rrrf_execute,
|
|
53
|
+
%i[pointer pointer uint pointer pointer],
|
|
54
|
+
:int
|
|
55
|
+
|
|
56
|
+
# ── firfilt_rrrf (real FIR filter) ─────────────────────────────
|
|
57
|
+
attach_function :firfilt_rrrf_create_kaiser,
|
|
58
|
+
%i[uint float float float],
|
|
59
|
+
:pointer
|
|
60
|
+
attach_function :firfilt_rrrf_create_dc_blocker,
|
|
61
|
+
%i[uint float],
|
|
62
|
+
:pointer
|
|
63
|
+
attach_function :firfilt_rrrf_destroy, [:pointer], :int
|
|
64
|
+
attach_function :firfilt_rrrf_reset, [:pointer], :int
|
|
65
|
+
attach_function :firfilt_rrrf_execute_block,
|
|
66
|
+
%i[pointer pointer uint pointer],
|
|
67
|
+
:int
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Supported Method Parameters::
|
|
71
|
+
# PWN::FFI::Liquid.available?
|
|
72
|
+
|
|
73
|
+
public_class_method def self.available?
|
|
74
|
+
!@load_error && respond_to?(:freqdem_create, true)
|
|
75
|
+
rescue StandardError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Supported Method Parameters::
|
|
80
|
+
# audio = PWN::FFI::Liquid.freq_demod(
|
|
81
|
+
# iq: 'required - interleaved I/Q Array<Float> (even length)',
|
|
82
|
+
# kf: 'optional - modulation factor (default 0.5)'
|
|
83
|
+
# )
|
|
84
|
+
# Returns Array<Float> of demodulated real samples (length = iq.length/2).
|
|
85
|
+
|
|
86
|
+
public_class_method def self.freq_demod(opts = {})
|
|
87
|
+
raise 'ERROR: libliquid not available' unless available?
|
|
88
|
+
|
|
89
|
+
iq = opts[:iq]
|
|
90
|
+
kf = (opts[:kf] || 0.5).to_f
|
|
91
|
+
n = iq.length / 2
|
|
92
|
+
return [] if n.zero?
|
|
93
|
+
|
|
94
|
+
fd = freqdem_create(kf)
|
|
95
|
+
raise 'ERROR: freqdem_create failed' if fd.null?
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
in_ptr = float_ptr(iq.first(n * 2))
|
|
99
|
+
out_ptr = PubFFI::MemoryPointer.new(:float, n)
|
|
100
|
+
rc = freqdem_demodulate_block(fd, in_ptr, n, out_ptr)
|
|
101
|
+
raise "ERROR: freqdem_demodulate_block rc=#{rc}" unless rc.zero?
|
|
102
|
+
|
|
103
|
+
out_ptr.read_array_of_float(n)
|
|
104
|
+
ensure
|
|
105
|
+
freqdem_destroy(fd)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Supported Method Parameters::
|
|
110
|
+
# out = PWN::FFI::Liquid.resample(
|
|
111
|
+
# samples: 'required - Array<Float>',
|
|
112
|
+
# rate: 'required - output/input ratio (>0)',
|
|
113
|
+
# as_db: 'optional - stop-band attenuation dB (default 60.0)'
|
|
114
|
+
# )
|
|
115
|
+
# Returns Array<Float> of resampled samples.
|
|
116
|
+
|
|
117
|
+
public_class_method def self.resample(opts = {})
|
|
118
|
+
raise 'ERROR: libliquid not available' unless available?
|
|
119
|
+
|
|
120
|
+
samples = opts[:samples]
|
|
121
|
+
rate = opts[:rate].to_f
|
|
122
|
+
as_db = (opts[:as_db] || 60.0).to_f
|
|
123
|
+
raise 'ERROR: rate must be > 0' unless rate.positive?
|
|
124
|
+
return samples.dup if samples.empty?
|
|
125
|
+
|
|
126
|
+
q = msresamp_rrrf_create(rate, as_db)
|
|
127
|
+
raise 'ERROR: msresamp_rrrf_create failed' if q.null?
|
|
128
|
+
|
|
129
|
+
begin
|
|
130
|
+
nx = samples.length
|
|
131
|
+
# allocate generously: ceil(1 + 2*r*nx)
|
|
132
|
+
ny_max = [1, (1 + (2 * rate * nx)).ceil].max
|
|
133
|
+
in_ptr = float_ptr(samples)
|
|
134
|
+
out_ptr = PubFFI::MemoryPointer.new(:float, ny_max)
|
|
135
|
+
ny_ptr = PubFFI::MemoryPointer.new(:uint, 1)
|
|
136
|
+
rc = msresamp_rrrf_execute(q, in_ptr, nx, out_ptr, ny_ptr)
|
|
137
|
+
raise "ERROR: msresamp_rrrf_execute rc=#{rc}" unless rc.zero?
|
|
138
|
+
|
|
139
|
+
ny = ny_ptr.read_uint
|
|
140
|
+
out_ptr.read_array_of_float(ny)
|
|
141
|
+
ensure
|
|
142
|
+
msresamp_rrrf_destroy(q)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Supported Method Parameters::
|
|
147
|
+
# out = PWN::FFI::Liquid.fir_kaiser(
|
|
148
|
+
# samples: 'required - Array<Float>',
|
|
149
|
+
# length: 'optional - filter length (default 51, odd preferred)',
|
|
150
|
+
# fc: 'optional - normalised cutoff 0..0.5 (default 0.1)',
|
|
151
|
+
# as_db: 'optional - stop-band attenuation dB (default 60.0)',
|
|
152
|
+
# mu: 'optional - fractional sample offset (default 0.0)'
|
|
153
|
+
# )
|
|
154
|
+
# Returns Array<Float> same length as input.
|
|
155
|
+
|
|
156
|
+
public_class_method def self.fir_kaiser(opts = {})
|
|
157
|
+
raise 'ERROR: libliquid not available' unless available?
|
|
158
|
+
|
|
159
|
+
samples = opts[:samples]
|
|
160
|
+
length = (opts[:length] || 51).to_i
|
|
161
|
+
fc = (opts[:fc] || 0.1).to_f
|
|
162
|
+
as_db = (opts[:as_db] || 60.0).to_f
|
|
163
|
+
mu = (opts[:mu] || 0.0).to_f
|
|
164
|
+
return [] if samples.empty?
|
|
165
|
+
|
|
166
|
+
q = firfilt_rrrf_create_kaiser(length, fc, as_db, mu)
|
|
167
|
+
raise 'ERROR: firfilt_rrrf_create_kaiser failed' if q.null?
|
|
168
|
+
|
|
169
|
+
begin
|
|
170
|
+
n = samples.length
|
|
171
|
+
in_ptr = float_ptr(samples)
|
|
172
|
+
out_ptr = PubFFI::MemoryPointer.new(:float, n)
|
|
173
|
+
rc = firfilt_rrrf_execute_block(q, in_ptr, n, out_ptr)
|
|
174
|
+
raise "ERROR: firfilt_rrrf_execute_block rc=#{rc}" unless rc.zero?
|
|
175
|
+
|
|
176
|
+
out_ptr.read_array_of_float(n)
|
|
177
|
+
ensure
|
|
178
|
+
firfilt_rrrf_destroy(q)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Supported Method Parameters::
|
|
183
|
+
# out = PWN::FFI::Liquid.dc_block(
|
|
184
|
+
# samples: 'required - Array<Float>',
|
|
185
|
+
# m: 'optional - prototype semi-length (default 7 → len 15)',
|
|
186
|
+
# as_db: 'optional - stop-band attenuation dB (default 60.0)'
|
|
187
|
+
# )
|
|
188
|
+
|
|
189
|
+
public_class_method def self.dc_block(opts = {})
|
|
190
|
+
raise 'ERROR: libliquid not available' unless available?
|
|
191
|
+
|
|
192
|
+
samples = opts[:samples]
|
|
193
|
+
m = (opts[:m] || 7).to_i
|
|
194
|
+
as_db = (opts[:as_db] || 60.0).to_f
|
|
195
|
+
return [] if samples.empty?
|
|
196
|
+
|
|
197
|
+
q = firfilt_rrrf_create_dc_blocker(m, as_db)
|
|
198
|
+
raise 'ERROR: firfilt_rrrf_create_dc_blocker failed' if q.null?
|
|
199
|
+
|
|
200
|
+
begin
|
|
201
|
+
n = samples.length
|
|
202
|
+
in_ptr = float_ptr(samples)
|
|
203
|
+
out_ptr = PubFFI::MemoryPointer.new(:float, n)
|
|
204
|
+
rc = firfilt_rrrf_execute_block(q, in_ptr, n, out_ptr)
|
|
205
|
+
raise "ERROR: firfilt_rrrf_execute_block rc=#{rc}" unless rc.zero?
|
|
206
|
+
|
|
207
|
+
out_ptr.read_array_of_float(n)
|
|
208
|
+
ensure
|
|
209
|
+
firfilt_rrrf_destroy(q)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
214
|
+
|
|
215
|
+
public_class_method def self.authors
|
|
216
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Display Usage for this Module
|
|
220
|
+
|
|
221
|
+
public_class_method def self.help
|
|
222
|
+
puts "USAGE:
|
|
223
|
+
#{self}.available? # => true/false
|
|
224
|
+
#{self}.freq_demod(iq:, kf: 0.5) # FM I/Q → audio
|
|
225
|
+
#{self}.resample(samples:, rate:, as_db: 60.0) # arbitrary rate
|
|
226
|
+
#{self}.fir_kaiser(samples:, length: 51, fc: 0.1, as_db: 60.0)
|
|
227
|
+
#{self}.dc_block(samples:, m: 7, as_db: 60.0)
|
|
228
|
+
|
|
229
|
+
#{self}.authors
|
|
230
|
+
"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
class << self
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def float_ptr(arr)
|
|
237
|
+
ptr = PubFFI::MemoryPointer.new(:float, arr.length)
|
|
238
|
+
ptr.write_array_of_float(arr.map(&:to_f))
|
|
239
|
+
ptr
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|