lictor 0.4.0
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 +7 -0
- data/CHANGELOG.md +240 -0
- data/LICENSE +21 -0
- data/README.md +285 -0
- data/bin/lictor +609 -0
- data/lib/config/build_layouts.yml +67 -0
- data/lib/lictor/acquisition.rb +78 -0
- data/lib/lictor/config.rb +24 -0
- data/lib/lictor/executor.rb +119 -0
- data/lib/lictor/exploit_scaffold.rb +61 -0
- data/lib/lictor/handoff.rb +62 -0
- data/lib/lictor/harvest.rb +58 -0
- data/lib/lictor/pack_draft.rb +84 -0
- data/lib/lictor/payload_builder.rb +187 -0
- data/lib/lictor/priors.rb +30 -0
- data/lib/lictor/profiler.rb +38 -0
- data/lib/lictor/recon.rb +61 -0
- data/lib/lictor/report.rb +123 -0
- data/lib/lictor/run.rb +174 -0
- data/lib/lictor/scanner.rb +31 -0
- data/lib/lictor/suggestion.rb +40 -0
- data/lib/lictor/targets.rb +38 -0
- data/lib/lictor/technique_packs.rb +25 -0
- data/lib/lictor/templates/scaffold_exploit.rb.erb +47 -0
- data/lib/lictor/trail.rb +77 -0
- data/lib/lictor/tube.rb +76 -0
- data/lib/lictor/version.rb +5 -0
- data/lib/lictor.rb +26 -0
- metadata +101 -0
data/bin/lictor
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Bootstrap the bundler context from the checkout: the CLI runs from any
|
|
5
|
+
# working directory (when not already inside `bundle exec`).
|
|
6
|
+
unless defined?(Bundler)
|
|
7
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
8
|
+
require "bundler/setup"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require "optparse"
|
|
12
|
+
require "tmpdir"
|
|
13
|
+
require "fileutils"
|
|
14
|
+
require "json"
|
|
15
|
+
require_relative "../lib/lictor"
|
|
16
|
+
|
|
17
|
+
# Config first (lictor.yml in CWD), flags override it.
|
|
18
|
+
options = Lictor::Config.load
|
|
19
|
+
parser = OptionParser.new do |opts|
|
|
20
|
+
opts.banner = <<~USAGE
|
|
21
|
+
|
|
22
|
+
Usage: lictor <command> [options]
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
scan <host> run nmap against a target (trailed)
|
|
26
|
+
acquire <target> fetch the binary, profile it into target.facts
|
|
27
|
+
fuzz <binary> fuzz + triage a local binary (discovery layer)
|
|
28
|
+
verify <host:port> probe the remote with the crash input
|
|
29
|
+
harvest <host:port> parse the banner's hex leaks into facts
|
|
30
|
+
suggest <facts> "..." simulate a fact merge, print the delta (-o writes it)
|
|
31
|
+
plan <binary-or-facts> profile if ELF, decide, print the recipe
|
|
32
|
+
build <binary> measure the offset, emit the payload
|
|
33
|
+
run <target> the closed loop: decide, execute, outcome
|
|
34
|
+
outcome <run-id> record the result of a run
|
|
35
|
+
exploit scaffold <t> <bin> draft a ronin exploit from measured constants
|
|
36
|
+
draft <facts> auto-draft a technique pack into staging/
|
|
37
|
+
rule validate|install|list teach augure a technique (data-only packs)
|
|
38
|
+
runs / report the trail, rendered (--defender: the blue view)
|
|
39
|
+
target add|list named targets: a dir + the registry (ADR)
|
|
40
|
+
doctor / init verify the environment, scaffold the setup
|
|
41
|
+
|
|
42
|
+
Setup: lictor.yml in the working directory
|
|
43
|
+
(trail: runs.jsonl / nmap: / seed: / profile_cmd:). Flags override
|
|
44
|
+
the config. You are the operator: what this tool does, you did.
|
|
45
|
+
The trail is the record.
|
|
46
|
+
USAGE
|
|
47
|
+
opts.on("-s", "--seed N", Integer, "Seed for reproducible decisions") { |v| options[:seed] = v }
|
|
48
|
+
opts.on("-t", "--trail PATH", "JSONL trail (default: lictor-trail.jsonl)") { |v| options[:trail] = v }
|
|
49
|
+
opts.on("--nmap PATH", "Path to the nmap binary (default: nmap in PATH)") { |v| options[:nmap] = v }
|
|
50
|
+
opts.on("--profile-cmd PATH", "Path to augure-profile (default: augure-profile in PATH)") { |v| options[:profile_cmd] = v }
|
|
51
|
+
opts.on("--success", "Record a successful outcome (outcome only)") { options[:success] = true }
|
|
52
|
+
opts.on("--failure", "Record a failed outcome (outcome only)") { options[:success] = false }
|
|
53
|
+
opts.on("--technique NAME", "The technique the outcome applies to") { |v| options[:technique] = v }
|
|
54
|
+
opts.on("--packs DIR", "Technique packs directory (default: packs/)") { |v| options[:packs] = v }
|
|
55
|
+
opts.on("--max-inputs N", Integer, "Fuzz budget: mutated inputs to try (default 50)") { |v| options[:max_inputs] = v }
|
|
56
|
+
opts.on("--mutator NAME", "ruby (default) or radamsa if installed") { |v| options[:mutator] = v }
|
|
57
|
+
opts.on("--executor NAME", "manual (default) or ronin") { |v| options[:executor] = v }
|
|
58
|
+
opts.on("--technique NAME", "Override the technique the build implements (default: augure's pick)") { |v| options[:technique] = v }
|
|
59
|
+
opts.on("--payload FILE", "Send a pre-built payload (from `lictor build`) with run") { |v| options[:payload] = v }
|
|
60
|
+
opts.on("--shellcode-path PATH", "The flag path embedded in the shellcode stubs (build)") { |v| options[:shellcode_path] = v }
|
|
61
|
+
opts.on("--flag-regex RE", "Success oracle: the flag pattern in the runner's output (the exit code lies)") { |v| options[:flag_regex] = v }
|
|
62
|
+
opts.on("--resume RUN-ID", "Continue an interrupted run (the manifest carries the id)") { |v| options[:resume] = v }
|
|
63
|
+
opts.on("--payload-file FILE", "Custom shellcode/payload bytes for build") { |v| options[:payload_file] = v }
|
|
64
|
+
opts.on("--out FILE", "Output path for build (default payload.bin)") { |v| options[:out] = v }
|
|
65
|
+
opts.on("--repo-dir DIR", "Exploit scaffold destination directory (default: .)") { |v| options[:repo_dir] = v }
|
|
66
|
+
opts.on("--author NAME", "The scaffolded exploit's author line") { |v| options[:author] = v }
|
|
67
|
+
opts.on("--max-attempts N", Integer, "Run budget: attempts before giving up (default 3)") { |v| options[:max_attempts] = v }
|
|
68
|
+
opts.on("--facts PATH", "Facts file for run/plan (the profiler output)") { |v| options[:facts] = v }
|
|
69
|
+
opts.on("--target NAME", "Scope the state into targets/NAME (facts, payloads, trail)") { |v| options[:target] = v }
|
|
70
|
+
opts.on("--from URL", "Acquire the binary from an http(s) URL") { |v| options[:from] = v }
|
|
71
|
+
opts.on("--bin PATH", "Local binary: the acquire source, or the mirror for run auto-scaffold") { |v| options[:bin] = v }
|
|
72
|
+
opts.on("--input PATH", "Crash input for verify (the fuzzer's discovery)") { |v| options[:input] = v }
|
|
73
|
+
opts.on("--triage-backend NAME", "gdb (default), rizin or lldb") { |v| options[:triage_backend] = v }
|
|
74
|
+
opts.on("-o", "--output PATH", "Report output path, or the merged facts for suggest") { |v| options[:output] = v }
|
|
75
|
+
opts.on("-j", "--json", "Machine payload instead of human output") { options[:json] = true }
|
|
76
|
+
opts.on("--defender", "report: the blue-side view (the timeline + the packs' detection hints)") { options[:defender] = true }
|
|
77
|
+
opts.on("-h", "--help", "Show this help") { puts opts; exit 0 }
|
|
78
|
+
end
|
|
79
|
+
parser.parse!
|
|
80
|
+
|
|
81
|
+
HELP = {
|
|
82
|
+
"recon" => "recon <nmap.xml>\n Print service candidates from an nmap XML scan (passive).",
|
|
83
|
+
"scan" => "scan <host>\n Run nmap against a target (trailed).",
|
|
84
|
+
"plan" => "plan <binary-or-facts>\n Profile if ELF (augure-profile), decide, print the handoff recipe.",
|
|
85
|
+
"outcome" => "outcome <run-id> --success|--failure --technique NAME\n Record the result; the next plan adapts the priors.",
|
|
86
|
+
"runs" => "runs\n List the trailed runs.",
|
|
87
|
+
"report" => "report [--json] [--defender] [-o PATH]\n The operator's report, or (--defender) the blue-side view.",
|
|
88
|
+
"rule" => "rule validate|install|list <pack.yml>\n Teach augure a technique: data-only pack, validated + corpus-guarded.\n install is interactive and trailed.",
|
|
89
|
+
"init" => "init\n Scaffold lictor.yml and an example facts file.",
|
|
90
|
+
"doctor" => "doctor\n Verify the environment; print the exact fix per check.",
|
|
91
|
+
"target" => "target add <name> <host:port> [--from URL | --bin PATH] | target list\n A named target owns a dir: facts, payloads, its own trail. --target scopes the commands.",
|
|
92
|
+
"acquire" => "acquire <target> [--from URL | --bin PATH]\n Fetch (or copy) the binary, check it is an ELF, profile it into target.facts.",
|
|
93
|
+
"verify" => "verify <host:port> [--input PATH]\n Probe the remote with the crash input; a fault anchors verified(\"remote\").",
|
|
94
|
+
"fuzz" => "fuzz <binary> [--max-inputs N] [--mutator ruby|radamsa] [--triage-backend gdb]\n Discover crashes, triage the best one, trail both.",
|
|
95
|
+
"harvest" => "harvest <host:port> [--facts PATH]\n Parse the service banner's hex leaks into leaked_address facts.",
|
|
96
|
+
"suggest" => "suggest <facts-file> \"fact(\"atom\").\n...\" [-o PATH]\n Simulate the fact merge, print the decision delta; -o writes the merged base.",
|
|
97
|
+
"run" => "run <target> --facts PATH [--executor manual|ronin] [--payload FILE]\n The closed loop: decide, execute (or hand off), outcome, re-decide.",
|
|
98
|
+
"build" => "build <binary> [--facts PATH] [-o payload.bin]\n Measure the offset, resolve the addresses from the facts, emit the payload.",
|
|
99
|
+
"exploit" => "exploit scaffold <technique> <binary>\n Draft a ronin exploit from the measured constants into your repo.",
|
|
100
|
+
"draft" => "draft <facts> [--bin PATH]\n Auto-draft a technique pack from the facts into staging/."
|
|
101
|
+
}.freeze
|
|
102
|
+
|
|
103
|
+
if ARGV[0] == "help"
|
|
104
|
+
verb = ARGV[1]
|
|
105
|
+
abort("lictor: no such command '#{verb}' (see lictor --help)") unless HELP[verb]
|
|
106
|
+
puts HELP[verb]
|
|
107
|
+
exit 0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
options[:technique] = nil unless options.key?(:technique)
|
|
111
|
+
options[:success] = nil unless options.key?(:success)
|
|
112
|
+
command = ARGV.shift or abort(parser.to_s)
|
|
113
|
+
|
|
114
|
+
when_state = if options[:target]
|
|
115
|
+
entry = Lictor::Targets.resolve(options[:target])
|
|
116
|
+
entry[:dir]
|
|
117
|
+
else
|
|
118
|
+
"."
|
|
119
|
+
end
|
|
120
|
+
options[:trail] ||= File.join(when_state, "trail.jsonl")
|
|
121
|
+
if options[:target]
|
|
122
|
+
options[:facts] ||= File.join(when_state, "target.facts")
|
|
123
|
+
options[:out] ||= File.join(when_state, "payload.bin")
|
|
124
|
+
end
|
|
125
|
+
trail = Lictor::Trail.new(options[:trail])
|
|
126
|
+
|
|
127
|
+
def scaffold_files
|
|
128
|
+
{
|
|
129
|
+
"lictor.yml" => <<~YML,
|
|
130
|
+
# lictor setup - flags override these values
|
|
131
|
+
trail: lictor-trail.jsonl
|
|
132
|
+
YML
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
case command
|
|
137
|
+
when "init"
|
|
138
|
+
scaffold_files.each do |name, content|
|
|
139
|
+
path = File.join(Dir.pwd, name)
|
|
140
|
+
if File.exist?(path)
|
|
141
|
+
abort("lictor: #{name} already exists - keep your setup, run `lictor doctor`")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
scaffold_files.each do |name, content|
|
|
145
|
+
dir = File.dirname(File.join(Dir.pwd, name))
|
|
146
|
+
FileUtils.mkdir_p(dir)
|
|
147
|
+
File.write(File.join(Dir.pwd, name), content)
|
|
148
|
+
end
|
|
149
|
+
# The one example that proves the pipeline: straight from the corpus.
|
|
150
|
+
# Same resolution as the Gemfile: dev sibling or CI in-tree checkout.
|
|
151
|
+
FileUtils.mkdir_p("examples")
|
|
152
|
+
corpus_candidates = [
|
|
153
|
+
File.expand_path("../../augure/lib/augure/ctf_corpus.json", __dir__),
|
|
154
|
+
File.expand_path("../augure/lib/augure/ctf_corpus.json", __dir__)
|
|
155
|
+
]
|
|
156
|
+
augure_corpus = corpus_candidates.find { |p| File.file?(p) }
|
|
157
|
+
if augure_corpus
|
|
158
|
+
entry = JSON.parse(File.read(augure_corpus))["suites"]["ropemporium"].find { |e| e["name"] == "ret2win" }
|
|
159
|
+
File.write("examples/ret2win.facts", entry["facts"]) if entry
|
|
160
|
+
end
|
|
161
|
+
scaffolded = ["lictor.yml"]
|
|
162
|
+
scaffolded << "examples/ret2win.facts" if File.file?("examples/ret2win.facts")
|
|
163
|
+
puts "scaffolded: #{scaffolded.join(", ")}"
|
|
164
|
+
puts
|
|
165
|
+
puts "next:"
|
|
166
|
+
puts " 1. lictor doctor # verify the environment"
|
|
167
|
+
puts " 2. lictor plan examples/ret2win.facts # your first decision"
|
|
168
|
+
when "target"
|
|
169
|
+
sub = ARGV.shift or abort(parser.to_s)
|
|
170
|
+
case sub
|
|
171
|
+
when "add"
|
|
172
|
+
name = ARGV.shift or abort(parser.to_s)
|
|
173
|
+
host = ARGV.shift or abort(parser.to_s)
|
|
174
|
+
info = Lictor::Targets.add(name, host, from: options[:from], bin: options[:bin])
|
|
175
|
+
puts "target: #{name} (#{info[:registry]["host"]})"
|
|
176
|
+
puts "dir: #{info[:dir]}/ (facts, payloads, trail - scoped)"
|
|
177
|
+
puts "registry: #{Lictor::Targets::REGISTRY}"
|
|
178
|
+
puts "next: lictor acquire #{name} --target #{name} --from <url>"
|
|
179
|
+
when "list"
|
|
180
|
+
Lictor::Targets.load.each { |name, entry| puts "#{name}\t#{entry["host"]}" }
|
|
181
|
+
else
|
|
182
|
+
abort("lictor: target add|list")
|
|
183
|
+
end
|
|
184
|
+
when "doctor"
|
|
185
|
+
checks = []
|
|
186
|
+
checks << ["ruby >= 3.4", RUBY_VERSION >= "3.4" ? nil : "upgrade ruby (mise install ruby@3.4)"]
|
|
187
|
+
nmap = options[:nmap] || "nmap"
|
|
188
|
+
nmap_found = system(nmap, "--version", out: File::NULL, err: File::NULL)
|
|
189
|
+
checks << ["nmap binary", nmap_found ? nil : "install nmap (apt install nmap) or set nmap: in lictor.yml"]
|
|
190
|
+
profiler = options[:profile_cmd] || Lictor::Profiler::DEFAULT_CMD
|
|
191
|
+
# Same candidate resolution as init's corpus: dev sibling or CI in-tree.
|
|
192
|
+
profiler_candidates = [
|
|
193
|
+
File.expand_path("../../augure/profiler/exe/augure-profile", __dir__),
|
|
194
|
+
File.expand_path("../augure/profiler/exe/augure-profile", __dir__)
|
|
195
|
+
]
|
|
196
|
+
# Availability only: missing gems at runtime are ProfilerError's job.
|
|
197
|
+
profiler_found = [profiler, *profiler_candidates].find do |c|
|
|
198
|
+
system("sh", "-c", "command -v '#{c}' >/dev/null 2>&1")
|
|
199
|
+
end
|
|
200
|
+
checks << ["augure-profile",
|
|
201
|
+
profiler_found ? nil : "gem install augure-profiler (or set profile_cmd: in lictor.yml)"]
|
|
202
|
+
config_path = "lictor.yml"
|
|
203
|
+
checks << ["lictor.yml", File.file?(config_path) ? nil : "run `lictor init`"]
|
|
204
|
+
# optional tooling: warned, never failed - plan/report run without them
|
|
205
|
+
@optional = []
|
|
206
|
+
gdb_found = system("sh", "-c", "command -v gdb >/dev/null 2>&1")
|
|
207
|
+
@optional << ["gdb binary", gdb_found ? nil : "install gdb (apt install gdb) - the offset measurement, fuzz and triage need it"]
|
|
208
|
+
ronin_found = system("sh", "-c", "gem list -i ronin-exploits >/dev/null 2>&1")
|
|
209
|
+
@optional << ["ronin-exploits (run executor)", ronin_found ? nil : "gem install ronin-exploits - only the ronin executor needs it"]
|
|
210
|
+
docker_found = system("sh", "-c", "command -v docker >/dev/null 2>&1")
|
|
211
|
+
@optional << ["docker (the lab)", docker_found ? nil : "install docker to run the lab bench - optional outside the lab"]
|
|
212
|
+
|
|
213
|
+
trail_path = options[:trail] || Lictor::Config.load[:trail] || "lictor-trail.jsonl"
|
|
214
|
+
writable = begin
|
|
215
|
+
File.open(trail_path, "a") { |f| f.write("") }
|
|
216
|
+
true
|
|
217
|
+
rescue StandardError
|
|
218
|
+
false
|
|
219
|
+
end
|
|
220
|
+
checks << ["trail writable: #{trail_path}", writable ? nil : "check directory permissions"]
|
|
221
|
+
|
|
222
|
+
failed = false
|
|
223
|
+
checks.each do |name, problem|
|
|
224
|
+
if problem
|
|
225
|
+
failed = true
|
|
226
|
+
puts " FAIL #{name} - fix: #{problem}"
|
|
227
|
+
else
|
|
228
|
+
puts " ok #{name}"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
@optional.each do |name, problem|
|
|
232
|
+
if problem
|
|
233
|
+
puts " warn #{name} - note: #{problem}"
|
|
234
|
+
else
|
|
235
|
+
puts " ok #{name}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
if failed
|
|
239
|
+
n = checks.count { |_, p| p }
|
|
240
|
+
abort("lictor: doctor found #{n} issue(s) - fixes above (fresh setup? run lictor init)")
|
|
241
|
+
end
|
|
242
|
+
when "recon"
|
|
243
|
+
xml = ARGV.shift or abort(parser.to_s)
|
|
244
|
+
puts Lictor::Recon.service_candidates(xml)
|
|
245
|
+
when "rule"
|
|
246
|
+
sub = ARGV.shift or abort(parser.to_s)
|
|
247
|
+
packs_dir = options[:packs] || Lictor::Config.load[:packs] || "packs"
|
|
248
|
+
case sub
|
|
249
|
+
when "list"
|
|
250
|
+
Dir.glob(File.join(packs_dir, "*.{yml,yaml}")).sort.each do |p|
|
|
251
|
+
pack = Augure::PackLoader.load_file(p)
|
|
252
|
+
puts "#{pack.technique} by #{pack.author} (#{p})"
|
|
253
|
+
end
|
|
254
|
+
when "validate", "install"
|
|
255
|
+
path = ARGV.shift or abort("lictor: rule #{sub} <pack.yml>")
|
|
256
|
+
begin
|
|
257
|
+
pack = Augure::PackLoader.load_file(path)
|
|
258
|
+
Augure::PackLoader.corpus_guard([pack])
|
|
259
|
+
rescue Augure::PackError => e
|
|
260
|
+
abort("lictor: pack refused - #{e.message}")
|
|
261
|
+
end
|
|
262
|
+
puts "pack: #{pack.technique}"
|
|
263
|
+
puts "author: #{pack.author}"
|
|
264
|
+
puts "source: #{pack.source}"
|
|
265
|
+
puts "corpus: all 34 documented verdicts intact"
|
|
266
|
+
if sub == "validate"
|
|
267
|
+
puts "verdict: valid (dry-run; `lictor rule install #{path}` to teach augure)"
|
|
268
|
+
else
|
|
269
|
+
# Interactive confirmation: friction with information. Type the
|
|
270
|
+
# technique name - a flag can be typed by reflex, this cannot.
|
|
271
|
+
print "type the technique name to confirm: "
|
|
272
|
+
confirm = $stdin.gets.to_s.strip
|
|
273
|
+
abort("lictor: confirmation mismatch - nothing installed") unless confirm == pack.technique
|
|
274
|
+
FileUtils.mkdir_p(packs_dir)
|
|
275
|
+
installed = File.join(packs_dir, File.basename(path))
|
|
276
|
+
FileUtils.cp(path, installed)
|
|
277
|
+
trail.record(run: SecureRandom.uuid, type: "rule_install", technique: pack.technique,
|
|
278
|
+
author: pack.author, source: pack.source, pack_file: installed)
|
|
279
|
+
puts "installed: #{installed} (trailed)"
|
|
280
|
+
puts "next plan run will consider #{pack.technique}"
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
abort("lictor: rule validate|install|list")
|
|
284
|
+
end
|
|
285
|
+
when "verify"
|
|
286
|
+
# The differential: the fuzzer's crash input goes to the REMOTE
|
|
287
|
+
# service. If the remote faults like the local model, the model is
|
|
288
|
+
# remote-verified (the facts carry the confidence level).
|
|
289
|
+
target = ARGV.shift or abort(parser.to_s)
|
|
290
|
+
abort("lictor: verify needs --input <crash-input-file> (lictor fuzz discovers it)") unless options[:input]
|
|
291
|
+
# Remote crash detection: deliver the input, then read until the
|
|
292
|
+
# connection dies. A crashed child dies IN vuln_copy: the normal path
|
|
293
|
+
# prints "hello" first, the crash path does not. A reset = death too.
|
|
294
|
+
# The socket work is the Tube's (ADR lictor 0001).
|
|
295
|
+
host, port = target.split(":", 2)
|
|
296
|
+
begin
|
|
297
|
+
tube = Lictor::Tube.tcp(host, port, timeout: 5)
|
|
298
|
+
tube.gets("\n") # the greeting, if any
|
|
299
|
+
tube.write(File.binread(options[:input]))
|
|
300
|
+
reply = tube.read_bytes(limit: 4096)
|
|
301
|
+
reset_seen = tube.reset?
|
|
302
|
+
tube.close
|
|
303
|
+
rescue Lictor::TubeError => e
|
|
304
|
+
reset_seen = false
|
|
305
|
+
reply = ""
|
|
306
|
+
puts " (the connection itself failed: #{e.message})"
|
|
307
|
+
end
|
|
308
|
+
crashed = (reset_seen || reply.empty?) && !reply.include?("hello")
|
|
309
|
+
trail.record(run: SecureRandom.uuid, type: "verify", target: target,
|
|
310
|
+
input: options[:input], remote_crashed: crashed)
|
|
311
|
+
if crashed
|
|
312
|
+
puts "remote-verified: the remote service faulted on the same input -"
|
|
313
|
+
puts " the local model (offsets, gadgets) is representative. The facts"
|
|
314
|
+
puts " gained verified(\"remote\")."
|
|
315
|
+
facts_path = options[:facts] || "target.facts"
|
|
316
|
+
File.open(facts_path, "a") { |f| f.puts 'verified("remote").' }
|
|
317
|
+
else
|
|
318
|
+
puts "not verified: the remote survived - the local model may not apply"
|
|
319
|
+
puts " (version drift? a different build?). Do not trust the offsets."
|
|
320
|
+
end
|
|
321
|
+
when "fuzz"
|
|
322
|
+
# Fuzzing is an ACTION on a local binary: TRAILED, and its discoveries
|
|
323
|
+
# feed the plan.
|
|
324
|
+
binary = ARGV.shift or abort(parser.to_s)
|
|
325
|
+
begin
|
|
326
|
+
require "augure-profiler"
|
|
327
|
+
binary = File.expand_path(binary)
|
|
328
|
+
rescue LoadError
|
|
329
|
+
abort("lictor: fuzzing needs the augure-profiler gem (gem install augure-profiler)")
|
|
330
|
+
end
|
|
331
|
+
crashes = AugureProfiler::Fuzzer.run(binary, inputs: options[:max_inputs] || 50,
|
|
332
|
+
mutator: (options[:mutator] || :ruby).to_sym)
|
|
333
|
+
trail.record(run: SecureRandom.uuid, type: "fuzz", binary: binary,
|
|
334
|
+
inputs: options[:max_inputs] || 50, crashes: crashes.size)
|
|
335
|
+
if crashes.empty?
|
|
336
|
+
puts "fuzzed: #{binary} - no crash (nothing to triage)"
|
|
337
|
+
else
|
|
338
|
+
puts "fuzzed: #{binary} - #{crashes.size} crash(es)"
|
|
339
|
+
# The LONGEST crash input is the most robust across environments
|
|
340
|
+
# (it smashes past any offset): the shortest may be a marginal,
|
|
341
|
+
# layout-dependent crash - exactly what `verify` is there to catch.
|
|
342
|
+
robust = crashes.max_by { |c| c[:input].bytesize }
|
|
343
|
+
crash_file = File.join(Dir.pwd, "crash-input.bin")
|
|
344
|
+
File.binwrite(crash_file, robust[:input])
|
|
345
|
+
puts "saved: #{crash_file} (the longest crash input - robust for verify)"
|
|
346
|
+
triage = AugureProfiler::Triage.run(binary, robust[:input])
|
|
347
|
+
trail.record(run: SecureRandom.uuid, type: "triage", binary: binary,
|
|
348
|
+
signal: triage[:signal], control: triage[:control].to_s)
|
|
349
|
+
puts "triage: #{triage[:signal]} control=#{triage[:control]}"
|
|
350
|
+
facts_text = AugureProfiler::Triage.facts(triage)
|
|
351
|
+
puts facts_text
|
|
352
|
+
puts "next: lictor plan <binary> (facts above are decidable)"
|
|
353
|
+
end
|
|
354
|
+
when "run"
|
|
355
|
+
# The closed loop, opt-in: decide -> execute -> outcome -> re-decide.
|
|
356
|
+
target = ARGV.shift or abort(parser.to_s)
|
|
357
|
+
abort("lictor: run needs --facts <facts-file> (scan/profile first)") unless options[:facts]
|
|
358
|
+
executor_kind = (options[:executor] || :manual).to_sym
|
|
359
|
+
|
|
360
|
+
result = Lictor::Run.run(target: target, facts: File.read(options[:facts]), trail: trail,
|
|
361
|
+
executor: Lictor::Executor.resolve(executor_kind),
|
|
362
|
+
max_attempts: options[:max_attempts] || 3,
|
|
363
|
+
packs: Lictor::TechniquePacks.load(options[:packs] || Lictor::Config.load[:packs] || "packs"),
|
|
364
|
+
payload: options[:payload],
|
|
365
|
+
seed: options[:seed],
|
|
366
|
+
binary: options[:bin],
|
|
367
|
+
repo_dir: options[:repo_dir],
|
|
368
|
+
flag_regex: options[:flag_regex],
|
|
369
|
+
manifest_path: File.join(when_state, "run.json"),
|
|
370
|
+
resume: options[:resume],
|
|
371
|
+
technique: options[:technique])
|
|
372
|
+
puts "run: #{result[:run_id]} (interrupted? resume: lictor run ... --resume #{result[:run_id]})"
|
|
373
|
+
puts "status: #{result[:status]}"
|
|
374
|
+
result[:steps].each do |s|
|
|
375
|
+
executed = s[:executed] ? "executed" : "handoff"
|
|
376
|
+
ok = s[:success] ? "success" : (s[:success].nil? ? "-" : "failed")
|
|
377
|
+
puts " #{s[:technique]} [#{executed}/#{ok}]"
|
|
378
|
+
end
|
|
379
|
+
# The flag in the output is the proof the loop actually closed - the
|
|
380
|
+
# exit code lies, the flag does not.
|
|
381
|
+
flag = result[:steps].filter_map { |s| s[:flag] }.first
|
|
382
|
+
puts "flag: #{flag}" if flag
|
|
383
|
+
when "acquire"
|
|
384
|
+
# The nmap -> binary bridge: local copy or http download, ELF-checked,
|
|
385
|
+
# profiled, trailed.
|
|
386
|
+
target = ARGV.shift or abort(parser.to_s)
|
|
387
|
+
# the registry carries the source: --from/--bin override, the
|
|
388
|
+
# target's recorded `from`/`bin` is the fallback (the engagement
|
|
389
|
+
# flow: one `target add` and the acquire needs no flags)
|
|
390
|
+
source = options[:from] || (options[:bin] && "local:#{options[:bin]}")
|
|
391
|
+
source ||= begin
|
|
392
|
+
entry = Lictor::Targets.resolve(target)
|
|
393
|
+
(entry[:from] || (entry[:bin] && "local:#{entry[:bin]}"))
|
|
394
|
+
rescue ArgumentError
|
|
395
|
+
nil
|
|
396
|
+
end
|
|
397
|
+
source or abort("lictor: acquire needs --from <url> or --bin <path> (or a target add --from/--bin)")
|
|
398
|
+
result = Lictor::Acquisition.acquire(target, source: source, trail: trail,
|
|
399
|
+
out_dir: when_state,
|
|
400
|
+
profiler_cmd: options[:profile_cmd])
|
|
401
|
+
puts "acquired: #{result[:binary_path]}"
|
|
402
|
+
puts "facts: #{result[:facts_path]} (#{result[:facts].lines.size} facts)"
|
|
403
|
+
puts "next: lictor plan target.facts"
|
|
404
|
+
when "scan"
|
|
405
|
+
# A scan is an ACTION, trailed.
|
|
406
|
+
target = ARGV.shift or abort(parser.to_s)
|
|
407
|
+
out_xml = File.join(Dir.mktmpdir("lictor_scan"), "scan.xml")
|
|
408
|
+
begin
|
|
409
|
+
targets = Lictor::Scanner.run(target, out: out_xml, nmap_cmd: options[:nmap] || "nmap")
|
|
410
|
+
rescue Lictor::ScanError => e
|
|
411
|
+
abort("lictor: #{e.message}")
|
|
412
|
+
end
|
|
413
|
+
trail.record(run: SecureRandom.uuid, type: "scan", target: target,
|
|
414
|
+
nmap_args: [options[:nmap] || "nmap", *Lictor::Scanner::NMAP_ARGS])
|
|
415
|
+
puts "scanned: #{target}"
|
|
416
|
+
targets.each do |t|
|
|
417
|
+
puts "#{t[:host]}:#{t[:port]} #{t[:service]}#{t[:product] ? " (#{t[:product]} #{t[:version]})" : ''}"
|
|
418
|
+
end
|
|
419
|
+
when "suggest"
|
|
420
|
+
facts_path = ARGV.shift or abort(parser.to_s)
|
|
421
|
+
suggested = ARGV.shift or abort("lictor: suggest <facts-file> \"fact(\\\"atom\\\").\\n...\" - the suggestion text")
|
|
422
|
+
begin
|
|
423
|
+
result = Lictor::Suggestion.apply(base_facts: File.read(facts_path), suggested: suggested,
|
|
424
|
+
trail: trail, target: facts_path, seed: options[:seed],
|
|
425
|
+
packs: Lictor::TechniquePacks.load(options[:packs] || Lictor::Config.load[:packs] || "packs"))
|
|
426
|
+
rescue Augure::MalformedFact, Augure::UnknownPredicate => e
|
|
427
|
+
abort("lictor: #{e.message}")
|
|
428
|
+
end
|
|
429
|
+
puts "added: #{result[:added].inspect}"
|
|
430
|
+
puts "applicable: #{result[:applicable_before].join(', ')} -> #{result[:applicable_after].join(', ')}"
|
|
431
|
+
puts "selected: #{result[:selected_before].inspect} -> #{result[:selected_after].inspect}"
|
|
432
|
+
if options[:output]
|
|
433
|
+
File.write(options[:output], result[:facts].to_s)
|
|
434
|
+
puts "written: #{options[:output]} (the merged base - the enrichment is not a dry run)"
|
|
435
|
+
end
|
|
436
|
+
when "harvest"
|
|
437
|
+
target = ARGV.shift or abort(parser.to_s)
|
|
438
|
+
begin
|
|
439
|
+
result = Lictor::Harvest.harvest(target, trail: trail,
|
|
440
|
+
timeout: options[:timeout] || 5, read_bytes: 512)
|
|
441
|
+
puts "leaked: #{result[:addresses].map { |k, v| "#{k}=#{format('0x%x', v)}" }.join(', ')}"
|
|
442
|
+
if options[:facts]
|
|
443
|
+
File.open(options[:facts], "a") { |f| f.write(result[:facts]) }
|
|
444
|
+
puts "appended: #{options[:facts]} (leaked_address facts, observed-live)"
|
|
445
|
+
else
|
|
446
|
+
puts "facts: merge them with lictor suggest or pass --facts PATH to append"
|
|
447
|
+
end
|
|
448
|
+
rescue Lictor::Harvest::HarvestError => e
|
|
449
|
+
abort("lictor: #{e.message}")
|
|
450
|
+
end
|
|
451
|
+
when "exploit"
|
|
452
|
+
sub = ARGV.shift
|
|
453
|
+
abort("lictor: exploit <scaffold> ... - only scaffold is implemented") unless sub == "scaffold"
|
|
454
|
+
technique = ARGV.shift or abort(parser.to_s)
|
|
455
|
+
abort("lictor: the technique must match [a-z0-9_]+ (it becomes a filename and lands in generated code)") unless technique =~ /\A[a-z0-9_]+\z/
|
|
456
|
+
binary = ARGV.shift or abort(parser.to_s)
|
|
457
|
+
begin
|
|
458
|
+
facts_path = options[:facts] ||
|
|
459
|
+
abort("lictor: exploit scaffold needs --facts <facts-file>")
|
|
460
|
+
result = Lictor::PayloadBuilder.build(binary: binary, facts: File.read(facts_path),
|
|
461
|
+
technique: technique, out: options[:out] || "payload.bin",
|
|
462
|
+
payload_file: options[:payload_file])
|
|
463
|
+
repo_dir = File.expand_path(options[:repo_dir] || ".")
|
|
464
|
+
FileUtils.mkdir_p(repo_dir)
|
|
465
|
+
dest = Lictor::ExploitScaffold.scaffold(technique: technique, constants: result,
|
|
466
|
+
repo_dir: repo_dir, author: options[:author] || "lictor scaffold")
|
|
467
|
+
trail.record(run: SecureRandom.uuid, type: "scaffold", technique: technique,
|
|
468
|
+
offset: result[:offset], offset_source: result[:offset_source],
|
|
469
|
+
ret: result[:ret_source], file: dest)
|
|
470
|
+
puts "scaffolded: #{dest}"
|
|
471
|
+
puts "review it, commit it, then ronin-exploits finds it by name"
|
|
472
|
+
rescue Lictor::PayloadBuilder::BuildError, Lictor::ExploitScaffold::ScaffoldError => e
|
|
473
|
+
abort("lictor: #{e.message}")
|
|
474
|
+
end
|
|
475
|
+
when "draft"
|
|
476
|
+
facts_path = ARGV.shift or abort(parser.to_s)
|
|
477
|
+
binary = ARGV.shift
|
|
478
|
+
begin
|
|
479
|
+
require "augure-profiler" # lazy: metasm loads on the draft path only
|
|
480
|
+
measurements = {}
|
|
481
|
+
if binary && File.file?(binary)
|
|
482
|
+
static = AugureProfiler::StaticOffset.offset(binary) rescue nil
|
|
483
|
+
measurements[:sink] = static&.dig(:sink)
|
|
484
|
+
measurements[:offset] = static&.dig(:offset)
|
|
485
|
+
measurements[:offset_source] = static&.dig(:offset) ? "static-frame" : nil
|
|
486
|
+
end
|
|
487
|
+
measurements[:run_id] = options[:run_id] || "manual"
|
|
488
|
+
staging = options[:repo_dir] || "staging"
|
|
489
|
+
result = Lictor::PackDraft.draft(facts: File.read(facts_path),
|
|
490
|
+
measurements: measurements, staging_dir: File.expand_path(staging))
|
|
491
|
+
abort("lictor: the built-in techniques already cover these facts - no draft needed") if result.nil?
|
|
492
|
+
trail.record(run: SecureRandom.uuid, type: "pack_draft", technique: result[:technique],
|
|
493
|
+
status: result[:status], file: result[:path])
|
|
494
|
+
puts "draft: #{result[:path]} (#{result[:technique]})"
|
|
495
|
+
puts "armor: #{result[:status]}"
|
|
496
|
+
puts "staging: review it, then lictor rule install once it is yours"
|
|
497
|
+
rescue Lictor::PackDraft::DraftError => e
|
|
498
|
+
abort("lictor: #{e.message}")
|
|
499
|
+
end
|
|
500
|
+
when "build"
|
|
501
|
+
input = ARGV.shift or abort(parser.to_s)
|
|
502
|
+
facts_path = options[:facts]
|
|
503
|
+
unless facts_path
|
|
504
|
+
abort("lictor: build needs --facts <facts-file> (scan/profile first)") unless File.file?("target.facts")
|
|
505
|
+
facts_path = "target.facts"
|
|
506
|
+
end
|
|
507
|
+
begin
|
|
508
|
+
packs = Lictor::TechniquePacks.load(options[:packs] || Lictor::Config.load[:packs] || "packs")
|
|
509
|
+
decision = Lictor::Handoff.plan(facts: File.read(facts_path), seed: options[:seed],
|
|
510
|
+
priors: Lictor::Priors.from_trail(trail), packs: packs)
|
|
511
|
+
technique = options[:technique] || decision[:selected] or
|
|
512
|
+
abort("lictor: build needs a selected technique - plan decided nothing")
|
|
513
|
+
result = Lictor::PayloadBuilder.build(binary: input, facts: File.read(facts_path),
|
|
514
|
+
shellcode_path: options[:shellcode_path],
|
|
515
|
+
technique: technique, out: options[:out] || "payload.bin",
|
|
516
|
+
payload_file: options[:payload_file],
|
|
517
|
+
packs: packs)
|
|
518
|
+
trail.record(run: SecureRandom.uuid, type: "build", technique: technique,
|
|
519
|
+
offset: result[:offset], ret: result[:ret_source], arch: result[:arch].to_s,
|
|
520
|
+
size: result[:size], out: result[:path])
|
|
521
|
+
puts "technique: #{technique}"
|
|
522
|
+
puts "offset: #{result[:offset]} (measured: cyclic pattern under the debugger)"
|
|
523
|
+
puts "ret: #{result[:ret_source]}"
|
|
524
|
+
puts "payload: #{result[:path]} (#{result[:size]} bytes, arch #{result[:arch]})"
|
|
525
|
+
rescue Lictor::PayloadBuilder::BuildError => e
|
|
526
|
+
abort("lictor: #{e.message}")
|
|
527
|
+
rescue Augure::MalformedFact, Augure::UnknownPredicate => e
|
|
528
|
+
abort("lictor: #{e.message}")
|
|
529
|
+
end
|
|
530
|
+
when "plan"
|
|
531
|
+
# --target: the facts default to the target's dir (the ARGV input
|
|
532
|
+
# stays for the direct path)
|
|
533
|
+
input = ARGV.shift || options[:facts] or abort(parser.to_s)
|
|
534
|
+
mode = "handoff (prints a recipe; executes nothing)"
|
|
535
|
+
|
|
536
|
+
begin
|
|
537
|
+
if Lictor::Profiler.elf?(input)
|
|
538
|
+
facts = Lictor::Profiler.run(input, cmd: options[:profile_cmd] || Lictor::Profiler::DEFAULT_CMD)
|
|
539
|
+
trail.record(run: SecureRandom.uuid, type: "profile", binary: input)
|
|
540
|
+
else
|
|
541
|
+
facts = File.read(input)
|
|
542
|
+
end
|
|
543
|
+
priors = Lictor::Priors.from_trail(trail)
|
|
544
|
+
packs = Lictor::TechniquePacks.load(options[:packs] || Lictor::Config.load[:packs] || "packs")
|
|
545
|
+
plan = Lictor::Handoff.plan(facts: facts, seed: options[:seed], priors: priors, packs: packs)
|
|
546
|
+
rescue Lictor::ProfilerError => e
|
|
547
|
+
abort("lictor: #{e.message}")
|
|
548
|
+
rescue Augure::MalformedFact, Augure::UnknownPredicate => e
|
|
549
|
+
abort("lictor: #{e.message}")
|
|
550
|
+
end
|
|
551
|
+
trail.record(run: SecureRandom.uuid, type: "plan", mode: mode, facts_file: input,
|
|
552
|
+
profiled: Lictor::Profiler.elf?(input) ? input : nil,
|
|
553
|
+
selected: plan[:selected], applicable: plan[:applicable],
|
|
554
|
+
explain: plan[:explain],
|
|
555
|
+
priors_adapted: Lictor::Priors.adapted?(trail))
|
|
556
|
+
|
|
557
|
+
if options[:json]
|
|
558
|
+
puts JSON.generate(plan)
|
|
559
|
+
else
|
|
560
|
+
puts "mode: #{mode}"
|
|
561
|
+
puts "applicable: #{plan[:applicable].join(', ')}"
|
|
562
|
+
puts "ranking: #{plan[:ranking].join(' > ')}"
|
|
563
|
+
puts "selected: #{plan[:selected]}"
|
|
564
|
+
if plan[:recipe]
|
|
565
|
+
puts "recipe: #{plan[:recipe][:console]}"
|
|
566
|
+
end
|
|
567
|
+
plan[:explain][:provenance].each do |tech, provs|
|
|
568
|
+
provs.each do |p|
|
|
569
|
+
puts " #{tech} <- #{p[:rule]} (#{p[:evidence].map { |e| e.join('=') }.join(', ')})"
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
when "outcome"
|
|
574
|
+
run_id = ARGV.shift or abort(parser.to_s)
|
|
575
|
+
if options[:success].nil? || options[:technique].nil?
|
|
576
|
+
abort("lictor: outcome needs --success or --failure, and --technique NAME")
|
|
577
|
+
end
|
|
578
|
+
trail.record(run: run_id, type: "outcome", technique: options[:technique],
|
|
579
|
+
success: options[:success])
|
|
580
|
+
puts "recorded: #{run_id} #{options[:technique]} #{options[:success] ? 'success' : 'failure'}"
|
|
581
|
+
puts "next plan run adapts the priors (the closed loop)"
|
|
582
|
+
when "runs"
|
|
583
|
+
runs = trail.runs
|
|
584
|
+
abort("lictor: empty trail") if runs.empty?
|
|
585
|
+
runs.each do |r|
|
|
586
|
+
puts "#{r[:run]} #{r[:mode]} selected=#{r[:selected]} " \
|
|
587
|
+
"outcomes=#{r[:outcomes].map { |o| "#{o[:technique]}:#{o[:success] ? 'ok' : 'ko'}" }.join(',')}"
|
|
588
|
+
end
|
|
589
|
+
when "report"
|
|
590
|
+
if options[:defender]
|
|
591
|
+
packs = Lictor::TechniquePacks.load(options[:packs] || Lictor::Config.load[:packs] || "packs")
|
|
592
|
+
content = Lictor::Report.defender(trail, packs: packs)
|
|
593
|
+
if options[:output]
|
|
594
|
+
File.write(options[:output], content)
|
|
595
|
+
puts "wrote #{options[:output]}"
|
|
596
|
+
else
|
|
597
|
+
puts content
|
|
598
|
+
end
|
|
599
|
+
elsif options[:json]
|
|
600
|
+
puts JSON.generate(runs: trail.runs)
|
|
601
|
+
elsif options[:output]
|
|
602
|
+
Lictor::Report.to_file(options[:output], trail)
|
|
603
|
+
puts "wrote #{options[:output]}"
|
|
604
|
+
else
|
|
605
|
+
puts Lictor::Report.from_trail(trail)
|
|
606
|
+
end
|
|
607
|
+
else
|
|
608
|
+
abort(parser.to_s)
|
|
609
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# The build layouts: how a decided technique becomes bytes. Data, not
|
|
2
|
+
# code - a pack can add or override a layout for its own techniques
|
|
3
|
+
# (pack `build:` sections follow the same slot vocabulary and pass the
|
|
4
|
+
# same armor).
|
|
5
|
+
#
|
|
6
|
+
# Slots:
|
|
7
|
+
# [padding, measured_offset] - the measured offset, as filler
|
|
8
|
+
# [qword, win] - the win_symbol address, 8 bytes
|
|
9
|
+
# [qword, gadget:TYPE] - the address of that gadget type
|
|
10
|
+
# [dword, ...] - the same, 4 bytes (x86)
|
|
11
|
+
# [shellcode, sh|bash] - the assembled stub
|
|
12
|
+
# [shellcode, file] - the operator-provided payload file
|
|
13
|
+
# [chain, REF, REF, ...] - a ROP chain: one arch-width word
|
|
14
|
+
# per REF, resolved in order
|
|
15
|
+
#
|
|
16
|
+
# chain references:
|
|
17
|
+
# gadget:TYPE - the address of that gadget type (facts)
|
|
18
|
+
# plt_addr:NAME - the PLT stub address (profiler facts)
|
|
19
|
+
# got_addr:NAME - the GOT entry address (profiler facts)
|
|
20
|
+
# win - the win_symbol address
|
|
21
|
+
# leak:NAME - a leaked_address(name) fact (harvest/banner)
|
|
22
|
+
# const:N - the literal N (a syscall number, an offset)
|
|
23
|
+
#
|
|
24
|
+
# resolved at build time from the facts + the measurement; anything the
|
|
25
|
+
# facts cannot supply is a loud BuildError, never a guessed byte.
|
|
26
|
+
|
|
27
|
+
ret2func:
|
|
28
|
+
arch: any
|
|
29
|
+
layout:
|
|
30
|
+
- [padding, measured_offset]
|
|
31
|
+
- [qword, win]
|
|
32
|
+
|
|
33
|
+
ret2win:
|
|
34
|
+
arch: any
|
|
35
|
+
layout:
|
|
36
|
+
- [padding, measured_offset]
|
|
37
|
+
- [qword, win]
|
|
38
|
+
|
|
39
|
+
shellcode:
|
|
40
|
+
arch: any
|
|
41
|
+
layout:
|
|
42
|
+
- [padding, measured_offset, sled]
|
|
43
|
+
- [qword, leak:buf]
|
|
44
|
+
- [shellcode, sh]
|
|
45
|
+
|
|
46
|
+
banner_leak:
|
|
47
|
+
arch: any
|
|
48
|
+
layout:
|
|
49
|
+
- [padding, measured_offset]
|
|
50
|
+
- [qword, leak:win]
|
|
51
|
+
ret2plt:
|
|
52
|
+
arch: any
|
|
53
|
+
layout:
|
|
54
|
+
- [padding, measured_offset]
|
|
55
|
+
- [chain, "gadget:pop_rdi_ret", "got_addr:puts", "plt_addr:puts"]
|
|
56
|
+
|
|
57
|
+
ret2libc:
|
|
58
|
+
arch: any
|
|
59
|
+
layout:
|
|
60
|
+
- [padding, measured_offset]
|
|
61
|
+
- [chain, "gadget:ret_align", "gadget:pop_rdi_ret", "leak:binsh", "leak:system"]
|
|
62
|
+
orw:
|
|
63
|
+
arch: any
|
|
64
|
+
layout:
|
|
65
|
+
- [padding, measured_offset, sled]
|
|
66
|
+
- [qword, leak:input]
|
|
67
|
+
- [shellcode, orw, flag.txt]
|