Skip to content
Browse files

Add msfrop, a tool for collecting and ROP gadgets, features include e…

…xport and import in CSV format, powerful regex searching

git-svn-id: file:///home/svn/framework3/trunk@12904 4d416f70-5f16-0410-b530-b9f4589650da
  • Loading branch information...
1 parent 2191055 commit a0c2321efc0b4fcea6fd917da9739f5f01d30d2d @bannedit bannedit committed Jun 10, 2011
Showing with 379 additions and 0 deletions.
  1. +7 −0 lib/rex/ropbuilder.rb
  2. +229 −0 lib/rex/ropbuilder/rop.rb
  3. +143 −0 msfrop
View
7 lib/rex/ropbuilder.rb
@@ -0,0 +1,7 @@
+module Rex
+module RopBuilder
+
+require 'rex/ropbuilder/rop'
+require 'metasm/metasm'
+end
+end
View
229 lib/rex/ropbuilder/rop.rb
@@ -0,0 +1,229 @@
+require 'metasm'
+require 'rex/compat'
+require 'rex/ui/text/table'
+require 'rex/ui/text/output/stdio'
+require 'rex/ui/text/color'
+
+module Rex
+module RopBuilder
+
+class RopBase
+ def initialize()
+ @stdio = Rex::Ui::Text::Output::Stdio.new
+ @gadgets = []
+ end
+
+ def to_csv(gadgets = [])
+ if gadgets.empty? and @gadgets.nil? or @gadgets.empty?
+ print_error("No gadgets collected to convert to CSV format.")
+ return
+ end
+
+ # allow the users to import gadget collections from multiple files
+ if @gadgets.empty? or @gadgets.nil?
+ @gadgets = gadgets
+ end
+
+ table = Rex::Ui::Text::Table.new(
+ 'Header' => "#{@file} ROP Gadgets",
+ 'Indent' => 1,
+ 'Columns' =>
+ [
+ "Address",
+ "Raw",
+ "Disassembly",
+ ])
+
+ @gadgets.each do |gadget|
+ table << [gadget[:address], gadget[:raw].unpack('H*')[0], gadget[:disasm].gsub(/\n/, ' | ')]
+ end
+
+ return table.to_csv
+ end
+
+ def import(file)
+ begin
+ data = File.new(file, 'r').read
+ rescue
+ print_error("Error reading #{file}")
+ end
+
+ data.gsub!(/\"/, '')
+ data.gsub!("Address,Raw,Disassembly\n", '')
+ @gadgets = []
+ data.each_line do |line|
+ addr, raw, disasm = line.split(',', 3)
+ disasm.gsub!(/: /, ":\t")
+ disasm.gsub!(' | ', "\n")
+ raw = [raw].pack('H*')
+ @gadgets << {:file => file, :address => addr, :raw => raw, :disasm => disasm.chomp!}
+ end
+ @gadgets
+ end
+
+ def print_msg(msg)
+ if not @stdio
+ @stdio = Rex::Ui::Text::Output::Stdio.new
+ end
+
+ @stdio.auto_color
+ @stdio.print_raw(@stdio.substitute_colors(msg))
+ end
+end
+
+class RopCollect < RopBase
+ def initialize(file="")
+ @file = file if not file.empty?
+ @bin = Metasm::AutoExe.decode_file(file) if not file.empty?
+ @disassembler = @bin.disassembler if not @bin.nil?
+ super()
+ end
+
+ def collect(depth)
+ rets = ["\xc3", "\xc2"] # ret, retn
+ matches = []
+ gadgets = []
+ rets.each do |ret|
+ # find matches by scanning for the pattern
+ matches = @disassembler.pattern_scan(ret)
+
+ if @bin.kind_of?(Metasm::PE)
+ @bin.sections.each do |section|
+ next if section.characteristics.include? 'MEM_EXECUTE'
+ # delete matches if the address is outside the virtual address space
+ matches.delete_if do |ea|
+ va = section.virtaddr + @bin.optheader.image_base
+ va <= ea and ea < va + section.virtsize
+ end
+ end
+ elsif @bin.kind_of?(Metasm::ELF)
+ @bin.segments.each do |seg|
+ next if seg.flags.include? 'X'
+ matches.delete_if do |ea|
+ ea >= seg.vaddr and ea < seg.vaddr + seg.memsz
+ end
+ end
+ end
+ end
+ gadgets = self.process_gadgets(matches, depth)
+ @gadgets = gadgets
+ gadgets
+ end
+
+ def pattern_search(pattern)
+ p = Regexp.new("(" + pattern + ")")
+ matches = []
+ @gadgets.each do |gadget|
+ disasm = ""
+ addrs = []
+
+ gadget[:disasm].each_line do |line|
+ addr, asm = line.split("\t", 2)
+ addrs << addr
+ disasm << asm
+ end
+
+ if gadget[:raw] =~ p or gadget[:disasm] =~ p or disasm =~ p
+ matches << {:gadget => gadget, :disasm => disasm, :addrs => addrs}
+ end
+ end
+ matches.each do |match|
+ @stdio.print_status("gadget with address: %bld%cya#{match[:gadget][:address]}%clr matched")
+ color_pattern(match[:gadget], match[:disasm], match[:addrs], p)
+ end
+ matches
+ end
+
+ def color_pattern(gadget, disasm, addrs, p)
+ idx = disasm.index(p)
+ if idx.nil?
+ print_msg(gadget[:disasm])
+ return
+ end
+
+ disasm = disasm.insert(idx, "%bld%grn")
+
+ asm = ""
+ cnt = 0
+ colors = false
+ disasm.each_line do |line|
+ # if we find this then we are in the matching area
+ if line.index(/\%bld\%grn/)
+ colors = true
+ end
+ asm << "%clr" + addrs[cnt] + "\t"
+
+ # color the remaining parts of the gadget
+ if colors and line.index("%bld%grn").nil?
+ asm << "%bld%grn" + line
+ else
+ asm << line
+ end
+
+ cnt += 1
+ end
+ asm << "%clr\n"
+ print_msg(asm)
+ end
+
+ def process_gadgets(rets, num)
+ ret = {}
+ gadgets = []
+ rets.each do |ea|
+ insn = @disassembler.disassemble_instruction(ea)
+ next if not insn
+
+ xtra = insn.bin_length
+
+ 1.upto(num) do |x|
+ addr = ea - x
+
+ # get the disassembled instruction at this address
+ di = @disassembler.disassemble_instruction(addr)
+
+ # skip invalid instructions
+ next if not di
+ next if di.opcode.props[:setip]
+ next if di.opcode.props[:stopexec]
+
+ # get raw bytes
+ buf = @disassembler.read_raw_data(addr, x + xtra)
+
+ # see if we have it already, if so, add the ea to the list
+ arr = ret[buf]
+ if arr
+ arr << addr
+ next
+ end
+
+ # make sure disassembling forward leads to our instruction
+ next if not ends_with_addr(buf, addr, ea)
+ dasm = ""
+ while addr <= ea
+ di = @disassembler.disassemble_instruction(addr)
+ dasm << ("0x%08x:\t" % addr) + di.instruction.to_s + "\n"
+ addr = addr + di.bin_length
+ end
+ # otherwise, we create a new tailchunk and add it to the list
+ ret = {:file => @file, :address => ("0x%08x" % (ea - x)), :raw => buf, :disasm => dasm}
+ gadgets << ret
+ end
+ end
+ gadgets
+ end
+
+ private
+ def ends_with_addr(raw, base, addr)
+ dasm2 = Metasm::Shellcode.decode(raw, @disassembler.cpu).disassembler
+ offset = 0
+ while ((di = dasm2.disassemble_instruction(offset)))
+ return true if (base + offset) == addr
+ return false if di.opcode.props[:setip]
+ return false if di.opcode.props[:stopexec]
+ offset = di.next_addr
+ end
+ false
+ end
+end
+end
+end
View
143 msfrop
@@ -0,0 +1,143 @@
+#!/usr/bin/env ruby
+#
+# $Id$
+#
+# This tool will collect, export, and import ROP gadgets
+# from various file formats (PE, ELF, Macho)
+# $Revision$
+#
+
+msfbase = __FILE__
+while File.symlink?(msfbase)
+ msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
+end
+
+$:.unshift(File.join(File.dirname(msfbase), 'lib'))
+$:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']
+
+require 'rex'
+require 'rex/ropbuilder'
+require 'rex/ui/text/output/stdio'
+require 'rex/ui/text/color'
+require 'optparse'
+
+def opt2i(o)
+ o.index("0x")==0 ? o.hex : o.to_i
+end
+
+opts = {}
+
+# set defaults
+opts[:depth] = 3
+
+opt = OptionParser.new
+opt.banner = "Usage #{$PROGRAM_NAME} <option> [targets]"
+opt.separator('')
+opt.separator('Options:')
+
+opt.on('-d', '--depth [size]', 'Number of maximum bytes to backwards disassemble from return instructions') do |d|
+ opts[:depth] = opt2i(d)
+end
+
+opt.on('-s', '--search [regex]', 'Search for gadgets matching a regex, match intel syntax or raw bytes') do |regex|
+ opts[:pattern] = regex
+end
+
+opt.on('-x', '--export [filename]', 'Export gadgets to CSV format') do |csv|
+ opts[:export] = csv
+end
+
+opt.on('-i', '--import [filename]', 'Import gadgets from previous collections') do |csv|
+ opts[:import] = csv
+end
+
+opt.on('-v', '--verbose', 'Output very verbosely') do
+ opts[:verbose] = true
+end
+
+opt.on_tail('-h', '--help', 'Show this message') do
+ puts opt
+ exit(1)
+end
+
+begin
+ opt.parse!
+rescue OptionParser::InvalidOption
+ puts "Invalid option, try -h for usage"
+ exit(1)
+end
+
+gadgets = []
+
+if opts[:import].nil?
+ files = []
+ ARGV.each do |file|
+ if(File.directory?(file))
+ dir = Dir.open(file)
+ dir.entries.each do |ent|
+ path = File.join(file, ent)
+ next if not File.file?(path)
+ files << File.join(path)
+ end
+ else
+ files << file
+ end
+ end
+
+ ropbuilder = Rex::RopBuilder::RopCollect.new
+ files.each do |file|
+ found = []
+ ropbuilder = Rex::RopBuilder::RopCollect.new(file)
+ ropbuilder.print_msg("Collecting gadgets from %bld%cya#{file}%clr\n")
+ found = ropbuilder.collect(opts[:depth])
+ ropbuilder.print_msg("Found %grn#{found.count}%clr gadgets\n\n")
+
+ # compile a list of all gadgets from all files
+ found.each do |gadget|
+ gadgets << gadget
+ end
+ end
+
+ if opts[:verbose]
+ gadgets.each do |gadget|
+ ropbuilder.print_msg("#{gadget[:file]} gadget: %bld%grn#{gadget[:address]}%clr\n")
+ ropbuilder.print_msg gadget[:disasm] + "\n"
+ end
+ end
+ ropbuilder.print_msg("Found %bld%grn#{gadgets.count}%clr gadgets total\n\n")
+end
+
+if opts[:import]
+
+ ropbuilder = Rex::RopBuilder::RopCollect.new()
+ ropbuilder.print_msg("Importing gadgets from %bld%cya#{opts[:import]}\n")
+ gadgets = ropbuilder.import(opts[:import])
+
+ gadgets.each do |gadget|
+ ropbuilder.print_msg("gadget: %bld%cya#{gadget[:address]}%clr\n")
+ ropbuilder.print_msg gadget[:disasm] + "\n"
+ end
+
+ ropbuilder.print_msg("Imported %grn#{gadgets.count}%clr gadgets\n")
+end
+
+if opts[:pattern]
+ matches = ropbuilder.pattern_search(opts[:pattern])
+ if opts[:verbose]
+ ropbuilder.print_msg("Found %grn#{matches.count}%clr matches\n")
+ end
+end
+
+if opts[:export]
+ ropbuilder.print_msg("Exporting %grn#{gadgets.count}%clr gadgets to %bld%cya#{opts[:export]}%clr\n")
+ csv = ropbuilder.to_csv(gadgets)
+ begin
+ fd = File.new(opts[:export], 'w')
+ fd.puts csv
+ fd.close
+ rescue
+ puts "Error writing #{opts[:export]} file"
+ exit(1)
+ end
+ ropbuilder.print_msg("%bld%redSuccess!%clr gadgets exported to %bld%cya#{opts[:export]}%clr\n")
+end

0 comments on commit a0c2321

Please sign in to comment.
Something went wrong with that request. Please try again.