pwn 0.5.620 → 0.5.621
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/README.md +1 -1
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Cron.md +13 -0
- data/documentation/Diagrams.md +1 -1
- data/documentation/Extrospection.md +71 -5
- data/documentation/Home.md +2 -2
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Mistakes.md +2 -1
- data/documentation/Persistence.md +2 -1
- data/documentation/Skills-Memory-Learning.md +1 -0
- data/documentation/Transparent-Browser.md +18 -1
- data/documentation/diagrams/agent-tool-registry.svg +55 -54
- 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 +29 -8
- data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +2 -2
- data/documentation/diagrams/dot/persistence-filesystem.dot +2 -1
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +229 -99
- data/documentation/diagrams/fuzzing-workflow.svg +24 -24
- data/documentation/diagrams/hardware-hacking.svg +18 -18
- data/documentation/diagrams/history-to-drivers.svg +18 -18
- data/documentation/diagrams/memory-skills-detailed.svg +124 -101
- data/documentation/diagrams/mistakes-negative-feedback.svg +55 -55
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +85 -85
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +94 -85
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +226 -180
- 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 +31 -31
- 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 +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +433 -4
- data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
- data/lib/pwn/sdr/decoder/adsb.rb +14 -33
- data/lib/pwn/sdr/decoder/apt.rb +97 -36
- data/lib/pwn/sdr/decoder/base.rb +258 -171
- data/lib/pwn/sdr/decoder/bluetooth.rb +13 -20
- data/lib/pwn/sdr/decoder/dect.rb +13 -27
- data/lib/pwn/sdr/decoder/dsp.rb +396 -0
- data/lib/pwn/sdr/decoder/flex.rb +177 -210
- data/lib/pwn/sdr/decoder/gps.rb +12 -21
- data/lib/pwn/sdr/decoder/gsm.rb +21 -73
- data/lib/pwn/sdr/decoder/iridium.rb +11 -26
- data/lib/pwn/sdr/decoder/lora.rb +16 -23
- data/lib/pwn/sdr/decoder/lte.rb +11 -32
- data/lib/pwn/sdr/decoder/morse.rb +95 -21
- data/lib/pwn/sdr/decoder/p25.rb +19 -16
- data/lib/pwn/sdr/decoder/pager.rb +22 -38
- data/lib/pwn/sdr/decoder/pocsag.rb +175 -68
- data/lib/pwn/sdr/decoder/rfid.rb +24 -33
- data/lib/pwn/sdr/decoder/rtl433.rb +21 -30
- data/lib/pwn/sdr/decoder/rtty.rb +100 -27
- data/lib/pwn/sdr/decoder/wifi.rb +21 -42
- data/lib/pwn/sdr/decoder/zigbee.rb +18 -40
- data/lib/pwn/sdr/decoder.rb +8 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +24 -3
- metadata +3 -1
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
# POCSAG (CCIR Radiopaging Code No. 1) decoder
|
|
6
|
+
# Pure-Ruby POCSAG (CCIR Radiopaging Code No. 1) decoder.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# POCSAG1200: Address: 123456 Function: 2 Alpha: <partial><EOT>
|
|
16
|
-
#
|
|
17
|
-
# Each line is parsed into { protocol, baud, address/capcode, function,
|
|
18
|
-
# function_desc, type, type_desc, message } and JSON-logged.
|
|
8
|
+
# GQRX supplies NBFM-discriminator audio on its 48 kHz UDP tap; for a
|
|
9
|
+
# 2-FSK pager channel that is already an NRZ baseband whose sign
|
|
10
|
+
# encodes the bit. This module NRZ-slices at 512/1200/2400 baud,
|
|
11
|
+
# locks onto the 32-bit Frame Sync Codeword (0x7CD215D8), then walks
|
|
12
|
+
# each 8-frame batch of BCH(31,21)+parity codewords, extracting
|
|
13
|
+
# address (RIC/capcode + function bits) and message codewords
|
|
14
|
+
# (numeric BCD or 7-bit ASCII). No `multimon-ng`, no `sox`.
|
|
19
15
|
module POCSAG
|
|
16
|
+
FSC = 0x7CD215D8
|
|
17
|
+
IDLE_CW = 0x7A89C197
|
|
18
|
+
BAUDS = [1200, 512, 2400].freeze
|
|
19
|
+
BCD_TABLE = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', 'U', ' ', '-', ')', '('].freeze
|
|
20
|
+
|
|
20
21
|
FUNCTION_DESC = {
|
|
21
22
|
0 => 'Numeric (Tone/A)',
|
|
22
23
|
1 => 'Tone only (B)',
|
|
@@ -24,73 +25,173 @@ module PWN
|
|
|
24
25
|
3 => 'Alphanumeric (D)'
|
|
25
26
|
}.freeze
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
# Streaming POCSAG demodulator fed by Base.run_native.
|
|
29
|
+
class Demod
|
|
30
|
+
def initialize(rate: 48_000)
|
|
31
|
+
@rate = rate
|
|
32
|
+
@buf = []
|
|
33
|
+
@baud = nil
|
|
34
|
+
@inv = false
|
|
35
|
+
@carry_bits = []
|
|
36
|
+
end
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
def feed(samples, &)
|
|
39
|
+
@buf.concat(samples)
|
|
40
|
+
max = (@rate * 3.5).to_i
|
|
41
|
+
@buf.shift(@buf.length - max) if @buf.length > max
|
|
42
|
+
return if @buf.length < @rate # need ≥1 s to lock
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
try_lock unless @baud
|
|
45
|
+
return unless @baud
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
decode_cmd = 'multimon-ng -q -t raw -e -u -p -f alpha ' \
|
|
48
|
-
'-a POCSAG512 -a POCSAG1200 -a POCSAG2400 -'
|
|
47
|
+
bits = PWN::SDR::Decoder::DSP.nrz_slice(samples: @buf, rate: @rate, baud: @baud, invert: @inv)
|
|
48
|
+
@buf.clear
|
|
49
|
+
bits = @carry_bits + bits
|
|
50
|
+
@carry_bits = POCSAG.decode_bits(bits: bits, baud: @baud, &)
|
|
51
|
+
end
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Try each baud × polarity until FSC (≤2 bit errors) is found.
|
|
56
|
+
def try_lock
|
|
57
|
+
hit = BAUDS.product([false, true]).find do |bd, inv|
|
|
58
|
+
bits = PWN::SDR::Decoder::DSP.nrz_slice(samples: @buf, rate: @rate, baud: bd, invert: inv)
|
|
59
|
+
PWN::SDR::Decoder::DSP.find_sync(bits: bits, pattern: FSC, width: 32, max_err: 2)
|
|
60
|
+
end
|
|
61
|
+
@baud, @inv = hit if hit
|
|
62
|
+
end
|
|
58
63
|
end
|
|
59
64
|
|
|
60
65
|
# Supported Method Parameters::
|
|
61
|
-
# PWN::SDR::Decoder::POCSAG.
|
|
66
|
+
# carry = PWN::SDR::Decoder::POCSAG.decode_bits(bits: [...], baud: 1200) { |msg| ... }
|
|
67
|
+
# Returns the trailing (unconsumed) bits so the caller can prepend
|
|
68
|
+
# them to the next chunk for streaming continuity.
|
|
69
|
+
|
|
70
|
+
public_class_method def self.decode_bits(opts = {})
|
|
71
|
+
bits = opts[:bits] || []
|
|
72
|
+
baud = opts[:baud]
|
|
73
|
+
i = 0
|
|
74
|
+
pending = nil
|
|
75
|
+
flush = proc do
|
|
76
|
+
yield assemble(pending: pending, baud: baud) if pending && block_given?
|
|
77
|
+
pending = nil
|
|
78
|
+
end
|
|
79
|
+
loop do
|
|
80
|
+
idx = PWN::SDR::Decoder::DSP.find_sync(bits: bits, pattern: FSC, width: 32, max_err: 2, from: i)
|
|
81
|
+
break unless idx
|
|
82
|
+
|
|
83
|
+
i = idx + 32
|
|
84
|
+
# One batch = 8 frames × 2 codewords × 32 bits = 512 bits
|
|
85
|
+
8.times do |frame|
|
|
86
|
+
2.times do
|
|
87
|
+
break if i + 32 > bits.length
|
|
88
|
+
|
|
89
|
+
cw = PWN::SDR::Decoder::DSP.bits_to_int(bits: bits[i, 32])
|
|
90
|
+
i += 32
|
|
91
|
+
next if [IDLE_CW, FSC].include?(cw)
|
|
92
|
+
|
|
93
|
+
if cw.nobits?(0x80000000)
|
|
94
|
+
flush.call
|
|
95
|
+
addr18 = (cw >> 13) & 0x3FFFF
|
|
96
|
+
func = (cw >> 11) & 0x3
|
|
97
|
+
ric = (addr18 << 3) | frame
|
|
98
|
+
pending = { ric: ric, func: func, msg_words: [] }
|
|
99
|
+
elsif pending
|
|
100
|
+
pending[:msg_words] << ((cw >> 11) & 0xFFFFF)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
flush.call
|
|
106
|
+
tail_from = [bits.length - 576, 0].max
|
|
107
|
+
bits[tail_from..] || []
|
|
108
|
+
end
|
|
62
109
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
110
|
+
# Supported Method Parameters::
|
|
111
|
+
# h = PWN::SDR::Decoder::POCSAG.assemble(pending: {ric:,func:,msg_words:}, baud: 1200)
|
|
112
|
+
|
|
113
|
+
public_class_method def self.assemble(opts = {})
|
|
114
|
+
pending = opts[:pending]
|
|
115
|
+
baud = opts[:baud]
|
|
116
|
+
return {} unless pending
|
|
117
|
+
|
|
118
|
+
words = pending[:msg_words] || []
|
|
119
|
+
func = pending[:func]
|
|
120
|
+
type, text =
|
|
121
|
+
if words.empty?
|
|
122
|
+
['Tone', nil]
|
|
123
|
+
elsif func == 3
|
|
124
|
+
['Alpha', alpha_decode(words: words)]
|
|
125
|
+
else
|
|
126
|
+
['Numeric', numeric_decode(words: words)]
|
|
127
|
+
end
|
|
128
|
+
out = {
|
|
129
|
+
protocol: 'POCSAG',
|
|
130
|
+
baud: baud,
|
|
131
|
+
address: pending[:ric],
|
|
132
|
+
capcode: pending[:ric].to_s.rjust(7, '0'),
|
|
133
|
+
function: func,
|
|
134
|
+
function_desc: FUNCTION_DESC[func] || 'Unknown',
|
|
135
|
+
type: type,
|
|
136
|
+
message: text
|
|
137
|
+
}.compact
|
|
138
|
+
summary = ["POCSAG#{baud}", "RIC=#{out[:capcode]}", "F#{func}(#{out[:function_desc]})"]
|
|
139
|
+
summary << "#{type}: #{text}" if text
|
|
140
|
+
out[:summary] = summary.join(' ')
|
|
141
|
+
out
|
|
142
|
+
end
|
|
66
143
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
144
|
+
# Supported Method Parameters::
|
|
145
|
+
# str = PWN::SDR::Decoder::POCSAG.numeric_decode(words: [Integer, ...])
|
|
146
|
+
|
|
147
|
+
public_class_method def self.numeric_decode(opts = {})
|
|
148
|
+
words = opts[:words] || []
|
|
149
|
+
out = +''
|
|
150
|
+
words.each do |w|
|
|
151
|
+
5.times do |d|
|
|
152
|
+
nib = (w >> (16 - (d * 4))) & 0xF
|
|
153
|
+
# POCSAG BCD nibbles are bit-reversed within each 4-bit group
|
|
154
|
+
rev = ((nib & 1) << 3) | ((nib & 2) << 1) | ((nib & 4) >> 1) | ((nib & 8) >> 3)
|
|
155
|
+
out << BCD_TABLE[rev]
|
|
156
|
+
end
|
|
73
157
|
end
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
158
|
+
out.gsub(/ +$/, '')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Supported Method Parameters::
|
|
162
|
+
# str = PWN::SDR::Decoder::POCSAG.alpha_decode(words: [Integer, ...])
|
|
163
|
+
|
|
164
|
+
public_class_method def self.alpha_decode(opts = {})
|
|
165
|
+
words = opts[:words] || []
|
|
166
|
+
bitstream = []
|
|
167
|
+
words.each do |w|
|
|
168
|
+
19.downto(0) { |b| bitstream << ((w >> b) & 1) }
|
|
78
169
|
end
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
170
|
+
out = +''
|
|
171
|
+
bitstream.each_slice(7) do |ch|
|
|
172
|
+
break if ch.length < 7
|
|
173
|
+
|
|
174
|
+
# 7-bit ASCII, LSB first within each character
|
|
175
|
+
code = ch.each_with_index.sum { |b, i| b << i }
|
|
176
|
+
next if code.zero? || code == 0x03 || code == 0x17
|
|
177
|
+
|
|
178
|
+
out << (code.between?(0x20, 0x7E) ? code.chr : '.')
|
|
85
179
|
end
|
|
180
|
+
out.strip
|
|
181
|
+
end
|
|
86
182
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
183
|
+
# Supported Method Parameters::
|
|
184
|
+
# PWN::SDR::Decoder::POCSAG.decode(
|
|
185
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
186
|
+
# )
|
|
187
|
+
|
|
188
|
+
public_class_method def self.decode(opts = {})
|
|
189
|
+
freq_obj = opts[:freq_obj]
|
|
190
|
+
PWN::SDR::Decoder::Base.run_native(
|
|
191
|
+
freq_obj: freq_obj,
|
|
192
|
+
protocol: 'POCSAG',
|
|
193
|
+
demod: Demod.new
|
|
194
|
+
)
|
|
94
195
|
end
|
|
95
196
|
|
|
96
197
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
@@ -102,12 +203,18 @@ module PWN
|
|
|
102
203
|
# Display Usage for this Module
|
|
103
204
|
|
|
104
205
|
public_class_method def self.help
|
|
105
|
-
puts "USAGE:
|
|
206
|
+
puts "USAGE (ruby-native, no external binaries):
|
|
106
207
|
#{self}.decode(
|
|
107
208
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
108
209
|
)
|
|
109
210
|
|
|
110
|
-
#{self}.
|
|
211
|
+
#{self}.decode_bits(bits: [0,1,...], baud: 1200) { |msg| ... }
|
|
212
|
+
#{self}.assemble(pending: {ric:, func:, msg_words:[]}, baud: 1200)
|
|
213
|
+
#{self}.numeric_decode(words: [Integer, ...])
|
|
214
|
+
#{self}.alpha_decode(words: [Integer, ...])
|
|
215
|
+
|
|
216
|
+
NOTE: Set GQRX to Narrow FM. Baud (512/1200/2400) and NRZ
|
|
217
|
+
polarity are auto-detected from the FSC 0x7CD215D8.
|
|
111
218
|
|
|
112
219
|
#{self}.authors
|
|
113
220
|
"
|
data/lib/pwn/sdr/decoder/rfid.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
module PWN
|
|
6
4
|
module SDR
|
|
7
5
|
module Decoder
|
|
8
|
-
# RFID
|
|
9
|
-
# (860–960 MHz EPC Gen2).
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
6
|
+
# Pure-Ruby RFID activity detector for LF (125/134 kHz), HF
|
|
7
|
+
# (13.56 MHz) and UHF (860–960 MHz EPC Gen2).
|
|
8
|
+
#
|
|
9
|
+
# Near-field ASK/load-modulation on LF/HF requires an inductive
|
|
10
|
+
# coupler (not an SDR antenna); UHF backscatter is 40–640 kbps ASK.
|
|
11
|
+
# Native mode reports reader-carrier presence and tag-response
|
|
12
|
+
# bursts by band. `parse_line` retained for offline text analysis.
|
|
13
13
|
module RFID
|
|
14
14
|
# Supported Method Parameters::
|
|
15
15
|
# PWN::SDR::Decoder::RFID.decode(
|
|
@@ -18,30 +18,24 @@ module PWN
|
|
|
18
18
|
|
|
19
19
|
public_class_method def self.decode(opts = {})
|
|
20
20
|
freq_obj = opts[:freq_obj]
|
|
21
|
-
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
22
|
-
|
|
23
21
|
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if PWN::SDR::Decoder::Base.bin_available?(bin: 'pm3')
|
|
30
|
-
["pm3 -c 'hf search'", %w[pm3], 'RFID-HF']
|
|
31
|
-
else
|
|
32
|
-
['nfc-list -v', %w[nfc-list], 'RFID-HF']
|
|
33
|
-
end
|
|
34
|
-
else
|
|
35
|
-
["rtl_433 -f #{hz} -A -F json", %w[rtl_433], 'RFID-UHF']
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
PWN::SDR::Decoder::Base.run_pipeline(
|
|
22
|
+
band = if hz < 1_000_000 then 'LF'
|
|
23
|
+
elsif hz.between?(13_000_000, 14_000_000) then 'HF'
|
|
24
|
+
else 'UHF'
|
|
25
|
+
end
|
|
26
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
39
27
|
freq_obj: freq_obj,
|
|
40
|
-
protocol:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
protocol: "RFID-#{band}",
|
|
29
|
+
note: 'Native mode reports reader-carrier and tag-backscatter bursts by band.',
|
|
30
|
+
threshold: 6.0,
|
|
31
|
+
describe: proc { |b|
|
|
32
|
+
kind = case band
|
|
33
|
+
when 'LF' then b[:duration_ms] > 100 ? 'reader-CW' : 'EM4x/HID-response'
|
|
34
|
+
when 'HF' then b[:duration_ms] > 5 ? 'ISO14443-REQA/frame' : 'ISO15693-slot'
|
|
35
|
+
else b[:duration_ms] > 20 ? 'reader-Query' : 'EPC-backscatter'
|
|
36
|
+
end
|
|
37
|
+
{ band: band, modulation: 'ASK/load-mod', classification: kind }
|
|
38
|
+
}
|
|
45
39
|
)
|
|
46
40
|
end
|
|
47
41
|
|
|
@@ -69,14 +63,11 @@ module PWN
|
|
|
69
63
|
# Display Usage for this Module
|
|
70
64
|
|
|
71
65
|
public_class_method def self.help
|
|
72
|
-
puts "USAGE:
|
|
66
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
73
67
|
#{self}.decode(
|
|
74
68
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
75
69
|
)
|
|
76
70
|
|
|
77
|
-
NOTE: LF/HF need a Proxmark3 (`pm3`) or libnfc reader; UHF uses
|
|
78
|
-
`rtl_433 -A`. Band chosen automatically from freq_obj[:freq].
|
|
79
|
-
|
|
80
71
|
#{self}.parse_line(line: 'UID: 04 A1 B2 C3 D4 E5 F6 SAK: 08 Mifare Classic 1K')
|
|
81
72
|
|
|
82
73
|
#{self}.authors
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require 'shellwords'
|
|
5
4
|
|
|
6
5
|
module PWN
|
|
7
6
|
module SDR
|
|
8
7
|
module Decoder
|
|
9
|
-
#
|
|
8
|
+
# Pure-Ruby ISM/keyfob/sensor activity detector for the 315 / 390 /
|
|
9
|
+
# 433.92 / 868 / 915 MHz device zoo.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# The upstream `rtl_433` binary carries ~250 device-specific protocol
|
|
12
|
+
# dissectors; re-implementing that library is out of scope. This
|
|
13
|
+
# module instead characterises OOK/ASK/FSK bursts natively (count,
|
|
14
|
+
# duration, gap, peak dBFS) — enough to fingerprint a keyfob press,
|
|
15
|
+
# a periodic weather-station beacon, or a TPMS chirp — without
|
|
16
|
+
# invoking any external binary. `parse_line` still accepts rtl_433
|
|
17
|
+
# `-F json` output for offline analysis.
|
|
16
18
|
module RTL433
|
|
17
19
|
# Supported Method Parameters::
|
|
18
20
|
# PWN::SDR::Decoder::RTL433.decode(
|
|
@@ -21,24 +23,18 @@ module PWN
|
|
|
21
23
|
|
|
22
24
|
public_class_method def self.decode(opts = {})
|
|
23
25
|
freq_obj = opts[:freq_obj]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
27
|
-
gain = freq_obj[:rf_gain]
|
|
28
|
-
sdr_args = freq_obj[:sdr_args].to_s
|
|
29
|
-
|
|
30
|
-
cmd = ['rtl_433', '-f', hz.to_s, '-F', 'json', '-M', 'level', '-M', 'protocol']
|
|
31
|
-
cmd.push('-g', gain.to_s) if gain
|
|
32
|
-
cmd.push('-d', sdr_args) unless sdr_args.empty?
|
|
33
|
-
direct_cmd = Shellwords.join(cmd)
|
|
34
|
-
|
|
35
|
-
PWN::SDR::Decoder::Base.run_pipeline(
|
|
26
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
36
27
|
freq_obj: freq_obj,
|
|
37
|
-
protocol: '
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
protocol: 'ISM-433',
|
|
29
|
+
note: 'Native OOK/FSK burst characteriser (no rtl_433 binary). Feed captured `rtl_433 -F json` lines to .parse_line for per-device decode.',
|
|
30
|
+
threshold: 10.0,
|
|
31
|
+
describe: proc { |b|
|
|
32
|
+
kind = if b[:duration_ms] < 20 then 'keyfob/OOK-short'
|
|
33
|
+
elsif b[:duration_ms] < 120 then 'sensor/OOK-packet'
|
|
34
|
+
else 'FSK-continuous'
|
|
35
|
+
end
|
|
36
|
+
{ modulation: 'OOK/ASK/FSK', classification: kind }
|
|
37
|
+
}
|
|
42
38
|
)
|
|
43
39
|
end
|
|
44
40
|
|
|
@@ -53,7 +49,6 @@ module PWN
|
|
|
53
49
|
{ unparsed: line }
|
|
54
50
|
end
|
|
55
51
|
out = { protocol: 'RTL433' }.merge(h)
|
|
56
|
-
|
|
57
52
|
bits = []
|
|
58
53
|
bits << out[:model].to_s if out[:model]
|
|
59
54
|
bits << "id=#{out[:id]}" if out[:id]
|
|
@@ -75,15 +70,11 @@ module PWN
|
|
|
75
70
|
# Display Usage for this Module
|
|
76
71
|
|
|
77
72
|
public_class_method def self.help
|
|
78
|
-
puts "USAGE:
|
|
73
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
79
74
|
#{self}.decode(
|
|
80
75
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
81
76
|
)
|
|
82
77
|
|
|
83
|
-
NOTE: Requires `rtl_433`. Owns the SDR directly (pass
|
|
84
|
-
freq_obj[:sdr_args] like ':1' or 'driver=hackrf' to select
|
|
85
|
-
a device other than the one GQRX is holding).
|
|
86
|
-
|
|
87
78
|
#{self}.parse_line(line: '{\"model\":\"Acurite-Tower\",\"id\":1234,...}')
|
|
88
79
|
|
|
89
80
|
#{self}.authors
|
data/lib/pwn/sdr/decoder/rtty.rb
CHANGED
|
@@ -3,11 +3,102 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
# RTTY (Radioteletype, ITA2/Baudot) decoder
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Pure-Ruby RTTY (Radioteletype, ITA2/Baudot) decoder.
|
|
7
|
+
#
|
|
8
|
+
# Amateur RTTY is 45.45 baud 2-FSK with a 170 Hz shift; on USB the
|
|
9
|
+
# convention is mark ≈ 2125 Hz, space ≈ 2295 Hz in the demodulated
|
|
10
|
+
# audio. This module runs a per-symbol Goertzel on both tones,
|
|
11
|
+
# frames 1-start / 5-data / 1.5-stop asynchronously, and decodes
|
|
12
|
+
# ITA2 via DSP::BAUDOT_LTRS/FIGS. No `minimodem`, no `sox`.
|
|
10
13
|
module RTTY
|
|
14
|
+
# Streaming 2-FSK Baudot demodulator fed by Base.run_native.
|
|
15
|
+
class Demod
|
|
16
|
+
def initialize(rate: 48_000, baud: 45.45, mark_hz: 2125.0, space_hz: 2295.0)
|
|
17
|
+
@rate = rate
|
|
18
|
+
@baud = baud
|
|
19
|
+
@mark_hz = mark_hz
|
|
20
|
+
@space_hz = space_hz
|
|
21
|
+
@spb = rate / baud
|
|
22
|
+
@buf = []
|
|
23
|
+
@figs = false
|
|
24
|
+
@line = +''
|
|
25
|
+
@idle_bits = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def feed(samples, &)
|
|
29
|
+
@buf.concat(samples)
|
|
30
|
+
# keep at most ~2 s of audio buffered
|
|
31
|
+
@buf.shift(@buf.length - (@rate * 2)) if @buf.length > @rate * 2
|
|
32
|
+
demod_buffer(&)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def bit_at(offset)
|
|
38
|
+
a = offset.floor
|
|
39
|
+
b = (offset + @spb).floor
|
|
40
|
+
win = @buf[a...b]
|
|
41
|
+
pm = PWN::SDR::Decoder::DSP.goertzel(samples: win, rate: @rate, freq: @mark_hz)
|
|
42
|
+
ps = PWN::SDR::Decoder::DSP.goertzel(samples: win, rate: @rate, freq: @space_hz)
|
|
43
|
+
pm >= ps ? 1 : 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Async framing: idle = mark(1), start bit = space(0), 5 data bits
|
|
47
|
+
# LSB-first, ≥1 stop bit = mark(1).
|
|
48
|
+
def demod_buffer(&)
|
|
49
|
+
char_span = (@spb * 7.5).ceil
|
|
50
|
+
pos = 0.0
|
|
51
|
+
while @buf.length - pos > char_span
|
|
52
|
+
if bit_at(pos) == 1
|
|
53
|
+
pos += @spb
|
|
54
|
+
@idle_bits += 1
|
|
55
|
+
flush_line(&) if @idle_bits > 20 && !@line.empty?
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
@idle_bits = 0
|
|
59
|
+
# start bit found — sample 5 data bits at their centres
|
|
60
|
+
data = Array.new(5) { |i| bit_at(pos + (@spb * (i + 1))) }
|
|
61
|
+
stop = bit_at(pos + (@spb * 6))
|
|
62
|
+
pos += @spb * 7.5
|
|
63
|
+
next unless stop == 1
|
|
64
|
+
|
|
65
|
+
handle_code(data.each_with_index.sum { |b, i| b << i }, &)
|
|
66
|
+
end
|
|
67
|
+
consumed = pos.floor
|
|
68
|
+
@buf.shift(consumed) if consumed.positive?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def handle_code(code, &)
|
|
72
|
+
case code
|
|
73
|
+
when 31 then @figs = false
|
|
74
|
+
when 27 then @figs = true
|
|
75
|
+
else
|
|
76
|
+
tbl = @figs ? PWN::SDR::Decoder::DSP::BAUDOT_FIGS : PWN::SDR::Decoder::DSP::BAUDOT_LTRS
|
|
77
|
+
c = tbl[code]
|
|
78
|
+
return unless c
|
|
79
|
+
|
|
80
|
+
if ["\n", "\r"].include?(c)
|
|
81
|
+
flush_line(&)
|
|
82
|
+
else
|
|
83
|
+
@line << c
|
|
84
|
+
flush_line(&) if @line.length >= 80
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def flush_line
|
|
90
|
+
return if @line.strip.empty?
|
|
91
|
+
|
|
92
|
+
out = { protocol: 'RTTY', baud: @baud, shift_hz: (@space_hz - @mark_hz).round, text: @line.dup }
|
|
93
|
+
if (m = @line.match(/\bDE\s+([A-Z0-9]{1,3}\d[A-Z]{1,4})\b/))
|
|
94
|
+
out[:callsign] = m[1]
|
|
95
|
+
end
|
|
96
|
+
out[:summary] = "RTTY #{@line.strip}"[0, 120]
|
|
97
|
+
yield out
|
|
98
|
+
@line = +''
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
11
102
|
# Supported Method Parameters::
|
|
12
103
|
# PWN::SDR::Decoder::RTTY.decode(
|
|
13
104
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -15,32 +106,13 @@ module PWN
|
|
|
15
106
|
|
|
16
107
|
public_class_method def self.decode(opts = {})
|
|
17
108
|
freq_obj = opts[:freq_obj]
|
|
18
|
-
|
|
19
|
-
# minimodem reads s16le mono from stdin at the rate we hand it; keep
|
|
20
|
-
# sox at 22 050 Hz (Base default) and tell minimodem the same.
|
|
21
|
-
PWN::SDR::Decoder::Base.run_pipeline(
|
|
109
|
+
PWN::SDR::Decoder::Base.run_native(
|
|
22
110
|
freq_obj: freq_obj,
|
|
23
111
|
protocol: 'RTTY',
|
|
24
|
-
|
|
25
|
-
decode_cmd: 'minimodem --rx --quiet --rtty -R 22050 -f -',
|
|
26
|
-
line_match: /\S/,
|
|
27
|
-
parser: proc { |line| parse_line(line: line) }
|
|
112
|
+
demod: Demod.new
|
|
28
113
|
)
|
|
29
114
|
end
|
|
30
115
|
|
|
31
|
-
# Supported Method Parameters::
|
|
32
|
-
# PWN::SDR::Decoder::RTTY.parse_line(line: 'RYRYRY DE W1AW')
|
|
33
|
-
|
|
34
|
-
public_class_method def self.parse_line(opts = {})
|
|
35
|
-
line = opts[:line].to_s.strip
|
|
36
|
-
out = { protocol: 'RTTY', text: line }
|
|
37
|
-
if (m = line.match(/\bDE\s+([A-Z0-9]{1,3}\d[A-Z]{1,4})\b/))
|
|
38
|
-
out[:callsign] = m[1]
|
|
39
|
-
end
|
|
40
|
-
out[:summary] = "RTTY #{line}"[0, 120]
|
|
41
|
-
out
|
|
42
|
-
end
|
|
43
|
-
|
|
44
116
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
45
117
|
|
|
46
118
|
public_class_method def self.authors
|
|
@@ -50,12 +122,13 @@ module PWN
|
|
|
50
122
|
# Display Usage for this Module
|
|
51
123
|
|
|
52
124
|
public_class_method def self.help
|
|
53
|
-
puts "USAGE:
|
|
125
|
+
puts "USAGE (ruby-native 2-FSK ITA2, no external binaries):
|
|
54
126
|
#{self}.decode(
|
|
55
127
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
56
128
|
)
|
|
57
129
|
|
|
58
|
-
|
|
130
|
+
NOTE: Set GQRX to USB, tune so mark≈2125 Hz / space≈2295 Hz in
|
|
131
|
+
the audio passband. 45.45 baud, 170 Hz shift, 1N5+1.5.
|
|
59
132
|
|
|
60
133
|
#{self}.authors
|
|
61
134
|
"
|