pwn 0.5.618 → 0.5.620
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 +3 -3
- data/documentation/Diagrams.md +6 -2
- data/documentation/Extrospection.md +30 -11
- data/documentation/Home.md +5 -4
- data/documentation/How-PWN-Works.md +7 -4
- data/documentation/Mistakes.md +96 -0
- data/documentation/Persistence.md +3 -1
- data/documentation/SDR.md +23 -5
- data/documentation/Skills-Memory-Learning.md +15 -5
- data/documentation/diagrams/agent-tool-registry.svg +94 -92
- data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +9 -7
- data/documentation/diagrams/dot/memory-skills-detailed.dot +12 -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 +3 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +19 -7
- data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
- data/documentation/diagrams/extrospection-world-awareness.svg +111 -93
- data/documentation/diagrams/memory-skills-detailed.svg +146 -97
- data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
- data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
- data/documentation/diagrams/persistence-filesystem.svg +77 -62
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +217 -149
- data/documentation/diagrams/sdr-radio-flow.svg +76 -56
- data/documentation/pwn-ai-Agent.md +28 -15
- data/lib/pwn/ai/agent/extrospection.rb +65 -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 +2 -2
- 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 +101 -0
- data/lib/pwn/sdr/decoder/apt.rb +75 -0
- data/lib/pwn/sdr/decoder/bluetooth.rb +78 -0
- data/lib/pwn/sdr/decoder/dect.rb +84 -0
- data/lib/pwn/sdr/decoder/gps.rb +83 -0
- data/lib/pwn/sdr/decoder/gsm.rb +50 -29
- data/lib/pwn/sdr/decoder/iridium.rb +81 -0
- data/lib/pwn/sdr/decoder/lora.rb +87 -0
- data/lib/pwn/sdr/decoder/lte.rb +88 -0
- data/lib/pwn/sdr/decoder/morse.rb +64 -0
- data/lib/pwn/sdr/decoder/p25.rb +71 -0
- data/lib/pwn/sdr/decoder/pager.rb +77 -0
- data/lib/pwn/sdr/decoder/pocsag.rb +26 -11
- data/lib/pwn/sdr/decoder/rfid.rb +88 -0
- data/lib/pwn/sdr/decoder/rtl433.rb +95 -0
- data/lib/pwn/sdr/decoder/rtty.rb +66 -0
- data/lib/pwn/sdr/decoder/wifi.rb +98 -0
- data/lib/pwn/sdr/decoder/zigbee.rb +96 -0
- data/lib/pwn/sdr/decoder.rb +67 -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/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 +92 -3
- metadata +44 -6
- data/README.md.bak +0 -200
data/lib/pwn/sdr/decoder/gsm.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
3
5
|
module PWN
|
|
4
6
|
module SDR
|
|
5
7
|
module Decoder
|
|
@@ -7,14 +9,32 @@ module PWN
|
|
|
7
9
|
#
|
|
8
10
|
# GSM is 270.833 kbit/s GMSK — it CANNOT be recovered from GQRX's 48 kHz
|
|
9
11
|
# demodulated-audio UDP tap. This module therefore drives the SDR
|
|
10
|
-
# directly via `grgsm_livemon_headless` (from gr-gsm) which publishes
|
|
11
|
-
# decoded Um bursts as GSMTAP on udp/4729,
|
|
12
|
+
# directly via `grgsm_livemon_headless` (from gr-gsm), which publishes
|
|
13
|
+
# decoded Um bursts as GSMTAP on udp/4729, and reads them back with
|
|
12
14
|
# `tshark` for structured field extraction (MCC/MNC/LAC/CI/ARFCN, paging
|
|
13
15
|
# IMSIs/TMSIs, System Information messages, etc.).
|
|
14
16
|
#
|
|
15
17
|
# Interface matches PWN::SDR::Decoder::Flex / ::RDS so the GQRX
|
|
16
18
|
# dispatcher (`decoder: :gsm`) works uniformly.
|
|
19
|
+
#
|
|
20
|
+
# NOTE: grgsm_livemon_headless opens the SDR hardware itself. If GQRX
|
|
21
|
+
# already owns the device, pass a distinct `--args` string via
|
|
22
|
+
# freq_obj[:sdr_args] (e.g. 'rtl=1' or 'hackrf=0') or stop GQRX's DSP
|
|
23
|
+
# first (`U DSP 0`).
|
|
17
24
|
module GSM
|
|
25
|
+
TSHARK_FIELDS = %w[
|
|
26
|
+
frame.time
|
|
27
|
+
gsmtap.arfcn
|
|
28
|
+
gsmtap.chan_type
|
|
29
|
+
gsm_a.imsi
|
|
30
|
+
gsm_a.tmsi
|
|
31
|
+
e212.mcc
|
|
32
|
+
e212.mnc
|
|
33
|
+
gsm_a.lac
|
|
34
|
+
gsm_a.bssmap.cell_ci
|
|
35
|
+
gsm_a.dtap.msg_rr_type
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
18
38
|
# Supported Method Parameters::
|
|
19
39
|
# PWN::SDR::Decoder::GSM.decode(
|
|
20
40
|
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
@@ -24,26 +44,25 @@ module PWN
|
|
|
24
44
|
freq_obj = opts[:freq_obj]
|
|
25
45
|
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
26
46
|
|
|
27
|
-
hz
|
|
28
|
-
gain
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"#{tshark_fields.map { |f| "-e #{f}" }.join(' ')}'"
|
|
47
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
48
|
+
gain = (freq_obj[:rf_gain] || 40).to_s.to_f
|
|
49
|
+
sdr_args = freq_obj[:sdr_args].to_s
|
|
50
|
+
gsmtap = (freq_obj[:gsmtap_port] || 4729).to_i
|
|
51
|
+
|
|
52
|
+
grgsm = ['grgsm_livemon_headless', '-f', hz.to_s, '-g', gain.to_s]
|
|
53
|
+
grgsm.push('--args', sdr_args) unless sdr_args.empty?
|
|
54
|
+
|
|
55
|
+
tshark = ['tshark', '-i', 'lo', '-l', '-n',
|
|
56
|
+
'-f', "udp port #{gsmtap}",
|
|
57
|
+
'-Y', 'gsmtap',
|
|
58
|
+
'-T', 'fields', '-E', 'separator=|']
|
|
59
|
+
TSHARK_FIELDS.each { |f| tshark.push('-e', f) }
|
|
60
|
+
|
|
61
|
+
# bash -c '<grgsm> >/dev/null 2>&1 & pid=$!; trap ... ; <tshark>'
|
|
62
|
+
inner = "#{Shellwords.join(grgsm)} >/dev/null 2>&1 & " \
|
|
63
|
+
'LMPID=$!; trap "kill $LMPID 2>/dev/null" EXIT INT TERM; ' \
|
|
64
|
+
"sleep 2; exec #{Shellwords.join(tshark)}"
|
|
65
|
+
direct_cmd = "bash -c #{Shellwords.escape(inner)}"
|
|
47
66
|
|
|
48
67
|
PWN::SDR::Decoder::Base.run_pipeline(
|
|
49
68
|
freq_obj: freq_obj,
|
|
@@ -63,6 +82,7 @@ module PWN
|
|
|
63
82
|
f = line.split('|', -1)
|
|
64
83
|
out = {
|
|
65
84
|
protocol: 'GSM',
|
|
85
|
+
frame_time: f[0],
|
|
66
86
|
arfcn: f[1],
|
|
67
87
|
chan_type: f[2],
|
|
68
88
|
imsi: f[3],
|
|
@@ -80,13 +100,13 @@ module PWN
|
|
|
80
100
|
out[:imsi_msin] = out[:imsi][6..]
|
|
81
101
|
end
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
out[:summary] = "GSM #{
|
|
103
|
+
bits = []
|
|
104
|
+
bits << "ARFCN=#{out[:arfcn]}" if out[:arfcn]
|
|
105
|
+
bits << "MCC/MNC=#{out[:mcc]}/#{out[:mnc]}" if out[:mcc]
|
|
106
|
+
bits << "LAC=#{out[:lac]} CI=#{out[:cell_id]}" if out[:lac]
|
|
107
|
+
bits << "IMSI=#{out[:imsi]}" if out[:imsi]
|
|
108
|
+
bits << "TMSI=#{out[:tmsi]}" if out[:tmsi]
|
|
109
|
+
out[:summary] = "GSM #{bits.join(' ')}".strip
|
|
90
110
|
out
|
|
91
111
|
end
|
|
92
112
|
|
|
@@ -107,6 +127,7 @@ module PWN
|
|
|
107
127
|
NOTE: Requires `grgsm_livemon_headless` (gr-gsm) and `tshark`.
|
|
108
128
|
GSM cannot be decoded from GQRX's 48 kHz audio tap; this
|
|
109
129
|
module drives the SDR directly and reads GSMTAP on lo:4729.
|
|
130
|
+
Set freq_obj[:sdr_args] (e.g. 'rtl=1') if GQRX owns device 0.
|
|
110
131
|
|
|
111
132
|
#{self}.parse_line(line: 'ts|arfcn|chan|imsi|tmsi|mcc|mnc|lac|ci|rr')
|
|
112
133
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module PWN
|
|
6
|
+
module SDR
|
|
7
|
+
module Decoder
|
|
8
|
+
# Iridium L-band (1.616–1.6265 GHz) burst decoder. Drives gr-iridium's
|
|
9
|
+
# `iridium-extractor` against the SDR to demodulate 25 kbit/s DE-QPSK
|
|
10
|
+
# simplex/duplex bursts, then pipes them through iridium-toolkit's
|
|
11
|
+
# `iridium-parser.py` for frame classification (IRA/IBC/IDA/ISY/VOC/...).
|
|
12
|
+
module Iridium
|
|
13
|
+
# Supported Method Parameters::
|
|
14
|
+
# PWN::SDR::Decoder::Iridium.decode(
|
|
15
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
16
|
+
# )
|
|
17
|
+
|
|
18
|
+
public_class_method def self.decode(opts = {})
|
|
19
|
+
freq_obj = opts[:freq_obj]
|
|
20
|
+
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
23
|
+
gain = (freq_obj[:rf_gain] || 40).to_s.to_f
|
|
24
|
+
sdr_args = (freq_obj[:sdr_args] || 'rtl=0').to_s
|
|
25
|
+
|
|
26
|
+
inner = "iridium-extractor -D 4 --multi-frame -c #{hz} -r 2000000 " \
|
|
27
|
+
"-g #{gain} -o - #{Shellwords.escape(sdr_args)} 2>/dev/null " \
|
|
28
|
+
'| iridium-parser.py --harder /dev/stdin'
|
|
29
|
+
direct_cmd = "bash -c #{Shellwords.escape(inner)}"
|
|
30
|
+
|
|
31
|
+
PWN::SDR::Decoder::Base.run_pipeline(
|
|
32
|
+
freq_obj: freq_obj,
|
|
33
|
+
protocol: 'IRIDIUM',
|
|
34
|
+
required_bins: %w[iridium-extractor iridium-parser.py],
|
|
35
|
+
direct_cmd: direct_cmd,
|
|
36
|
+
line_match: /^(IRA|IBC|IDA|ISY|ITL|MSG|VOC|RAW):/,
|
|
37
|
+
parser: proc { |line| parse_line(line: line) }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Supported Method Parameters::
|
|
42
|
+
# PWN::SDR::Decoder::Iridium.parse_line(line: 'IRA: sat:23 beam:14 ...')
|
|
43
|
+
|
|
44
|
+
public_class_method def self.parse_line(opts = {})
|
|
45
|
+
line = opts[:line].to_s
|
|
46
|
+
out = { protocol: 'IRIDIUM' }
|
|
47
|
+
out[:frame_type] = ::Regexp.last_match(1) if line =~ /^([A-Z]{3}):/
|
|
48
|
+
out[:sat] = ::Regexp.last_match(1) if line =~ /sat:(\d+)/
|
|
49
|
+
out[:beam] = ::Regexp.last_match(1) if line =~ /beam:(\d+)/
|
|
50
|
+
out[:pos] = ::Regexp.last_match(1) if line =~ /pos=\(([^)]+)\)/
|
|
51
|
+
out[:ra_id] = ::Regexp.last_match(1) if line =~ /ric:(\d+)/
|
|
52
|
+
out[:summary] = "IRIDIUM #{out[:frame_type]} sat=#{out[:sat]} beam=#{out[:beam]}"
|
|
53
|
+
out.compact
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
57
|
+
|
|
58
|
+
public_class_method def self.authors
|
|
59
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Display Usage for this Module
|
|
63
|
+
|
|
64
|
+
public_class_method def self.help
|
|
65
|
+
puts "USAGE:
|
|
66
|
+
#{self}.decode(
|
|
67
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
NOTE: Requires `iridium-extractor` (gr-iridium) and
|
|
71
|
+
`iridium-parser.py` (iridium-toolkit). Owns the SDR.
|
|
72
|
+
|
|
73
|
+
#{self}.parse_line(line: 'IRA: sat:23 beam:14 pos=(+32.1/-097.0) ...')
|
|
74
|
+
|
|
75
|
+
#{self}.authors
|
|
76
|
+
"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module PWN
|
|
7
|
+
module SDR
|
|
8
|
+
module Decoder
|
|
9
|
+
# LoRa / LoRaWAN CSS decoder for the 433 / 868 / 902–928 MHz ISM
|
|
10
|
+
# allocations. Chirp-spread-spectrum needs raw I/Q, so this drives
|
|
11
|
+
# `rtl_433` (which ships a native LoRa demod, protocol #264) directly.
|
|
12
|
+
# Emits one JSON line per decoded uplink containing SF/BW/CR, DevAddr,
|
|
13
|
+
# FCnt, and raw PHYPayload hex.
|
|
14
|
+
module LoRa
|
|
15
|
+
# Supported Method Parameters::
|
|
16
|
+
# PWN::SDR::Decoder::LoRa.decode(
|
|
17
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
18
|
+
# )
|
|
19
|
+
|
|
20
|
+
public_class_method def self.decode(opts = {})
|
|
21
|
+
freq_obj = opts[:freq_obj]
|
|
22
|
+
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
25
|
+
sdr_args = freq_obj[:sdr_args].to_s
|
|
26
|
+
|
|
27
|
+
cmd = ['rtl_433', '-f', hz.to_s, '-s', '1024k',
|
|
28
|
+
'-R', '264', '-F', 'json', '-M', 'level']
|
|
29
|
+
cmd.push('-d', sdr_args) unless sdr_args.empty?
|
|
30
|
+
|
|
31
|
+
PWN::SDR::Decoder::Base.run_pipeline(
|
|
32
|
+
freq_obj: freq_obj,
|
|
33
|
+
protocol: 'LoRa',
|
|
34
|
+
required_bins: %w[rtl_433],
|
|
35
|
+
direct_cmd: Shellwords.join(cmd),
|
|
36
|
+
line_match: /^\s*{/,
|
|
37
|
+
parser: proc { |line| parse_line(line: line) }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Supported Method Parameters::
|
|
42
|
+
# PWN::SDR::Decoder::LoRa.parse_line(line: '{"model":"LoRa",...}')
|
|
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: 'LoRa' }.merge(h)
|
|
52
|
+
bits = []
|
|
53
|
+
bits << "SF#{out[:sf]}" if out[:sf]
|
|
54
|
+
bits << "BW#{out[:bw]}" if out[:bw]
|
|
55
|
+
bits << "CR#{out[:cr]}" if out[:cr]
|
|
56
|
+
bits << "DevAddr=#{out[:devaddr] || out[:id]}" if out[:devaddr] || out[:id]
|
|
57
|
+
bits << "FCnt=#{out[:fcnt]}" if out[:fcnt]
|
|
58
|
+
bits << "len=#{out[:len] || (out[:data].to_s.length / 2)}" if out[:len] || out[:data]
|
|
59
|
+
out[:summary] = "LoRa #{bits.join(' ')}".strip
|
|
60
|
+
out
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
64
|
+
|
|
65
|
+
public_class_method def self.authors
|
|
66
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Display Usage for this Module
|
|
70
|
+
|
|
71
|
+
public_class_method def self.help
|
|
72
|
+
puts "USAGE:
|
|
73
|
+
#{self}.decode(
|
|
74
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
NOTE: Requires `rtl_433` >= 23.x (native LoRa demod). Owns SDR.
|
|
78
|
+
|
|
79
|
+
#{self}.parse_line(line: '{\"model\":\"LoRa\",\"sf\":7,\"bw\":125,...}')
|
|
80
|
+
|
|
81
|
+
#{self}.authors
|
|
82
|
+
"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module PWN
|
|
6
|
+
module SDR
|
|
7
|
+
module Decoder
|
|
8
|
+
# LTE / UMTS / CDMA / PCS / AWS wideband cellular cell-search decoder.
|
|
9
|
+
#
|
|
10
|
+
# 1.4–20 MHz OFDMA/WCDMA cannot be recovered from GQRX audio; this
|
|
11
|
+
# module drives srsRAN's `cell_search` (or `LTE-Cell-Scanner`'s
|
|
12
|
+
# `CellSearch`) directly against the SDR to enumerate physical cells:
|
|
13
|
+
# EARFCN, PCI, CP type, RSRP/RSRQ, PSS/SSS correlation.
|
|
14
|
+
module LTE
|
|
15
|
+
# Supported Method Parameters::
|
|
16
|
+
# PWN::SDR::Decoder::LTE.decode(
|
|
17
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
18
|
+
# )
|
|
19
|
+
|
|
20
|
+
public_class_method def self.decode(opts = {})
|
|
21
|
+
freq_obj = opts[:freq_obj]
|
|
22
|
+
raise 'ERROR: :freq_obj is required' unless freq_obj.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
|
|
25
|
+
gain = (freq_obj[:rf_gain] || 40).to_s.to_f
|
|
26
|
+
sdr_args = (freq_obj[:sdr_args] || 'driver=rtlsdr').to_s
|
|
27
|
+
|
|
28
|
+
direct_cmd =
|
|
29
|
+
if PWN::SDR::Decoder::Base.bin_available?(bin: 'cell_search')
|
|
30
|
+
# srsRAN cell_search takes a band number; fall back to explicit
|
|
31
|
+
# DL frequency via -s/-e range when unavailable.
|
|
32
|
+
"cell_search -a #{Shellwords.escape(sdr_args)} -g #{gain} -s #{hz} -e #{hz}"
|
|
33
|
+
else
|
|
34
|
+
"CellSearch -s #{hz / 1_000_000.0} -e #{hz / 1_000_000.0} -g #{gain}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
PWN::SDR::Decoder::Base.run_pipeline(
|
|
38
|
+
freq_obj: freq_obj,
|
|
39
|
+
protocol: 'LTE',
|
|
40
|
+
required_bins: %w[cell_search],
|
|
41
|
+
direct_cmd: direct_cmd,
|
|
42
|
+
line_match: /(CELL|PCI|EARFCN|Found|RSRP|CID)/i,
|
|
43
|
+
parser: proc { |line| parse_line(line: line) }
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Supported Method Parameters::
|
|
48
|
+
# PWN::SDR::Decoder::LTE.parse_line(line: 'Found CELL ... PCI: 123 ... PRB: 50 ... -85.0 dBm')
|
|
49
|
+
|
|
50
|
+
public_class_method def self.parse_line(opts = {})
|
|
51
|
+
line = opts[:line].to_s
|
|
52
|
+
out = { protocol: 'LTE' }
|
|
53
|
+
out[:earfcn] = ::Regexp.last_match(1) if line =~ /EARFCN[:= ]+(\d+)/i
|
|
54
|
+
out[:pci] = ::Regexp.last_match(1) if line =~ /(?:PCI|N_id_cell|Id)[:= ]+(\d{1,3})/i
|
|
55
|
+
out[:prb] = (::Regexp.last_match(1) || ::Regexp.last_match(2)) if line =~ /(?:PRB[:= ]+(\d+)|(\d+)\s*PRB)/i
|
|
56
|
+
out[:cp] = ::Regexp.last_match(1) if line =~ /\b(Normal|Extended)\b\s*CP/i
|
|
57
|
+
out[:rsrp] = ::Regexp.last_match(1) if line =~ /(-?\d+(?:\.\d+)?)\s*dBm/
|
|
58
|
+
out[:freq_mhz] = ::Regexp.last_match(1) if line =~ /(\d{3,4}\.\d)\s*MHz/
|
|
59
|
+
out[:summary] = "LTE PCI=#{out[:pci]} EARFCN=#{out[:earfcn]} PRB=#{out[:prb]} RSRP=#{out[:rsrp]}dBm"
|
|
60
|
+
out.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
64
|
+
|
|
65
|
+
public_class_method def self.authors
|
|
66
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Display Usage for this Module
|
|
70
|
+
|
|
71
|
+
public_class_method def self.help
|
|
72
|
+
puts "USAGE:
|
|
73
|
+
#{self}.decode(
|
|
74
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
NOTE: Requires srsRAN `cell_search` (or LTE-Cell-Scanner
|
|
78
|
+
`CellSearch`). Owns the SDR — set freq_obj[:sdr_args].
|
|
79
|
+
|
|
80
|
+
#{self}.parse_line(line: 'Found CELL 739.0 MHz, EARFCN=5110, PCI=123, 50 PRB, -85.2 dBm')
|
|
81
|
+
|
|
82
|
+
#{self}.authors
|
|
83
|
+
"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PWN
|
|
4
|
+
module SDR
|
|
5
|
+
module Decoder
|
|
6
|
+
# CW / Morse decoder for the amateur CW sub-bands (cw20/cw40/cw80,
|
|
7
|
+
# amateur_30m). GQRX's CW demodulator produces a ~700 Hz sidetone in the
|
|
8
|
+
# 48 kHz UDP audio stream; multimon-ng's MORSE_CW demod recovers the
|
|
9
|
+
# dit/dah timing and prints decoded characters one line at a time.
|
|
10
|
+
module Morse
|
|
11
|
+
# Supported Method Parameters::
|
|
12
|
+
# PWN::SDR::Decoder::Morse.decode(
|
|
13
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
14
|
+
# )
|
|
15
|
+
|
|
16
|
+
public_class_method def self.decode(opts = {})
|
|
17
|
+
freq_obj = opts[:freq_obj]
|
|
18
|
+
|
|
19
|
+
PWN::SDR::Decoder::Base.run_pipeline(
|
|
20
|
+
freq_obj: freq_obj,
|
|
21
|
+
protocol: 'MORSE-CW',
|
|
22
|
+
required_bins: %w[sox multimon-ng],
|
|
23
|
+
decode_cmd: 'multimon-ng -q -t raw -a MORSE_CW -',
|
|
24
|
+
line_match: /\S/,
|
|
25
|
+
parser: proc { |line| parse_line(line: line) }
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Supported Method Parameters::
|
|
30
|
+
# PWN::SDR::Decoder::Morse.parse_line(line: 'CQ CQ DE W1AW')
|
|
31
|
+
|
|
32
|
+
public_class_method def self.parse_line(opts = {})
|
|
33
|
+
line = opts[:line].to_s.gsub(/^MORSE(?:_CW)?:\s*/i, '').strip
|
|
34
|
+
out = { protocol: 'MORSE-CW', text: line }
|
|
35
|
+
if (m = line.match(/\b([A-Z0-9]{1,3}\d[A-Z]{1,4})\b/))
|
|
36
|
+
out[:callsign] = m[1]
|
|
37
|
+
end
|
|
38
|
+
out[:summary] = "CW #{line}"[0, 120]
|
|
39
|
+
out
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
43
|
+
|
|
44
|
+
public_class_method def self.authors
|
|
45
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Display Usage for this Module
|
|
49
|
+
|
|
50
|
+
public_class_method def self.help
|
|
51
|
+
puts "USAGE:
|
|
52
|
+
#{self}.decode(
|
|
53
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
#{self}.parse_line(line: 'CQ CQ DE W1AW W1AW K')
|
|
57
|
+
|
|
58
|
+
#{self}.authors
|
|
59
|
+
"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PWN
|
|
4
|
+
module SDR
|
|
5
|
+
module Decoder
|
|
6
|
+
# APCO Project 25 (P25) Phase-1 C4FM decoder for the 700/800 MHz
|
|
7
|
+
# public-safety allocations. GQRX supplies NBFM-discriminator audio via
|
|
8
|
+
# the UDP tap; `dsd` (Digital Speech Decoder) recovers the C4FM symbol
|
|
9
|
+
# stream and prints trunking-control frames (NAC, TGID, RID, DUID, ...).
|
|
10
|
+
module P25
|
|
11
|
+
# Supported Method Parameters::
|
|
12
|
+
# PWN::SDR::Decoder::P25.decode(
|
|
13
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
14
|
+
# )
|
|
15
|
+
|
|
16
|
+
public_class_method def self.decode(opts = {})
|
|
17
|
+
freq_obj = opts[:freq_obj]
|
|
18
|
+
|
|
19
|
+
# dsd expects 48 kHz s16le on stdin — bypass Base's 22 050 Hz resample
|
|
20
|
+
# by asking for 48 000 (sox becomes a passthrough / format guard).
|
|
21
|
+
PWN::SDR::Decoder::Base.run_pipeline(
|
|
22
|
+
freq_obj: freq_obj,
|
|
23
|
+
protocol: 'P25',
|
|
24
|
+
required_bins: %w[sox dsd],
|
|
25
|
+
resample_hz: 48_000,
|
|
26
|
+
decode_cmd: 'dsd -q -i - -o /dev/null -f1',
|
|
27
|
+
line_match: /(NAC|TGID|TG:|RID|src:|P25|Sync:)/i,
|
|
28
|
+
parser: proc { |line| parse_line(line: line) }
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Supported Method Parameters::
|
|
33
|
+
# PWN::SDR::Decoder::P25.parse_line(line: 'Sync: +P25p1 NAC: 293 src: 1234 tg: 5678')
|
|
34
|
+
|
|
35
|
+
public_class_method def self.parse_line(opts = {})
|
|
36
|
+
line = opts[:line].to_s
|
|
37
|
+
out = { protocol: 'P25' }
|
|
38
|
+
out[:nac] = ::Regexp.last_match(1) if line =~ /NAC:\s*([0-9A-Fa-f]+)/
|
|
39
|
+
out[:talkgroup] = ::Regexp.last_match(1) if line =~ /(?:TGID|tg[: ])\s*(\d+)/i
|
|
40
|
+
out[:radio_id] = ::Regexp.last_match(1) if line =~ /(?:RID|src[: ])\s*(\d+)/i
|
|
41
|
+
out[:duid] = ::Regexp.last_match(1) if line =~ /DUID:\s*(\S+)/
|
|
42
|
+
out[:sync] = ::Regexp.last_match(1) if line =~ /Sync:\s*(\S+)/
|
|
43
|
+
out[:summary] = "P25 NAC=#{out[:nac]} TG=#{out[:talkgroup]} RID=#{out[:radio_id]}".squeeze(' ')
|
|
44
|
+
out.compact
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
48
|
+
|
|
49
|
+
public_class_method def self.authors
|
|
50
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Display Usage for this Module
|
|
54
|
+
|
|
55
|
+
public_class_method def self.help
|
|
56
|
+
puts "USAGE:
|
|
57
|
+
#{self}.decode(
|
|
58
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
NOTE: Requires `dsd`. Set GQRX demod to Narrow FM, ~12.5 kHz.
|
|
62
|
+
|
|
63
|
+
#{self}.parse_line(line: 'Sync: +P25p1 NAC: 293 src: 1234 tg: 5678')
|
|
64
|
+
|
|
65
|
+
#{self}.authors
|
|
66
|
+
"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PWN
|
|
4
|
+
module SDR
|
|
5
|
+
module Decoder
|
|
6
|
+
# Combined pager decoder for the mixed-protocol `pager_all` band plan.
|
|
7
|
+
#
|
|
8
|
+
# Runs multimon-ng with FLEX + FLEX_NEXT + POCSAG512/1200/2400 enabled
|
|
9
|
+
# simultaneously and dispatches each output line to the appropriate
|
|
10
|
+
# per-protocol parser (::Flex-style or ::POCSAG.parse_line).
|
|
11
|
+
module Pager
|
|
12
|
+
# Supported Method Parameters::
|
|
13
|
+
# PWN::SDR::Decoder::Pager.decode(
|
|
14
|
+
# freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
15
|
+
# )
|
|
16
|
+
|
|
17
|
+
public_class_method def self.decode(opts = {})
|
|
18
|
+
freq_obj = opts[:freq_obj]
|
|
19
|
+
|
|
20
|
+
decode_cmd = 'multimon-ng -q -t raw -e -u -p ' \
|
|
21
|
+
'-a FLEX -a FLEX_NEXT ' \
|
|
22
|
+
'-a POCSAG512 -a POCSAG1200 -a POCSAG2400 -'
|
|
23
|
+
|
|
24
|
+
PWN::SDR::Decoder::Base.run_pipeline(
|
|
25
|
+
freq_obj: freq_obj,
|
|
26
|
+
protocol: 'PAGER',
|
|
27
|
+
required_bins: %w[sox multimon-ng],
|
|
28
|
+
decode_cmd: decode_cmd,
|
|
29
|
+
line_match: /^(FLEX|POCSAG)/,
|
|
30
|
+
parser: proc { |line| parse_line(line: line) }
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Supported Method Parameters::
|
|
35
|
+
# PWN::SDR::Decoder::Pager.parse_line(line: 'FLEX|... or POCSAG1200: ...')
|
|
36
|
+
|
|
37
|
+
public_class_method def self.parse_line(opts = {})
|
|
38
|
+
line = opts[:line].to_s
|
|
39
|
+
return PWN::SDR::Decoder::POCSAG.parse_line(line: line) if line.start_with?('POCSAG')
|
|
40
|
+
|
|
41
|
+
# FLEX / FLEX_NEXT — pipe- or space-delimited (see ::Flex for format).
|
|
42
|
+
delim = line.start_with?('FLEX: ') ? ' ' : '|'
|
|
43
|
+
parts = line.split(delim)
|
|
44
|
+
proto = line.start_with?('FLEX_NEXT') ? 'FLEX_NEXT' : 'FLEX'
|
|
45
|
+
types = %w[ALN BIN HEX NUM TON TONE UNK]
|
|
46
|
+
t_idx = parts.index { |p| types.include?(p) }
|
|
47
|
+
out = { protocol: proto, raw_inspected: line.inspect }
|
|
48
|
+
out[:capcode] = parts.find { |p| p.match?(/^\[?\d{7,10}\]?$/) }
|
|
49
|
+
out[:type] = t_idx ? parts[t_idx] : nil
|
|
50
|
+
out[:type_payload] = t_idx ? parts[(t_idx + 1)..].join(delim) : nil
|
|
51
|
+
out[:summary] = "#{proto} RIC=#{out[:capcode]} #{out[:type]}: #{out[:type_payload]}"
|
|
52
|
+
out.compact
|
|
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:
|
|
65
|
+
#{self}.decode(
|
|
66
|
+
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
#{self}.parse_line(line: 'FLEX|3200|...|ALN|MESSAGE')
|
|
70
|
+
|
|
71
|
+
#{self}.authors
|
|
72
|
+
"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -5,7 +5,8 @@ module PWN
|
|
|
5
5
|
module Decoder
|
|
6
6
|
# POCSAG (CCIR Radiopaging Code No. 1) decoder for pager networks.
|
|
7
7
|
#
|
|
8
|
-
# Pipeline
|
|
8
|
+
# Pipeline (identical shape to PWN::SDR::Decoder::Flex):
|
|
9
|
+
# GQRX 48 kHz s16le UDP audio → sox → 22 050 Hz → multimon-ng.
|
|
9
10
|
#
|
|
10
11
|
# multimon-ng emits lines of the form:
|
|
11
12
|
# POCSAG1200: Address: 123456 Function: 3 Alpha: FIRE ALARM ZONE 4
|
|
@@ -13,8 +14,8 @@ module PWN
|
|
|
13
14
|
# POCSAG2400: Address: 987654 Function: 1 Skyper: ...
|
|
14
15
|
# POCSAG1200: Address: 123456 Function: 2 Alpha: <partial><EOT>
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
# function_desc, type, type_desc, message } and JSON-
|
|
17
|
+
# Each line is parsed into { protocol, baud, address/capcode, function,
|
|
18
|
+
# function_desc, type, type_desc, message } and JSON-logged.
|
|
18
19
|
module POCSAG
|
|
19
20
|
FUNCTION_DESC = {
|
|
20
21
|
0 => 'Numeric (Tone/A)',
|
|
@@ -38,12 +39,19 @@ module PWN
|
|
|
38
39
|
public_class_method def self.decode(opts = {})
|
|
39
40
|
freq_obj = opts[:freq_obj]
|
|
40
41
|
|
|
42
|
+
# -e hide empty messages
|
|
43
|
+
# -u heuristically prune unlikely decodes (cuts BCH false-positives)
|
|
44
|
+
# -p emit partially received messages (still useful intel)
|
|
45
|
+
# -f alpha force Alpha framing when function bits are ambiguous —
|
|
46
|
+
# mirrors the behaviour operators expect from Flex ALN.
|
|
47
|
+
decode_cmd = 'multimon-ng -q -t raw -e -u -p -f alpha ' \
|
|
48
|
+
'-a POCSAG512 -a POCSAG1200 -a POCSAG2400 -'
|
|
49
|
+
|
|
41
50
|
PWN::SDR::Decoder::Base.run_pipeline(
|
|
42
51
|
freq_obj: freq_obj,
|
|
43
52
|
protocol: 'POCSAG',
|
|
44
53
|
required_bins: %w[sox multimon-ng],
|
|
45
|
-
decode_cmd:
|
|
46
|
-
'-a POCSAG512 -a POCSAG1200 -a POCSAG2400 -f auto -',
|
|
54
|
+
decode_cmd: decode_cmd,
|
|
47
55
|
line_match: /^POCSAG(?:512|1200|2400):/,
|
|
48
56
|
parser: proc { |line| parse_line(line: line) }
|
|
49
57
|
)
|
|
@@ -54,14 +62,14 @@ module PWN
|
|
|
54
62
|
|
|
55
63
|
public_class_method def self.parse_line(opts = {})
|
|
56
64
|
line = opts[:line].to_s
|
|
57
|
-
out = { protocol: 'POCSAG' }
|
|
65
|
+
out = { protocol: 'POCSAG', raw_inspected: line.inspect }
|
|
58
66
|
|
|
59
67
|
if (m = line.match(/^POCSAG(\d+):/))
|
|
60
68
|
out[:baud] = m[1].to_i
|
|
61
69
|
end
|
|
62
70
|
if (m = line.match(/Address:\s*(\d+)/i))
|
|
63
71
|
out[:address] = m[1].to_i
|
|
64
|
-
out[:capcode] = m[1]
|
|
72
|
+
out[:capcode] = m[1].rjust(7, '0')
|
|
65
73
|
end
|
|
66
74
|
if (m = line.match(/Function:\s*(\d+)/i))
|
|
67
75
|
fn = m[1].to_i
|
|
@@ -70,11 +78,18 @@ module PWN
|
|
|
70
78
|
end
|
|
71
79
|
if (m = line.match(/\b(Alpha|Numeric|Skyper|Tone):\s*(.*)$/i))
|
|
72
80
|
type = m[1].capitalize
|
|
73
|
-
out[:type]
|
|
74
|
-
out[:type_desc]
|
|
75
|
-
out[:
|
|
81
|
+
out[:type] = type
|
|
82
|
+
out[:type_desc] = TYPE_DESC[type] || 'Unknown'
|
|
83
|
+
out[:type_payload] = m[2].to_s.gsub(/<[A-Z]{2,4}>/, '').strip
|
|
84
|
+
out[:message] = out[:type_payload]
|
|
76
85
|
end
|
|
77
|
-
|
|
86
|
+
|
|
87
|
+
bits = []
|
|
88
|
+
bits << "POCSAG#{out[:baud]}" if out[:baud]
|
|
89
|
+
bits << "RIC=#{out[:capcode]}" if out[:capcode]
|
|
90
|
+
bits << "F#{out[:function]}(#{out[:function_desc]})" if out[:function]
|
|
91
|
+
bits << "#{out[:type]}: #{out[:message]}" if out[:type]
|
|
92
|
+
out[:summary] = bits.join(' ')
|
|
78
93
|
out
|
|
79
94
|
end
|
|
80
95
|
|