pwn 0.5.627 → 0.5.628
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_todo.yml +20 -30
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/lib/pwn/ai/agent/dispatch.rb +76 -4
- data/lib/pwn/ai/agent/learning.rb +124 -1
- data/lib/pwn/ai/agent/loop.rb +104 -12
- data/lib/pwn/ai/agent/metrics.rb +60 -27
- data/lib/pwn/ai/agent/prompt_builder.rb +63 -12
- data/lib/pwn/ai/agent/reflect.rb +41 -7
- data/lib/pwn/ai/agent/registry.rb +69 -4
- data/lib/pwn/ai/ollama.rb +39 -10
- data/lib/pwn/config.rb +11 -0
- data/lib/pwn/ffi/liquid.rb +179 -0
- data/lib/pwn/memory.rb +15 -0
- data/lib/pwn/memory_index.rb +233 -0
- data/lib/pwn/sdr/decoder/bluetooth.rb +141 -75
- data/lib/pwn/sdr/decoder/dect.rb +89 -63
- data/lib/pwn/sdr/decoder/dsp.rb +443 -0
- data/lib/pwn/sdr/decoder/gps.rb +93 -73
- data/lib/pwn/sdr/decoder/gsm.rb +167 -63
- data/lib/pwn/sdr/decoder/lora.rb +118 -65
- data/lib/pwn/sdr/decoder/lte.rb +188 -65
- data/lib/pwn/sdr/decoder/p25.rb +83 -67
- data/lib/pwn/sdr/decoder/zigbee.rb +140 -67
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +1 -0
- data/spec/lib/pwn/memory_index_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +65 -8
- metadata +6 -4
|
@@ -3,66 +3,144 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# IEEE 802.15.4 O-QPSK (2.4 GHz ZigBee/Thread) true-air decoder.
|
|
7
|
+
#
|
|
8
|
+
# 2 Mchip/s half-sine O-QPSK ≡ MSK, so I/Q → PWN::FFI::Liquid
|
|
9
|
+
# gmskdem (BT=0.5) at 2 Msps → chip stream. Each 4-bit symbol maps
|
|
10
|
+
# to a 32-chip PN sequence (Table 73, IEEE 802.15.4-2011); soft-
|
|
11
|
+
# correlate every 32 chips against the 16 sequences → symbols →
|
|
12
|
+
# nibbles → bytes. Hunt SHR (4×0x00 preamble + SFD 0xA7) → PHR len
|
|
13
|
+
# → MHR (FCF/seq/PAN/addr) → FCS (CRC-16-KERMIT). Emits per-frame
|
|
14
|
+
# {pan_id:, src:, dst:, frame_type:, len:, fcs_ok:}.
|
|
9
15
|
module ZigBee
|
|
10
|
-
|
|
16
|
+
CHIP_RATE = 2_000_000
|
|
17
|
+
# 16 × 32-chip PN sequences (symbol 0..15). Each row is one 32-bit
|
|
18
|
+
# word; chips are LSB-first (c0 = bit0).
|
|
19
|
+
PN32 = [
|
|
20
|
+
0xD9C3522E, 0xED9C3522, 0x2ED9C352, 0x22ED9C35,
|
|
21
|
+
0x522ED9C3, 0x3522ED9C, 0xC3522ED9, 0x9C3522ED,
|
|
22
|
+
0x8C96077B, 0xB8C96077, 0x7B8C9607, 0x77B8C960,
|
|
23
|
+
0x077B8C96, 0x6077B8C9, 0x96077B8C, 0xC96077B8
|
|
24
|
+
].freeze
|
|
25
|
+
PN_CHIPS = PN32.map { |w| Array.new(32) { |i| (w >> i) & 1 } }.freeze
|
|
26
|
+
SFD = 0xA7
|
|
27
|
+
FRAME_TYPE = { 0 => 'Beacon', 1 => 'Data', 2 => 'ACK', 3 => 'MAC-Cmd' }.freeze
|
|
28
|
+
|
|
29
|
+
# Streaming O-QPSK/MSK chip demod for Base.run_iq — I/Q → 802.15.4 MPDU.
|
|
11
30
|
class DemodIQ
|
|
12
|
-
def initialize(rate:,
|
|
13
|
-
@rate
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
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
|
|
31
|
+
def initialize(rate:, channel: nil)
|
|
32
|
+
@rate = rate.to_f
|
|
33
|
+
@channel = channel
|
|
34
|
+
@chips = []
|
|
35
|
+
@seen = {}
|
|
23
36
|
end
|
|
24
37
|
|
|
25
38
|
def feed_iq(samples, rate: nil, &)
|
|
26
39
|
@rate = rate.to_f if rate
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
chips = PWN::SDR::Decoder::DSP.gfsk_slice(
|
|
41
|
+
iq: samples, rate: @rate, baud: CHIP_RATE, bt: 0.5
|
|
42
|
+
)
|
|
43
|
+
@chips.concat(chips)
|
|
44
|
+
scan(&) if block_given?
|
|
45
|
+
@chips.shift(@chips.length - 8192) if @chips.length > 65_536
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def correlate_symbol(win)
|
|
51
|
+
best = [0, -1]
|
|
52
|
+
PN_CHIPS.each_with_index do |pn, sym|
|
|
53
|
+
# ±1 correlation, ignore chip0/16 (Q-branch alignment tolerance)
|
|
54
|
+
s = 0
|
|
55
|
+
32.times { |i| s += (win[i] == pn[i] ? 1 : -1) }
|
|
56
|
+
best = [sym, s] if s > best[1]
|
|
57
|
+
end
|
|
58
|
+
best
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def chips_to_bytes(chips)
|
|
62
|
+
nsym = chips.length / 32
|
|
63
|
+
syms = Array.new(nsym) { |i| correlate_symbol(chips[i * 32, 32]).first }
|
|
64
|
+
# two nibbles → byte, LSB-nibble first (symbol 2n = low nibble)
|
|
65
|
+
out = []
|
|
66
|
+
syms.each_slice(2) { |lo, hi| out << ((hi.to_i << 4) | lo.to_i) if hi }
|
|
67
|
+
out
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def scan(&)
|
|
71
|
+
# sliding-align on 32-chip boundary until 4 consecutive sym-0
|
|
29
72
|
i = 0
|
|
30
|
-
while i
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
while i <= @chips.length - (32 * 12)
|
|
74
|
+
ok = true
|
|
75
|
+
4.times do |p|
|
|
76
|
+
sym, sc = correlate_symbol(@chips[i + (p * 32), 32])
|
|
77
|
+
ok &&= sym.zero? && sc >= 20
|
|
78
|
+
break unless ok
|
|
79
|
+
end
|
|
80
|
+
if ok
|
|
81
|
+
# SFD (0xA7 = symbols 0x7, 0xA)
|
|
82
|
+
s5, = correlate_symbol(@chips[i + (8 * 32), 32])
|
|
83
|
+
s6, = correlate_symbol(@chips[i + (9 * 32), 32])
|
|
84
|
+
if s5 == 0x7 && s6 == 0xA
|
|
85
|
+
yield_frame(i, &)
|
|
86
|
+
# skip past SHR
|
|
87
|
+
i += 32 * 10
|
|
88
|
+
next
|
|
43
89
|
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
90
|
end
|
|
64
|
-
i +=
|
|
91
|
+
i += 1
|
|
65
92
|
end
|
|
93
|
+
@chips.shift([i - 32, 0].max) if i > 32
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def yield_frame(shr_i)
|
|
97
|
+
phr_i = shr_i + (32 * 10)
|
|
98
|
+
return if phr_i + (32 * 2) > @chips.length
|
|
99
|
+
|
|
100
|
+
phr = chips_to_bytes(@chips[phr_i, 32 * 2]).first.to_i & 0x7F
|
|
101
|
+
body_i = phr_i + (32 * 2)
|
|
102
|
+
return if body_i + (phr * 32 * 2) > @chips.length
|
|
103
|
+
|
|
104
|
+
bytes = chips_to_bytes(@chips[body_i, phr * 32 * 2])
|
|
105
|
+
return if bytes.length < 3
|
|
106
|
+
|
|
107
|
+
fcs_rx = bytes[-2].to_i | (bytes[-1].to_i << 8)
|
|
108
|
+
fcs_ok = PWN::SDR::Decoder::DSP.crc16(
|
|
109
|
+
bytes: bytes[0, bytes.length - 2], poly: 0x1021,
|
|
110
|
+
init: 0x0000, refin: true, refout: true
|
|
111
|
+
) == fcs_rx
|
|
112
|
+
fcf = bytes[0].to_i | (bytes[1].to_i << 8)
|
|
113
|
+
ftype = fcf & 0x7
|
|
114
|
+
seq = bytes[2]
|
|
115
|
+
dst_mode = (fcf >> 10) & 0x3
|
|
116
|
+
src_mode = (fcf >> 14) & 0x3
|
|
117
|
+
j = 3
|
|
118
|
+
pan = dst = src = nil
|
|
119
|
+
if dst_mode.positive?
|
|
120
|
+
pan = bytes[j, 2]&.reverse&.map { |b| format('%02X', b) }&.join
|
|
121
|
+
j += 2
|
|
122
|
+
dl = dst_mode == 3 ? 8 : 2
|
|
123
|
+
dst = bytes[j, dl]&.reverse&.map { |b| format('%02X', b) }&.join
|
|
124
|
+
j += dl
|
|
125
|
+
end
|
|
126
|
+
if src_mode.positive?
|
|
127
|
+
j += 2 unless (fcf >> 6).allbits?(1) # PAN-ID compression
|
|
128
|
+
sl = src_mode == 3 ? 8 : 2
|
|
129
|
+
src = bytes[j, sl]&.reverse&.map { |b| format('%02X', b) }&.join
|
|
130
|
+
end
|
|
131
|
+
key = "#{pan}:#{src}:#{seq}"
|
|
132
|
+
return if @seen[key]
|
|
133
|
+
|
|
134
|
+
@seen[key] = true
|
|
135
|
+
@seen.shift if @seen.length > 128
|
|
136
|
+
yield(
|
|
137
|
+
protocol: 'ZigBee', event: 'mpdu', modulation: 'O-QPSK',
|
|
138
|
+
channel: @channel, len: phr, seq: seq,
|
|
139
|
+
frame_type: FRAME_TYPE[ftype] || ftype, fcf: format('%04X', fcf),
|
|
140
|
+
pan_id: pan, dst: dst, src: src, fcs_ok: fcs_ok,
|
|
141
|
+
payload_hex: bytes.map { |b| format('%02X', b) }.join,
|
|
142
|
+
summary: "802.15.4 #{FRAME_TYPE[ftype] || ftype} PAN=#{pan} #{src}→#{dst} seq=#{seq} len=#{phr} FCS=#{fcs_ok ? 'OK' : 'BAD'}"
|
|
143
|
+
)
|
|
66
144
|
end
|
|
67
145
|
end
|
|
68
146
|
|
|
@@ -73,33 +151,28 @@ module PWN
|
|
|
73
151
|
|
|
74
152
|
public_class_method def self.decode(opts = {})
|
|
75
153
|
freq_obj = opts[:freq_obj]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
demod = DemodIQ.new(
|
|
80
|
-
rate: rate, protocol: proto, modulation: 'O-QPSK',
|
|
81
|
-
extra: { threshold: 8.0 }
|
|
82
|
-
)
|
|
154
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
155
|
+
ch = ((hz - 2_405_000_000) / 5_000_000).round + 11
|
|
156
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 4_000_000).to_i
|
|
83
157
|
PWN::SDR::Decoder::Base.run_iq(
|
|
84
158
|
freq_obj: freq_obj,
|
|
85
|
-
protocol:
|
|
159
|
+
protocol: 'ZigBee',
|
|
86
160
|
sample_rate: rate,
|
|
87
161
|
source: opts[:source],
|
|
88
162
|
file: opts[:file],
|
|
89
|
-
demod:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
describe: proc { |b| { modulation: 'O-QPSK', symbol_rate: 62_500, classification: b[:duration_ms] < 5 ? 'ACK' : 'MAC-frame' } }
|
|
163
|
+
demod: DemodIQ.new(rate: rate, channel: ch),
|
|
164
|
+
note: 'O-QPSK 2 Mcps ≡ MSK — I/Q→gmskdem→32-chip PN correlate→SHR/SFD→PHR/MHR/FCS.',
|
|
165
|
+
describe: proc { |b| { modulation: 'O-QPSK', channel: ch, classification: b[:duration_ms] < 5 ? 'ACK' : 'MAC-frame' } }
|
|
93
166
|
)
|
|
94
167
|
end
|
|
95
168
|
|
|
96
169
|
public_class_method def self.parse_line(opts = {})
|
|
97
170
|
line = opts[:line].to_s
|
|
98
171
|
out = { protocol: 'ZigBee' }
|
|
99
|
-
out[:pan]
|
|
100
|
-
out[:src]
|
|
101
|
-
out[:dst]
|
|
102
|
-
out[:cmd]
|
|
172
|
+
out[:pan] = ::Regexp.last_match(1) if line =~ /PAN[:= ]+([0-9A-Fa-f]+)/i
|
|
173
|
+
out[:src] = ::Regexp.last_match(1) if line =~ /src[:= ]+([0-9A-Fa-f:]+)/i
|
|
174
|
+
out[:dst] = ::Regexp.last_match(1) if line =~ /dst[:= ]+([0-9A-Fa-f:]+)/i
|
|
175
|
+
out[:cmd] = ::Regexp.last_match(1) if line =~ /\b(Beacon|Data|ACK|Cmd)\b/i
|
|
103
176
|
out[:summary] = "ZigBee #{out[:cmd]} PAN=#{out[:pan]} #{out[:src]}→#{out[:dst]}"
|
|
104
177
|
out.compact
|
|
105
178
|
end
|
|
@@ -115,7 +188,7 @@ module PWN
|
|
|
115
188
|
#{self}.decode(
|
|
116
189
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
117
190
|
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
118
|
-
file: 'optional - .cu8/.cs16 capture'
|
|
191
|
+
file: 'optional - .cu8/.cs16 capture (≥4 Msps)'
|
|
119
192
|
)
|
|
120
193
|
|
|
121
194
|
#{self}.authors
|
data/lib/pwn/version.rb
CHANGED
data/lib/pwn.rb
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe PWN::MemoryIndex do
|
|
6
|
+
it 'should display information for authors' do
|
|
7
|
+
authors_response = PWN::MemoryIndex
|
|
8
|
+
expect(authors_response).to respond_to :authors
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'should display information for existing help method' do
|
|
12
|
+
help_response = PWN::MemoryIndex
|
|
13
|
+
expect(help_response).to respond_to :help
|
|
14
|
+
end
|
|
15
|
+
end
|