Class: OneGadget::Fetchers::Base
- Inherits:
-
Object
- Object
- OneGadget::Fetchers::Base
- 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).
To add an architecture, see docs/adding-an-architecture.md; AArch64 is
the simplest example.
Constant Summary collapse
- MAX_FORKS =
Give up on a control-flow path once it has crossed this many conditional branches.
4- PATH_BUDGET =
Hard cap on a single path's length (loop/runaway guard).
80- WINDOW_BACK =
How much to disassemble around each terminal call when an architecture can locate the calls cheaply (see #terminal_call_sites / #windowed_disasm).
The walk runs backwards from the call, but what it reaches does not: a branch predecessor can sit after the call, so the window has to hold the loop or later block that jumps back into the region.
Both directions were measured by windowing every architecture against its own full disassembly across the spec corpus. Gadgets start being lost below 0x400 either way, and a window too small to hold a predecessor invents them as well: the line before the first of a window is the last of another, and nothing about it follows. These leave 16x that, and still disassemble about a fifth of a libc -- terminal calls cluster into a handful of merged windows, so a wider one costs little.
A gadget whose code and branch-predecessors exceed the window is missed, which is why windowing is opt-in per arch (fast disassembly arches keep the full, exhaustive scan) and falls back to full disassembly if the call scan comes up empty.
0x4000- WINDOW_FWD =
As far past the call, for a predecessor that branches back into the region.
0x4000
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
-
#candidates {|cand| ... } ⇒ Array<String>
Fetch candidates that end with call exec*.
-
#executed_windows(lines) {|window| ... } ⇒ void
Every suffix of
lines-- a start line and everything after it, longest last -- cut down to the part that runs: emulation ends at the terminal call, 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) ⇒ Object
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?).
Constructor Details
Instance Attribute Details
#file ⇒ String (readonly)
The absolute path to glibc.
22 23 24 |
# File 'lib/one_gadget/fetchers/base.rb', line 22 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.
63 64 65 66 |
# File 'lib/one_gadget/fetchers/base.rb', line 63 def self.cached(kind, command) @cached ||= Hash.new { |h, k| h[k] = {} } @cached[kind][command] ||= yield end |
Instance Method Details
#candidates {|cand| ... } ⇒ Array<String>
Fetch candidates that end with call exec*.
Provide a block to filter gadget candidates.
178 179 180 |
# File 'lib/one_gadget/fetchers/base.rb', line 178 def candidates(&) branch_aware_candidates(&) end |
#executed_windows(lines) {|window| ... } ⇒ void
This method returns an undefined value.
Every suffix of lines -- a start line and everything after it, longest
last -- cut down to the part that runs: emulation ends at the terminal
call, so anything past one never executes. A suffix reaches beyond a
terminal call when the function holds several and the candidate was walked
back from a later one. Bounding each window here lets everything
downstream -- #emulate and its overrides, the dedup key in #find --
read a window as executed code.
Each line is classified once per candidate rather than once per suffix containing it: walking the start backwards, the first terminal call at or after it only moves when the start is itself one.
113 114 115 116 117 118 119 |
# File 'lib/one_gadget/fetchers/base.rb', line 113 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.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/one_gadget/fetchers/base.rb', line 79 def find str_offset('/bin/sh') # ensure it's glibc-like; raises "not glibc?" if not found gadgets = [] # Overlapping candidate paths share tails, so the same suffix (a start line # and everything after it) recurs across candidates; emulate each once. seen = {} candidates.each do |cand| executed_windows(cand.lines) do |suffix| next if seen.key?(key = suffix.join) seen[key] = true next if refused_before_call?(suffix) gadget = resolve_suffix(suffix) gadgets << gadget unless gadget.nil? end end gadgets 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.
142 143 144 145 146 |
# File 'lib/one_gadget/fetchers/base.rb', line 142 def refused_before_call?(window) return false if @refused.nil? (window.size - 1).times.any? { |i| @refused.key?(window[i]) } end |
#resolve_suffix(lines) ⇒ Object
Emulate a candidate suffix and turn it into a gadget, or nil if it isn't one.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/one_gadget/fetchers/base.rb', line 149 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
#terminal_call_regexp, which is looser so it can locate call sites: it
also matches the posix_spawn setup helpers, which emulation runs through.
The mnemonic must be a call, so a branch whose target symbol merely looks
similar (+<execlp@@GLIBC_2.4+0x136>+) doesn't end the window.
127 128 129 130 131 132 |
# File 'lib/one_gadget/fetchers/base.rb', line 127 def terminal_call_line?(line) return false unless line[/\A\s*[0-9a-f]+:\s*(\S+)/, 1]&.match?(/\A#{call_str}x?(?:\.[wn])?\z/) name = line[/<([^@>]+)/, 1] !name.nil? && OneGadget::Emulators::Processor::TERMINAL_CALL_RE.match?(name) end |