Add msfrop, a tool for collecting and ROP gadgets, features include export and import in CSV format, powerful regex searching

git-svn-id: file:///home/svn/framework3/trunk@12904 4d416f70-5f16-0410-b530-b9f4589650da
unstable
David Rude 2011-06-10 12:54:01 +00:00
parent 2191055a71
commit a0c2321efc
3 changed files with 379 additions and 0 deletions

7
lib/rex/ropbuilder.rb Normal file
View File

@ -0,0 +1,7 @@
module Rex
module RopBuilder
require 'rex/ropbuilder/rop'
require 'metasm/metasm'
end
end

229
lib/rex/ropbuilder/rop.rb Normal file
View File

@ -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

143
msfrop Executable file
View File

@ -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