module OneGadget::Helper

Define some helpful methods here.

Constants

BUILD_ID_FORMAT

Format of build-id, 40 hex numbers.

COLOR_CODE

Color codes for pretty print

Public Instance Methods

abspath(path) click to toggle source

Get absolute path from relative path. Support symlink. @param [String] path Relative path. @return [String] Absolute path, with symlink resolved. @example

Helper.abspath('/lib/x86_64-linux-gnu/libc.so.6')
#=> '/lib/x86_64-linux-gnu/libc-2.23.so'
# File lib/one_gadget/helper.rb, line 47
def abspath(path)
  Pathname.new(File.expand_path(path)).realpath.to_s
end
arch_specific_objdump(arch) click to toggle source

Returns the binary name of objdump. @param [Symbol] arch @return [String]

# File lib/one_gadget/helper.rb, line 317
def arch_specific_objdump(arch)
  {
    aarch64: 'aarch64-linux-gnu-objdump',
    amd64: 'x86_64-linux-gnu-objdump',
    i386: 'i686-linux-gnu-objdump'
  }[arch]
end
architecture(file) click to toggle source

Fetch the ELF architecture of file. @param [String] file The target ELF filename. @return [Symbol]

Currently supports amd64, i386, arm, aarch64, and mips.

@example

Helper.architecture('/bin/cat')
#=> :amd64
# File lib/one_gadget/helper.rb, line 197
def architecture(file)
  return :invalid unless File.exist?(file)

  f = File.open(file)
  str = ELFTools::ELFFile.new(f).machine
  {
    'Advanced Micro Devices X86-64' => :amd64,
    'Intel 80386' => :i386,
    'ARM' => :arm,
    'AArch64' => :aarch64,
    'MIPS R3000' => :mips
  }[str] || :unknown
rescue ELFTools::ELFError # not a valid ELF
  :invalid
ensure
  f&.close
end
build_id_of(path) click to toggle source

Get the Build ID of target ELF. @param [String] path Absolute file path. @return [String] Target build id. @example

Helper.build_id_of('/lib/x86_64-linux-gnu/libc-2.23.so')
#=> '60131540dadc6796cab33388349e6e4e68692053'
# File lib/one_gadget/helper.rb, line 88
def build_id_of(path)
  File.open(path) { |f| ELFTools::ELFFile.new(f).build_id }
end
color_enabled?() click to toggle source

Is colorize output enabled? @return [Boolean]

True or false.
# File lib/one_gadget/helper.rb, line 101
def color_enabled?
  # if not set, use tty to check
  return $stdout.tty? unless instance_variable_defined?(:@disable_color)

  !@disable_color
end
color_off!() click to toggle source

Disable colorize. @return [void]

# File lib/one_gadget/helper.rb, line 94
def color_off!
  @disable_color = true
end
colored_hex(val) click to toggle source

Returns the hexified and colorized integer. @param [Integer] val @return [String]

# File lib/one_gadget/helper.rb, line 133
def colored_hex(val)
  colorize(hex(val), sev: :integer)
end
colorize(str, sev: :normal_s) click to toggle source

Wrap string with color codes for pretty inspect. @param [String] str Contents to colorize. @param [Symbol] sev Specify which kind of color to use, valid symbols are defined in {.COLOR_CODE}. @return [String] String wrapped with color codes.

# File lib/one_gadget/helper.rb, line 122
def colorize(str, sev: :normal_s)
  return str unless color_enabled?

  cc = COLOR_CODE
  color = cc.key?(sev) ? cc[sev] : ''
  "#{color}#{str.sub(cc[:esc_m], color)}#{cc[:esc_m]}"
end
comments_of_file(file) click to toggle source

Fetch lines start with ‘#’. @param [String] file

Filename.

@return [Array<String>]

Lines of comments.
# File lib/one_gadget/helper.rb, line 37
def comments_of_file(file)
  File.readlines(file).map { |s| s[2..-1].rstrip if s.start_with?('# ') }.compact
end
download_build(file) click to toggle source

Download the latest version of file in lib/one_gadget/builds/ from remote repo.

@param [String] file The filename desired. @return [Tempfile] The temp file be created.

# File lib/one_gadget/helper.rb, line 156
def download_build(file)
  temp = Tempfile.new(['gadgets', "#{file}.rb"])
  temp.write(url_request(url_of_file(File.join('lib', 'one_gadget', 'builds', "#{file}.rb"))))
  temp.tap(&:close)
end
find_objdump(arch) click to toggle source

Find objdump that supports architecture arch. @param [String] arch @return [String?] @example

Helper.find_objdump(:amd64)
#=> '/usr/bin/objdump'
Helper.find_objdump(:aarch64)
#=> '/usr/bin/aarch64-linux-gnu-objdump'
# File lib/one_gadget/helper.rb, line 278
def find_objdump(arch)
  [
    which('objdump'),
    which(arch_specific_objdump(arch))
  ].find { |bin| objdump_arch_supported?(bin, arch) }
end
function_offsets(file, functions) click to toggle source

Returns a dictionary that maps functions to their offsets. @param [String] file @param [Array<String>] functions @return [Hash{String => Integer}]

# File lib/one_gadget/helper.rb, line 338
def function_offsets(file, functions)
  arch = architecture(file)
  objdump_bin = find_objdump(arch)
  objdump_cmd = ::Shellwords.join([objdump_bin, '-T', file])
  functions.map! { |f| "\\b#{f}\\b" }
  ret = {}
  `#{objdump_cmd} | grep -iP '(#{functions.join('|')})'`.lines.map(&:chomp).each do |line|
    tokens = line.split
    ret[tokens.last] = tokens.first.to_i(16)
  end
  ret
end
got_functions(file) click to toggle source

Returns the names of functions from the file’s global offset table. @param [String] file @return [Array<String>]

# File lib/one_gadget/helper.rb, line 328
def got_functions(file)
  arch = architecture(file)
  objdump_bin = find_objdump(arch)
  `#{::Shellwords.join([objdump_bin, '-T', file])} | grep -iPo 'GLIBC_.+?\\s+\\K.*'`.split
end
hex(val, psign: false) click to toggle source

Present number in hex format. @param [Integer] val

The number.

@param [Boolean] psign

If needs to show the plus sign when +val >= 0+.

@return [String]

String in hex format.

@example

Helper.hex(32) #=> '0x20'
Helper.hex(32, psign: true) #=> '+0x20'
Helper.hex(-40) #=> '-0x28'
Helper.hex(0) #=> '0x0'
Helper.hex(0, psign: true) #=> '+0x0'
# File lib/one_gadget/helper.rb, line 228
def hex(val, psign: false)
  return format("#{psign ? '+' : ''}0x%x", val) if val >= 0

  format('-0x%x', -val)
end
integer?(str) click to toggle source

Checks if a string can be converted into an integer. @param [String] str

String to be checked.

@return [Boolean]

If +str+ can be converted into an integer.

@example

Helper.integer? '1234'
#=> true
Helper.integer? '0x1234'
#=> true
Helper.integer? '0xheapoverflow'
#=> false
# File lib/one_gadget/helper.rb, line 246
def integer?(str)
  true if Integer(str)
rescue ArgumentError, TypeError
  false
end
latest_tag() click to toggle source

Fetch the latest release version’s tag name. @return [String] The tag name, in form vX.X.X.

# File lib/one_gadget/helper.rb, line 139
def latest_tag
  releases_url = 'https://github.com/david942j/one_gadget/releases/latest'
  @latest_tag ||= url_request(releases_url).split('/').last
end
objdump_arch(arch) click to toggle source

Converts to the architecture name shown in objdump’s --help command. @param [Symbol] arch @return [String] @example

Helper.objdump_arch(:i386)
#=> 'i386'
Helper.objdump_arch(:amd64)
#=> 'i386:x86-64'
# File lib/one_gadget/helper.rb, line 307
def objdump_arch(arch)
  case arch
  when :amd64 then 'i386:x86-64'
  else arch.to_s
  end
end
objdump_arch_supported?(bin, arch) click to toggle source

Checks if the given objdump supports certain architecture. @param [String] bin @param [Symbol] arch @return [Boolean] @example

Helper.objdump_arch_supported?('/usr/bin/objdump', :i386)
#=> true
# File lib/one_gadget/helper.rb, line 292
def objdump_arch_supported?(bin, arch)
  return false if bin.nil?

  arch = objdump_arch(arch)
  `#{::Shellwords.join([bin, '--help'])}`.lines.any? { |c| c.split.include?(arch) }
end
remote_builds() click to toggle source

Get the latest builds list from repo. @return [Array<String>] List of build ids.

# File lib/one_gadget/helper.rb, line 164
def remote_builds
  @remote_builds ||= url_request(url_of_file('builds_list')).lines.map(&:strip)
end
url_of_file(filename) click to toggle source

Get the url which can fetch filename from remote repo. @param [String] filename @return [String] The url.

# File lib/one_gadget/helper.rb, line 147
def url_of_file(filename)
  raw_file_url = 'https://raw.githubusercontent.com/david942j/one_gadget/@tag/@file'
  raw_file_url.sub('@tag', latest_tag).sub('@file', filename)
end
url_request(url) click to toggle source

Get request. @param [String] url The url. @return [String]

The request response body.
If the response is +302 Found+, returns the location in header.
# File lib/one_gadget/helper.rb, line 173
def url_request(url)
  uri = URI.parse(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.verify_mode = ::OpenSSL::SSL::VERIFY_NONE

  request = Net::HTTP::Get.new(uri.request_uri)

  response = http.request(request)
  raise ArgumentError, "Fail to get response of #{url}" unless %w[200 302].include?(response.code)

  response.code == '302' ? response['location'] : response.body
rescue NoMethodError, SocketError, ArgumentError => e
  OneGadget::Logger.error(e.message)
  nil
end
valid_elf_file?(path) click to toggle source

Checks if the file of given path is a valid ELF file.

@param [String] path Path to target file. @return [Boolean] If the file is an ELF or not. @example

Helper.valid_elf_file?('/etc/passwd')
#=> false
Helper.valid_elf_file?('/lib64/ld-linux-x86-64.so.2')
#=> true
# File lib/one_gadget/helper.rb, line 60
def valid_elf_file?(path)
  # A light-weight way to check if is a valid ELF file
  # Checks at least one phdr should present.
  File.open(path) { |f| ELFTools::ELFFile.new(f).each_segments.first }
  true
rescue ELFTools::ELFError
  false
end
verify_build_id!(build_id) click to toggle source

Checks if build_id is a valid SHA1 hex format. @param [String] build_id

BuildID.

@raise [Error::ArgumentError]

Raises error if invalid.

@return [void]

# File lib/one_gadget/helper.rb, line 26
def verify_build_id!(build_id)
  return if build_id =~ /\A#{OneGadget::Helper::BUILD_ID_FORMAT}\Z/

  raise OneGadget::Error::ArgumentError, format('invalid BuildID format: %p', build_id)
end
verify_elf_file!(path) click to toggle source

Checks if the file of given path is a valid ELF file.

An error message will be shown if given path is not a valid ELF.

@param [String] path Path to target file. @return [void] @raise [Error::ArgumentError] Raise exception if not a valid ELF.

# File lib/one_gadget/helper.rb, line 76
def verify_elf_file!(path)
  return if valid_elf_file?(path)

  raise Error::ArgumentError, 'Not an ELF file, expected glibc as input'
end
which(cmd) click to toggle source

Cross-platform way of finding an executable in +$PATH+.

@param [String] cmd @return [String?] @example

Helper.which('ruby')
#=> "/usr/bin/ruby"
# File lib/one_gadget/helper.rb, line 259
def which(cmd)
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
  ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
    exts.each do |ext|
      exe = File.join(path, "#{cmd}#{ext}")
      return exe if File.executable?(exe) && !File.directory?(exe)
    end
  end
  nil
end