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/ai/open_ai.rb
CHANGED
|
@@ -250,17 +250,21 @@ module PWN
|
|
|
250
250
|
end
|
|
251
251
|
messages.push(user_role)
|
|
252
252
|
|
|
253
|
-
# `max_tokens`
|
|
254
|
-
#
|
|
253
|
+
# Wire config key `max_tokens` (kept for cross-engine naming parity with
|
|
254
|
+
# Anthropic/Gemini) to the OpenAI wire-format field `max_completion_tokens`.
|
|
255
|
+
# OpenAI deprecated the request-body key `max_tokens` on /v1/chat/completions
|
|
256
|
+
# in favour of `max_completion_tokens`, which works for every chat model
|
|
255
257
|
# including the reasoning family. Don't try to guess per-model caps —
|
|
256
|
-
# let the server clamp; default to a generous ceiling that the
|
|
257
|
-
#
|
|
258
|
-
|
|
258
|
+
# let the server clamp; default to a generous ceiling that the operator
|
|
259
|
+
# can override via PWN::Env[:ai][:openai][:max_tokens].
|
|
260
|
+
# Accept legacy :max_completion_tokens as a one-release alias so existing
|
|
261
|
+
# pwn.yaml files keep working without a silent clamp to the default.
|
|
262
|
+
max_tokens = (engine[:max_tokens] || engine[:max_completion_tokens] || 16_384).to_i
|
|
259
263
|
|
|
260
264
|
http_body = {
|
|
261
265
|
model: model,
|
|
262
266
|
messages: messages,
|
|
263
|
-
max_completion_tokens:
|
|
267
|
+
max_completion_tokens: max_tokens
|
|
264
268
|
}
|
|
265
269
|
# Reasoning models reject sampler params (temperature, top_p, etc.)
|
|
266
270
|
http_body[:temperature] = temp unless reasoning
|
data/lib/pwn/config.rb
CHANGED
|
@@ -29,7 +29,7 @@ module PWN
|
|
|
29
29
|
env = {
|
|
30
30
|
ai: {
|
|
31
31
|
active: 'grok',
|
|
32
|
-
|
|
32
|
+
module_reflection: false,
|
|
33
33
|
grok: {
|
|
34
34
|
base_uri: 'optional - Base URI for Grok - Use private base OR defaults to https://api.x.ai/v1',
|
|
35
35
|
key: 'required - xAI Grok API Key',
|
|
@@ -58,6 +58,7 @@ module PWN
|
|
|
58
58
|
model: 'optional - OpenAI model to use',
|
|
59
59
|
system_role_content: 'You are an ethically hacking OpenAI agent.',
|
|
60
60
|
temp: 'optional - OpenAI temperature',
|
|
61
|
+
max_tokens: 'optional - Max output tokens per response (default 16384). Mapped to OpenAI wire param max_completion_tokens.',
|
|
61
62
|
max_prompt_length: 128_000
|
|
62
63
|
},
|
|
63
64
|
ollama: {
|
|
@@ -90,9 +91,9 @@ module PWN
|
|
|
90
91
|
max_iters: 25,
|
|
91
92
|
# Swarm (agent_ask/agent_debate) sub-agent recursion cap
|
|
92
93
|
max_depth: 3,
|
|
93
|
-
# run PWN::AI::Agent::Learning.
|
|
94
|
-
|
|
95
|
-
# also run PWN::AI::Agent::Extrospection.auto_extrospect from
|
|
94
|
+
# run PWN::AI::Agent::Learning.auto_introspect after every final answer
|
|
95
|
+
auto_introspect: true,
|
|
96
|
+
# also run PWN::AI::Agent::Extrospection.auto_extrospect from auto_introspect
|
|
96
97
|
auto_extrospect: false,
|
|
97
98
|
toolsets: nil
|
|
98
99
|
# multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
|
|
@@ -0,0 +1,498 @@
|
|
|
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 libiio binding specialised for the ADALM-PLUTO (AD9363).
|
|
10
|
+
#
|
|
11
|
+
# Control-plane + blocking RX of interleaved CS16 I/Q so
|
|
12
|
+
# PWN::SDR::Decoder::* can pull MHz-rate complex samples without
|
|
13
|
+
# GQRX's 48 kHz audio tap and without shelling out to iio_*.
|
|
14
|
+
# libad9361 is used only for optional bb_rate helpers when present.
|
|
15
|
+
#
|
|
16
|
+
# If libiio is missing `.available?` is false and callers fall back
|
|
17
|
+
# to RTLSdr / HackRF / SoapySDR / pure-Ruby detector paths.
|
|
18
|
+
#
|
|
19
|
+
# Default URI tries USB (local) first: "ip:192.168.2.1" is the stock
|
|
20
|
+
# Ethernet/USB-gadget address of an unconfigured Pluto.
|
|
21
|
+
module AdalmPluto
|
|
22
|
+
extend PubFFI::Library
|
|
23
|
+
|
|
24
|
+
@load_error = nil
|
|
25
|
+
begin
|
|
26
|
+
ffi_lib %w[iio libiio.so.0 libiio.so]
|
|
27
|
+
rescue LoadError => e
|
|
28
|
+
@load_error = e
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
attr_reader :load_error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Optional libad9361 (bb rate convenience). Soft-fail.
|
|
36
|
+
@ad9361_load_error = nil
|
|
37
|
+
# Optional soft binding to libad9361 (bb_rate helper only).
|
|
38
|
+
module Ad9361
|
|
39
|
+
extend PubFFI::Library
|
|
40
|
+
|
|
41
|
+
@load_error = nil
|
|
42
|
+
begin
|
|
43
|
+
ffi_lib %w[ad9361 libad9361.so.0 libad9361.so]
|
|
44
|
+
rescue LoadError => e
|
|
45
|
+
@load_error = e
|
|
46
|
+
end
|
|
47
|
+
class << self
|
|
48
|
+
attr_reader :load_error
|
|
49
|
+
end
|
|
50
|
+
attach_function :ad9361_set_bb_rate, %i[pointer ulong], :int unless @load_error
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless @load_error
|
|
54
|
+
# ── library / strerror ─────────────────────────────────────────
|
|
55
|
+
attach_function :iio_library_get_version,
|
|
56
|
+
%i[pointer pointer pointer],
|
|
57
|
+
:void
|
|
58
|
+
attach_function :iio_strerror, %i[int pointer size_t], :void
|
|
59
|
+
|
|
60
|
+
# ── scan / enumerate ───────────────────────────────────────────
|
|
61
|
+
attach_function :iio_create_scan_context, %i[string int], :pointer
|
|
62
|
+
attach_function :iio_scan_context_destroy, [:pointer], :void
|
|
63
|
+
attach_function :iio_scan_context_get_info_list,
|
|
64
|
+
%i[pointer pointer],
|
|
65
|
+
:ssize_t
|
|
66
|
+
attach_function :iio_context_info_list_free, [:pointer], :void
|
|
67
|
+
attach_function :iio_context_info_get_description, [:pointer], :string
|
|
68
|
+
attach_function :iio_context_info_get_uri, [:pointer], :string
|
|
69
|
+
|
|
70
|
+
# ── context ────────────────────────────────────────────────────
|
|
71
|
+
attach_function :iio_create_default_context, [], :pointer
|
|
72
|
+
attach_function :iio_create_context_from_uri, [:string], :pointer
|
|
73
|
+
attach_function :iio_create_network_context, [:string], :pointer
|
|
74
|
+
attach_function :iio_context_destroy, [:pointer], :void
|
|
75
|
+
attach_function :iio_context_get_name, [:pointer], :string
|
|
76
|
+
attach_function :iio_context_get_description, [:pointer], :string
|
|
77
|
+
attach_function :iio_context_get_devices_count, [:pointer], :uint
|
|
78
|
+
attach_function :iio_context_get_device, %i[pointer uint], :pointer
|
|
79
|
+
attach_function :iio_context_find_device, %i[pointer string], :pointer
|
|
80
|
+
attach_function :iio_context_set_timeout, %i[pointer uint], :int
|
|
81
|
+
|
|
82
|
+
# ── device ─────────────────────────────────────────────────────
|
|
83
|
+
attach_function :iio_device_get_id, [:pointer], :string
|
|
84
|
+
attach_function :iio_device_get_name, [:pointer], :string
|
|
85
|
+
attach_function :iio_device_get_channels_count, [:pointer], :uint
|
|
86
|
+
attach_function :iio_device_get_channel, %i[pointer uint], :pointer
|
|
87
|
+
attach_function :iio_device_find_channel,
|
|
88
|
+
%i[pointer string bool],
|
|
89
|
+
:pointer
|
|
90
|
+
attach_function :iio_device_attr_write,
|
|
91
|
+
%i[pointer string string],
|
|
92
|
+
:ssize_t
|
|
93
|
+
attach_function :iio_device_attr_write_longlong,
|
|
94
|
+
%i[pointer string long_long],
|
|
95
|
+
:int
|
|
96
|
+
attach_function :iio_device_attr_read,
|
|
97
|
+
%i[pointer string pointer size_t],
|
|
98
|
+
:ssize_t
|
|
99
|
+
attach_function :iio_device_attr_read_longlong,
|
|
100
|
+
%i[pointer string pointer],
|
|
101
|
+
:int
|
|
102
|
+
attach_function :iio_device_create_buffer,
|
|
103
|
+
%i[pointer size_t bool],
|
|
104
|
+
:pointer
|
|
105
|
+
|
|
106
|
+
# ── channel ────────────────────────────────────────────────────
|
|
107
|
+
attach_function :iio_channel_get_id, [:pointer], :string
|
|
108
|
+
attach_function :iio_channel_get_name, [:pointer], :string
|
|
109
|
+
attach_function :iio_channel_is_output, [:pointer], :bool
|
|
110
|
+
attach_function :iio_channel_is_scan_element, [:pointer], :bool
|
|
111
|
+
attach_function :iio_channel_enable, [:pointer], :void
|
|
112
|
+
attach_function :iio_channel_disable, [:pointer], :void
|
|
113
|
+
attach_function :iio_channel_attr_write,
|
|
114
|
+
%i[pointer string string],
|
|
115
|
+
:ssize_t
|
|
116
|
+
attach_function :iio_channel_attr_write_longlong,
|
|
117
|
+
%i[pointer string long_long],
|
|
118
|
+
:int
|
|
119
|
+
attach_function :iio_channel_attr_read_longlong,
|
|
120
|
+
%i[pointer string pointer],
|
|
121
|
+
:int
|
|
122
|
+
|
|
123
|
+
# ── buffer ─────────────────────────────────────────────────────
|
|
124
|
+
attach_function :iio_buffer_destroy, [:pointer], :void
|
|
125
|
+
attach_function :iio_buffer_refill, [:pointer], :ssize_t
|
|
126
|
+
attach_function :iio_buffer_start, [:pointer], :pointer
|
|
127
|
+
attach_function :iio_buffer_end, [:pointer], :pointer
|
|
128
|
+
attach_function :iio_buffer_step, [:pointer], :ptrdiff_t
|
|
129
|
+
attach_function :iio_buffer_first, %i[pointer pointer], :pointer
|
|
130
|
+
attach_function :iio_buffer_set_blocking_mode, %i[pointer bool], :int
|
|
131
|
+
attach_function :iio_buffer_cancel, [:pointer], :void
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
DEFAULT_URI = 'ip:192.168.2.1'
|
|
135
|
+
PHY_NAME = 'ad9361-phy'
|
|
136
|
+
RX_NAME = 'cf-ad9361-lpc'
|
|
137
|
+
# Stock Pluto RX LO channel
|
|
138
|
+
RX_LO_NAME = 'altvoltage0'
|
|
139
|
+
|
|
140
|
+
# Supported Method Parameters::
|
|
141
|
+
# PWN::FFI::AdalmPluto.available?
|
|
142
|
+
|
|
143
|
+
public_class_method def self.available?
|
|
144
|
+
!@load_error && respond_to?(:iio_create_default_context, true)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
false
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Supported Method Parameters::
|
|
150
|
+
# info = PWN::FFI::AdalmPluto.info
|
|
151
|
+
# Returns { available:, major:, minor:, git_tag:, ad9361: }
|
|
152
|
+
|
|
153
|
+
public_class_method def self.info
|
|
154
|
+
return { available: false, error: @load_error&.message } unless available?
|
|
155
|
+
|
|
156
|
+
maj = PubFFI::MemoryPointer.new(:uint)
|
|
157
|
+
min = PubFFI::MemoryPointer.new(:uint)
|
|
158
|
+
# git_tag is a caller-owned char[8] buffer (NOT char**).
|
|
159
|
+
git = PubFFI::MemoryPointer.new(:char, 8)
|
|
160
|
+
git.put_bytes(0, "\0" * 8)
|
|
161
|
+
iio_library_get_version(maj, min, git)
|
|
162
|
+
tag = git.read_string
|
|
163
|
+
{
|
|
164
|
+
available: true,
|
|
165
|
+
major: maj.read_uint,
|
|
166
|
+
minor: min.read_uint,
|
|
167
|
+
git_tag: tag,
|
|
168
|
+
ad9361: Ad9361.load_error.nil? && Ad9361.respond_to?(:ad9361_set_bb_rate, true)
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Supported Method Parameters::
|
|
173
|
+
# uris = PWN::FFI::AdalmPluto.list_uris(backends: 'usb,ip,local')
|
|
174
|
+
# Returns Array<Hash> of { uri:, description: }
|
|
175
|
+
|
|
176
|
+
public_class_method def self.list_uris(opts = {})
|
|
177
|
+
raise 'ERROR: libiio not available' unless available?
|
|
178
|
+
|
|
179
|
+
backends = (opts[:backends] || 'usb,ip,local').to_s
|
|
180
|
+
scan = iio_create_scan_context(backends, 0)
|
|
181
|
+
return [] if scan.null?
|
|
182
|
+
|
|
183
|
+
info_ptr = PubFFI::MemoryPointer.new(:pointer)
|
|
184
|
+
info_ptr.write_pointer(PubFFI::Pointer::NULL)
|
|
185
|
+
begin
|
|
186
|
+
n = iio_scan_context_get_info_list(scan, info_ptr)
|
|
187
|
+
return [] if n <= 0
|
|
188
|
+
|
|
189
|
+
base = info_ptr.read_pointer
|
|
190
|
+
return [] if base.null?
|
|
191
|
+
|
|
192
|
+
Array.new(n) do |i|
|
|
193
|
+
entry = base.get_pointer(i * PubFFI::Pointer.size)
|
|
194
|
+
next { uri: '', description: '' } if entry.null?
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
uri: iio_context_info_get_uri(entry).to_s,
|
|
198
|
+
description: iio_context_info_get_description(entry).to_s
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
ensure
|
|
202
|
+
base = info_ptr.read_pointer
|
|
203
|
+
iio_context_info_list_free(base) if base && !base.null?
|
|
204
|
+
iio_scan_context_destroy(scan)
|
|
205
|
+
end
|
|
206
|
+
rescue StandardError
|
|
207
|
+
[]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Supported Method Parameters::
|
|
211
|
+
# ctx = PWN::FFI::AdalmPluto.open(uri: 'ip:192.168.2.1')
|
|
212
|
+
# Returns opaque iio_context* pointer. ALWAYS pair with .close.
|
|
213
|
+
|
|
214
|
+
public_class_method def self.open(opts = {})
|
|
215
|
+
raise 'ERROR: libiio not available' unless available?
|
|
216
|
+
|
|
217
|
+
uri = opts[:uri]
|
|
218
|
+
ctx =
|
|
219
|
+
if uri.to_s.empty?
|
|
220
|
+
# try default USB/local first, then stock IP
|
|
221
|
+
c = iio_create_default_context
|
|
222
|
+
(c.null? ? iio_create_context_from_uri(DEFAULT_URI) : c)
|
|
223
|
+
else
|
|
224
|
+
iio_create_context_from_uri(uri.to_s)
|
|
225
|
+
end
|
|
226
|
+
raise "ERROR: iio_create_context failed for #{uri.inspect}" if ctx.null?
|
|
227
|
+
|
|
228
|
+
iio_context_set_timeout(ctx, (opts[:timeout_ms] || 5_000).to_i)
|
|
229
|
+
ctx
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Supported Method Parameters::
|
|
233
|
+
# PWN::FFI::AdalmPluto.close(context: pointer)
|
|
234
|
+
|
|
235
|
+
public_class_method def self.close(opts = {})
|
|
236
|
+
ctx = opts[:context]
|
|
237
|
+
return unless ctx && !ctx.null?
|
|
238
|
+
|
|
239
|
+
iio_context_destroy(ctx)
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Supported Method Parameters::
|
|
244
|
+
# meta = PWN::FFI::AdalmPluto.device_info(context: pointer)
|
|
245
|
+
# Returns { name:, description:, devices: [{id:, name:}, ...] }
|
|
246
|
+
|
|
247
|
+
public_class_method def self.device_info(opts = {})
|
|
248
|
+
ctx = opts[:context]
|
|
249
|
+
raise 'ERROR: :context required' if ctx.nil? || ctx.null?
|
|
250
|
+
|
|
251
|
+
n = iio_context_get_devices_count(ctx)
|
|
252
|
+
devices = Array.new(n) do |i|
|
|
253
|
+
d = iio_context_get_device(ctx, i)
|
|
254
|
+
{
|
|
255
|
+
id: iio_device_get_id(d).to_s,
|
|
256
|
+
name: (iio_device_get_name(d) || '').to_s
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
{
|
|
260
|
+
name: iio_context_get_name(ctx).to_s,
|
|
261
|
+
description: iio_context_get_description(ctx).to_s,
|
|
262
|
+
devices: devices
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Supported Method Parameters::
|
|
267
|
+
# PWN::FFI::AdalmPluto.configure(
|
|
268
|
+
# context: 'required - pointer from .open',
|
|
269
|
+
# freq_hz: 'required - RX LO frequency Hz',
|
|
270
|
+
# rate_hz: 'optional - sample rate (default 2_500_000)',
|
|
271
|
+
# bw_hz: 'optional - RF bandwidth (default = rate_hz)',
|
|
272
|
+
# gain_db: 'optional - manual gain dB; nil = slow_attack AGC',
|
|
273
|
+
# gain_mode:'optional - slow_attack|fast_attack|manual|hybrid'
|
|
274
|
+
# )
|
|
275
|
+
|
|
276
|
+
public_class_method def self.configure(opts = {}) # rubocop:disable Naming/PredicateMethod
|
|
277
|
+
ctx = opts[:context]
|
|
278
|
+
raise 'ERROR: :context required' if ctx.nil? || ctx.null?
|
|
279
|
+
|
|
280
|
+
freq = opts[:freq_hz].to_i
|
|
281
|
+
rate = (opts[:rate_hz] || 2_500_000).to_i
|
|
282
|
+
bw = (opts[:bw_hz] || rate).to_i
|
|
283
|
+
raise 'ERROR: :freq_hz required' if freq <= 0
|
|
284
|
+
|
|
285
|
+
phy = iio_context_find_device(ctx, PHY_NAME)
|
|
286
|
+
raise "ERROR: phy device #{PHY_NAME.inspect} not found" if phy.null?
|
|
287
|
+
|
|
288
|
+
# RX LO
|
|
289
|
+
lo = iio_device_find_channel(phy, RX_LO_NAME, true) # output channel
|
|
290
|
+
if lo.null?
|
|
291
|
+
# some firmwares expose "RX_LO"
|
|
292
|
+
lo = iio_device_find_channel(phy, 'RX_LO', true)
|
|
293
|
+
end
|
|
294
|
+
raise 'ERROR: RX LO channel not found' if lo.null?
|
|
295
|
+
|
|
296
|
+
check_ssize!(iio_channel_attr_write_longlong(lo, 'frequency', freq), 'RX LO frequency')
|
|
297
|
+
|
|
298
|
+
# Sampling frequency + RF bandwidth on voltage0 (RX)
|
|
299
|
+
rx_chn = iio_device_find_channel(phy, 'voltage0', false)
|
|
300
|
+
raise 'ERROR: phy voltage0 (RX) not found' if rx_chn.null?
|
|
301
|
+
|
|
302
|
+
check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'sampling_frequency', rate), 'sampling_frequency')
|
|
303
|
+
check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'rf_bandwidth', bw), 'rf_bandwidth')
|
|
304
|
+
|
|
305
|
+
gain_mode = opts[:gain_mode]
|
|
306
|
+
if opts.key?(:gain_db) && !opts[:gain_db].nil?
|
|
307
|
+
gain_mode ||= 'manual'
|
|
308
|
+
check_ssize!(iio_channel_attr_write(rx_chn, 'gain_control_mode', gain_mode), 'gain_control_mode')
|
|
309
|
+
check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'hardwaregain', opts[:gain_db].to_i), 'hardwaregain')
|
|
310
|
+
else
|
|
311
|
+
gain_mode ||= 'slow_attack'
|
|
312
|
+
check_ssize!(iio_channel_attr_write(rx_chn, 'gain_control_mode', gain_mode), 'gain_control_mode')
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Prefer libad9361 bb_rate when linked (programs FIR + HB chain)
|
|
316
|
+
if Ad9361.load_error.nil? && Ad9361.respond_to?(:ad9361_set_bb_rate, true)
|
|
317
|
+
begin
|
|
318
|
+
Ad9361.ad9361_set_bb_rate(phy, rate)
|
|
319
|
+
rescue StandardError
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
true
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Supported Method Parameters::
|
|
328
|
+
# handle = PWN::FFI::AdalmPluto.start_rx(
|
|
329
|
+
# context: 'required - pointer from .open',
|
|
330
|
+
# samples: 'optional - samples-per-refill (default 262144)',
|
|
331
|
+
# cyclic: 'optional - cyclic buffer (default false)'
|
|
332
|
+
# )
|
|
333
|
+
# Returns { buffer:, rx:, i_chn:, q_chn:, samples: } — pair with .stop_rx.
|
|
334
|
+
|
|
335
|
+
public_class_method def self.start_rx(opts = {})
|
|
336
|
+
ctx = opts[:context]
|
|
337
|
+
raise 'ERROR: :context required' if ctx.nil? || ctx.null?
|
|
338
|
+
|
|
339
|
+
nsamps = (opts[:samples] || 262_144).to_i
|
|
340
|
+
rx = iio_context_find_device(ctx, RX_NAME)
|
|
341
|
+
raise "ERROR: RX streaming device #{RX_NAME.inspect} not found" if rx.null?
|
|
342
|
+
|
|
343
|
+
# Enable voltage0 (I) + voltage1 (Q) scan elements
|
|
344
|
+
i_chn = iio_device_find_channel(rx, 'voltage0', false)
|
|
345
|
+
q_chn = iio_device_find_channel(rx, 'voltage1', false)
|
|
346
|
+
raise 'ERROR: RX I/Q channels not found' if i_chn.null? || q_chn.null?
|
|
347
|
+
|
|
348
|
+
iio_channel_enable(i_chn)
|
|
349
|
+
iio_channel_enable(q_chn)
|
|
350
|
+
|
|
351
|
+
buf = iio_device_create_buffer(rx, nsamps, opts[:cyclic] ? true : false)
|
|
352
|
+
raise 'ERROR: iio_device_create_buffer failed' if buf.null?
|
|
353
|
+
|
|
354
|
+
iio_buffer_set_blocking_mode(buf, true)
|
|
355
|
+
{ buffer: buf, rx: rx, i_chn: i_chn, q_chn: q_chn, samples: nsamps }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Supported Method Parameters::
|
|
359
|
+
# iq_cs16 = PWN::FFI::AdalmPluto.read_sync(handle: hash_from_start_rx)
|
|
360
|
+
# Returns binary String of interleaved little-endian signed-16 I/Q
|
|
361
|
+
# (I0,Q0,I1,Q1,…) — same contract as a cs16 capture file.
|
|
362
|
+
|
|
363
|
+
public_class_method def self.read_sync(opts = {})
|
|
364
|
+
h = opts[:handle]
|
|
365
|
+
raise 'ERROR: :handle required' unless h.is_a?(Hash)
|
|
366
|
+
|
|
367
|
+
buf = h[:buffer]
|
|
368
|
+
raise 'ERROR: handle missing :buffer' if buf.nil? || buf.null?
|
|
369
|
+
|
|
370
|
+
nbytes = iio_buffer_refill(buf)
|
|
371
|
+
raise "ERROR: iio_buffer_refill rc=#{nbytes}" if nbytes.negative?
|
|
372
|
+
|
|
373
|
+
start = iio_buffer_start(buf)
|
|
374
|
+
# Pluto packs I then Q as sequential int16 scan elements; step covers both.
|
|
375
|
+
start.read_string(nbytes)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Supported Method Parameters::
|
|
379
|
+
# PWN::FFI::AdalmPluto.stop_rx(handle: hash_from_start_rx)
|
|
380
|
+
|
|
381
|
+
public_class_method def self.stop_rx(opts = {})
|
|
382
|
+
h = opts[:handle]
|
|
383
|
+
return unless h.is_a?(Hash)
|
|
384
|
+
|
|
385
|
+
buf = h[:buffer]
|
|
386
|
+
if buf && !buf.null?
|
|
387
|
+
begin
|
|
388
|
+
iio_buffer_cancel(buf)
|
|
389
|
+
rescue StandardError
|
|
390
|
+
nil
|
|
391
|
+
end
|
|
392
|
+
iio_buffer_destroy(buf)
|
|
393
|
+
end
|
|
394
|
+
nil
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# High-level one-shot: open → configure → start_rx → N refills → stop → close.
|
|
398
|
+
# Supported Method Parameters::
|
|
399
|
+
# iq = PWN::FFI::AdalmPluto.capture(
|
|
400
|
+
# freq_hz: 'required',
|
|
401
|
+
# rate_hz: 2_500_000,
|
|
402
|
+
# bytes: 1_048_576, # approximate payload size
|
|
403
|
+
# uri: nil,
|
|
404
|
+
# gain_db: nil
|
|
405
|
+
# )
|
|
406
|
+
# Returns { iq_cs16: String, rate_hz:, freq_hz:, samples: }
|
|
407
|
+
|
|
408
|
+
public_class_method def self.capture(opts = {})
|
|
409
|
+
raise 'ERROR: :freq_hz required' unless opts[:freq_hz]
|
|
410
|
+
|
|
411
|
+
rate = (opts[:rate_hz] || 2_500_000).to_i
|
|
412
|
+
want = (opts[:bytes] || 1_048_576).to_i
|
|
413
|
+
ctx = self.open(uri: opts[:uri], timeout_ms: opts[:timeout_ms])
|
|
414
|
+
handle = nil
|
|
415
|
+
begin
|
|
416
|
+
configure(
|
|
417
|
+
context: ctx,
|
|
418
|
+
freq_hz: opts[:freq_hz],
|
|
419
|
+
rate_hz: rate,
|
|
420
|
+
bw_hz: opts[:bw_hz],
|
|
421
|
+
gain_db: opts[:gain_db],
|
|
422
|
+
gain_mode: opts[:gain_mode]
|
|
423
|
+
)
|
|
424
|
+
# each sample = 4 bytes (I s16 + Q s16)
|
|
425
|
+
nsamps = (want / 4).clamp(4_096, 1_048_576)
|
|
426
|
+
handle = start_rx(context: ctx, samples: nsamps)
|
|
427
|
+
chunks = []
|
|
428
|
+
got = 0
|
|
429
|
+
while got < want
|
|
430
|
+
chunk = read_sync(handle: handle)
|
|
431
|
+
break if chunk.bytesize.zero?
|
|
432
|
+
|
|
433
|
+
chunks << chunk
|
|
434
|
+
got += chunk.bytesize
|
|
435
|
+
end
|
|
436
|
+
{
|
|
437
|
+
iq_cs16: chunks.join,
|
|
438
|
+
rate_hz: rate,
|
|
439
|
+
freq_hz: opts[:freq_hz].to_i,
|
|
440
|
+
samples: got / 4
|
|
441
|
+
}
|
|
442
|
+
ensure
|
|
443
|
+
stop_rx(handle: handle) if handle
|
|
444
|
+
close(context: ctx)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
449
|
+
|
|
450
|
+
public_class_method def self.authors
|
|
451
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Display Usage for this Module
|
|
455
|
+
|
|
456
|
+
public_class_method def self.help
|
|
457
|
+
puts "USAGE:
|
|
458
|
+
#{self}.available?
|
|
459
|
+
#{self}.info
|
|
460
|
+
#{self}.list_uris
|
|
461
|
+
ctx = #{self}.open(uri: 'ip:192.168.2.1') # or nil for default
|
|
462
|
+
#{self}.device_info(context: ctx)
|
|
463
|
+
#{self}.configure(context: ctx, freq_hz:, rate_hz: 2_500_000, gain_db: nil)
|
|
464
|
+
h = #{self}.start_rx(context: ctx, samples: 262_144)
|
|
465
|
+
iq = #{self}.read_sync(handle: h) # cs16le interleaved String
|
|
466
|
+
#{self}.stop_rx(handle: h)
|
|
467
|
+
#{self}.close(context: ctx)
|
|
468
|
+
|
|
469
|
+
# one-shot
|
|
470
|
+
cap = #{self}.capture(freq_hz: 1090e6, rate_hz: 2_500_000, bytes: 1_048_576)
|
|
471
|
+
|
|
472
|
+
#{self}.authors
|
|
473
|
+
"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
class << self
|
|
477
|
+
private
|
|
478
|
+
|
|
479
|
+
def check_ssize!(rc, what) # rubocop:disable Naming/MethodParameterName
|
|
480
|
+
return if rc.is_a?(Integer) && rc >= 0
|
|
481
|
+
|
|
482
|
+
msg = strerror((-rc).to_i)
|
|
483
|
+
raise "ERROR: AdalmPluto #{what} rc=#{rc} (#{msg})"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def strerror(err)
|
|
487
|
+
return err.to_s unless available?
|
|
488
|
+
|
|
489
|
+
buf = PubFFI::MemoryPointer.new(:char, 256)
|
|
490
|
+
iio_strerror(err, buf, 256)
|
|
491
|
+
buf.read_string
|
|
492
|
+
rescue StandardError
|
|
493
|
+
err.to_s
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|