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 =

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,
  ELFTools::Constants::EM::EM_RISCV => :riscv64,
  ELFTools::Constants::EM::EM_MIPS => :mips
}.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.



342
343
344
345
346
347
348
349
350
# File 'lib/one_gadget/helper.rb', line 342

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',
    riscv64: 'riscv64-linux-gnu-objdump'
  }[arch]
end

.architecture(file) ⇒ Symbol

Fetch the ELF architecture of file.

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.



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

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.



290
291
292
293
294
295
# File 'lib/one_gadget/helper.rb', line 290

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.



366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/one_gadget/helper.rb', line 366

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.



355
356
357
358
359
# File 'lib/one_gadget/helper.rb', line 355

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.



239
240
241
242
243
# File 'lib/one_gadget/helper.rb', line 239

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.



257
258
259
# File 'lib/one_gadget/helper.rb', line 257

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.



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

def objdump_arch(arch)
  case arch
  when :amd64 then 'i386:x86-64'
  when :riscv64 then 'riscv:rv64'
  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.



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

def objdump_arch_supported?(bin, arch)
  return false if bin.nil?

  objdump_targets(bin).include?(objdump_arch(arch))
end

.objdump_targets(bin) ⇒ Array<String>

The target names an objdump binary lists as supported. Asked of the binary once: the answer is a property of that build, while the question is asked again for every command a search assembles.

Examples:

Every word of the help text, a target looked for among them.

objdump_targets('/usr/bin/objdump').include?('elf64-x86-64') #=> true

Parameters:

  • bin (String)

    Path to the objdump binary.

Returns:

  • (Array<String>)


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

def objdump_targets(bin)
  (@objdump_targets ||= {})[bin] ||= `#{::Shellwords.join([bin, '--help'])}`.split
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.



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

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