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.
Files changed (107) 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 +4 -4
  6. data/documentation/Cron.md +13 -0
  7. data/documentation/Diagrams.md +6 -2
  8. data/documentation/Extrospection.md +96 -11
  9. data/documentation/Home.md +5 -4
  10. data/documentation/How-PWN-Works.md +7 -4
  11. data/documentation/Mistakes.md +97 -0
  12. data/documentation/Persistence.md +4 -1
  13. data/documentation/SDR.md +23 -5
  14. data/documentation/Skills-Memory-Learning.md +16 -5
  15. data/documentation/Transparent-Browser.md +18 -1
  16. data/documentation/diagrams/agent-tool-registry.svg +103 -100
  17. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  18. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  19. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  20. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  21. data/documentation/diagrams/cron-scheduling.svg +23 -23
  22. data/documentation/diagrams/dot/agent-tool-registry.dot +3 -3
  23. data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -10
  24. data/documentation/diagrams/dot/memory-skills-detailed.dot +16 -5
  25. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +93 -0
  26. data/documentation/diagrams/dot/overall-pwn-architecture.dot +7 -5
  27. data/documentation/diagrams/dot/persistence-filesystem.dot +4 -2
  28. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +25 -7
  29. data/documentation/diagrams/dot/sdr-radio-flow.dot +13 -11
  30. data/documentation/diagrams/driver-framework.svg +13 -13
  31. data/documentation/diagrams/extrospection-world-awareness.svg +241 -93
  32. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  33. data/documentation/diagrams/hardware-hacking.svg +18 -18
  34. data/documentation/diagrams/history-to-drivers.svg +18 -18
  35. data/documentation/diagrams/memory-skills-detailed.svg +169 -97
  36. data/documentation/diagrams/mistakes-negative-feedback.svg +266 -0
  37. data/documentation/diagrams/network-infra-testing.svg +27 -27
  38. data/documentation/diagrams/overall-pwn-architecture.svg +184 -173
  39. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  40. data/documentation/diagrams/persistence-filesystem.svg +111 -87
  41. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  42. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +263 -149
  43. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  44. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  45. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  46. data/documentation/diagrams/sdr-radio-flow.svg +76 -56
  47. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  48. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  49. data/documentation/diagrams/web-application-testing.svg +26 -26
  50. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  51. data/documentation/pwn-ai-Agent.md +28 -15
  52. data/lib/pwn/ai/agent/extrospection.rb +494 -6
  53. data/lib/pwn/ai/agent/learning.rb +67 -7
  54. data/lib/pwn/ai/agent/loop.rb +72 -6
  55. data/lib/pwn/ai/agent/mistakes.rb +369 -0
  56. data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
  57. data/lib/pwn/ai/agent/tools/extrospection.rb +88 -3
  58. data/lib/pwn/ai/agent/tools/mistakes.rb +132 -0
  59. data/lib/pwn/ai/agent.rb +1 -0
  60. data/lib/pwn/plugins/jira_data_center.rb +2 -2
  61. data/lib/pwn/sdr/decoder/adsb.rb +82 -0
  62. data/lib/pwn/sdr/decoder/apt.rb +136 -0
  63. data/lib/pwn/sdr/decoder/base.rb +258 -171
  64. data/lib/pwn/sdr/decoder/bluetooth.rb +71 -0
  65. data/lib/pwn/sdr/decoder/dect.rb +70 -0
  66. data/lib/pwn/sdr/decoder/dsp.rb +396 -0
  67. data/lib/pwn/sdr/decoder/flex.rb +177 -210
  68. data/lib/pwn/sdr/decoder/gps.rb +74 -0
  69. data/lib/pwn/sdr/decoder/gsm.rb +31 -62
  70. data/lib/pwn/sdr/decoder/iridium.rb +66 -0
  71. data/lib/pwn/sdr/decoder/lora.rb +80 -0
  72. data/lib/pwn/sdr/decoder/lte.rb +67 -0
  73. data/lib/pwn/sdr/decoder/morse.rb +138 -0
  74. data/lib/pwn/sdr/decoder/p25.rb +74 -0
  75. data/lib/pwn/sdr/decoder/pager.rb +61 -0
  76. data/lib/pwn/sdr/decoder/pocsag.rb +176 -54
  77. data/lib/pwn/sdr/decoder/rfid.rb +79 -0
  78. data/lib/pwn/sdr/decoder/rtl433.rb +86 -0
  79. data/lib/pwn/sdr/decoder/rtty.rb +139 -0
  80. data/lib/pwn/sdr/decoder/wifi.rb +77 -0
  81. data/lib/pwn/sdr/decoder/zigbee.rb +74 -0
  82. data/lib/pwn/sdr/decoder.rb +75 -7
  83. data/lib/pwn/sdr/frequency_allocation.rb +82 -41
  84. data/lib/pwn/sdr/gqrx.rb +186 -95
  85. data/lib/pwn/version.rb +1 -1
  86. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +36 -0
  87. data/spec/lib/pwn/ai/agent/tools/mistakes_spec.rb +12 -0
  88. data/spec/lib/pwn/sdr/decoder/adsb_spec.rb +15 -0
  89. data/spec/lib/pwn/sdr/decoder/apt_spec.rb +15 -0
  90. data/spec/lib/pwn/sdr/decoder/bluetooth_spec.rb +15 -0
  91. data/spec/lib/pwn/sdr/decoder/dect_spec.rb +15 -0
  92. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +15 -0
  93. data/spec/lib/pwn/sdr/decoder/gps_spec.rb +15 -0
  94. data/spec/lib/pwn/sdr/decoder/iridium_spec.rb +15 -0
  95. data/spec/lib/pwn/sdr/decoder/lora_spec.rb +15 -0
  96. data/spec/lib/pwn/sdr/decoder/lte_spec.rb +15 -0
  97. data/spec/lib/pwn/sdr/decoder/morse_spec.rb +15 -0
  98. data/spec/lib/pwn/sdr/decoder/p25_spec.rb +15 -0
  99. data/spec/lib/pwn/sdr/decoder/pager_spec.rb +15 -0
  100. data/spec/lib/pwn/sdr/decoder/rfid_spec.rb +15 -0
  101. data/spec/lib/pwn/sdr/decoder/rtl433_spec.rb +15 -0
  102. data/spec/lib/pwn/sdr/decoder/rtty_spec.rb +15 -0
  103. data/spec/lib/pwn/sdr/decoder/wifi_spec.rb +15 -0
  104. data/spec/lib/pwn/sdr/decoder/zigbee_spec.rb +15 -0
  105. data/third_party/pwn_rdoc.jsonl +115 -5
  106. metadata +46 -6
  107. 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 for pager networks.
6
+ # Pure-Ruby POCSAG (CCIR Radiopaging Code No. 1) decoder.
7
7
  #
8
- # Pipeline: GQRX UDP audio sox 48k→22.05k multimon-ng POCSAG512/1200/2400.
9
- #
10
- # multimon-ng emits lines of the form:
11
- # POCSAG1200: Address: 123456 Function: 3 Alpha: FIRE ALARM ZONE 4
12
- # POCSAG512: Address: 000042 Function: 0 Numeric: 5551234
13
- # POCSAG2400: Address: 987654 Function: 1 Skyper: ...
14
- # POCSAG1200: Address: 123456 Function: 2 Alpha: <partial><EOT>
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
- TYPE_DESC = {
27
- 'Alpha' => 'Alphanumeric text (7-bit ASCII)',
28
- 'Numeric' => 'Numeric-only (BCD)',
29
- 'Skyper' => 'Skyper network encoded',
30
- 'Tone' => 'Tone-only alert (no message body)'
31
- }.freeze
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
- # Supported Method Parameters::
34
- # PWN::SDR::Decoder::POCSAG.decode(
35
- # freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq'
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
- public_class_method def self.decode(opts = {})
39
- freq_obj = opts[:freq_obj]
44
+ try_lock unless @baud
45
+ return unless @baud
40
46
 
41
- PWN::SDR::Decoder::Base.run_pipeline(
42
- freq_obj: freq_obj,
43
- protocol: 'POCSAG',
44
- 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 -',
47
- line_match: /^POCSAG(?:512|1200|2400):/,
48
- parser: proc { |line| parse_line(line: line) }
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.parse_line(line: 'POCSAG1200: Address: ...')
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
- public_class_method def self.parse_line(opts = {})
56
- line = opts[:line].to_s
57
- out = { protocol: 'POCSAG' }
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
- if (m = line.match(/^POCSAG(\d+):/))
60
- out[:baud] = m[1].to_i
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
- if (m = line.match(/Address:\s*(\d+)/i))
63
- out[:address] = m[1].to_i
64
- out[:capcode] = m[1]
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
- if (m = line.match(/Function:\s*(\d+)/i))
67
- fn = m[1].to_i
68
- out[:function] = fn
69
- out[:function_desc] = FUNCTION_DESC[fn] || 'Unknown'
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
- if (m = line.match(/\b(Alpha|Numeric|Skyper|Tone):\s*(.*)$/i))
72
- 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
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[:summary] = "POCSAG#{out[:baud]} RIC=#{out[:capcode]} F#{out[:function]} #{out[:type]}: #{out[:message]}"
78
- out
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}.parse_line(line: 'POCSAG1200: Address: 123456 Function: 3 Alpha: HELLO')
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