pwn 0.5.739 → 0.5.741
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/documentation/Agent-Tool-Registry.md +163 -6
- data/documentation/Diagrams.md +1 -1
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Plugin-Degradation.md +6 -2
- data/documentation/Recon-Findings-API.md +33 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/etc/default_skills/pwn/ai/agent/confirmation/SKILL.md +49 -0
- data/etc/default_skills/pwn/ai/agent/result/SKILL.md +4 -1
- data/etc/default_skills/pwn/ai/agent/task_dag/SKILL.md +50 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_goto/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nmap_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nuclei_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/radare2/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/router/SKILL.md +51 -0
- data/etc/default_skills/pwn/engagement/SKILL.md +2 -0
- data/etc/default_skills/pwn/ffi/capstone/SKILL.md +54 -0
- data/etc/default_skills/pwn/ffi/keystone/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +5 -1
- data/etc/default_skills/pwn/plugins/ai_sandbox/SKILL.md +52 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/assembly/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +8 -2
- data/etc/default_skills/pwn/plugins/httpx/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/method_catalog/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -1
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/radare2/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/rop/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/vault/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/SKILL.md +2 -0
- data/lib/pwn/ai/agent/confirmation.rb +163 -0
- data/lib/pwn/ai/agent/dispatch.rb +43 -5
- data/lib/pwn/ai/agent/loop.rb +45 -11
- data/lib/pwn/ai/agent/prompt_builder.rb +7 -4
- data/lib/pwn/ai/agent/registry.rb +2 -1
- data/lib/pwn/ai/agent/result.rb +76 -6
- data/lib/pwn/ai/agent/task_dag.rb +220 -0
- data/lib/pwn/ai/agent/tool_guard.rb +166 -24
- data/lib/pwn/ai/agent/tools/artifacts.rb +35 -5
- data/lib/pwn/ai/agent/tools/binary_triage.rb +7 -3
- data/lib/pwn/ai/agent/tools/browser_goto.rb +32 -0
- data/lib/pwn/ai/agent/tools/debug_lane.rb +23 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +32 -12
- data/lib/pwn/ai/agent/tools/finding_record.rb +31 -5
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -21
- data/lib/pwn/ai/agent/tools/job_run.rb +30 -18
- data/lib/pwn/ai/agent/tools/loot_query.rb +44 -0
- data/lib/pwn/ai/agent/tools/nmap_scan.rb +32 -0
- data/lib/pwn/ai/agent/tools/nuclei_scan.rb +31 -0
- data/lib/pwn/ai/agent/tools/radare2.rb +55 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +19 -2
- data/lib/pwn/ai/agent/tools/shell.rb +22 -10
- data/lib/pwn/ai/agent/turn_finalizer.rb +46 -5
- data/lib/pwn/ai/agent.rb +3 -1
- data/lib/pwn/ai/cli.rb +26 -2
- data/lib/pwn/ai/context.rb +4 -2
- data/lib/pwn/ai/router.rb +196 -0
- data/lib/pwn/ai/tools/README.md +5 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +10 -0
- data/lib/pwn/engagement.rb +45 -1
- data/lib/pwn/ffi/capstone.rb +120 -0
- data/lib/pwn/ffi/keystone.rb +107 -0
- data/lib/pwn/ffi.rb +2 -0
- data/lib/pwn/migrate.rb +5 -1
- data/lib/pwn/plugins/aflplusplus.rb +238 -13
- data/lib/pwn/plugins/ai_sandbox.rb +240 -0
- data/lib/pwn/plugins/artifact_registry.rb +188 -22
- data/lib/pwn/plugins/assembly.rb +73 -2
- data/lib/pwn/plugins/basic_auth.rb +17 -4
- data/lib/pwn/plugins/binary_parser.rb +439 -38
- data/lib/pwn/plugins/exploit_dev.rb +83 -14
- data/lib/pwn/plugins/findings.rb +208 -42
- data/lib/pwn/plugins/gdbmi.rb +265 -38
- data/lib/pwn/plugins/httpx.rb +93 -0
- data/lib/pwn/plugins/jobs.rb +260 -102
- data/lib/pwn/plugins/method_catalog.rb +328 -0
- data/lib/pwn/plugins/nmap_it.rb +157 -26
- data/lib/pwn/plugins/nuclei.rb +122 -29
- data/lib/pwn/plugins/preflight_checker.rb +3 -2
- data/lib/pwn/plugins/process_tube.rb +51 -2
- data/lib/pwn/plugins/radare2.rb +120 -26
- data/lib/pwn/plugins/recon.rb +33 -0
- data/lib/pwn/plugins/repl/mesh.rb +203 -13
- data/lib/pwn/plugins/rop.rb +121 -31
- data/lib/pwn/plugins/transparent_browser.rb +212 -14
- data/lib/pwn/plugins/vault.rb +182 -24
- data/lib/pwn/plugins.rb +4 -0
- data/lib/pwn/reports/engagement.rb +8 -2
- data/lib/pwn/reports/html.rb +28 -9
- data/lib/pwn/reports/json.rb +2 -1
- data/lib/pwn/reports/markdown.rb +49 -12
- data/lib/pwn/reports/sarif.rb +17 -3
- data/lib/pwn/reports.rb +184 -34
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reports_spec.rb +93 -0
- data/spec/integration/result_condition_spec.rb +11 -8
- data/spec/lib/pwn/ai/agent/confirmation_spec.rb +59 -0
- data/spec/lib/pwn/ai/agent/dispatch_confirmation_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/dispatch_policy_spec.rb +51 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/result_spec.rb +84 -1
- data/spec/lib/pwn/ai/agent/task_dag_spec.rb +63 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/ai_sandbox_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/tools/browser_goto_spec.rb +57 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +42 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +46 -2
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/nmap_scan_spec.rb +26 -0
- data/spec/lib/pwn/ai/agent/tools/nuclei_scan_spec.rb +37 -0
- data/spec/lib/pwn/ai/agent/tools/pwn_eval_schema_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/tools/radare2_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +76 -0
- data/spec/lib/pwn/ai/cli_spec.rb +12 -1
- data/spec/lib/pwn/ai/router_spec.rb +57 -0
- data/spec/lib/pwn/ffi/capstone_spec.rb +33 -0
- data/spec/lib/pwn/ffi/keystone_spec.rb +33 -0
- data/spec/lib/pwn/migrate_spec.rb +30 -2
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +40 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +44 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +116 -1
- data/spec/lib/pwn/plugins/assembly_spec.rb +12 -0
- data/spec/lib/pwn/plugins/basic_auth_spec.rb +15 -0
- data/spec/lib/pwn/plugins/binary_parser_spec.rb +59 -2
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +44 -0
- data/spec/lib/pwn/plugins/findings_structured_spec.rb +196 -1
- data/spec/lib/pwn/plugins/gdbmi_spec.rb +99 -1
- data/spec/lib/pwn/plugins/httpx_spec.rb +23 -0
- data/spec/lib/pwn/plugins/jobs_spec.rb +141 -0
- data/spec/lib/pwn/plugins/method_catalog_spec.rb +29 -0
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +49 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +33 -1
- data/spec/lib/pwn/plugins/radare2_spec.rb +115 -0
- data/spec/lib/pwn/plugins/recon_spec.rb +15 -0
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +107 -1
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +122 -0
- data/spec/lib/pwn/plugins/rop_spec.rb +19 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +71 -0
- data/spec/lib/pwn/plugins/vault_spec.rb +41 -0
- data/spec/lib/pwn/reports/engagement_spec.rb +51 -0
- data/spec/lib/pwn/reports_spec.rb +196 -0
- data/spec/spec_helper.rb +3 -0
- data/third_party/pwn_rdoc.jsonl +230 -4
- metadata +47 -5
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module FFI
|
|
9
|
+
# Thin libcapstone disassembler.
|
|
10
|
+
module Capstone
|
|
11
|
+
extend PubFFI::Library
|
|
12
|
+
|
|
13
|
+
CS_ARCH_ARM = 0
|
|
14
|
+
CS_ARCH_ARM64 = 1
|
|
15
|
+
CS_ARCH_X86 = 3
|
|
16
|
+
CS_MODE_LITTLE_ENDIAN = 0
|
|
17
|
+
CS_MODE_32 = 4
|
|
18
|
+
CS_MODE_64 = 8
|
|
19
|
+
|
|
20
|
+
# Capstone cs_insn layout used by cs_disasm.
|
|
21
|
+
class Insn < PubFFI::Struct
|
|
22
|
+
layout :id, :uint,
|
|
23
|
+
:address, :uint64,
|
|
24
|
+
:size, :ushort,
|
|
25
|
+
:bytes, [:uchar, 24],
|
|
26
|
+
:mnemonic, [:char, 32],
|
|
27
|
+
:op_str, [:char, 160],
|
|
28
|
+
:detail, :pointer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@load_error = nil
|
|
32
|
+
begin
|
|
33
|
+
ffi_lib %w[capstone libcapstone.so.5 libcapstone.so.4 libcapstone.so]
|
|
34
|
+
rescue LoadError => e
|
|
35
|
+
@load_error = e
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
attr_reader :load_error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
unless @load_error
|
|
43
|
+
attach_function :cs_open, %i[int int pointer], :int
|
|
44
|
+
attach_function :cs_disasm, %i[size_t pointer size_t uint64 size_t pointer], :size_t
|
|
45
|
+
attach_function :cs_free, %i[pointer size_t], :void
|
|
46
|
+
attach_function :cs_close, [:pointer], :int
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
public_class_method def self.available?(opts = {})
|
|
50
|
+
opts[:mod]
|
|
51
|
+
load_error.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
public_class_method def self.disassemble(opts = {})
|
|
55
|
+
raise 'ERROR: libcapstone not available' unless available?(mod: self)
|
|
56
|
+
|
|
57
|
+
raw = opts[:bytes] || opts[:opcodes]
|
|
58
|
+
raise 'ERROR: bytes is required' if raw.to_s.empty?
|
|
59
|
+
|
|
60
|
+
bytes = raw.to_s.b
|
|
61
|
+
arch, mode = arch_mode(arch: opts[:arch], endian: opts[:endian])
|
|
62
|
+
handle = PubFFI::MemoryPointer.new(:size_t)
|
|
63
|
+
raise 'ERROR: cs_open failed' unless cs_open(arch, mode, handle).zero?
|
|
64
|
+
|
|
65
|
+
buf = PubFFI::MemoryPointer.from_string(bytes)
|
|
66
|
+
insn_ptr = PubFFI::MemoryPointer.new(:pointer)
|
|
67
|
+
count = cs_disasm(handle.read_ulong, buf, bytes.bytesize, (opts[:address] || 0).to_i, 0, insn_ptr)
|
|
68
|
+
insns = []
|
|
69
|
+
base = insn_ptr.read_pointer
|
|
70
|
+
count.times do |i|
|
|
71
|
+
insn = Insn.new(base + (i * Insn.size))
|
|
72
|
+
insns << { address: insn[:address], mnemonic: insn[:mnemonic].to_s, op_str: insn[:op_str].to_s, size: insn[:size] }
|
|
73
|
+
end
|
|
74
|
+
cs_free(base, count) unless base.null?
|
|
75
|
+
cs_close(handle)
|
|
76
|
+
{ engine: 'capstone', insns: insns, count: count }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private_class_method def self.arch_mode(opts = {})
|
|
80
|
+
arch = opts[:arch].to_s.downcase
|
|
81
|
+
case arch
|
|
82
|
+
when 'i386', 'i686', 'x86'
|
|
83
|
+
[CS_ARCH_X86, CS_MODE_32]
|
|
84
|
+
when 'aarch64', 'arm64'
|
|
85
|
+
[CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN]
|
|
86
|
+
when /arm/
|
|
87
|
+
[CS_ARCH_ARM, CS_MODE_LITTLE_ENDIAN]
|
|
88
|
+
else
|
|
89
|
+
[CS_ARCH_X86, CS_MODE_64]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
public_class_method def self.authors
|
|
94
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
public_class_method def self.help
|
|
98
|
+
puts "USAGE:
|
|
99
|
+
# True when libcapstone is loadable.
|
|
100
|
+
#{self}.available?(
|
|
101
|
+
mod: 'optional - ignored; present so (opts = {}) reads opts['
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Disassemble bytes with Capstone.
|
|
105
|
+
#{self}.disassemble(
|
|
106
|
+
bytes: 'required - raw machine-code bytes',
|
|
107
|
+
opcodes: 'optional - alias for bytes',
|
|
108
|
+
arch: 'optional - x86_64|x86|arm|aarch64',
|
|
109
|
+
endian: 'optional - :little or :big byte order',
|
|
110
|
+
address: 'optional - start address'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Print the AUTHOR(S) string for this module.
|
|
114
|
+
#{self}.authors
|
|
115
|
+
"
|
|
116
|
+
constants.sort
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module FFI
|
|
9
|
+
# Thin libkeystone assembler.
|
|
10
|
+
module Keystone
|
|
11
|
+
extend PubFFI::Library
|
|
12
|
+
|
|
13
|
+
KS_ARCH_ARM = 1
|
|
14
|
+
KS_ARCH_ARM64 = 2
|
|
15
|
+
KS_ARCH_X86 = 4
|
|
16
|
+
KS_MODE_LITTLE_ENDIAN = 0
|
|
17
|
+
KS_MODE_32 = 4
|
|
18
|
+
KS_MODE_64 = 8
|
|
19
|
+
|
|
20
|
+
@load_error = nil
|
|
21
|
+
begin
|
|
22
|
+
ffi_lib %w[keystone libkeystone.so.0 libkeystone.so]
|
|
23
|
+
rescue LoadError => e
|
|
24
|
+
@load_error = e
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
attr_reader :load_error
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unless @load_error
|
|
32
|
+
attach_function :ks_open, %i[int int pointer], :int
|
|
33
|
+
attach_function :ks_asm, %i[pointer string uint64 pointer pointer pointer], :int
|
|
34
|
+
attach_function :ks_free, [:pointer], :void
|
|
35
|
+
attach_function :ks_close, [:pointer], :int
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
public_class_method def self.available?(opts = {})
|
|
39
|
+
opts[:mod]
|
|
40
|
+
load_error.nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
public_class_method def self.assemble(opts = {})
|
|
44
|
+
raise 'ERROR: libkeystone not available' unless available?(mod: self)
|
|
45
|
+
|
|
46
|
+
asm = opts[:asm].to_s
|
|
47
|
+
raise 'ERROR: asm is required' if asm.empty?
|
|
48
|
+
|
|
49
|
+
arch, mode = arch_mode(arch: opts[:arch], endian: opts[:endian])
|
|
50
|
+
ks_ptr = PubFFI::MemoryPointer.new(:pointer)
|
|
51
|
+
raise 'ERROR: ks_open failed' unless ks_open(arch, mode, ks_ptr).zero?
|
|
52
|
+
|
|
53
|
+
ks = ks_ptr.read_pointer
|
|
54
|
+
enc = PubFFI::MemoryPointer.new(:pointer)
|
|
55
|
+
size = PubFFI::MemoryPointer.new(:ulong)
|
|
56
|
+
count = PubFFI::MemoryPointer.new(:ulong)
|
|
57
|
+
rc = ks_asm(ks, asm, (opts[:address] || 0).to_i, enc, size, count)
|
|
58
|
+
raise 'ERROR: ks_asm failed' unless rc.zero?
|
|
59
|
+
|
|
60
|
+
n = size.read_ulong
|
|
61
|
+
bytes = enc.read_pointer.read_string(n)
|
|
62
|
+
ks_free(enc.read_pointer)
|
|
63
|
+
ks_close(ks)
|
|
64
|
+
{ engine: 'keystone', bytes: bytes, hex: bytes.unpack1('H*'), count: count.read_ulong }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private_class_method def self.arch_mode(opts = {})
|
|
68
|
+
arch = opts[:arch].to_s.downcase
|
|
69
|
+
case arch
|
|
70
|
+
when 'i386', 'i686', 'x86'
|
|
71
|
+
[KS_ARCH_X86, KS_MODE_32]
|
|
72
|
+
when 'aarch64', 'arm64'
|
|
73
|
+
[KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN]
|
|
74
|
+
when /arm/
|
|
75
|
+
[KS_ARCH_ARM, KS_MODE_LITTLE_ENDIAN]
|
|
76
|
+
else
|
|
77
|
+
[KS_ARCH_X86, KS_MODE_64]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
public_class_method def self.authors
|
|
82
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
public_class_method def self.help
|
|
86
|
+
puts "USAGE:
|
|
87
|
+
# True when libkeystone is loadable.
|
|
88
|
+
#{self}.available?(
|
|
89
|
+
mod: 'optional - ignored; present so (opts = {}) reads opts['
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Assemble instructions with Keystone.
|
|
93
|
+
#{self}.assemble(
|
|
94
|
+
asm: 'required - assembly source (one instruction per line)',
|
|
95
|
+
arch: 'optional - x86_64|x86|arm|aarch64',
|
|
96
|
+
endian: 'optional - :little or :big byte order',
|
|
97
|
+
address: 'optional - start address'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Print the AUTHOR(S) string for this module.
|
|
101
|
+
#{self}.authors
|
|
102
|
+
"
|
|
103
|
+
constants.sort
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/pwn/ffi.rb
CHANGED
|
@@ -21,9 +21,11 @@ module PWN
|
|
|
21
21
|
# `.available?` returns false so callers can fall back to pure Ruby.
|
|
22
22
|
module FFI
|
|
23
23
|
autoload :AdalmPluto, 'pwn/ffi/adalm_pluto'
|
|
24
|
+
autoload :Capstone, 'pwn/ffi/capstone'
|
|
24
25
|
autoload :DSPNative, 'pwn/ffi/dsp_native'
|
|
25
26
|
autoload :FFTW, 'pwn/ffi/fftw'
|
|
26
27
|
autoload :HackRF, 'pwn/ffi/hack_rf'
|
|
28
|
+
autoload :Keystone, 'pwn/ffi/keystone'
|
|
27
29
|
autoload :Liquid, 'pwn/ffi/liquid'
|
|
28
30
|
autoload :RTLSdr, 'pwn/ffi/rtl_sdr'
|
|
29
31
|
autoload :SoapySDR, 'pwn/ffi/soapy_sdr'
|
data/lib/pwn/migrate.rb
CHANGED
|
@@ -49,7 +49,7 @@ module PWN
|
|
|
49
49
|
# Bump this whenever the shape of any file under ~/.pwn changes in a
|
|
50
50
|
# way that requires a one-time transform. Add the transform as an
|
|
51
51
|
# entry in MIGRATIONS keyed by the NEW schema number.
|
|
52
|
-
SCHEMA_VERSION =
|
|
52
|
+
SCHEMA_VERSION = 4
|
|
53
53
|
|
|
54
54
|
OK = "\e[32mok\e[0m"
|
|
55
55
|
BAD = "\e[31mFAIL\e[0m"
|
|
@@ -241,6 +241,10 @@ module PWN
|
|
|
241
241
|
|
|
242
242
|
File.write(path, YAML.dump(raw))
|
|
243
243
|
io.puts " · agents.yml added default model to #{patched} personas"
|
|
244
|
+
},
|
|
245
|
+
4 => lambda { |_root, io|
|
|
246
|
+
io.puts ' · pwn.yaml ai_sandbox, ai_router, model_routes task classes'
|
|
247
|
+
PWN::Migrate.backfill_vault(io: io)
|
|
244
248
|
}
|
|
245
249
|
}.freeze
|
|
246
250
|
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'digest'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'shellwords'
|
|
8
|
+
require 'rbconfig'
|
|
9
|
+
require 'base64'
|
|
6
10
|
|
|
7
11
|
module PWN
|
|
8
12
|
module Plugins
|
|
9
|
-
# AFL++ campaign wrapper.
|
|
13
|
+
# AFL++ / libFuzzer campaign wrapper with corpus, dictionary, and RE triage handoff.
|
|
10
14
|
module AFLplusplus
|
|
11
15
|
public_class_method def self.required_bins
|
|
12
16
|
%w[afl-fuzz]
|
|
@@ -41,22 +45,107 @@ module PWN
|
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
|
|
48
|
+
public_class_method def self.dictionary_from_binary(opts = {})
|
|
49
|
+
path = (opts[:path] || opts[:target] || opts[:bin]).to_s
|
|
50
|
+
raise ArgumentError, 'path is required' if path.empty?
|
|
51
|
+
|
|
52
|
+
triage = PWN::Plugins::BinaryParser.triage(path: path, session_id: opts[:session_id] || 'fuzz-dict')
|
|
53
|
+
strings = triage[:interesting_strings] || {}
|
|
54
|
+
tokens = []
|
|
55
|
+
tokens.concat(Array(strings[:urls]))
|
|
56
|
+
tokens.concat(Array(strings[:paths]))
|
|
57
|
+
tokens.concat(Array(strings[:format_strings]))
|
|
58
|
+
tokens.concat(Array(strings[:keys]))
|
|
59
|
+
tokens.concat(Array(triage[:imports]).map(&:to_s).grep(/\A[A-Za-z_]{3,}/).first(20))
|
|
60
|
+
tokens = tokens.map(&:to_s).uniq.reject(&:empty?).first(200)
|
|
61
|
+
quoted = tokens.map { |token| format('"%s"', token.gsub('\\', '\\\\').gsub('"', '\\"')) }
|
|
62
|
+
{ path: path, tokens: tokens, dict: quoted.join("\n") }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
public_class_method def self.prepare_corpus(opts = {})
|
|
66
|
+
in_dir = (opts[:in_dir] || opts[:corpus]).to_s
|
|
67
|
+
raise ArgumentError, 'in_dir is required' if in_dir.empty?
|
|
68
|
+
|
|
69
|
+
FileUtils.mkdir_p(in_dir)
|
|
70
|
+
File.binwrite(File.join(in_dir, 'seed'), "A\n") unless Dir.children(in_dir).any? { |name| File.file?(File.join(in_dir, name)) }
|
|
71
|
+
Array(opts[:seeds]).each_with_index do |seed, idx|
|
|
72
|
+
FileUtils.cp(seed, File.join(in_dir, "seed-#{idx}")) if File.file?(seed.to_s)
|
|
73
|
+
end
|
|
74
|
+
in_dir
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
public_class_method def self.campaign_command(opts = {})
|
|
78
|
+
payload = {
|
|
79
|
+
engine: opts[:engine] || 'aflplusplus',
|
|
80
|
+
in_dir: opts[:in_dir],
|
|
81
|
+
out_dir: opts[:out_dir],
|
|
82
|
+
target: opts[:target],
|
|
83
|
+
max_runtime: opts[:max_runtime],
|
|
84
|
+
cwd: opts[:cwd],
|
|
85
|
+
env: opts[:env],
|
|
86
|
+
handoff: opts.fetch(:handoff, true)
|
|
87
|
+
}
|
|
88
|
+
b64 = Base64.strict_encode64(JSON.generate(payload))
|
|
89
|
+
Shellwords.join(
|
|
90
|
+
[
|
|
91
|
+
RbConfig.ruby,
|
|
92
|
+
'-rpwn',
|
|
93
|
+
'-rjson',
|
|
94
|
+
'-rbase64',
|
|
95
|
+
'-e',
|
|
96
|
+
'PWN::Plugins::AFLplusplus.campaign(JSON.parse(Base64.decode64(ARGV[0]), symbolize_names: true))',
|
|
97
|
+
b64
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
public_class_method def self.campaign(opts = {})
|
|
103
|
+
opts = opts.transform_keys(&:to_sym)
|
|
104
|
+
in_dir = prepare_corpus(opts)
|
|
105
|
+
out_dir = (opts[:out_dir] || opts[:output]).to_s
|
|
106
|
+
raise ArgumentError, 'out_dir is required' if out_dir.empty?
|
|
107
|
+
|
|
108
|
+
FileUtils.mkdir_p(out_dir)
|
|
109
|
+
bin = binary_from_target(target: opts[:target])
|
|
110
|
+
dict_info = File.file?(bin.to_s) ? dictionary_from_binary(path: bin) : { dict: '', tokens: [] }
|
|
111
|
+
dict_path = File.join(out_dir, 'tokens.dict')
|
|
112
|
+
File.write(dict_path, dict_info[:dict].to_s)
|
|
113
|
+
unless opts[:skip_fuzz]
|
|
114
|
+
argv = fuzzer_argv(opts.merge(in_dir: in_dir, dict: dict_path, binary: bin, out_dir: out_dir))
|
|
115
|
+
run_fuzzer(argv: argv, max_runtime: opts[:max_runtime], env: opts[:env], cwd: opts[:cwd])
|
|
116
|
+
end
|
|
117
|
+
crash_triage(out_dir: out_dir, target: bin || opts[:target], handoff: opts.fetch(:handoff, true))
|
|
118
|
+
end
|
|
119
|
+
|
|
44
120
|
public_class_method def self.crash_triage(opts = {})
|
|
45
121
|
out_dir = (opts[:out_dir] || opts[:output]).to_s
|
|
46
122
|
raise 'ERROR: out_dir is required' if out_dir.empty?
|
|
47
123
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
File.
|
|
53
|
-
|
|
124
|
+
files = crash_files(out_dir: out_dir)
|
|
125
|
+
target = binary_from_target(target: opts[:target])
|
|
126
|
+
groups = Hash.new { |hash, key| hash[key] = { files: [] } }
|
|
127
|
+
files.each do |path|
|
|
128
|
+
stdin = File.binread(path)
|
|
129
|
+
crash = replay_crash(target: target, stdin: stdin)
|
|
130
|
+
key = backtrace_hash(crash: crash)
|
|
131
|
+
groups[key][:files] << path
|
|
132
|
+
groups[key][:crash] ||= crash
|
|
133
|
+
groups[key][:stdin] ||= stdin
|
|
54
134
|
end
|
|
135
|
+
triaged = []
|
|
136
|
+
if opts[:handoff] != false
|
|
137
|
+
groups.each do |key, group|
|
|
138
|
+
triaged << handoff_re003(target: target, crash: group[:crash], stdin: group[:stdin], files: group[:files], backtrace_hash: key)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
hashes = files.map { |path| [path, Digest::SHA256.file(path).hexdigest] }
|
|
55
142
|
{
|
|
56
|
-
crashes:
|
|
57
|
-
unique:
|
|
58
|
-
|
|
59
|
-
|
|
143
|
+
crashes: files,
|
|
144
|
+
unique: groups.keys,
|
|
145
|
+
triaged: triaged,
|
|
146
|
+
pipeline: 'pwn-re-003',
|
|
147
|
+
asan: files.any? { |path| File.binread(path).include?('AddressSanitizer') },
|
|
148
|
+
artifacts: hashes.map { |path, sha| { path: path, sha256: sha } }
|
|
60
149
|
}
|
|
61
150
|
end
|
|
62
151
|
|
|
@@ -96,10 +185,54 @@ module PWN
|
|
|
96
185
|
output: 'optional - alias for out_dir'
|
|
97
186
|
)
|
|
98
187
|
|
|
99
|
-
#
|
|
188
|
+
# Build an AFL/libFuzzer dictionary from interesting strings in a binary.
|
|
189
|
+
#{self}.dictionary_from_binary(
|
|
190
|
+
path: 'required - filesystem path of the fuzz target ELF',
|
|
191
|
+
target: 'optional - alias for path',
|
|
192
|
+
bin: 'optional - alias for path',
|
|
193
|
+
session_id: 'optional - artifact session id for BinaryParser.triage'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Ensure a corpus directory exists and contains at least one seed.
|
|
197
|
+
#{self}.prepare_corpus(
|
|
198
|
+
in_dir: 'required - corpus directory (defaults to opts[:corpus])',
|
|
199
|
+
corpus: 'optional - alias for in_dir',
|
|
200
|
+
seeds: 'optional - extra seed file paths to copy into the corpus'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Build a durable Jobs command that runs campaign then RE triage.
|
|
204
|
+
#{self}.campaign_command(
|
|
205
|
+
engine: 'optional - aflplusplus or libfuzzer (defaults to aflplusplus)',
|
|
206
|
+
in_dir: 'required - corpus directory',
|
|
207
|
+
out_dir: 'required - fuzzer output directory',
|
|
208
|
+
target: 'required - fuzz target command or binary path',
|
|
209
|
+
max_runtime: 'optional - seconds the supervisor may run',
|
|
210
|
+
cwd: 'optional - working directory',
|
|
211
|
+
env: 'optional - string environment map',
|
|
212
|
+
handoff: 'optional - false skips PWN-RE-003 crash handoff (defaults to true)'
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Prepare corpus/dict, run AFL++ or libFuzzer, then triage unique crashes.
|
|
216
|
+
#{self}.campaign(
|
|
217
|
+
engine: 'optional - aflplusplus or libfuzzer (defaults to aflplusplus)',
|
|
218
|
+
in_dir: 'required - corpus directory',
|
|
219
|
+
out_dir: 'required - fuzzer output directory',
|
|
220
|
+
output: 'optional - alias for out_dir',
|
|
221
|
+
target: 'required - fuzz target command or binary path',
|
|
222
|
+
max_runtime: 'optional - seconds forwarded to the fuzzer',
|
|
223
|
+
cwd: 'optional - working directory',
|
|
224
|
+
env: 'optional - string environment map',
|
|
225
|
+
skip_fuzz: 'optional - true harvests existing crashes without launching a fuzzer',
|
|
226
|
+
handoff: 'optional - false skips PWN-RE-003 crash handoff (defaults to true)',
|
|
227
|
+
seeds: 'optional - extra seed file paths to copy into the corpus'
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Dedup crashes by backtrace hash and hand unique ones to PWN-RE-003 triage.
|
|
100
231
|
#{self}.crash_triage(
|
|
101
232
|
out_dir: 'required - AFL output directory containing crashes/',
|
|
102
|
-
output: 'optional - alias for out_dir'
|
|
233
|
+
output: 'optional - alias for out_dir',
|
|
234
|
+
target: 'optional - fuzz target binary used to replay crashes',
|
|
235
|
+
handoff: 'optional - false skips GDBMI/BinaryParser/ExploitDev handoff (defaults to true)'
|
|
103
236
|
)
|
|
104
237
|
|
|
105
238
|
# Minimize a crash input with afl-tmin when present.
|
|
@@ -114,6 +247,98 @@ module PWN
|
|
|
114
247
|
"
|
|
115
248
|
constants.sort
|
|
116
249
|
end
|
|
250
|
+
|
|
251
|
+
private_class_method def self.binary_from_target(opts = {})
|
|
252
|
+
parts = begin
|
|
253
|
+
Shellwords.split(opts[:target].to_s)
|
|
254
|
+
rescue ArgumentError
|
|
255
|
+
[opts[:target].to_s]
|
|
256
|
+
end
|
|
257
|
+
parts.find { |part| part != '@@' && !part.start_with?('-') } || parts.first
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private_class_method def self.fuzzer_argv(opts = {})
|
|
261
|
+
engine = (opts[:engine] || 'aflplusplus').to_s
|
|
262
|
+
if engine.downcase.include?('libfuzzer')
|
|
263
|
+
argv = [opts[:binary] || binary_from_target(target: opts[:target])]
|
|
264
|
+
argv << "-max_total_time=#{opts[:max_runtime].to_i}" if opts[:max_runtime].to_i.positive?
|
|
265
|
+
argv << "-dict=#{opts[:dict]}" if opts[:dict].to_s != '' && File.file?(opts[:dict].to_s)
|
|
266
|
+
argv << "-artifact_prefix=#{File.join(opts[:out_dir].to_s, 'crash-')}"
|
|
267
|
+
argv << opts[:in_dir].to_s
|
|
268
|
+
else
|
|
269
|
+
argv = ['afl-fuzz', '-i', opts[:in_dir].to_s, '-o', opts[:out_dir].to_s]
|
|
270
|
+
argv += ['-x', opts[:dict].to_s] if opts[:dict].to_s != '' && File.file?(opts[:dict].to_s)
|
|
271
|
+
argv += ['--', *Shellwords.split(opts[:target].to_s)]
|
|
272
|
+
end
|
|
273
|
+
argv
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
private_class_method def self.run_fuzzer(opts = {})
|
|
277
|
+
env = { 'AFL_NO_UI' => '1', 'AFL_SKIP_CPUFREQ' => '1', 'AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES' => '1' }
|
|
278
|
+
env.merge!(opts[:env].transform_keys(&:to_s)) if opts[:env].is_a?(Hash)
|
|
279
|
+
kw = {}
|
|
280
|
+
kw[:chdir] = opts[:cwd] if opts[:cwd].to_s != ''
|
|
281
|
+
system(env, *opts[:argv], **kw)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private_class_method def self.crash_files(opts = {})
|
|
285
|
+
out_dir = opts[:out_dir]
|
|
286
|
+
files = Dir[File.join(out_dir, '**/crashes/id:*')]
|
|
287
|
+
files += Dir[File.join(out_dir, 'crash-*')]
|
|
288
|
+
files += Dir[File.join(out_dir, '**/crash-*')]
|
|
289
|
+
files.select { |path| File.file?(path) && File.basename(path) != 'README.txt' }.uniq
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private_class_method def self.replay_crash(opts = {})
|
|
293
|
+
target = opts[:target].to_s
|
|
294
|
+
stdin = opts[:stdin].to_s
|
|
295
|
+
return dummy_crash(stdin: stdin) if target.empty?
|
|
296
|
+
|
|
297
|
+
PWN::Plugins::GDBMI.run_to_crash(binary: target, stdin: stdin)
|
|
298
|
+
rescue StandardError
|
|
299
|
+
stdout, stderr, status = Open3.capture3(target, stdin_data: stdin)
|
|
300
|
+
sig = status.termsig ? Signal.signame(status.termsig) : nil
|
|
301
|
+
{
|
|
302
|
+
signal: sig ? "SIG#{sig}" : 'UNKNOWN',
|
|
303
|
+
pc: nil,
|
|
304
|
+
fault_addr: nil,
|
|
305
|
+
backtrace: stderr.lines.grep(/#\d+/).map(&:strip),
|
|
306
|
+
stdout: stdout,
|
|
307
|
+
stderr: stderr
|
|
308
|
+
}
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
private_class_method def self.dummy_crash(opts = {})
|
|
312
|
+
{ signal: 'SIGSEGV', pc: Digest::SHA256.hexdigest(opts[:stdin].to_s)[0, 8], fault_addr: nil, backtrace: [] }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private_class_method def self.backtrace_hash(opts = {})
|
|
316
|
+
crash = opts[:crash] || {}
|
|
317
|
+
Digest::SHA256.hexdigest([crash[:signal], crash[:pc], crash[:fault_addr], Array(crash[:backtrace]).inspect].join('|'))[0, 16]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private_class_method def self.handoff_re003(opts = {})
|
|
321
|
+
target = opts[:target].to_s
|
|
322
|
+
crash = opts[:crash] || {}
|
|
323
|
+
binary_triage = begin
|
|
324
|
+
PWN::Plugins::BinaryParser.triage(path: target, session_id: 'pwn-re-003') if File.file?(target)
|
|
325
|
+
rescue StandardError
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
exploitdev = begin
|
|
329
|
+
PWN::Plugins::ExploitDev.from_crash(crash: crash, payload: opts[:stdin]) if crash.is_a?(Hash)
|
|
330
|
+
rescue StandardError
|
|
331
|
+
nil
|
|
332
|
+
end
|
|
333
|
+
{
|
|
334
|
+
backtrace_hash: opts[:backtrace_hash],
|
|
335
|
+
crash: crash,
|
|
336
|
+
binary_triage: binary_triage,
|
|
337
|
+
exploitdev: exploitdev,
|
|
338
|
+
files: opts[:files],
|
|
339
|
+
pipeline: 'pwn-re-003'
|
|
340
|
+
}
|
|
341
|
+
end
|
|
117
342
|
end
|
|
118
343
|
end
|
|
119
344
|
end
|