Class: OneGadget::Emulators::Processor

Inherits:
Object
  • Object
show all
Includes:
Conditional, Constraints, DataProcessing, TrackedMemory
Defined in:
lib/one_gadget/emulators/processor.rb

Overview

Base of the per-architecture instruction emulators, used to symbolically execute a candidate and solve its constraints. A subclass implements the arch's supported instructions, calling convention and stack model; what it inherits here is the emulation lifecycle (parse, dispatch, and the calls a candidate may cross) plus four architecture-independent concerns kept in modules of their own: Conditional for the branch/compare machinery, Constraints for what a gadget requires of its caller, TrackedMemory for the memory a candidate reads and writes, and DataProcessing for what an instruction leaves in a register.

To add an architecture, see docs/adding-an-architecture.md.

Direct Known Subclasses

ArmFamily, Mips, Riscv64, X86

Constant Summary collapse

TERMINAL_CALL_RE =

Function names whose call ends a gadget: the real exec* entry points. Deliberately excludes the posix_spawn setup helpers (+posix_spawnattr_*+, posix_spawn_file_actions_*), which merely share the posix_spawn prefix.

Examples:

matches posix_spawn, execve, execveat, execlp; not posix_spawnattr_init


/\A(?:posix_spawnp?|exec(?:ve|l|v)[a-z]*)\z/

Constants included from Constraints

Constraints::ADDRESS_TYPES, Constraints::CLOBBERED, Constraints::NULLABLE_REQUIREMENTS, Constraints::POINTER_REQUIREMENTS

Constants included from Conditional

Conditional::COMPARE_OPS, Conditional::NEGATE, Conditional::RELATION, Conditional::ZERO

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from TrackedMemory

#bp_based_stack, #get_corresponding_stack, #resolve_address, #setup_frame_pointer, #sp_based_stack, #writes_through

Methods included from Constraints

#address_deref0?, #closed_fds, #constraint_key, #constraints, #drop_implied_nonzero, #drop_restated_null, #render_constraint

Methods included from Conditional

#branch_on_bit, #branch_on_compare, #branch_on_zero, #comparisons_on, #handle_compare, #mnemonic, #operand_str, #record_compare, #resolve_pending_branch, #satisfiable?, #value_str

Constructor Details

#initialize(registers, sp) ⇒ Processor

Instantiate a OneGadget::Emulators::Processor object.

Parameters:

  • registers (Array<String>)

    Registers that supported in the architecture.

  • sp (String)

    The stack register.



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/one_gadget/emulators/processor.rb', line 43

def initialize(registers, sp)
  @registers = RegisterFile.build(registers, OneGadget::ABI::NARROW_VIEWS.fetch(arch_name, {})) do |reg|
    to_lambda(reg)
  end
  @sp = sp
  @constraints = []
  @deferred_reads = [] # pointer args of safe calls, resolved once emulation ends
  @closed_fds = []     # where each descriptor closed before the terminal call comes from
  @flags = nil     # last compare, for a following conditional branch
  @pending = nil   # a conditional branch awaiting one-line-ahead resolution
end

Instance Attribute Details

#bpString? (readonly)

Returns Frame pointer, or nil when this arch tracks none.

Returns:

  • (String, nil)

    Frame pointer, or nil when this arch tracks none.



36
37
38
# File 'lib/one_gadget/emulators/processor.rb', line 36

def bp
  @bp
end

#pcString (readonly)

Returns Program counter.

Returns:

  • (String)

    Program counter.



35
36
37
# File 'lib/one_gadget/emulators/processor.rb', line 35

def pc
  @pc
end

#refused_lineString? (readonly)

The line this emulator could not run at all: an instruction outside #instructions. Only what #parse reads decides that -- the mnemonic and the operands, never the state the emulator holds -- so the same line stops every emulation that reaches it.

Returns:

  • (String, nil)


143
144
145
# File 'lib/one_gadget/emulators/processor.rb', line 143

def refused_line
  @refused_line
end

#registersRegisterFile (readonly)

Returns The current registers' state.

Returns:



33
34
35
# File 'lib/one_gadget/emulators/processor.rb', line 33

def registers
  @registers
end

#spString (readonly)

Returns Stack pointer.

Returns:

  • (String)

    Stack pointer.



34
35
36
# File 'lib/one_gadget/emulators/processor.rb', line 34

def sp
  @sp
end

Class Method Details

.bitsInteger

32 or 64.

Returns:

  • (Integer)

    32 or 64.

Raises:

  • (NotImplementedError)


298
299
# File 'lib/one_gadget/emulators/processor.rb', line 298

def bits; raise NotImplementedError
end

.instruction_table(Array<Instruction>, Hash{String => Instruction})

The architecture's supported instructions, and the same set indexed by mnemonic, built on first use and shared by every emulator of that architecture: the set is fixed, while an emulator is made for each of the thousands of windows a candidate yields.

Yield Returns:

  • (Array<Instruction>)

    The table, asked for only on first use.

Returns:



116
117
118
119
120
121
# File 'lib/one_gadget/emulators/processor.rb', line 116

def instruction_table
  @instruction_table ||= begin
    list = yield
    [list, list.each_with_object({}) { |i, h| h[i.inst] ||= i }]
  end
end

.line_memo(kind) ⇒ Hash

What a line always reads as, remembered per architecture and kind of reading: a candidate is emulated once for every window it yields, so the same line is read thousands of times, and nothing about how it reads depends on the state the emulator holds.

Parameters:

  • kind (Symbol)

Returns:

  • (Hash)


106
107
108
# File 'lib/one_gadget/emulators/processor.rb', line 106

def line_memo(kind)
  (@line_memo ||= Hash.new { |memo, k| memo[k] = {} })[kind]
end

Instance Method Details

#argument(_idx) ⇒ Lambda, Integer

To be inherited.

Parameters:

  • _idx (Integer)

    The idx-th argument.

Returns:

  • (Lambda, Integer)

    Return value can be a Lambda or an Integer.

Raises:

  • (NotImplementedError)


168
169
# File 'lib/one_gadget/emulators/processor.rb', line 168

def argument(_idx); raise NotImplementedError
end

#instructionsArray<Instruction>

Method need to be implemented in inheritors.

Returns:

Raises:

  • (NotImplementedError)


158
159
# File 'lib/one_gadget/emulators/processor.rb', line 158

def instructions; raise NotImplementedError
end

#parse(cmd) ⇒ (Instruction, Array<String>)

Parse one command into instruction and arguments.

Parameters:

  • cmd (String)

    One line of result of objdump.

Returns:

  • ((Instruction, Array<String>))

    The parsing result.



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/one_gadget/emulators/processor.rb', line 86

def parse(cmd)
  self.class.line_memo(:parse)[cmd] ||= begin
    list, index = self.class.instruction_table { instructions }
    mnem = cmd[/\A[0-9a-f]+:\s*(\S+)/, 1] || cmd[/\A\s*(\S+)/, 1]
    inst = index[mnem]
    # Fall back to the original scan for any mnemonic that isn't a bare word.
    inst ||= list.find { |i| i.match?(cmd) }
    raise Error::UnsupportedInstructionError, "Not implemented instruction in #{cmd}" if inst.nil?

    [inst, inst.fetch_args(cmd)]
  end
end

#process(cmd) ⇒ Boolean

Process one command, without raising any exceptions.

Parameters:

  • cmd (String)

    See #process! for more information.

Returns:

  • (Boolean)


128
129
130
131
132
133
134
135
136
# File 'lib/one_gadget/emulators/processor.rb', line 128

def process(cmd)
  process!(cmd)
# rescue OneGadget::Error::UnsupportedError => e; p e # for debugging
rescue OneGadget::Error::UnsupportedInstructionError
  @refused_line = cmd
  false
rescue OneGadget::Error::Error
  false
end

#process!(_cmd) ⇒ Boolean

Method need to be implemented in inheritors.

Process one command. Will raise exceptions when encounter unhandled instruction.

Parameters:

  • _cmd (String)

    One line from result of objdump.

Returns:

  • (Boolean)

    If successfully processed.

Raises:

  • (NotImplementedError)


153
154
# File 'lib/one_gadget/emulators/processor.rb', line 153

def process!(_cmd); raise NotImplementedError
end

#reach_terminal_call(addr) ⇒ Symbol

Record a reached terminal exec* call as the gadget's effect and stop emulating: it is the gadget's goal, and any following instruction would clobber the argument registers that Fetchers::ArgumentResolution#resolve reads to describe it.

Parameters:

  • addr (String)

    The call target.

Returns:

  • (Symbol)

    :fail, the sentinel #process! maps to "stop".



77
78
79
80
# File 'lib/one_gadget/emulators/processor.rb', line 77

def reach_terminal_call(addr)
  registers[pc] = addr
  :fail
end

#terminal_call?(addr) ⇒ Boolean

Whether addr calls a terminal exec* entry point (see #reach_terminal_call). Matches the resolved symbol name exactly so a setup helper isn't mistaken for the call it precedes.

Parameters:

  • addr (String)

    The call target, e.g. "10c7d0 <posix_spawn@@GLIBC_2.15>".

Returns:

  • (Boolean)


67
68
69
70
# File 'lib/one_gadget/emulators/processor.rb', line 67

def terminal_call?(addr)
  name = addr[/<([^@>]+)/, 1]
  !name.nil? && TERMINAL_CALL_RE.match?(name)
end