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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/README.md +14 -6
  4. data/bin/pwn_gqrx_scanner +25 -5
  5. data/documentation/Agent-Tool-Registry.md +3 -3
  6. data/documentation/Diagrams.md +6 -2
  7. data/documentation/Extrospection.md +30 -11
  8. data/documentation/Home.md +5 -4
  9. data/documentation/How-PWN-Works.md +7 -4
  10. data/documentation/Mistakes.md +96 -0
  11. data/documentation/Persistence.md +3 -1
  12. data/documentation/SDR.md +23 -5
  13. data/documentation/Skills-Memory-Learning.md +15 -5
  14. data/documentation/diagrams/agent-tool-registry.svg +94 -92
  15. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  16. data/documentation/diagrams/dot/extrospection-world-awareness.dot +9 -7
  17. data/documentation/diagrams/dot/memory-skills-detailed.dot +12 -5
  18. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
  19. data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
  20. data/documentation/diagrams/dot/persistence-filesystem.dot +3 -2
  21. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +19 -7
  22. data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
  23. data/documentation/diagrams/extrospection-world-awareness.svg +111 -93
  24. data/documentation/diagrams/memory-skills-detailed.svg +146 -97
  25. data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
  26. data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
  27. data/documentation/diagrams/persistence-filesystem.svg +77 -62
  28. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +217 -149
  29. data/documentation/diagrams/sdr-radio-flow.svg +76 -56
  30. data/documentation/pwn-ai-Agent.md +28 -15
  31. data/lib/pwn/ai/agent/extrospection.rb +65 -6
  32. data/lib/pwn/ai/agent/learning.rb +67 -7
  33. data/lib/pwn/ai/agent/loop.rb +72 -6
  34. data/lib/pwn/ai/agent/mistakes.rb +369 -0
  35. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  36. data/lib/pwn/ai/agent/tools/extrospection.rb +2 -2
  37. data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
  38. data/lib/pwn/ai/agent.rb +1 -0
  39. data/lib/pwn/plugins/jira_data_center.rb +2 -2
  40. data/lib/pwn/sdr/decoder/adsb.rb +101 -0
  41. data/lib/pwn/sdr/decoder/apt.rb +75 -0
  42. data/lib/pwn/sdr/decoder/bluetooth.rb +78 -0
  43. data/lib/pwn/sdr/decoder/dect.rb +84 -0
  44. data/lib/pwn/sdr/decoder/gps.rb +83 -0
  45. data/lib/pwn/sdr/decoder/gsm.rb +50 -29
  46. data/lib/pwn/sdr/decoder/iridium.rb +81 -0
  47. data/lib/pwn/sdr/decoder/lora.rb +87 -0
  48. data/lib/pwn/sdr/decoder/lte.rb +88 -0
  49. data/lib/pwn/sdr/decoder/morse.rb +64 -0
  50. data/lib/pwn/sdr/decoder/p25.rb +71 -0
  51. data/lib/pwn/sdr/decoder/pager.rb +77 -0
  52. data/lib/pwn/sdr/decoder/pocsag.rb +26 -11
  53. data/lib/pwn/sdr/decoder/rfid.rb +88 -0
  54. data/lib/pwn/sdr/decoder/rtl433.rb +95 -0
  55. data/lib/pwn/sdr/decoder/rtty.rb +66 -0
  56. data/lib/pwn/sdr/decoder/wifi.rb +98 -0
  57. data/lib/pwn/sdr/decoder/zigbee.rb +96 -0
  58. data/lib/pwn/sdr/decoder.rb +67 -7
  59. data/lib/pwn/sdr/frequency_allocation.rb +82 -41
  60. data/lib/pwn/sdr/gqrx.rb +186 -95
  61. data/lib/pwn/version.rb +1 -1
  62. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
  63. data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
  64. data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
  65. data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
  66. data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
  67. data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
  68. data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
  69. data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
  70. data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
  71. data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
  72. data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
  73. data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
  74. data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
  75. data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
  76. data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
  77. data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
  78. data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
  79. data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
  80. data/third_party/pwn_rdoc.jsonl +92 -3
  81. metadata +44 -6
  82. data/README.md.bak +0 -200
@@ -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, then reads them back with
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 = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
28
- gain = (freq_obj[:rf_gain] || 40).to_s.to_f
29
-
30
- tshark_fields = %w[
31
- frame.time
32
- gsmtap.arfcn
33
- gsmtap.chan_type
34
- gsm_a.imsi
35
- gsm_a.tmsi
36
- e212.mcc
37
- e212.mnc
38
- gsm_a.lac
39
- gsm_a.bssmap.cell_ci
40
- gsm_a.dtap.msg_rr_type
41
- ]
42
-
43
- direct_cmd = "sh -c 'grgsm_livemon_headless -f #{hz} -g #{gain} >/dev/null 2>&1 & " \
44
- 'LMPID=$!; trap "kill $LMPID 2>/dev/null" EXIT; sleep 2; ' \
45
- 'tshark -i lo -l -Y gsmtap -T fields -E separator=\| ' \
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
- summary_bits = []
84
- summary_bits << "ARFCN=#{out[:arfcn]}" if out[:arfcn]
85
- summary_bits << "MCC/MNC=#{out[:mcc]}/#{out[:mnc]}" if out[:mcc]
86
- summary_bits << "LAC=#{out[:lac]} CI=#{out[:cell_id]}" if out[:lac]
87
- summary_bits << "IMSI=#{out[:imsi]}" if out[:imsi]
88
- summary_bits << "TMSI=#{out[:tmsi]}" if out[:tmsi]
89
- out[:summary] = "GSM #{summary_bits.join(' ')}".strip
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: GQRX UDP audio → sox 48k→22.05k → multimon-ng POCSAG512/1200/2400.
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
- # This module parses those into { protocol, baud, address, function,
17
- # function_desc, type, type_desc, message } and JSON-logs each page.
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: 'multimon-ng -q -t raw -e -u -p ' \
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] = type
74
- out[:type_desc] = TYPE_DESC[type] || 'Unknown'
75
- out[:message] = m[2].to_s.gsub(/<[A-Z]{2,3}>/, '').strip
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
- out[:summary] = "POCSAG#{out[:baud]} RIC=#{out[:capcode]} F#{out[:function]} #{out[:type]}: #{out[:message]}"
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