From c97a84ab3cf2dd8b2b9dfe3434cb9cc5aed908e6 Mon Sep 17 00:00:00 2001 From: david942j Date: Sat, 4 May 2019 19:39:14 +0800 Subject: [PATCH] Introduced CLI module (#81) * Introduced CLI module --- README.md | 4 +- bin/one_gadget | 112 +--------------------- lib/one_gadget/cli.rb | 216 ++++++++++++++++++++++++++++++++++++++++++ spec/bin_spec.rb | 76 +-------------- spec/cli_spec.rb | 151 +++++++++++++++++++++++++++++ spec/spec_helper.rb | 4 + 6 files changed, 377 insertions(+), 186 deletions(-) create mode 100644 lib/one_gadget/cli.rb create mode 100644 spec/cli_spec.rb diff --git a/README.md b/README.md index 7e71def..6f751cd 100644 --- a/README.md +++ b/README.md @@ -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 [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'. diff --git a/bin/one_gadget b/bin/one_gadget index d704270..cee7388 100755 --- a/bin/one_gadget +++ b/bin/one_gadget @@ -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) diff --git a/lib/one_gadget/cli.rb b/lib/one_gadget/cli.rb new file mode 100644 index 0000000..392eaed --- /dev/null +++ b/lib/one_gadget/cli.rb @@ -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 [options]' + # Default options. + DEFAULT_OPTIONS = { raw: false, level: 0 }.freeze + + module_function + + # Main method of CLI. + # @param [Array] 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] 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: + # # . + # #=> 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] 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] 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] 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 diff --git a/spec/bin_spec.rb b/spec/bin_spec.rb index d77dc33..9ba6fa1 100644 --- a/spec/bin_spec.rb +++ b/spec/bin_spec.rb @@ -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 [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 diff --git a/spec/cli_spec.rb b/spec/cli_spec.rb new file mode 100644 index 0000000..e1581a4 --- /dev/null +++ b/spec/cli_spec.rb @@ -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: + . + 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ba7fb8f..b722061 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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