Module: OneGadget::Helper

Defined in:
lib/one_gadget/helper.rb

Overview

Define some helpful methods here.

Constant Summary collapse

BUILD_ID_FORMAT =

Format of build-id, 40 hex numbers.

/[0-9a-f]{40}/
COLOR_CODE =

Color codes for pretty print

{
  esc_m: "\e[0m",
  normal_s: "\e[38;5;203m", # red
  integer: "\e[38;5;189m", # light purple
  reg: "\e[38;5;82m", # light green
  warn: "\e[38;5;230m", # light yellow
  error: "\e[38;5;196m" # heavy red
}.freeze
MACHINES =

Fetch the ELF architecture of file. What this tool calls each architecture it can search. Keyed by the header's own value, which is fixed by the ELF ABI, rather than by the name a reader library prints for it -- that is the library's wording and does change, as elftools 2.0.0 renamed "AArch64" to "ARM 64-bit architecture".

{
  ELFTools::Constants::EM::EM_386 => :i386,
  ELFTools::Constants::EM::EM_ARM => :arm,
  ELFTools::Constants::EM::EM_X86_64 => :amd64,
  ELFTools::Constants::EM::EM_AARCH64 => :aarch64
}.freeze

Class Method Summary collapse

Class Method Details

.abspath(path) ⇒ String

Get absolute path from relative path. Support symlink.

Examples:

Helper.abspath('/lib/x86_64-linux-gnu/libc.so.6')
#=> '/lib/x86_64-linux-gnu/libc-2.23.so'

Parameters:

  • path (String)

    Relative path.

Returns:

  • (String)

    Absolute path, with symlink resolved.



45
46
47
# File 'lib/one_gadget/helper.rb', line 45

def abspath(path)
  Pathname.new(File.expand_path(path)).realpath.to_s
end

.arch_specific_objdump(arch) ⇒ String?

Returns the architecture-specific objdump binary name for arch.

Parameters:

  • arch (Symbol)

    The target architecture, e.g. :aarch64.

Returns:

  • (String?)

    The cross-objdump binary name (e.g. aarch64-linux-gnu-objdump), or nil if arch has no dedicated binary.



329
330
331
332
333
334
335
336
# File 'lib/one_gadget/helper.rb', line 329

def arch_specific_objdump(arch)
  {
    aarch64: 'aarch64-linux-gnu-objdump',
    amd64: 'x86_64-linux-gnu-objdump',
    arm: 'arm-linux-gnueabihf-objdump',
    i386: 'i686-linux-gnu-objdump'
  }[arch]
end

.architecture(file) ⇒ Symbol

Returns One of :amd64, :i386, :arm or :aarch64, :unknown for a valid ELF this tool cannot search, or :invalid when file does not exist or is not a valid ELF.

Examples:

Helper.architecture('/bin/cat')
#=> :amd64

Parameters:

  • file (String)

    The target ELF filename.

Returns:

  • (Symbol)

    One of :amd64, :i386, :arm or :aarch64, :unknown for a valid ELF this tool cannot search, or :invalid when file does not exist or is not a valid ELF.



213
214
215
216
217
218
219
220
221
222
# File 'lib/one_gadget/helper.rb', line 213

def architecture(file)
  return :invalid unless File.exist?(file)

  f = File.open(file) # rubocop:disable Style/FileOpen
  MACHINES[ELFTools::ELFFile.new(f).header.e_machine.to_i] || :unknown
rescue ELFTools::ELFError # not a valid ELF
  :invalid
ensure
  f&.close
end

.build_id_of(path) ⇒ String

Get the Build ID of target ELF.

Examples:

Helper.build_id_of('/lib/x86_64-linux-gnu/libc-2.23.so')
#=> '60131540dadc6796cab33388349e6e4e68692053'

Parameters:

  • path (String)

    Absolute file path.

Returns:

  • (String)

    Target build id.



86
87
88
# File 'lib/one_gadget/helper.rb', line 86

def build_id_of(path)
  File.open(path) { |f| ELFTools::ELFFile.new(f).build_id }
end

.color_enabled?Boolean

Is colorize output enabled?

Returns:

  • (Boolean)

    True or false.



99
100
101
102
103
104
# File 'lib/one_gadget/helper.rb', line 99

def color_enabled?
  # if not set, use tty to check
  return $stdout.tty? unless instance_variable_defined?(:@disable_color)

  !@disable_color
end

.color_off!void

This method returns an undefined value.

Disable colorize.



92
93
94
# File 'lib/one_gadget/helper.rb', line 92

def color_off!
  @disable_color = true
end

.colored_hex(val) ⇒ String

Returns the hexified and colorized integer.

Parameters:

  • val (Integer)

    The number to be formatted.

Returns:

  • (String)

    The value in hex format, wrapped with integer color codes.



131
132
133
# File 'lib/one_gadget/helper.rb', line 131

def colored_hex(val)
  colorize(hex(val), sev: :integer)
end

.colorize(str, sev: :normal_s) ⇒ String

Wrap string with color codes for pretty inspect.

Parameters:

  • str (String)

    Contents to colorize.

  • sev (Symbol) (defaults to: :normal_s)

    Specify which kind of color to use, valid symbols are defined in COLOR_CODE.

Returns:

  • (String)

    String wrapped with color codes.



120
121
122
123
124
125
126
# File 'lib/one_gadget/helper.rb', line 120

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) ⇒ Array<String>

Fetch lines start with '#'.

Parameters:

  • file (String)

    Filename.

Returns:

  • (Array<String>)

    Lines of comments.



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

def comments_of_file(file)
  File.readlines(file).map { |s| s[2..].rstrip if s.start_with?('# ') }.compact
end

.download_build(file) ⇒ Tempfile

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

Parameters:

  • file (String)

    The filename desired.

Returns:

  • (Tempfile)

    The temp file be created.



154
155
156
157
158
# File 'lib/one_gadget/helper.rb', line 154

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) ⇒ String?

Find objdump that supports architecture arch.

Prefers the generic objdump if it supports arch, otherwise falls back to the architecture-specific binary (see arch_specific_objdump).

Examples:

Helper.find_objdump(:amd64)
#=> '/usr/bin/objdump'
Helper.find_objdump(:aarch64)
#=> '/usr/bin/aarch64-linux-gnu-objdump'

Parameters:

  • arch (Symbol)

    The target architecture, e.g. :amd64.

Returns:

  • (String?)

    The path of a suitable objdump, or nil if none supports arch.



288
289
290
291
292
293
# File 'lib/one_gadget/helper.rb', line 288

def find_objdump(arch)
  [
    which('objdump'),
    which(arch_specific_objdump(arch))
  ].find { |bin| objdump_arch_supported?(bin, arch) }
end

.function_offsets(file, functions) ⇒ Hash{String => Integer}

Returns a dictionary that maps functions to their offsets.

Parameters:

  • file (String)

    Path to the target ELF file.

  • functions (Array<String>)

    The function names to look up.

Returns:

  • (Hash{String => Integer})

    Maps each matched function name to its offset within file.



352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/one_gadget/helper.rb', line 352

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) ⇒ Array<String>

Returns the names of functions from the file's global offset table.

Parameters:

  • file (String)

    Path to the target ELF file.

Returns:

  • (Array<String>)

    The names of the GLIBC dynamic symbols found in file.



341
342
343
344
345
# File 'lib/one_gadget/helper.rb', line 341

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) ⇒ String

Present number in hex format.

Examples:

Helper.hex(32) #=> '0x20'
Helper.hex(32, psign: true) #=> '+0x20'
Helper.hex(-40) #=> '-0x28'
Helper.hex(0) #=> '0x0'
Helper.hex(0, psign: true) #=> '+0x0'

Parameters:

  • val (Integer)

    The number.

  • psign (Boolean) (defaults to: false)

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

Returns:

  • (String)

    String in hex format.



237
238
239
240
241
# File 'lib/one_gadget/helper.rb', line 237

def hex(val, psign: false)
  return format("#{'+' if psign}0x%x", val) if val >= 0

  format('-0x%x', -val)
end

.integer?(str) ⇒ Boolean

Checks if a string can be converted into an integer.

Examples:

Helper.integer? '1234'
#=> true
Helper.integer? '0x1234'
#=> true
Helper.integer? '0xheapoverflow'
#=> false

Parameters:

  • str (String)

    String to be checked.

Returns:

  • (Boolean)

    If str can be converted into an integer.



255
256
257
# File 'lib/one_gadget/helper.rb', line 255

def integer?(str)
  !Integer(str, exception: false).nil?
end

.latest_tagString

Fetch the latest release version's tag name.

Returns:

  • (String)

    The tag name, in form vX.X.X.



137
138
139
140
# File 'lib/one_gadget/helper.rb', line 137

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) ⇒ String

Converts to the architecture name shown in objdump's --help command.

Examples:

Helper.objdump_arch(:i386)
#=> 'i386'
Helper.objdump_arch(:amd64)
#=> 'i386:x86-64'

Parameters:

  • arch (Symbol)

    The internal architecture symbol, e.g. :amd64.

Returns:

  • (String)

    The corresponding target name as reported by objdump.



317
318
319
320
321
322
# File 'lib/one_gadget/helper.rb', line 317

def objdump_arch(arch)
  case arch
  when :amd64 then 'i386:x86-64'
  else arch.to_s
  end
end

.objdump_arch_supported?(bin, arch) ⇒ Boolean

Checks if the given objdump supports certain architecture.

Examples:

Helper.objdump_arch_supported?('/usr/bin/objdump', :i386)
#=> true

Parameters:

  • bin (String?)

    Path to the objdump binary, or nil.

  • arch (Symbol)

    The target architecture, e.g. :i386.

Returns:

  • (Boolean)

    true if bin exists and lists arch among its supported targets.



302
303
304
305
306
307
# File 'lib/one_gadget/helper.rb', line 302

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_buildsArray<String>

Get the latest builds list from repo.

Returns:

  • (Array<String>)

    List of build ids.



162
163
164
# File 'lib/one_gadget/helper.rb', line 162

def remote_builds
  @remote_builds ||= url_request(url_of_file('builds_list')).lines.map(&:strip)
end

.url_of_file(filename) ⇒ String

Get the url which can fetch filename from remote repo.

Parameters:

  • filename (String)

    The path of the file relative to the repository root.

Returns:

  • (String)

    The raw-content url of filename at the latest release tag.



145
146
147
148
# File 'lib/one_gadget/helper.rb', line 145

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) ⇒ String

Get request.

Parameters:

  • url (String)

    The url.

Returns:

  • (String)

    The request response body. If the response is 302 Found, returns the location in header.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/one_gadget/helper.rb', line 171

def url_request(url)
  # Asked for here rather than with the file: only reaching the build database
  # over the network needs them, and together they are a quarter of what it
  # takes to load one_gadget at all.
  require 'net/http'
  require 'openssl'

  uri = URI.parse(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  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) ⇒ Boolean

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

Examples:

Helper.valid_elf_file?('/etc/passwd')
#=> false
Helper.valid_elf_file?('/lib64/ld-linux-x86-64.so.2')
#=> true

Parameters:

  • path (String)

    Path to target file.

Returns:

  • (Boolean)

    If the file is an ELF or not.



58
59
60
61
62
63
64
65
# File 'lib/one_gadget/helper.rb', line 58

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) ⇒ void

This method returns an undefined value.

Checks if build_id is a valid SHA1 hex format.

Parameters:

  • build_id (String)

    BuildID.

Raises:



24
25
26
27
28
# File 'lib/one_gadget/helper.rb', line 24

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) ⇒ void

This method returns an undefined value.

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.

Parameters:

  • path (String)

    Path to target file.

Raises:



74
75
76
77
78
# File 'lib/one_gadget/helper.rb', line 74

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) ⇒ String?

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

Examples:

Helper.which('ruby')
#=> "/usr/bin/ruby"

Parameters:

  • cmd (String)

    The executable name to look for.

Returns:

  • (String?)

    The absolute path of the executable, or nil if not found in $PATH.



266
267
268
269
270
271
272
273
274
275
# File 'lib/one_gadget/helper.rb', line 266

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