pwn 0.5.742 → 0.5.743
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/lib/pwn/plugins/jobs.rb +9 -3
- data/lib/pwn/plugins/redb.rb +1 -1
- data/lib/pwn/plugins/repl/mesh.rb +119 -76
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +1 -1
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +1 -1
- data/spec/lib/pwn/plugins/jobs_spec.rb +10 -1
- data/spec/lib/pwn/plugins/redb_spec.rb +19 -1
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +1 -0
- data/spec/lib/pwn/plugins/repl_mesh_chunk_spec.rb +46 -5
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +29 -0
- data/spec/spec_helper.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8089c1c616fe3d19f70170b55204b0dd762a14faa40432e6acf421bd6d55197d
|
|
4
|
+
data.tar.gz: 6633cdc11ab8b811913d6b6e30608a79fa85c06c5bde599d4155c9c63d5a3884
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83c1b09ae27724b7e5df7972f018f11b7bee7f77e38d295c45395bf4c828f6c57e9736306687ae243e1fc05dc4dd63e4b33407d4101b916c2e32a8b241f39958
|
|
7
|
+
data.tar.gz: 19ad087ec133a1194b2fd60d7348edff887b94940647b5a0e34792fa5084742dcb7a04f213ee17b404e4fffda5fc1b8db4821fd1bc6c047c0867c96a2752d8af
|
data/lib/pwn/plugins/jobs.rb
CHANGED
|
@@ -49,10 +49,12 @@ module PWN
|
|
|
49
49
|
'PWN::Plugins::Jobs.supervise(root: ARGV[0], id: ARGV[1])', root, id,
|
|
50
50
|
in: File::NULL, out: File::NULL, err: File::NULL, close_others: true)
|
|
51
51
|
Process.detach(worker)
|
|
52
|
-
|
|
52
|
+
row[:worker_pid] = worker
|
|
53
|
+
persist(root: root, row: row)
|
|
54
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 8
|
|
53
55
|
loop do
|
|
54
56
|
row = load_job(id: id)
|
|
55
|
-
break if row[:
|
|
57
|
+
break if row[:worker_identity] || row[:status] != 'RUNNING' || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
56
58
|
|
|
57
59
|
sleep 0.02
|
|
58
60
|
end
|
|
@@ -372,7 +374,11 @@ module PWN
|
|
|
372
374
|
next unless row[:status] == 'RUNNING'
|
|
373
375
|
|
|
374
376
|
begin
|
|
375
|
-
|
|
377
|
+
begin
|
|
378
|
+
Process.setsid
|
|
379
|
+
rescue Errno::EPERM
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
376
382
|
row.merge!(worker_pid: Process.pid, worker_identity: SecureRandom.hex(16), started_at: Time.now.utc.iso8601(6))
|
|
377
383
|
pid = spawn(row[:env].transform_keys(&:to_s), '/bin/sh', '-c', row[:command], chdir: row[:cwd],
|
|
378
384
|
in: File::NULL, %i[out err] => [row[:log], 'ab'], pgroup: true, close_others: true)
|
data/lib/pwn/plugins/redb.rb
CHANGED
|
@@ -181,7 +181,7 @@ module PWN
|
|
|
181
181
|
current = nil
|
|
182
182
|
dump.each_line do |line|
|
|
183
183
|
current = Regexp.last_match(1) if line =~ /<([^>]+)>:/
|
|
184
|
-
next unless current && (line =~
|
|
184
|
+
next unless current && (line =~ /\b(?:call\w*|blx?r?|jalr?)\b.*<([^>@]+)/ || line =~ /\b(?:call\w*|blx?r?|jalr?)\s+([A-Za-z_]\w*)(?:@plt)?/)
|
|
185
185
|
|
|
186
186
|
db.execute('INSERT INTO xrefs(src, dst) VALUES(?,?)', [current, Regexp.last_match(1)])
|
|
187
187
|
end
|
|
@@ -614,31 +614,49 @@ module PWN
|
|
|
614
614
|
end
|
|
615
615
|
|
|
616
616
|
def mesh_tx_row_ready?(opts = {})
|
|
617
|
-
|
|
617
|
+
mesh_tx_outcome(opts) == :ok
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def mesh_tx_outcome(opts = {})
|
|
621
|
+
return :idle unless opts.is_a?(Hash)
|
|
618
622
|
|
|
619
623
|
row = opts[:row]
|
|
620
|
-
return
|
|
624
|
+
return :idle unless row.is_a?(Hash)
|
|
621
625
|
|
|
622
626
|
packet = row[:packet] || row['packet']
|
|
623
|
-
return
|
|
627
|
+
return :idle unless packet.is_a?(Hash)
|
|
624
628
|
|
|
625
629
|
decoded = packet[:decoded] || packet['decoded']
|
|
626
|
-
return
|
|
630
|
+
return :idle unless decoded.is_a?(Hash)
|
|
627
631
|
|
|
628
632
|
port = decoded[:portnum] || decoded['portnum']
|
|
629
|
-
%w[5 ROUTING_APP].include?(port.to_s) || port == :ROUTING_APP
|
|
633
|
+
return :idle unless %w[5 ROUTING_APP].include?(port.to_s) || port == :ROUTING_APP
|
|
634
|
+
|
|
635
|
+
want = opts[:request_id]
|
|
636
|
+
got = decoded[:request_id] || decoded['request_id']
|
|
637
|
+
return :idle if want && got && want.to_i != got.to_i
|
|
638
|
+
|
|
639
|
+
routing = decoded[:payload] || decoded['payload']
|
|
640
|
+
routing = Meshtastic::Routing.decode(routing).to_h if routing.is_a?(String)
|
|
641
|
+
reason = routing[:error_reason] if routing.is_a?(Hash)
|
|
642
|
+
reason = Meshtastic::Routing::Error.lookup(reason) || reason if reason.is_a?(Integer)
|
|
643
|
+
name = reason.to_s
|
|
644
|
+
return :ok if reason.nil? || %w[0 NONE].include?(name)
|
|
645
|
+
return :rate_limited if name == 'RATE_LIMIT_EXCEEDED'
|
|
646
|
+
|
|
647
|
+
:error
|
|
630
648
|
end
|
|
631
649
|
|
|
632
650
|
def mesh_wait_tx_slot(opts = {})
|
|
633
|
-
return unless opts.is_a?(Hash)
|
|
651
|
+
return :ok unless opts.is_a?(Hash)
|
|
634
652
|
|
|
635
653
|
obj = opts[:obj]
|
|
636
|
-
return unless obj.is_a?(Hash)
|
|
654
|
+
return :ok unless obj.is_a?(Hash)
|
|
637
655
|
|
|
638
656
|
timeout = opts[:timeout]
|
|
639
657
|
timeout = 8 if timeout.nil?
|
|
640
658
|
timeout = Float(timeout)
|
|
641
|
-
return if timeout <= 0
|
|
659
|
+
return :ok if timeout <= 0
|
|
642
660
|
|
|
643
661
|
since = opts[:since].to_i
|
|
644
662
|
deadline = Time.now + timeout
|
|
@@ -648,11 +666,11 @@ module PWN
|
|
|
648
666
|
else
|
|
649
667
|
Array(obj[:proto_data])
|
|
650
668
|
end
|
|
651
|
-
|
|
652
|
-
|
|
669
|
+
rows.drop(since).each do |row|
|
|
670
|
+
outcome = mesh_tx_outcome(row: row, request_id: opts[:request_id])
|
|
671
|
+
return outcome unless outcome == :idle
|
|
653
672
|
end
|
|
654
|
-
return if
|
|
655
|
-
return if Time.now >= deadline
|
|
673
|
+
return :timeout if Time.now >= deadline
|
|
656
674
|
|
|
657
675
|
sleep 0.05
|
|
658
676
|
end
|
|
@@ -682,6 +700,13 @@ module PWN
|
|
|
682
700
|
end
|
|
683
701
|
bodies << piece
|
|
684
702
|
offset += piece.length
|
|
703
|
+
next unless offset < text.length && piece.match?(/[^[:space:]]\z/)
|
|
704
|
+
|
|
705
|
+
cut = piece.rindex(/[[:space:]]/)
|
|
706
|
+
next unless cut&.positive?
|
|
707
|
+
|
|
708
|
+
bodies[-1] = piece[0, cut + 1]
|
|
709
|
+
offset = offset - piece.length + bodies[-1].length
|
|
685
710
|
end
|
|
686
711
|
if bodies.size <= n
|
|
687
712
|
total = bodies.size
|
|
@@ -772,69 +797,77 @@ module PWN
|
|
|
772
797
|
dm_key = mesh_dm_key(obj: obj, kind: kind, to: dest, radio: radio, timeout: opts.fetch(:key_timeout, 15))
|
|
773
798
|
end
|
|
774
799
|
chunks = mesh_text_chunks(text: opts[:text])
|
|
775
|
-
chunks.each_with_index do |piece,
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
tx[:channel] = radio unless radio.nil?
|
|
790
|
-
Meshtastic::Serial.send_text(tx)
|
|
791
|
-
when :bluetooth
|
|
792
|
-
tx = { bluetooth_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
|
|
793
|
-
tx[:channel] = radio unless radio.nil?
|
|
794
|
-
Meshtastic::Bluetooth.send_text(tx)
|
|
795
|
-
when :tcp
|
|
796
|
-
tx = { tcp_obj: obj, to: dest, text: piece, last_packet_id: packet_id - 1 }
|
|
797
|
-
tx[:channel] = radio unless radio.nil?
|
|
798
|
-
Meshtastic::TCP.send_text(tx)
|
|
800
|
+
chunks.each_with_index do |piece, _idx|
|
|
801
|
+
echoed = false
|
|
802
|
+
attempts = 0
|
|
803
|
+
loop do
|
|
804
|
+
attempts += 1
|
|
805
|
+
packet_id = Random.rand(2...0xffffffff)
|
|
806
|
+
since = obj.is_a?(Hash) ? Array(obj[:proto_data]).size : 0
|
|
807
|
+
if dm_key
|
|
808
|
+
bytes = Meshtastic::MeshInterface.new.send_text(from: obj[:my_node_num] || 0, to: dest, channel: radio || 0, text: piece, want_ack: true, psks: nil, last_packet_id: packet_id - 1)
|
|
809
|
+
packet = Meshtastic::ToRadio.decode(bytes)
|
|
810
|
+
packet.packet.pki_encrypted = true
|
|
811
|
+
packet.packet.public_key = dm_key
|
|
812
|
+
transport = { serial: Meshtastic::Serial, bluetooth: Meshtastic::Bluetooth, tcp: Meshtastic::TCP }.fetch(kind)
|
|
813
|
+
transport.send_to_radio({ "#{kind}_obj": obj, to_radio: packet.to_proto })
|
|
799
814
|
else
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
to: dest,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
text: piece,
|
|
811
|
-
|
|
815
|
+
case kind
|
|
816
|
+
when :serial
|
|
817
|
+
tx = { serial_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
|
|
818
|
+
tx[:channel] = radio unless radio.nil?
|
|
819
|
+
Meshtastic::Serial.send_text(tx)
|
|
820
|
+
when :bluetooth
|
|
821
|
+
tx = { bluetooth_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
|
|
822
|
+
tx[:channel] = radio unless radio.nil?
|
|
823
|
+
Meshtastic::Bluetooth.send_text(tx)
|
|
824
|
+
when :tcp
|
|
825
|
+
tx = { tcp_obj: obj, to: dest, text: piece, want_ack: true, last_packet_id: packet_id - 1 }
|
|
826
|
+
tx[:channel] = radio unless radio.nil?
|
|
827
|
+
Meshtastic::TCP.send_text(tx)
|
|
828
|
+
else
|
|
829
|
+
send_psks = psks
|
|
830
|
+
send_psks = mesh_channel_psks(env: env) if send_psks.nil? || send_psks.empty?
|
|
831
|
+
Meshtastic::MQTT.send_text(
|
|
832
|
+
mqtt_obj: obj,
|
|
833
|
+
last_packet_id: packet_id - 1,
|
|
834
|
+
from: from,
|
|
835
|
+
to: dest,
|
|
836
|
+
region: mesh_mqtt_region(region: opts[:region], env: env),
|
|
837
|
+
topic: mesh_mqtt_topic(env: env, topic: opts[:topic]),
|
|
838
|
+
channel: channel,
|
|
839
|
+
text: piece,
|
|
840
|
+
psks: send_psks
|
|
841
|
+
)
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
unless echoed
|
|
845
|
+
from_id = from.to_s
|
|
846
|
+
from_id = mesh_self_node_id(env: env, obj: obj) if from_id.empty?
|
|
847
|
+
from_id = mesh_format_node_id(id: from_id)
|
|
848
|
+
PWN.send(:remove_const, :MeshLastTx) if PWN.const_defined?(:MeshLastTx)
|
|
849
|
+
PWN.const_set(:MeshLastTx, { from: from_id, to: dest, text: piece.to_s, at: Time.now })
|
|
850
|
+
mesh_handle_rx(
|
|
851
|
+
local: true,
|
|
852
|
+
channel_name: channel_name,
|
|
853
|
+
msg: {
|
|
854
|
+
packet: {
|
|
855
|
+
id: packet_id,
|
|
856
|
+
channel: radio,
|
|
857
|
+
pki_encrypted: !dm_key.nil?,
|
|
858
|
+
node_id_from: from_id,
|
|
859
|
+
node_id_to: dest,
|
|
860
|
+
decoded: { portnum: :TEXT_MESSAGE_APP, payload: piece.to_s }
|
|
861
|
+
}
|
|
862
|
+
}
|
|
812
863
|
)
|
|
864
|
+
echoed = true
|
|
813
865
|
end
|
|
814
|
-
|
|
815
|
-
from_id = from.to_s
|
|
816
|
-
from_id = mesh_self_node_id(env: env, obj: obj) if from_id.empty?
|
|
817
|
-
from_id = mesh_format_node_id(id: from_id)
|
|
818
|
-
PWN.send(:remove_const, :MeshLastTx) if PWN.const_defined?(:MeshLastTx)
|
|
819
|
-
PWN.const_set(:MeshLastTx, { from: from_id, to: dest, text: piece.to_s, at: Time.now })
|
|
820
|
-
mesh_handle_rx(
|
|
821
|
-
local: true,
|
|
822
|
-
channel_name: channel_name,
|
|
823
|
-
msg: {
|
|
824
|
-
packet: {
|
|
825
|
-
id: packet_id,
|
|
826
|
-
channel: radio,
|
|
827
|
-
pki_encrypted: !dm_key.nil?,
|
|
828
|
-
node_id_from: from_id,
|
|
829
|
-
node_id_to: dest,
|
|
830
|
-
decoded: { portnum: :TEXT_MESSAGE_APP, payload: piece.to_s }
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
)
|
|
834
|
-
next unless idx + 1 < chunks.size
|
|
835
|
-
next unless %i[serial bluetooth tcp].include?(kind)
|
|
866
|
+
break unless %i[serial bluetooth tcp].include?(kind)
|
|
836
867
|
|
|
837
|
-
|
|
868
|
+
slot = mesh_wait_tx_slot(obj: obj, since: since, request_id: packet_id)
|
|
869
|
+
break if slot == :ok || slot == :timeout || slot == :error || attempts >= 8
|
|
870
|
+
end
|
|
838
871
|
end
|
|
839
872
|
return unless dm_key && PWN.const_defined?(:MeshPendingDm)
|
|
840
873
|
|
|
@@ -1016,7 +1049,8 @@ module PWN
|
|
|
1016
1049
|
next if event[:obj] && (!PWN.const_defined?(:MeshObj) || !event[:obj].equal?(PWN::MeshObj))
|
|
1017
1050
|
|
|
1018
1051
|
if event[:notice]
|
|
1019
|
-
|
|
1052
|
+
notice = mesh_notice_text(text: event[:notice])
|
|
1053
|
+
mesh_notice(text: notice) if notice
|
|
1020
1054
|
elsif event[:msg]
|
|
1021
1055
|
mesh_handle_rx(msg: event[:msg])
|
|
1022
1056
|
else
|
|
@@ -1102,6 +1136,14 @@ module PWN
|
|
|
1102
1136
|
text
|
|
1103
1137
|
end
|
|
1104
1138
|
|
|
1139
|
+
def mesh_notice_text(opts = {})
|
|
1140
|
+
text = opts[:text].to_s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
1141
|
+
text.gsub!(/\e\[[0-9;?]*[A-Za-z]/, '')
|
|
1142
|
+
text.gsub!(/[\x00-\x08\x0b\x0c\x0e-\x1f]/, '')
|
|
1143
|
+
text.strip!
|
|
1144
|
+
text.empty? ? nil : text
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1105
1147
|
def mesh_capture_output(opts = {})
|
|
1106
1148
|
original_out = $stdout
|
|
1107
1149
|
original_err = $stderr
|
|
@@ -1116,7 +1158,8 @@ module PWN
|
|
|
1116
1158
|
rescue EOFError
|
|
1117
1159
|
# Flush the last diagnostic even when the TUI is closing.
|
|
1118
1160
|
end
|
|
1119
|
-
|
|
1161
|
+
notice = mesh_notice_text(text: text)
|
|
1162
|
+
events << { notice: notice } if notice
|
|
1120
1163
|
end
|
|
1121
1164
|
rescue IOError
|
|
1122
1165
|
nil
|
|
@@ -1173,7 +1216,7 @@ module PWN
|
|
|
1173
1216
|
end
|
|
1174
1217
|
end
|
|
1175
1218
|
|
|
1176
|
-
private :mesh_capture_output, :mesh_notice
|
|
1219
|
+
private :mesh_capture_output, :mesh_notice, :mesh_notice_text
|
|
1177
1220
|
|
|
1178
1221
|
# Drop the submitted TX buffer so the next prompt is empty (Reline keeps the old line).
|
|
1179
1222
|
def mesh_reset_input!(opts = {})
|
|
@@ -1551,7 +1594,7 @@ module PWN
|
|
|
1551
1594
|
routing = Meshtastic::Routing.decode(routing).to_h if routing.is_a?(String)
|
|
1552
1595
|
reason = routing[:error_reason] if routing.is_a?(Hash)
|
|
1553
1596
|
reason = Meshtastic::Routing::Error.lookup(reason) || reason if reason.is_a?(Integer)
|
|
1554
|
-
mesh_ui_puts(text: "TX failed: packet #{decoded[:request_id]}: #{reason}") if reason && !%w[0 NONE].include?(reason.to_s)
|
|
1597
|
+
mesh_ui_puts(text: "TX failed: packet #{decoded[:request_id]}: #{reason}") if reason && !%w[0 NONE RATE_LIMIT_EXCEEDED].include?(reason.to_s)
|
|
1555
1598
|
return
|
|
1556
1599
|
end
|
|
1557
1600
|
return unless mesh_text_app?(portnum: decoded[:portnum])
|
|
@@ -2185,7 +2228,7 @@ module PWN
|
|
|
2185
2228
|
|
|
2186
2229
|
private :mesh_transport, :mesh_bound_transport, :mesh_layout, :mesh_box!, :mesh_mqtt_region, :mesh_mqtt_topic, :mesh_active_psks, :mesh_device_channel_meta, :mesh_device_channels
|
|
2187
2230
|
private :mesh_radio_channel, :mesh_radio_index_for_name, :mesh_channel_name_for_index, :mesh_unassigned_slot_map, :mesh_psk_b64, :mesh_psk_same?, :mesh_env_channel_name_for_psk, :mesh_whitelist_name_for_index, :mesh_channel_name_from_topic, :mesh_link_label
|
|
2188
|
-
private :mesh_connect_one, :mesh_connect, :mesh_mqtt_tls?, :mesh_subscribe, :mesh_text_payload_max, :mesh_payload_fits?, :mesh_text_chunks, :mesh_tx_row_ready?, :mesh_wait_tx_slot, :mesh_send_text, :mesh_disconnect, :mesh_compose_send, :mesh_channel_names, :mesh_env_hash
|
|
2231
|
+
private :mesh_connect_one, :mesh_connect, :mesh_mqtt_tls?, :mesh_subscribe, :mesh_text_payload_max, :mesh_payload_fits?, :mesh_text_chunks, :mesh_tx_row_ready?, :mesh_tx_outcome, :mesh_wait_tx_slot, :mesh_send_text, :mesh_disconnect, :mesh_compose_send, :mesh_channel_names, :mesh_env_hash
|
|
2189
2232
|
private :mesh_submit, :mesh_console_loop, :mesh_drain_events, :mesh_draw_input, :mesh_wrap_text, :mesh_ui_puts, :mesh_menu_root, :mesh_list_devices
|
|
2190
2233
|
private :mesh_start_rx!, :mesh_channel_psks, :mesh_text_app?, :mesh_rx_text, :mesh_self_node_id, :mesh_decorate_local_id, :mesh_public_psk?, :mesh_channel_securely_encrypted?
|
|
2191
2234
|
private :mesh_ai_whitelisted?, :mesh_ai_prompt, :mesh_broadcast?, :mesh_reply_target_label, :mesh_handle_rx, :mesh_maybe_dispatch_to_pwn_ai, :mesh_refresh_ui!, :mesh_reconnect!
|
data/lib/pwn/version.rb
CHANGED
|
@@ -15,7 +15,7 @@ describe 'PWN::AI::Agent::Tools exploitdev' do
|
|
|
15
15
|
expect(PWN::AI::Agent::Registry.lookup(name: 'exploitdev')).not_to be_nil
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
it 'builds a ret2libc chain end-to-end through Dispatch' do
|
|
18
|
+
it 'builds a ret2libc chain end-to-end through Dispatch', :x86_64_binary do
|
|
19
19
|
Dir.mktmpdir('pwn-ret2libc-tool-') do |dir|
|
|
20
20
|
src = File.join(dir, 'vuln.c')
|
|
21
21
|
bin = File.join(dir, 'vuln')
|
|
@@ -60,7 +60,7 @@ describe PWN::Plugins::ExploitDev do
|
|
|
60
60
|
expect(row[:gadgets].map { |g| g[:gadget] }).to include(a_string_matching(/pop rdi/i))
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
it 'ret2libc builds a packed chain from ELF resolve + pop rdi' do
|
|
63
|
+
it 'ret2libc builds a packed chain from ELF resolve + pop rdi', :x86_64_binary do
|
|
64
64
|
Dir.mktmpdir('pwn-ret2libc-') do |dir|
|
|
65
65
|
src = File.join(dir, 'vuln.c')
|
|
66
66
|
bin = File.join(dir, 'vuln')
|
|
@@ -64,7 +64,16 @@ describe PWN::Plugins::Jobs do
|
|
|
64
64
|
|
|
65
65
|
it 'pages binary logs with byte offsets and distinguishes EOF from completion' do
|
|
66
66
|
row = described_class.start(command: "printf 'abc\\377def'; sleep 1", max_runtime: 5)
|
|
67
|
-
first =
|
|
67
|
+
first = Timeout.timeout(8) do
|
|
68
|
+
loop do
|
|
69
|
+
page = described_class.job_tail(handle: row[:id], offset: 0, length: 4)
|
|
70
|
+
break page if page[:bytes].to_i.positive?
|
|
71
|
+
|
|
72
|
+
raise 'job ended before log bytes' unless page[:status] == 'RUNNING'
|
|
73
|
+
|
|
74
|
+
sleep 0.05
|
|
75
|
+
end
|
|
76
|
+
end
|
|
68
77
|
expect(first).to include(bytes: 4, next_offset: 4, encoding: 'base64', eof: false, status: 'RUNNING')
|
|
69
78
|
expect(Base64.strict_decode64(first[:data])).to eq("abc\xFF".b)
|
|
70
79
|
last = described_class.tail(id: row[:id], offset: 4, length: 100)
|
|
@@ -13,13 +13,31 @@ describe PWN::Plugins::REDB do
|
|
|
13
13
|
expect(described_class).to respond_to :help
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
it 'records ARM bl strcpy xrefs from objdump' do
|
|
17
|
+
Dir.mktmpdir('redb-arm') do |dir|
|
|
18
|
+
stub_const('PWN::Plugins::REDB::ROOT', File.join(dir, 'redb'))
|
|
19
|
+
bin = File.join(dir, 'x')
|
|
20
|
+
File.write(bin, 'fake')
|
|
21
|
+
dump = <<~ASM
|
|
22
|
+
0000000000000000 <caller>:
|
|
23
|
+
0: d10043ff sub sp, sp, #0x10
|
|
24
|
+
4: 94000000 bl 8 <strcpy@plt>
|
|
25
|
+
ASM
|
|
26
|
+
allow(Open3).to receive(:capture2).with('nm', '-C', bin).and_return(['', 1])
|
|
27
|
+
allow(Open3).to receive(:capture2).with('objdump', '-d', bin).and_return([dump, 1])
|
|
28
|
+
allow(Open3).to receive(:capture2).with('strings', '-a', bin).and_return(['', 1])
|
|
29
|
+
described_class.open(bin: bin)
|
|
30
|
+
expect(described_class.xrefs_to(bin: bin, sym: 'strcpy').map { |row| row['dst'] || row[:dst] }).to include('strcpy')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
16
34
|
it 'answers who calls strcpy from cache on a second open' do
|
|
17
35
|
Dir.mktmpdir('redb') do |dir|
|
|
18
36
|
stub_const('PWN::Plugins::REDB::ROOT', File.join(dir, 'redb'))
|
|
19
37
|
src = File.join(dir, 'x.c')
|
|
20
38
|
bin = File.join(dir, 'x')
|
|
21
39
|
File.write(src, "#include <string.h>\nvoid caller(void){ char b[8]; strcpy(b, \"x\"); }\nint main(void){ caller(); return 0; }\n")
|
|
22
|
-
system('cc', '-O0', '-g', '-fno-builtin', '-fno-inline', '-o', bin, src)
|
|
40
|
+
expect(system('cc', '-O0', '-g', '-fno-builtin', '-fno-inline', '-o', bin, src)).to be(true)
|
|
23
41
|
described_class.open(bin: bin)
|
|
24
42
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
25
43
|
described_class.open(bin: bin)
|
|
@@ -113,6 +113,7 @@ describe PWN::Plugins::REPL, 'mesh packet channel routing' do
|
|
|
113
113
|
sent = Meshtastic::ToRadio.decode(args[:to_radio]).packet
|
|
114
114
|
end
|
|
115
115
|
allow(described_class).to receive(:mesh_maybe_dispatch_to_pwn_ai)
|
|
116
|
+
allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:ok)
|
|
116
117
|
described_class.send(:mesh_send_text, env: mesh_env, obj: radio, text: 'Local message', channel_name: 'LongFast')
|
|
117
118
|
expect(sent.decoded.payload).to eq('Local message')
|
|
118
119
|
packet = { decoded: { emoji: 1, reply_id: sent.id } }
|
|
@@ -40,6 +40,47 @@ describe PWN::Plugins::REPL, 'mesh payload chunking' do
|
|
|
40
40
|
expect(reconstructed).to eq(body)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
it 'does not split a word across chunk boundaries' do
|
|
44
|
+
words = (1..80).map { |i| "token#{i}" }
|
|
45
|
+
body = words.join(' ')
|
|
46
|
+
chunks = described_class.send(:mesh_text_chunks, text: body)
|
|
47
|
+
bodies = chunks.map { |piece| piece.sub(%r{\A\(\d+/\d+\) }, '') }
|
|
48
|
+
expect(bodies.join).to eq(body)
|
|
49
|
+
expect(chunks.size).to be > 1
|
|
50
|
+
bodies[0..-2].each do |piece|
|
|
51
|
+
expect(piece).to match(/[[:space:]]\z/)
|
|
52
|
+
end
|
|
53
|
+
expect(bodies.join.split).to eq(words)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'still splits a single word that cannot fit in one payload' do
|
|
57
|
+
body = 'A' * (max + 10)
|
|
58
|
+
chunks = described_class.send(:mesh_text_chunks, text: body)
|
|
59
|
+
expect(chunks.size).to be > 1
|
|
60
|
+
expect(chunks.map { |piece| piece.sub(%r{\A\(\d+/\d+\) }, '') }.join).to eq(body)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'does not treat RATE_LIMIT_EXCEEDED as a free TX slot' do
|
|
64
|
+
obj = {
|
|
65
|
+
proto_data: [{ packet: { decoded: { portnum: :ROUTING_APP, request_id: 7, payload: { error_reason: :RATE_LIMIT_EXCEEDED } } } }],
|
|
66
|
+
rx_mutex: Mutex.new
|
|
67
|
+
}
|
|
68
|
+
t = Time.now
|
|
69
|
+
expect(described_class.send(:mesh_wait_tx_slot, obj: obj, since: 0, timeout: 0.25, request_id: 7)).to eq(:rate_limited)
|
|
70
|
+
expect(Time.now - t).to be < 0.2
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'retries a rate-limited chunk instead of sending the next one' do
|
|
74
|
+
body = 'A' * (max + 10)
|
|
75
|
+
sent = []
|
|
76
|
+
allow(Meshtastic::Serial).to receive(:send_text) { |h| sent << h[:text] }
|
|
77
|
+
allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:rate_limited, :ok, :ok)
|
|
78
|
+
described_class.send(:mesh_send_text, env: env, obj: { proto_data: [] }, text: body, to: '!ffffffff')
|
|
79
|
+
expect(sent.size).to be > 2
|
|
80
|
+
expect(sent[0]).to eq(sent[1])
|
|
81
|
+
expect(sent[2]).not_to eq(sent[0])
|
|
82
|
+
end
|
|
83
|
+
|
|
43
84
|
it 'keeps each chunk small enough that the encoded ToRadio fits a 256-byte LoRa frame' do
|
|
44
85
|
body = 'A' * max
|
|
45
86
|
chunks = described_class.send(:mesh_text_chunks, text: body)
|
|
@@ -76,17 +117,17 @@ describe PWN::Plugins::REPL, 'mesh payload chunking' do
|
|
|
76
117
|
body = 'A' * (max + 10)
|
|
77
118
|
obj = { proto_data: [], rx_mutex: Mutex.new }
|
|
78
119
|
allow(Meshtastic::Serial).to receive(:send_text)
|
|
79
|
-
allow(described_class).to receive(:mesh_wait_tx_slot)
|
|
120
|
+
allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:ok)
|
|
80
121
|
described_class.send(:mesh_send_text, env: env, obj: obj, text: body, to: '!ffffffff')
|
|
81
122
|
n = described_class.send(:mesh_text_chunks, text: body).size
|
|
82
|
-
expect(described_class).to have_received(:mesh_wait_tx_slot).exactly(n
|
|
123
|
+
expect(described_class).to have_received(:mesh_wait_tx_slot).exactly(n).times
|
|
83
124
|
end
|
|
84
125
|
|
|
85
|
-
it '
|
|
126
|
+
it 'waits for a TX slot after a single radio payload' do
|
|
86
127
|
allow(Meshtastic::Serial).to receive(:send_text)
|
|
87
|
-
allow(described_class).to receive(:mesh_wait_tx_slot)
|
|
128
|
+
allow(described_class).to receive(:mesh_wait_tx_slot).and_return(:ok)
|
|
88
129
|
described_class.send(:mesh_send_text, env: env, obj: { proto_data: [] }, text: 'hi', to: '!ffffffff')
|
|
89
|
-
expect(described_class).
|
|
130
|
+
expect(described_class).to have_received(:mesh_wait_tx_slot).once
|
|
90
131
|
end
|
|
91
132
|
|
|
92
133
|
it 'does not treat QueueStatus free as the chunk being on the air' do
|
|
@@ -40,6 +40,35 @@ describe PWN::Plugins::REPL, 'mesh compose history' do
|
|
|
40
40
|
described_class.send(:mesh_drain_events)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
it 'does not turn spinner cursor restores into a notice overlay' do
|
|
44
|
+
events = Queue.new
|
|
45
|
+
stub_const('PWN::MeshEvents', events)
|
|
46
|
+
described_class.send(:mesh_capture_output, run: proc {
|
|
47
|
+
3.times { $stdout.write("\e[?25h") }
|
|
48
|
+
$stderr.write("\e[?25h")
|
|
49
|
+
})
|
|
50
|
+
expect(events).to be_empty
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'does not notice TTYSpinner cursor restore during capture' do
|
|
54
|
+
events = Queue.new
|
|
55
|
+
stub_const('PWN::MeshEvents', events)
|
|
56
|
+
described_class.send(:mesh_capture_output, run: proc {
|
|
57
|
+
PWN::Plugins::TTYSpinner.send(:show_cursor, spin: nil)
|
|
58
|
+
})
|
|
59
|
+
expect(events).to be_empty
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'keeps real warnings after stripping cursor sequences' do
|
|
63
|
+
events = Queue.new
|
|
64
|
+
stub_const('PWN::MeshEvents', events)
|
|
65
|
+
described_class.send(:mesh_capture_output, run: proc {
|
|
66
|
+
$stderr.write("\e[?25hWARNING: decoder fixture\e[?25h\n")
|
|
67
|
+
})
|
|
68
|
+
expect(events.pop(true)[:notice]).to eq('WARNING: decoder fixture')
|
|
69
|
+
expect(events).to be_empty
|
|
70
|
+
end
|
|
71
|
+
|
|
43
72
|
it 'captures dependency stdout and stderr and restores both streams on failure' do
|
|
44
73
|
events = Queue.new
|
|
45
74
|
stub_const('PWN::MeshEvents', events)
|
data/spec/spec_helper.rb
CHANGED
|
@@ -31,6 +31,7 @@ RSpec.configure do |config|
|
|
|
31
31
|
end
|
|
32
32
|
config.filter_run_excluding :rtl_sdr_hardware unless ENV['PWN_TEST_RTL_SDR_HARDWARE'] == '1'
|
|
33
33
|
config.filter_run_excluding :combo_nation_mcp unless ENV['PWN_TEST_COMBO_NATION_MCP'] == '1'
|
|
34
|
+
config.filter_run_excluding :x86_64_binary unless RbConfig::CONFIG['host_cpu'].to_s.match?(/amd64|x86_64/)
|
|
34
35
|
|
|
35
36
|
next if ENV['PWN_SPEC_VERBOSE']
|
|
36
37
|
|