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

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'


47
48
49
# 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) ⇒ String

Returns the binary name of objdump.



317
318
319
320
321
322
323
# 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) ⇒ Symbol

Fetch the ELF architecture of file.

Examples:

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


197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# 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) ⇒ String

Get the Build ID of target ELF.

Examples:

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


88
89
90
# 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?Boolean

Is colorize output enabled?



101
102
103
104
105
106
# 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!void

This method returns an undefined value.

Disable colorize.



94
95
96
# File 'lib/one_gadget/helper.rb', line 94

def color_off!
  @disable_color = true
end

.colored_hex(val) ⇒ String

Returns the hexified and colorized integer.



133
134
135
# File 'lib/one_gadget/helper.rb', line 133

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

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

Wrap string with color codes for pretty inspect.



122
123
124
125
126
127
128
# 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) ⇒ Array<String>

Fetch lines start with ‘#’.



37
38
39
# File 'lib/one_gadget/helper.rb', line 37

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.



156
157
158
159
160
# 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) ⇒ String?

Find objdump that supports architecture arch.

Examples:

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


278
279
280
281
282
283
# 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) ⇒ Hash{String => Integer}

Returns a dictionary that maps functions to their offsets.



338
339
340
341
342
343
344
345
346
347
348
349
# 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) ⇒ Array<String>

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



328
329
330
331
332
# 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) ⇒ 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'


228
229
230
231
232
# File 'lib/one_gadget/helper.rb', line 228

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


246
247
248
249
250
# File 'lib/one_gadget/helper.rb', line 246

def integer?(str)
  true if Integer(str)
rescue ArgumentError, TypeError
  false
end

.latest_tagString

Fetch the latest release version’s tag name.



139
140
141
142
# 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) ⇒ 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'


307
308
309
310
311
312
# 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) ⇒ Boolean

Checks if the given objdump supports certain architecture.

Examples:

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


292
293
294
295
296
297
# 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_buildsArray<String>

Get the latest builds list from repo.



164
165
166
# 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) ⇒ String

Get the url which can fetch filename from remote repo.



147
148
149
150
# 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) ⇒ String

Get request.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# 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) ⇒ 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


60
61
62
63
64
65
66
67
# 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) ⇒ void

This method returns an undefined value.

Checks if build_id is a valid SHA1 hex format.

Raises:



26
27
28
29
30
# 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) ⇒ 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.

Raises:



76
77
78
79
80
# 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) ⇒ String?

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

Examples:

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


259
260
261
262
263
264
265
266
267
268
# 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