pwn 0.5.618 → 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/Gemfile +2 -2
- data/README.md +14 -6
- data/bin/pwn_gqrx_scanner +25 -5
- data/documentation/Agent-Tool-Registry.md +4 -4
- data/documentation/Cron.md +13 -0
- data/documentation/Diagrams.md +6 -2
- data/documentation/Extrospection.md +96 -11
- data/documentation/Home.md +5 -4
- data/documentation/How-PWN-Works.md +7 -4
- data/documentation/Mistakes.md +97 -0
- data/documentation/Persistence.md +4 -1
- data/documentation/SDR.md +23 -5
- data/documentation/Skills-Memory-Learning.md +16 -5
- data/documentation/Transparent-Browser.md +18 -1
- data/documentation/diagrams/agent-tool-registry.svg +103 -100
- 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 +3 -3
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -10
- data/documentation/diagrams/dot/memory-skills-detailed.dot +16 -5
- data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +4 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +25 -7
- data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
- data/documentation/diagrams/driver-framework.svg +13 -13
- data/documentation/diagrams/extrospection-world-awareness.svg +241 -93
- 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 +169 -97
- data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
- data/documentation/diagrams/network-infra-testing.svg +27 -27
- data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
- data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
- data/documentation/diagrams/persistence-filesystem.svg +111 -87
- data/documentation/diagrams/plugin-ecosystem.svg +35 -35
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +263 -149
- 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 +76 -56
- 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 +28 -15
- data/lib/pwn/ai/agent/extrospection.rb +494 -6
- data/lib/pwn/ai/agent/learning.rb +67 -7
- data/lib/pwn/ai/agent/loop.rb +72 -6
- data/lib/pwn/ai/agent/mistakes.rb +369 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
- data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
- data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/plugins/jira_data_center.rb +2 -2
- data/lib/pwn/sdr/decoder/adsb.rb +82 -0
- data/lib/pwn/sdr/decoder/apt.rb +136 -0
- data/lib/pwn/sdr/decoder/base.rb +258 -171
- data/lib/pwn/sdr/decoder/bluetooth.rb +71 -0
- data/lib/pwn/sdr/decoder/dect.rb +70 -0
- 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 +74 -0
- data/lib/pwn/sdr/decoder/gsm.rb +31 -62
- data/lib/pwn/sdr/decoder/iridium.rb +66 -0
- data/lib/pwn/sdr/decoder/lora.rb +80 -0
- data/lib/pwn/sdr/decoder/lte.rb +67 -0
- data/lib/pwn/sdr/decoder/morse.rb +138 -0
- data/lib/pwn/sdr/decoder/p25.rb +74 -0
- data/lib/pwn/sdr/decoder/pager.rb +61 -0
- data/lib/pwn/sdr/decoder/pocsag.rb +176 -54
- data/lib/pwn/sdr/decoder/rfid.rb +79 -0
- data/lib/pwn/sdr/decoder/rtl433.rb +86 -0
- data/lib/pwn/sdr/decoder/rtty.rb +139 -0
- data/lib/pwn/sdr/decoder/wifi.rb +77 -0
- data/lib/pwn/sdr/decoder/zigbee.rb +74 -0
- data/lib/pwn/sdr/decoder.rb +75 -7
- data/lib/pwn/sdr/frequency_allocation.rb +82 -41
- data/lib/pwn/sdr/gqrx.rb +186 -95
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
- data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
- data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +115 -5
- metadata +46 -6
- data/README.md.bak +0 -200
|
@@ -3,19 +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
|
-
#
|
|
16
|
-
# This module parses those into { protocol, baud, address, function,
|
|
17
|
-
# function_desc, type, type_desc, message } and JSON-logs each page.
|
|
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`.
|
|
18
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
|
+
|
|
19
21
|
FUNCTION_DESC = {
|
|
20
22
|
0 => 'Numeric (Tone/A)',
|
|
21
23
|
1 => 'Tone only (B)',
|
|
@@ -23,59 +25,173 @@ module PWN
|
|
|
23
25
|
3 => 'Alphanumeric (D)'
|
|
24
26
|
}.freeze
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
try_lock unless @baud
|
|
45
|
+
return unless @baud
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
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
|
|
52
|
+
|
|
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
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
# Supported Method Parameters::
|
|
53
|
-
# 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
|
|
54
82
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
104
|
end
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
105
|
+
flush.call
|
|
106
|
+
tail_from = [bits.length - 576, 0].max
|
|
107
|
+
bits[tail_from..] || []
|
|
108
|
+
end
|
|
109
|
+
|
|
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
|
|
143
|
+
|
|
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
|
|
65
157
|
end
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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) }
|
|
70
169
|
end
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 : '.')
|
|
76
179
|
end
|
|
77
|
-
out
|
|
78
|
-
|
|
180
|
+
out.strip
|
|
181
|
+
end
|
|
182
|
+
|
|
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
|
+
)
|
|
79
195
|
end
|
|
80
196
|
|
|
81
197
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
@@ -87,12 +203,18 @@ module PWN
|
|
|
87
203
|
# Display Usage for this Module
|
|
88
204
|
|
|
89
205
|
public_class_method def self.help
|
|
90
|
-
puts "USAGE:
|
|
206
|
+
puts "USAGE (ruby-native, no external binaries):
|
|
91
207
|
#{self}.decode(
|
|
92
208
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
93
209
|
)
|
|
94
210
|
|
|
95
|
-
#{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.
|
|
96
218
|
|
|
97
219
|
#{self}.authors
|
|
98
220
|
"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PWN
|
|
4
|
+
module SDR
|
|
5
|
+
module Decoder
|
|
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
|
+
module RFID
|
|
14
|
+
# Supported Method Parameters::
|
|
15
|
+
# PWN::SDR::Decoder::RFID.decode(
|
|
16
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
17
|
+
# )
|
|
18
|
+
|
|
19
|
+
public_class_method def self.decode(opts = {})
|
|
20
|
+
freq_obj = opts[:freq_obj]
|
|
21
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
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(
|
|
27
|
+
freq_obj: freq_obj,
|
|
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
|
+
}
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Supported Method Parameters::
|
|
43
|
+
# PWN::SDR::Decoder::RFID.parse_line(line: 'UID: 04 A1 B2 C3 D4 E5 F6')
|
|
44
|
+
|
|
45
|
+
public_class_method def self.parse_line(opts = {})
|
|
46
|
+
line = opts[:line].to_s
|
|
47
|
+
out = { protocol: 'RFID' }
|
|
48
|
+
out[:uid] = ::Regexp.last_match(1).delete(' ') if line =~ /UID[:=]?\s*((?:[0-9A-Fa-f]{2}\s*){4,10})/
|
|
49
|
+
out[:epc] = ::Regexp.last_match(1) if line =~ /EPC[:=]?\s*([0-9A-Fa-f]+)/
|
|
50
|
+
out[:atqa] = ::Regexp.last_match(1) if line =~ /ATQA[:=]?\s*([0-9A-Fa-f ]+)/
|
|
51
|
+
out[:sak] = ::Regexp.last_match(1) if line =~ /SAK[:=]?\s*([0-9A-Fa-f]+)/
|
|
52
|
+
out[:tag] = ::Regexp.last_match(1) if line =~ /(EM4\w+|HID\w*|Mifare\w*|NTAG\w*|ISO\s?\d+)/i
|
|
53
|
+
out[:summary] = "RFID #{out[:tag]} UID=#{out[:uid] || out[:epc]}".squeeze(' ')
|
|
54
|
+
out.compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
58
|
+
|
|
59
|
+
public_class_method def self.authors
|
|
60
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Display Usage for this Module
|
|
64
|
+
|
|
65
|
+
public_class_method def self.help
|
|
66
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
67
|
+
#{self}.decode(
|
|
68
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
#{self}.parse_line(line: 'UID: 04 A1 B2 C3 D4 E5 F6 SAK: 08 Mifare Classic 1K')
|
|
72
|
+
|
|
73
|
+
#{self}.authors
|
|
74
|
+
"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module PWN
|
|
6
|
+
module SDR
|
|
7
|
+
module Decoder
|
|
8
|
+
# Pure-Ruby ISM/keyfob/sensor activity detector for the 315 / 390 /
|
|
9
|
+
# 433.92 / 868 / 915 MHz device zoo.
|
|
10
|
+
#
|
|
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.
|
|
18
|
+
module RTL433
|
|
19
|
+
# Supported Method Parameters::
|
|
20
|
+
# PWN::SDR::Decoder::RTL433.decode(
|
|
21
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
22
|
+
# )
|
|
23
|
+
|
|
24
|
+
public_class_method def self.decode(opts = {})
|
|
25
|
+
freq_obj = opts[:freq_obj]
|
|
26
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
27
|
+
freq_obj: freq_obj,
|
|
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
|
+
}
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Supported Method Parameters::
|
|
42
|
+
# PWN::SDR::Decoder::RTL433.parse_line(line: '{"time":"...","model":"..."}')
|
|
43
|
+
|
|
44
|
+
public_class_method def self.parse_line(opts = {})
|
|
45
|
+
line = opts[:line].to_s
|
|
46
|
+
h = begin
|
|
47
|
+
JSON.parse(line, symbolize_names: true)
|
|
48
|
+
rescue StandardError
|
|
49
|
+
{ unparsed: line }
|
|
50
|
+
end
|
|
51
|
+
out = { protocol: 'RTL433' }.merge(h)
|
|
52
|
+
bits = []
|
|
53
|
+
bits << out[:model].to_s if out[:model]
|
|
54
|
+
bits << "id=#{out[:id]}" if out[:id]
|
|
55
|
+
bits << "ch=#{out[:channel]}" if out[:channel]
|
|
56
|
+
bits << "code=#{out[:code]}" if out[:code]
|
|
57
|
+
bits << "cmd=#{out[:cmd] || out[:button]}" if out[:cmd] || out[:button]
|
|
58
|
+
bits << "temp=#{out[:temperature_C]}C" if out[:temperature_C]
|
|
59
|
+
bits << "rssi=#{out[:rssi]}" if out[:rssi]
|
|
60
|
+
out[:summary] = bits.empty? ? line[0, 120] : bits.join(' ')
|
|
61
|
+
out
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
65
|
+
|
|
66
|
+
public_class_method def self.authors
|
|
67
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Display Usage for this Module
|
|
71
|
+
|
|
72
|
+
public_class_method def self.help
|
|
73
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
74
|
+
#{self}.decode(
|
|
75
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
#{self}.parse_line(line: '{\"model\":\"Acurite-Tower\",\"id\":1234,...}')
|
|
79
|
+
|
|
80
|
+
#{self}.authors
|
|
81
|
+
"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PWN
|
|
4
|
+
module SDR
|
|
5
|
+
module Decoder
|
|
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`.
|
|
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
|
+
|
|
102
|
+
# Supported Method Parameters::
|
|
103
|
+
# PWN::SDR::Decoder::RTTY.decode(
|
|
104
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
105
|
+
# )
|
|
106
|
+
|
|
107
|
+
public_class_method def self.decode(opts = {})
|
|
108
|
+
freq_obj = opts[:freq_obj]
|
|
109
|
+
PWN::SDR::Decoder::Base.run_native(
|
|
110
|
+
freq_obj: freq_obj,
|
|
111
|
+
protocol: 'RTTY',
|
|
112
|
+
demod: Demod.new
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
117
|
+
|
|
118
|
+
public_class_method def self.authors
|
|
119
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Display Usage for this Module
|
|
123
|
+
|
|
124
|
+
public_class_method def self.help
|
|
125
|
+
puts "USAGE (ruby-native 2-FSK ITA2, no external binaries):
|
|
126
|
+
#{self}.decode(
|
|
127
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
128
|
+
)
|
|
129
|
+
|
|
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.
|
|
132
|
+
|
|
133
|
+
#{self}.authors
|
|
134
|
+
"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PWN
|
|
4
|
+
module SDR
|
|
5
|
+
module Decoder
|
|
6
|
+
# Pure-Ruby 802.11 (WiFi) activity detector.
|
|
7
|
+
#
|
|
8
|
+
# 20+ MHz OFDM cannot be demodulated in interpreted Ruby in real
|
|
9
|
+
# time. Native mode reports channel occupancy/duty and maps
|
|
10
|
+
# freq_obj to a WLAN channel number. `parse_line` retained for
|
|
11
|
+
# offline pipe-delimited tshark-fields analysis.
|
|
12
|
+
module WiFi
|
|
13
|
+
TSHARK_FIELDS = %w[
|
|
14
|
+
frame.time_relative wlan.fc.type_subtype wlan.bssid wlan.sa
|
|
15
|
+
wlan.da wlan_radio.channel wlan_radio.signal_dbm wlan.ssid
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
# Supported Method Parameters::
|
|
19
|
+
# PWN::SDR::Decoder::WiFi.decode(
|
|
20
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
21
|
+
# )
|
|
22
|
+
|
|
23
|
+
public_class_method def self.decode(opts = {})
|
|
24
|
+
freq_obj = opts[:freq_obj]
|
|
25
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
26
|
+
mhz = (hz / 1_000_000.0).round
|
|
27
|
+
ch = case mhz
|
|
28
|
+
when 2412..2472 then ((mhz - 2412) / 5) + 1
|
|
29
|
+
when 2484 then 14
|
|
30
|
+
when 5000..5900 then (mhz - 5000) / 5
|
|
31
|
+
else 0
|
|
32
|
+
end
|
|
33
|
+
PWN::SDR::Decoder::Base.run_detector(
|
|
34
|
+
freq_obj: freq_obj,
|
|
35
|
+
protocol: 'WiFi-802.11',
|
|
36
|
+
note: '20+ MHz OFDM — native mode reports channel occupancy/duty only.',
|
|
37
|
+
threshold: 6.0,
|
|
38
|
+
describe: proc { |b| { modulation: 'OFDM', channel: ch, mhz: mhz, airtime_ms: b[:duration_ms] } }
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Supported Method Parameters::
|
|
43
|
+
# PWN::SDR::Decoder::WiFi.parse_line(line: 't|subtype|bssid|sa|da|ch|rssi|ssid')
|
|
44
|
+
|
|
45
|
+
public_class_method def self.parse_line(opts = {})
|
|
46
|
+
f = opts[:line].to_s.split('|', -1)
|
|
47
|
+
out = {
|
|
48
|
+
protocol: 'WiFi', subtype: f[1], bssid: f[2], sa: f[3], da: f[4],
|
|
49
|
+
channel: f[5], rssi_dbm: f[6], ssid: f[7]
|
|
50
|
+
}.reject { |_, v| v.to_s.empty? }
|
|
51
|
+
out[:summary] = "WiFi ch#{out[:channel]} #{out[:bssid]} '#{out[:ssid]}' #{out[:rssi_dbm]}dBm"
|
|
52
|
+
out
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
56
|
+
|
|
57
|
+
public_class_method def self.authors
|
|
58
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Display Usage for this Module
|
|
62
|
+
|
|
63
|
+
public_class_method def self.help
|
|
64
|
+
puts "USAGE (ruby-native detector, no external binaries):
|
|
65
|
+
#{self}.decode(
|
|
66
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
#{self}.parse_line(line: '0.1|8|aa:bb:..|aa:bb:..|ff:ff:..|6|-42|linksys')
|
|
70
|
+
|
|
71
|
+
#{self}.authors
|
|
72
|
+
"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|