Introduced CLI module (#81)

* Introduced CLI module
This commit is contained in:
david942j
2019-05-04 19:39:14 +08:00
committed by GitHub
parent a7c917e819
commit c97a84ab3c
6 changed files with 377 additions and 186 deletions
+2 -2
View File
@@ -44,14 +44,14 @@ The article introducing how I develop this tool can be found [on my blog](https:
```bash
$ one_gadget
# Usage: one_gadget [file] [options]
# Usage: one_gadget <FILE|-b BuildID> [options]
# -b, --build-id BuildID BuildID[sha1] of libc.
# -f, --[no-]force-file Force search gadgets in file instead of build id first.
# -n, --near FUNCTIONS/FILE Order gadgets by their distance to the given functions or to the GOT functions of the given file.
# -l, --level OUTPUT_LEVEL The output level.
# OneGadget automatically selects gadgets with higher successful probability.
# Increase this level to ask OneGadget show more gadgets it found.
# Default: 0
# -n, --near FUNCTIONS/FILE Order gadgets by their distance to the given functions or to the GOT functions of the given file.
# -r, --[no-]raw Output gadgets offset only, split with one space.
# -s, --script exploit-script Run exploit script with all possible gadgets.
# The script will be run as 'exploit-script $offset'.
+2 -110
View File
@@ -1,114 +1,6 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
require 'one_gadget/cli'
require 'one_gadget'
options = { raw: false }
usage = 'Usage: one_gadget [file] [options]'
parser = OptionParser.new do |opts|
opts.banner = usage
opts.on('-b', '--build-id BuildID', 'BuildID[sha1] of libc.') do |b|
options[:build_id] = b
end
opts.on('-f', '--[no-]force-file', 'Force search gadgets in file instead of build id first.') do |b|
options[:force_file] = b
end
opts.on('-n', '--near FUNCTIONS/FILE', 'Order gadgets by their distance to the given functions'\
' or to the GOT functions of the given file.') do |n|
options[:near] = n
end
opts.on('-l', '--level OUTPUT_LEVEL', Integer, 'The output level.',
'OneGadget automatically selects gadgets with higher successful probability.',
'Increase this level to ask OneGadget show more gadgets it found.',
'Default: 0') do |l|
options[:level] = l
end
opts.on('-r', '--[no-]raw', 'Output gadgets offset only, split with one space.') do |v|
options[:raw] = v
end
opts.on('-s', '--script exploit-script', 'Run exploit script with all possible gadgets.',
'The script will be run as \'exploit-script $offset\'.') do |script|
options[:script] = script
end
opts.on('--info BuildID', 'Show version information given BuildID.') do |b|
options[:info] = b
end
opts.on('--version', 'Current gem version.') do |v|
options[:version] = v
end
end
parser.parse!
if options[:version]
puts "OneGadget Version #{OneGadget::VERSION}"
exit(0)
end
def execute(script, offset)
pid = fork do
exec([script, offset.to_s].join(' '))
end
Process.wait pid
end
if options[:info]
result = OneGadget::Gadget.builds_info(options[:info])
exit(1) if result.nil? # error happend
OneGadget::Logger.info("Information of #{options[:info]}:\n#{result.join("\n")}\n")
exit(0)
end
level = options[:level] || 0
if options[:build_id]
gadgets = OneGadget.gadgets(build_id: options[:build_id], details: true, level: level)
elsif ARGV[0]
gadgets = OneGadget.gadgets(file: ARGV[0], details: true, force_file: options[:force_file], level: level)
else
puts parser.help
exit(1)
end
if options[:script]
gadgets.map(&:offset).each do |offset|
OneGadget::Logger.info("Trying #{OneGadget::Helper.colored_hex(offset)}...\n")
execute(options[:script], offset)
end
exit(0)
end
if !options[:build_id] && ARGV[0] && options[:near]
functions = if File.file?(options[:near]) && OneGadget::Helper.valid_elf_file?(options[:near])
OneGadget::Helper.got_functions(options[:near])
else
options[:near].split(',').map(&:strip)
end
function_offsets = OneGadget::Helper.function_offsets(ARGV[0], functions)
function_offsets.each do |function, offset|
colored_offset = OneGadget::Helper.colored_hex(offset)
OneGadget::Logger.warn("Gadgets near #{OneGadget::Helper.colorize(function)}(#{colored_offset}):\n")
gadgets.sort_by! { |gadget| (gadget.offset - offset).abs }
if options[:raw]
puts gadgets.map(&:offset).join(' ')
else
puts gadgets.map(&:inspect).join("\n")
end
puts "\n"
end
exit(0)
end
if options[:raw]
puts gadgets.map(&:offset).join(' ')
else
puts gadgets.map(&:inspect).join("\n")
end
exit OneGadget::CLI.work(ARGV.dup)
+216
View File
@@ -0,0 +1,216 @@
# frozen_string_literal: true
require 'optparse'
require 'one_gadget/logger'
require 'one_gadget/one_gadget'
require 'one_gadget/version'
module OneGadget
# Methods for command line interface.
module CLI
# Help message.
USAGE = 'Usage: one_gadget <FILE|-b BuildID> [options]'
# Default options.
DEFAULT_OPTIONS = { raw: false, level: 0 }.freeze
module_function
# Main method of CLI.
# @param [Array<String>] argv
# Command line arguments.
# @return [Boolean]
# Whether the command execute successfully.
# @example
# CLI.work(%w[--help])
# # usage message
# #=> true
# CLI.work(%w[--version])
# # version message
# #=> true
# @example
# CLI.work([])
# # usage message
# #=> false
# @example
# CLI.work(%w[-b b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0 -r])
# # 324293 324386 1090444
# #=> true
def work(argv)
@options = DEFAULT_OPTIONS.dup
parser.parse!(argv)
return show("OneGadget Version #{OneGadget::VERSION}") if @options[:version]
return info_build_id(@options[:info]) if @options[:info]
libc_file = argv.pop
build_id = @options[:build_id]
level = @options[:level]
return error("Either FILE or BuildID can be passed\n") if libc_file && @options[:build_id]
return show(parser.help) && false unless build_id || libc_file
gadgets = if build_id
OneGadget.gadgets(build_id: build_id, details: true, level: level)
else # libc_file
OneGadget.gadgets(file: libc_file, details: true, force_file: @options[:force_file], level: level)
end
handle_gadgets(gadgets, libc_file)
end
# Gadgets fetched, decides how to display them.
# @param [Array<OneGadget::Gadget::Gadget>] gadgets
# @param [String] libc_file
# @return [Boolean]
def handle_gadgets(gadgets, libc_file)
return handle_script(gadgets, @options[:script]) if @options[:script]
return handle_near(libc_file, gadgets, @options[:near]) if libc_file && @options[:near]
display_gadgets(gadgets, @options[:raw])
true
end
# Display libc information given BuildID.
# @param [String] id
# @return [Boolean]
# @example
# CLI.info_build_id('b417c')
# # [OneGadget] Information of b417c:
# # spec/data/libc-2.27-b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0.so
# #
# # Advanced Micro Devices X86-64
# #
# # GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
# # Copyright (C) 2018 Free Software Foundation, Inc.
# # This is free software; see the source for copying conditions.
# # There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
# # PARTICULAR PURPOSE.
# # Compiled by GNU CC version 7.3.0.
# # libc ABIs: UNIQUE IFUNC
# # For bug reporting instructions, please see:
# # <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
# #=> true
def info_build_id(id)
result = OneGadget::Gadget.builds_info(id)
return false if result.nil? # invalid form or BuildID not found
OneGadget::Logger.info("Information of #{id}:\n#{result.join("\n")}\n")
true
end
# The option parser.
# @return [OptionParser]
def parser
@parser ||= OptionParser.new do |opts|
opts.banner = USAGE
opts.on('-b', '--build-id BuildID', 'BuildID[sha1] of libc.') do |b|
@options[:build_id] = b
end
opts.on('-f', '--[no-]force-file', 'Force search gadgets in file instead of build id first.') do |b|
@options[:force_file] = b
end
opts.on('-l', '--level OUTPUT_LEVEL', Integer, 'The output level.',
'OneGadget automatically selects gadgets with higher successful probability.',
'Increase this level to ask OneGadget show more gadgets it found.',
'Default: 0') do |l|
@options[:level] = l
end
opts.on('-n', '--near FUNCTIONS/FILE', 'Order gadgets by their distance to the given functions'\
' or to the GOT functions of the given file.') do |n|
@options[:near] = n
end
opts.on('-r', '--[no-]raw', 'Output gadgets offset only, split with one space.') do |v|
@options[:raw] = v
end
opts.on('-s', '--script exploit-script', 'Run exploit script with all possible gadgets.',
'The script will be run as \'exploit-script $offset\'.') do |script|
@options[:script] = script
end
opts.on('--info BuildID', 'Show version information given BuildID.') do |b|
@options[:info] = b
end
opts.on('--version', 'Current gem version.') do |v|
@options[:version] = v
end
end
end
# Writes +msg+ to stdout and returns +true+.
# @param [String] msg
# @return [true]
def show(msg)
puts msg
true
end
# Forks and executes the command.
# @param [String] cmd
# @return [void]
def execute(cmd)
Process.wait(spawn(cmd))
end
# Handle the --script feature.
# @param [Array<OneGadget::Gadget::Gadget>] gadgets
# @param [String] script
# @return [true]
def handle_script(gadgets, script)
gadgets.map(&:offset).each do |offset|
OneGadget::Logger.info("Trying #{OneGadget::Helper.colored_hex(offset)}...\n")
execute("#{script} #{offset}")
end
true
end
# Write gadgets to stdout.
# @param [Array<OneGadget::Gadget::Gadget>] gadgets
# @param [Boolean] raw
# In raw mode, only the offset of gadgets are printed.
# @return [void]
def display_gadgets(gadgets, raw)
if raw
puts gadgets.map(&:offset).join(' ')
else
puts gadgets.map(&:inspect).join("\n")
end
end
# Log error.
# @param [String] msg
# @return [false]
def error(msg)
OneGadget::Logger.error(msg)
false
end
# Implements the --near feature.
# @param [String] libc_file
# @param [Array<OneGadget::Gadget::Gadget>] gadgets
# @param [String] near
# This can be name of functions or an ELF file.
# - Use one comma without spaces to specify a list of functions: +printf,scanf,free+.
# - Path to an ELF file and take its GOT functions to process: +/bin/ls+
def handle_near(libc_file, gadgets, near)
# TODO: show proper message for invalid +near+
functions = if File.file?(near) && OneGadget::Helper.valid_elf_file?(near)
OneGadget::Helper.got_functions(near)
else
near.split(',').map(&:strip)
end
function_offsets = OneGadget::Helper.function_offsets(libc_file, functions)
function_offsets.each do |function, offset|
colored_offset = OneGadget::Helper.colored_hex(offset)
OneGadget::Logger.warn("Gadgets near #{OneGadget::Helper.colorize(function)}(#{colored_offset}):\n")
display_gadgets(gadgets.sort_by { |gadget| (gadget.offset - offset).abs }, @options[:raw])
puts "\n"
end
true
end
end
end
+2 -74
View File
@@ -8,14 +8,14 @@ describe 'Binary' do
it 'help' do
expect(`env ruby -I#{@lib} #{@bin}`).to eq <<-EOS
Usage: one_gadget [file] [options]
Usage: one_gadget <FILE|-b BuildID> [options]
-b, --build-id BuildID BuildID[sha1] of libc.
-f, --[no-]force-file Force search gadgets in file instead of build id first.
-n, --near FUNCTIONS/FILE Order gadgets by their distance to the given functions or to the GOT functions of the given file.
-l, --level OUTPUT_LEVEL The output level.
OneGadget automatically selects gadgets with higher successful probability.
Increase this level to ask OneGadget show more gadgets it found.
Default: 0
-n, --near FUNCTIONS/FILE Order gadgets by their distance to the given functions or to the GOT functions of the given file.
-r, --[no-]raw Output gadgets offset only, split with one space.
-s, --script exploit-script Run exploit script with all possible gadgets.
The script will be run as 'exploit-script $offset'.
@@ -23,76 +23,4 @@ Usage: one_gadget [file] [options]
--version Current gem version.
EOS
end
context 'near' do
before do
skip_unless_objdump
end
it 'functions' do
file = data_path('libc-2.24-8cba3297f538691eb1875be62986993c004f3f4d.so')
expect(`env ruby -I#{@lib} #{@bin} -n system -l 1 #{file}`).to eq <<-EOS
[OneGadget] Gadgets near system(0x3f4d0):
0x3f3aa execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0x3f356 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0xb8a38 execve("/bin/sh", r13, r12)
constraints:
[r13] == NULL || r13 == NULL
[r12] == NULL || r12 == NULL
0xd67e5 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
0xd67f1 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL
EOS
end
it 'functions' do
file = data_path('libc-2.24-8cba3297f538691eb1875be62986993c004f3f4d.so')
expect(`env ruby -I#{@lib} #{@bin} -n wscanf,pwrite -l 1 -r #{file}`).to eq <<-EOS
[OneGadget] Gadgets near pwrite(0xd9b70):
878577 878565 756280 258986 258902
[OneGadget] Gadgets near wscanf(0x6afe0):
258986 258902 756280 878565 878577
EOS
end
it 'file' do
bin_file = data_path('test_near_file.elf')
lib_file = data_path('libc-2.24-8cba3297f538691eb1875be62986993c004f3f4d.so')
expect(`env ruby -I#{@lib} #{@bin} -n #{bin_file} -l 1 -r #{lib_file}`).to eq <<-EOS
[OneGadget] Gadgets near exit(0x359d0):
258902 258986 756280 878565 878577
[OneGadget] Gadgets near puts(0x68fe0):
258986 258902 756280 878565 878577
[OneGadget] Gadgets near printf(0x4f1e0):
258986 258902 756280 878565 878577
[OneGadget] Gadgets near strlen(0x80420):
756280 258986 258902 878565 878577
[OneGadget] Gadgets near __cxa_finalize(0x35c70):
258902 258986 756280 878565 878577
[OneGadget] Gadgets near __libc_start_main(0x201a0):
258902 258986 756280 878565 878577
EOS
end
end
end
+151
View File
@@ -0,0 +1,151 @@
# frozen_string_literal: true
require 'one_gadget/cli'
require 'one_gadget/version'
describe OneGadget::CLI do
let(:b_param) { %w[-b b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0] }
let(:libc_file) { data_path('libc-2.27-b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0.so') }
it 'version' do
expect { described_class.work(['--version']) }.to output("OneGadget Version #{OneGadget::VERSION}\n").to_stdout
end
it 'error' do
expect { hook_logger { described_class.work(b_param + ['file']) } }.to output(<<-EOS).to_stdout
[OneGadget] Either FILE or BuildID can be passed
EOS
end
it 'info' do
expect { hook_logger { described_class.work(%w[--info b417c]) } }.to output(<<-EOS).to_stdout
[OneGadget] Information of b417c:
spec/data/libc-2.27-b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0.so
Advanced Micro Devices X86-64
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.3.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
EOS
end
it 'build id' do
expect { described_class.work(b_param) }.to output(<<-EOS).to_stdout
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
EOS
end
it 'build id with raw' do
expect { described_class.work(b_param + %w[--raw --level 1]) }.to output(<<-EOS).to_stdout
324293 324386 939679 940120 940127 940131 1090444 1090456
EOS
end
it 'file with raw' do
skip_unless_objdump
expect { described_class.work(%w[--force --raw --level 1] + [libc_file]) }.to output(<<-EOS).to_stdout
324293 324386 939679 940120 940127 940131 1090444 1090456
EOS
end
it 'script' do
skip_on_windows
expect { hook_logger { described_class.work(b_param + %w[-s true]) } }.to output(<<-EOS).to_stdout
[OneGadget] Trying 0x4f2c5...
[OneGadget] Trying 0x4f322...
[OneGadget] Trying 0x10a38c...
EOS
end
context 'near' do
before do
skip_unless_objdump
end
it 'function' do
file = data_path('libc-2.24-8cba3297f538691eb1875be62986993c004f3f4d.so')
expect { hook_logger { described_class.work(%w[-n system -l 1] + [file]) } }.to output(<<-EOS).to_stdout
[OneGadget] Gadgets near system(0x3f4d0):
0x3f3aa execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0x3f356 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0xb8a38 execve("/bin/sh", r13, r12)
constraints:
[r13] == NULL || r13 == NULL
[r12] == NULL || r12 == NULL
0xd67e5 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
0xd67f1 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL
EOS
end
it 'functions' do
file = data_path('libc-2.24-8cba3297f538691eb1875be62986993c004f3f4d.so')
expect { hook_logger { described_class.work(%w[-n wscanf,pwrite -l 1 -r] + [file]) } }.to output(<<-EOS).to_stdout
[OneGadget] Gadgets near pwrite(0xd9b70):
878577 878565 756280 258986 258902
[OneGadget] Gadgets near wscanf(0x6afe0):
258986 258902 756280 878565 878577
EOS
end
it 'file' do
bin_file = data_path('test_near_file.elf')
lib_file = data_path('libc-2.24-8cba3297f538691eb1875be62986993c004f3f4d.so')
argv = ['-n', bin_file, '-l1', '-r', lib_file]
expect { hook_logger { described_class.work(argv) } }.to output(<<-EOS).to_stdout
[OneGadget] Gadgets near exit(0x359d0):
258902 258986 756280 878565 878577
[OneGadget] Gadgets near puts(0x68fe0):
258986 258902 756280 878565 878577
[OneGadget] Gadgets near printf(0x4f1e0):
258986 258902 756280 878565 878577
[OneGadget] Gadgets near strlen(0x80420):
756280 258986 258902 878565 878577
[OneGadget] Gadgets near __cxa_finalize(0x35c70):
258902 258986 756280 878565 878577
[OneGadget] Gadgets near __libc_start_main(0x201a0):
258902 258986 756280 878565 878577
EOS
end
end
end
+4
View File
@@ -21,6 +21,10 @@ module Helpers
yield
end
def skip_on_windows
skip 'Cannot run on Windows' if /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
end
def skip_unless_objdump
skip 'binutils not installed' if OneGadget::Helper.which('objdump').nil?
end