Class: OneGadget::Emulators::Arm

Inherits:
ArmFamily show all
Defined in:
lib/one_gadget/emulators/arm.rb

Overview

Emulator of 32-bit ARM (both A32 and Thumb-2 encodings).

Constant Summary collapse

FLAG_SETTING =

The flag-setting spelling of an instruction we model, which differs from the base mnemonic only by a trailing s (+movs+, ands, ...). The flags it sets are not modelled, so a branch reading them aborts the path anyway; what matters here is the value it also writes. Conditional variants (+moveq+, addne, ...) are deliberately absent and stay unsupported.

/\A(mov|add|sub|and|orr|eor|bic|mvn|lsl|lsr)s\z/

Constants inherited from ArmFamily

OneGadget::Emulators::ArmFamily::COMPARES, OneGadget::Emulators::ArmFamily::COND

Constants inherited from Processor

Processor::ADDRESS_TYPES, Processor::CLOBBERED, Processor::NULLABLE_REQUIREMENTS, Processor::POINTER_REQUIREMENTS, Processor::TERMINAL_CALL_RE

Constants included from Conditional

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

Instance Attribute Summary

Attributes inherited from Processor

#bp, #pc, #refused_line, #registers, #sp

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Processor

#address_deref0?, #bp_based_stack, #closed_fds, #constraint_key, #constraints, #drop_implied_nonzero, #drop_restated_null, #get_corresponding_stack, instruction_table, line_memo, #parse, #process, #reach_terminal_call, #render_constraint, #resolve_address, #setup_frame_pointer, #sp_based_stack, #terminal_call?

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(file = nil) ⇒ Arm

Instantiate an OneGadget::Emulators::Arm object.

Parameters:

  • file (String, nil) (defaults to: nil)

    Path to the target libc. Used to read words from the literal pool when resolving PC-relative ldr loads. May be nil in unit tests that don't exercise literal loads.



19
20
21
22
23
24
25
26
27
28
# File 'lib/one_gadget/emulators/arm.rb', line 19

def initialize(file = nil)
  super(OneGadget::ABI.arm, 'sp')
  @pc = 'pc'
  # find() builds a fresh emulator per candidate; cache the file's bytes so
  # the literal pool isn't re-read from disk thousands of times.
  @data = file && self.class.file_data(file)
  @prev_addr = nil
  # A32 until proven Thumb by a +.w+/+.n+ suffix or a 2-byte instruction stride.
  @thumb = false
end

Class Method Details

.bitsObject

ARM (32-bit) is 32-bit.



300
301
302
# File 'lib/one_gadget/emulators/arm.rb', line 300

def bits
  32
end

.file_data(file) ⇒ Object

Memoized bytes of file (the target libc), shared across emulator instances.



31
32
33
# File 'lib/one_gadget/emulators/arm.rb', line 31

def self.file_data(file)
  (@file_data ||= {})[file] ||= File.binread(file)
end

Instance Method Details

#argument(idx) ⇒ Lambda, Integer

Return the argument value of calling a function.

Parameters:

  • idx (Integer)

    The 0-based index of the argument.

Returns:

  • (Lambda, Integer)

    AAPCS passes the first four arguments in +r0+-+r3+; any further arguments are on the stack at [sp], +[sp+4]+, ... (needed for 6-argument calls such as posix_spawn).



97
98
99
100
101
# File 'lib/one_gadget/emulators/arm.rb', line 97

def argument(idx)
  return registers["r#{idx}"] if idx < 4

  sp_based_stack[(idx - 4) * size_t]
end

#instructionsArray<Instruction>

Supported instruction set. Any instruction not listed here aborts the current gadget candidate (mirrors the conservative aarch64 emulator).

Returns:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/one_gadget/emulators/arm.rb', line 61

def instructions
  [
    Instruction.new('push', 1),
    Instruction.new('pop', 1),
    Instruction.new('add', 2..3),
    Instruction.new('sub', 2..3),
    Instruction.new('mov', 2),
    Instruction.new('ldr', 2..3),
    Instruction.new('ldrb', 2..3),
    Instruction.new('str', 2..3),
    Instruction.new('bl', 1),
    Instruction.new('blx', 1),
    Instruction.new('nop', 0..1),
    Instruction.new('dmb', 0..1),
    Instruction.new('dsb', 0..1),
    Instruction.new('isb', 0..1),
    Instruction.new('and', 2..3),
    Instruction.new('orr', 2..3),
    Instruction.new('eor', 2..3),
    Instruction.new('bic', 2..3),
    Instruction.new('mvn', 2),
    Instruction.new('lsl', 2..3),
    Instruction.new('lsr', 2..3),
    Instruction.new('cmp', 2..3),
    Instruction.new('cmn', 2..3),
    Instruction.new('tst', 2..3),
    Instruction.new('svc', 1)
  ]
end

#note_instruction_set(lines) ⇒ Object

Settle Thumb vs A32 from a whole candidate, before any of it is emulated. #track_mode can only learn from lines already seen, so the FIRST instruction is judged on no evidence at all -- and when that instruction reads pc, the bias it picks decides an address the constraints go on to name. Applying the same evidence up front removes that dependence on whatever happened to be processed first.

A32 is left alone: it shows neither a width suffix nor a 2-byte stride, so a genuinely A32 candidate keeps the whole-word bias. A single-instruction Thumb candidate offers no evidence either and is no better served than before.

Parameters:

  • lines (Array<String>)

    The candidate's objdump lines.



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

def note_instruction_set(lines)
  return if @thumb

  addrs = lines.filter_map { |l| l[/\A\s*([0-9a-f]+):/, 1]&.to_i(16) }
  @thumb = lines.any? { |l| l.match?(/\.[wn]\b/) } ||
           addrs.each_cons(2).any? { |a, b| b - a == 2 }
end

#process!(cmd) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/one_gadget/emulators/arm.rb', line 36

def process!(cmd)
  resolve_pending_branch(cmd)
  line = cmd.strip
  track_mode(line)
  body, @literal = decompose(line)
  mnem, rest = body.split(/\s+/, 2)
  return handle_compare(COMPARES[mnem], rest) if COMPARES.key?(mnem)
  return handle_branch(mnem, rest) != :fail if branch_mnem?(mnem)
  # push/pop take a {reg-list} whose commas would confuse the generic parser.
  return __send__(:"inst_#{mnem}", rest) != :fail if %w[push pop].include?(mnem)

  inst, args = parse(body)
  __send__(:"inst_#{inst.inst}", *args) != :fail
end