Class: OneGadget::Fetchers::Base

Inherits:
Object
  • Object
show all
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.

Direct Known Subclasses

AArch64, Arm, Mips, Riscv64, X86

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

Class Method Summary collapse

Instance Method Summary collapse

Methods included from CandidateWalk

#candidates

Constructor Details

#initialize(file) ⇒ Base

Instantiate a fetcher object.

Parameters:

  • file (String)

    Absolute path to the target libc.



51
52
53
54
55
56
# File 'lib/one_gadget/fetchers/base.rb', line 51

def initialize(file)
  @file = file
  arch = self.class.name.split('::').last.downcase.to_sym
  @objdump = Objdump.new(file, arch)
  @objdump.extra_options = objdump_options
end

Instance Attribute Details

#fileString (readonly)

The absolute path to glibc.

Returns:

  • (String)

    The filename.



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.

Parameters:

  • kind (Symbol)

    What is being cached, so two kinds may share a command.

  • command (String)

    The objdump command the value is a function of.

Yield Returns:

  • (Object)

    The value, computed only on a miss.

Returns:

  • (Object)

    The cached value.



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.

Examples:

A candidate holding two terminal calls; no window runs past the

one it reaches.
lines = ['1000: mov r0, r1', '1004: bl <execve>',
         '1008: mov r1, r2', '100c: bl <execl>']
[].tap { |a| executed_windows(lines) { |w| a << w.map { |l| l[/\A\w+/] } } }
#=> [['1008', '100c'], ['1004'], ['1000', '1004']]

Parameters:

  • lines (Array<String>)

    One candidate, as a line list.

Yield Parameters:

  • window (Array<String>)


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

#findArray<OneGadget::Gadget::Gadget>

Do find gadgets in glibc.

Returns:



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.

Parameters:

  • window (Array<String>)

Returns:

  • (Boolean)


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.

Parameters:

  • lines (Array<String>)

    The suffix, ending at the terminal call.

Returns:



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.
  options = begin
    resolve(processor)
  rescue OneGadget::Error::Error
    nil
  end
  return if options.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 options[:constraints].any? { |c| contradiction?(c) }

  options[:constraints] = options[:constraints].reject { |c| tautology?(c) }
  options[:closed_fds] = processor.closed_fds
  OneGadget::Gadget::Gadget.new(offset_of(lines.first), **options)
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.

Parameters:

  • line (String)

    One disassembled line.

Returns:

  • (Boolean)


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