class OneGadget::Fetcher::Base

Define common methods for gadget fetchers.

Attributes

file[R]

The absolute path to glibc. @return [String] The filename.

Public Class Methods

new(file) click to toggle source

Instantiate a fetcher object. @param [String] file Absolute path to the target libc.

# File lib/one_gadget/fetchers/base.rb, line 16
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

Public Instance Methods

candidates(&block) click to toggle source

Fetch candidates that end with call exec*.

Provide a block to filter gadget candidates. @yieldparam [String] cand

Is this candidate valid?

@yieldreturn [Boolean]

True for valid.

@return [Array<String>]

Each +String+ returned is multi-lines of assembly code.
# File lib/one_gadget/fetchers/base.rb, line 51
def candidates(&block)
  call_regexp = "#{call_str}.*<(exec[^+]*|posix_spawn[^+]*)>$"
  cands = []
  `#{@objdump.command}|egrep '#{call_regexp}' -B 30`.split('--').each do |cand|
    lines = cand.lines.map(&:strip).reject(&:empty?)
    # split with call_regexp
    loop do
      idx = lines.index { |l| l =~ /#{call_regexp}/ }
      break if idx.nil?

      cands << lines.shift(idx + 1).join("\n")
    end
  end
  # remove all jmps
  cands = slice_prefix(cands, &method(:branch?))
  cands.select!(&block) if block_given?
  cands
end
find() click to toggle source

Do find gadgets in glibc. @return [Array<OneGadget::Gadget::Gadget>] Gadgets found.

# File lib/one_gadget/fetchers/base.rb, line 25
def find
  candidates.map do |cand|
    lines = cand.lines
    # use processor to find which can lead to a valid one-gadget call.
    gadgets = []
    (lines.size - 2).downto(0) do |i|
      processor = emulate(lines[i..-1])
      options = resolve(processor)
      next if options.nil? # impossible to be a gadget

      offset = offset_of(lines[i])
      gadgets << OneGadget::Gadget::Gadget.new(offset, **options)
    end
    gadgets
  end.flatten
end

Private Instance Methods

branch?(_str) click to toggle source

If str contains a branch instruction.

# File lib/one_gadget/fetchers/base.rb, line 222
def branch?(_str); raise NotImplementedError
end
call_str() click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 198
def call_str; raise NotImplementedError
end
check_envp(processor, arg) { |cons| ... } click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 131
def check_envp(processor, arg)
  # If str starts with [[ and is a global variable,
  # believe it is environ.
  # If it starts with [[ but not a global var, drop it.
  return global_var?(arg) if arg.start_with?('[[')

  # normal
  cons = check_execve_arg(processor, arg, true)
  return nil if cons.nil?

  yield cons
end
check_execve_arg(processor, arg, allow_null) click to toggle source

arg == NULL || [arg] == NULL

# File lib/one_gadget/fetchers/base.rb, line 116
def check_execve_arg(processor, arg, allow_null)
  if arg.start_with?(processor.sp) # arg = sp+<num>
    # in this case, the only chance is [sp+<num>] == NULL
    num = Integer(arg[processor.sp.size..-1])
    slot = processor.stack[num].to_s
    return if global_var?(slot)

    "#{slot} == NULL"
  elsif allow_null
    "[#{arg}] == NULL || #{arg} == NULL"
  else
    "[#{arg}] == NULL"
  end
end
emulate(cmds) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 201
def emulate(cmds)
  cmds.each_with_object(emulator) { |cmd, obj| break obj unless obj.process(cmd) }
end
emulator() click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 205
def emulator; raise NotImplementedError
end
global_var?(_str) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 189
def global_var?(_str); raise NotImplementedError
end
objdump_options() click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 208
def objdump_options
  []
end
offset_of(assembly) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 230
def offset_of(assembly)
  assembly.scan(/^([\da-f]+):/)[0][0].to_i(16)
end
resolve(processor) click to toggle source

Generating constraints for being a valid gadget. @param [OneGadget::Emulators::Processor] processor The processor after executing the gadgets. @return [Hash{Symbol => Array<String>, String}?]

The options to create a {OneGadget::Gadget::Gadget} object.
Keys might be:
1. constraints: Array<String> List of constraints.
2. effect: String Result function call of this gadget.
If the constraints can never be satisfied, +nil+ is returned.
# File lib/one_gadget/fetchers/base.rb, line 80
def resolve(processor)
  call = processor.registers[processor.pc].to_s
  return resolve_posix_spawn(processor) if call.include?('posix_spawn')
  return resolve_execve(processor) if call.include?('execve')
  return resolve_execl(processor) if call.include?('execl')
end
resolve_execl(processor) click to toggle source

Resolve +call execl+ cases.

# File lib/one_gadget/fetchers/base.rb, line 145
def resolve_execl(processor)
  return unless str_bin_sh?(processor.argument(0).to_s)

  args = []
  arg = processor.argument(1).to_s
  if str_sh?(arg)
    arg = processor.argument(2).to_s
    args << '"sh"'
  end
  return nil if global_var?(arg) # we don't want base-related constraints

  args << arg
  cons = processor.constraints + ["#{arg} == NULL"]
  { constraints: cons, effect: %(execl("/bin/sh", #{args.join(', ')})) }
end
resolve_execve(processor) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 87
def resolve_execve(processor)
  arg0 = processor.argument(0).to_s
  arg1 = processor.argument(1).to_s
  arg2 = processor.argument(2).to_s
  res = resolve_execve_args(processor, arg0, arg1, arg2)
  return nil if res.nil?

  { constraints: res[:constraints], effect: %(execve("/bin/sh", #{arg1}, #{res[:envp]})) }
end
resolve_execve_args(processor, arg0, arg1, arg2, allow_null_argv: true) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 97
def resolve_execve_args(processor, arg0, arg1, arg2, allow_null_argv: true)
  return unless str_bin_sh?(arg0)

  # arg1 == NULL || [arg1] == NULL
  # arg2 == NULL || [arg2] == NULL || arg[2] == envp
  cons = processor.constraints
  cons << check_execve_arg(processor, arg1, allow_null_argv)
  return nil unless cons.all?

  envp = 'environ'
  return nil unless check_envp(processor, arg2) do |c|
    cons << c
    envp = arg2
  end

  { constraints: cons, envp: envp }
end
resolve_posix_spawn(processor) click to toggle source

posix_spawn (*pid, *path, *file_actions, *attrp, argv[], envp[]) Constraints are

  • pid == NULL || *pid is writable

  • file_actions == NULL || (int) (file_actions->__used) <= 0

  • attrp == NULL || attrp->flags == 0

Meet all constraints then posix_spawn eventually calls execve(path, argv, envp)

# File lib/one_gadget/fetchers/base.rb, line 167
def resolve_posix_spawn(processor)
  args = Array.new(6) { |i| processor.argument(i) }
  res = resolve_execve_args(processor, args[1].to_s, args[4].to_s, args[5].to_s, allow_null_argv: false)
  return nil if res.nil?

  cons = res[:constraints]
  arg0 = args[0]
  if arg0.to_s != '0'
    if arg0.deref_count.zero? && arg0.to_s.include?(processor.sp)
      # Assume stack is always writable, no additional constraints.
    else
      cons << "#{arg0} == NULL || writable: #{arg0}"
    end
  end
  arg2 = args[2]
  cons << "#{arg2} == NULL || (s32)#{(arg2 + 4).deref} <= 0" if arg2.to_s != '0'
  arg3 = args[3]
  cons << "#{arg3} == NULL || (u16)#{arg3.deref} == NULL" if arg3.to_s != '0'

  { constraints: cons, effect: %(posix_spawn(#{arg0}, "/bin/sh", #{arg2}, #{arg3}, #{args[4]}, #{res[:envp]})) }
end
slice_prefix(cands, &block) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 212
def slice_prefix(cands, &block)
  cands.map do |cand|
    lines = cand.lines
    to_rm = lines[0...-1].rindex(&block)
    lines = lines[to_rm + 1..-1] unless to_rm.nil?
    lines.join
  end
end
str_bin_sh?(_str) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 192
def str_bin_sh?(_str); raise NotImplementedError
end
str_offset(str) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 225
def str_offset(str)
  File.binread(file).index("#{str}\x00") ||
    raise(Error::ArgumentError, "File #{file.inspect} doesn't contain string #{str.inspect}, not glibc?")
end
str_sh?(_str) click to toggle source
# File lib/one_gadget/fetchers/base.rb, line 195
def str_sh?(_str); raise NotImplementedError
end