Class: OneGadget::Fetchers::Base
- Inherits:
-
Object
- Object
- OneGadget::Fetchers::Base
- Includes:
- ArgumentResolution, CandidateWalk, Disassembly, DynamicSymbols
- Defined in:
- lib/one_gadget/fetchers/base.rb
Overview
Base of the per-architecture gadget fetchers. It discovers candidate instruction sequences - a backward control-flow walk from each +exec+/+posix_spawn+ call - and turns a solved candidate into a Gadget::Gadget. A subclass supplies only the arch-specific pieces (call mnemonic, string/global recognition, branch classification), every one of which is declared here whatever module implements the engine that calls it: CandidateWalk for the control-flow walk, Disassembly for reading the file, and ArgumentResolution for describing a reached call.
To add an architecture, see docs/adding-an-architecture.md; AArch64 is
the simplest example.
Constant Summary
Constants included from DynamicSymbols
DynamicSymbols::CONTROL_TARGET, DynamicSymbols::TRAILING_ADDRESS
Constants included from Disassembly
Disassembly::TERMINAL_PREFIXES, Disassembly::TERMINAL_SPAWN, Disassembly::WINDOW_BACK, Disassembly::WINDOW_FWD
Constants included from CandidateWalk
CandidateWalk::MAX_FORKS, CandidateWalk::PATH_BUDGET
Instance Attribute Summary collapse
-
#file ⇒ String
readonly
The absolute path to glibc.
Class Method Summary collapse
-
.cached(kind, command) ⇒ Object
Cache values that are a deterministic function of an objdump command (its output and everything derived from it), so re-analysing the same file - common in the specs, harmless for the CLI which reads a file once - doesn't redo the disassembly or the whole-binary scans.
Instance Method Summary collapse
-
#executed_windows(lines) {|window| ... } ⇒ void
Every suffix of
lines, longest last, cut at the terminal call it reaches -- emulation ends there, so anything past one never executes. -
#find ⇒ Array<OneGadget::Gadget::Gadget>
Do find gadgets in glibc.
-
#initialize(file) ⇒ Base
constructor
Instantiate a fetcher object.
-
#refused_before_call?(window) ⇒ Boolean
Whether emulating
windowcan only stop short of its terminal call: it runs a line the emulator has already refused (see Emulators::Processor#refused_line), and a window that never reaches the call is not a gadget. -
#resolve_suffix(lines) ⇒ OneGadget::Gadget::Gadget?
Emulate a candidate suffix and turn it into a gadget, or
nilif it isn't one. -
#terminal_call_line?(line) ⇒ Boolean
Whether
lineis the call that ends a gadget, by the same rule the emulator stops on (Emulators::Processor#terminal_call?).
Methods included from CandidateWalk
Constructor Details
Instance Attribute Details
#file ⇒ String (readonly)
The absolute path to glibc.
34 35 36 |
# File 'lib/one_gadget/fetchers/base.rb', line 34 def file @file end |
Class Method Details
.cached(kind, command) ⇒ Object
Cache values that are a deterministic function of an objdump command (its output and everything derived from it), so re-analysing the same file - common in the specs, harmless for the CLI which reads a file once - doesn't redo the disassembly or the whole-binary scans.
44 45 46 47 |
# File 'lib/one_gadget/fetchers/base.rb', line 44 def self.cached(kind, command) @cached ||= Hash.new { |h, k| h[k] = {} } @cached[kind][command] ||= yield end |
Instance Method Details
#executed_windows(lines) {|window| ... } ⇒ void
This method returns an undefined value.
Every suffix of lines, longest last, cut at the terminal call it reaches
-- emulation ends there, so anything past one never executes.
81 82 83 84 85 86 87 |
# File 'lib/one_gadget/fetchers/base.rb', line 81 def executed_windows(lines) stop = lines.size - 1 if terminal_call_line?(lines.last) (lines.size - 2).downto(0) do |i| stop = i if terminal_call_line?(lines[i]) yield(stop ? lines[i..stop] : lines[i..]) end end |
#find ⇒ Array<OneGadget::Gadget::Gadget>
Do find gadgets in glibc.
60 61 62 63 64 65 66 67 68 |
# File 'lib/one_gadget/fetchers/base.rb', line 60 def find str_offset('/bin/sh') # ensure it's glibc-like; raises "not glibc?" if not found # Reading the disassembly first settles the command the answer is a # function of: how a file with no section headers is read is decided # there. Each gadget is handed out as a copy, since a caller may write to # one (see {OneGadget::Gadget::Gadget#base}). disassembly Base.cached(:gadgets, @objdump.command) { search }.map(&:dup) end |
#refused_before_call?(window) ⇒ Boolean
Whether emulating window can only stop short of its terminal call: it
runs a line the emulator has already refused (see
Emulators::Processor#refused_line), and a window that never
reaches the call is not a gadget. Refusal belongs to the line, so one
learnt anywhere settles every window carrying it -- and overlapping
candidates carry the same lines over and over.
110 111 112 113 114 |
# File 'lib/one_gadget/fetchers/base.rb', line 110 def refused_before_call?(window) return false if @refused.nil? (window.size - 1).times.any? { |i| @refused.key?(window[i]) } end |
#resolve_suffix(lines) ⇒ OneGadget::Gadget::Gadget?
Emulate a candidate suffix and turn it into a gadget, or nil if it isn't one.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/one_gadget/fetchers/base.rb', line 119 def resolve_suffix(lines) processor = emulate(lines) (@refused ||= {})[processor.refused_line] = true if processor.refused_line # resolve reads argument registers, which may not be evaluable on an # exotic path; such a candidate simply isn't a gadget. = begin resolve(processor) rescue OneGadget::Error::Error nil end return if .nil? # A branch that compares a value with itself yields a trivial condition: # drop the gadget if it's unsatisfiable, else strip the always-true one. return if [:constraints].any? { |c| contradiction?(c) } [:constraints] = [:constraints].reject { |c| tautology?(c) } [:closed_fds] = processor.closed_fds OneGadget::Gadget::Gadget.new(offset_of(lines.first), **) end |
#terminal_call_line?(line) ⇒ Boolean
Whether line is the call that ends a gadget, by the same rule the emulator
stops on (Emulators::Processor#terminal_call?). Not
Disassembly#terminal_call_regexp, which is looser so it can locate call sites: it
also matches the posix_spawn setup helpers, which emulation runs through.
95 96 97 98 99 100 |
# File 'lib/one_gadget/fetchers/base.rb', line 95 def terminal_call_line?(line) return false unless call_line?(line) name = line[/<([^@>]+)/, 1] !name.nil? && OneGadget::Emulators::Processor::TERMINAL_CALL_RE.match?(name) end |