#!/usr/bin/env ruby
#
# $Id: msfbinscan 15108 2012-04-16 05:01:41Z rapid7 $
# $Revision: 15108 $
#

msfbase = __FILE__
while File.symlink?(msfbase)
	msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
end

$:.unshift(File.expand_path(File.join(File.dirname(msfbase), 'lib')))
require 'fastlib'
require 'msfenv'



$:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']

require 'metasm'
require 'rex/elfparsey'
require 'rex/elfscan'
require 'rex/machparsey'
require 'rex/machscan'
require 'rex/peparsey'
require 'rex/pescan'
require 'rex/arch/x86'
require 'optparse'

def opt2i(o)
	o.index("0x")==0 ? o.hex : o.to_i
end

opt = OptionParser.new

opt.banner = "Usage: #{$PROGRAM_NAME} [mode] <options> [targets]"
opt.separator('')
opt.separator('Modes:')

worker = nil
param = {}
files = []
mode = ""

opt.on('-j', '--jump [regA,regB,regC]', 'Search for jump equivalent instructions        [PE|ELF|MACHO]') do |t|
	# take csv of register names (like eax,ebx) and convert
	# them to an array of register numbers
	mode = "jump"
	regnums = t.split(',').collect { |o|
		begin
			Rex::Arch::X86.reg_number(o)
		rescue
			puts "Invalid register \"#{o}\""
			exit(1)
		end
	}
	param['args'] = regnums
end

opt.on('-p', '--poppopret', 'Search for pop+pop+ret combinations            [PE|ELF|MACHO]') do |t|
	mode = "pop"
	param['args'] = t
end

opt.on('-r', '--regex [regex]', 'Search for regex match                         [PE|ELF|MACHO]') do |t|
	mode = "regex"
	param['args'] = t
end

opt.on('-a', '--analyze-address [address]', 'Display the code at the specified address      [PE|ELF]') do |t|
	mode = "analyze-address"
	param['args'] = opt2i(t)
end

opt.on('-b', '--analyze-offset [offset]', 'Display the code at the specified offset       [PE|ELF]') do |t|
	mode = "analyze-offset"
	param['args'] = opt2i(t)
end

opt.on('-f', '--fingerprint', 'Attempt to identify the packer/compiler        [PE]') do |t|
	mode = "fingerprint"
	param['database'] = File.join(File.dirname(msfbase), 'data', 'msfpescan', 'identify.txt')
end

opt.on('-i', '--info', 'Display detailed information about the image   [PE]') do |t|
	mode = "info"
end

opt.on('-R', '--ripper [directory]', 'Rip all module resources to disk               [PE]') do |t|
	mode = "ripper"
	param['dir'] = t
end

opt.on('--context-map [directory]', 'Generate context-map files                     [PE]') do |t|
	mode = "context"
	param['dir'] = t
end

opt.separator('')
opt.separator('Options:')

opt.on('-A', '--after [bytes]', 'Number of bytes to show after match (-a/-b)    [PE|ELF|MACHO]') do |t|
	param['after'] = opt2i(t)
end

opt.on('-B', '--before [bytes]', 'Number of bytes to show before match (-a/-b)   [PE|ELF|MACHO]') do |t|
	param['before'] = opt2i(t)
end

opt.on('-I', '--image-base [address]', 'Specify an alternate ImageBase                 [PE|ELF|MACHO]') do |t|
	param['imagebase'] = opt2i(t)
end

opt.on('-D', '--disasm', 'Disassemble the bytes at this address          [PE]') do |t|
	param['disasm'] = true
end

opt.on('-F', '--filter-addresses [regex]', 'Filter addresses based on a regular expression [PE]') do |t|
	param['filteraddr'] = t
end

opt.on_tail("-h", "--help", "Show this message") do
	$stderr.puts opt
	exit(1)
end

begin
	opt.parse!
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
	$stderr.puts "Invalid option, try -h for usage"
	exit(1)
end


if mode.empty?
	$stderr.puts "A mode must be selected"
	$stderr.puts opt
	exit(1)
end

# check if the file is a directory if it is collect all the entries
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

# we need to do some work to figure out the file format
files.each do |file|
	param['file'] = file

	bin = Metasm::AutoExe.decode_file(file) if not file.empty?

	if bin.kind_of?(Metasm::PE)
		case mode
		when "jump"
			worker = Rex::PeScan::Scanner::JmpRegScanner
		when "pop"
			worker = Rex::PeScan::Scanner::PopPopRetScanner
		when "regex"
			worker = Rex::PeScan::Scanner::RegexScanner
		when "analyze-address"
			worker = Rex::PeScan::Search::DumpRVA
		when "analyze-offset"
			worker = Rex::PeScan::Search::DumpOffset
		when "fingerprint"
			worker = Rex::PeScan::Analyze::Fingerprint
		when "info"
			worker = Rex::PeScan::Analyze::Information
		when "ripper"
			worker = Rex::PeScan::Analyze::Ripper
		when "context"
			worker = Rex::PeScan::Analyze::ContextMapDumper
		else
			$stderr.puts("Mode unsupported by file format")
		end

		pe_klass = Rex::PeParsey::Pe
		begin
			pe = pe_klass.new_from_file(file, true)
		rescue ::Interrupt
			raise $!
		rescue Rex::PeParsey::FileHeaderError
			next if $!.message == "Couldn't find the PE magic!"
			raise $!
		rescue Errno::ENOENT
			$stdout.puts("File does not exist: #{file}")
			next
		rescue ::Rex::PeParsey::SkipError
			next
		rescue ::Exception => e
			$stdout.puts "[#{file}] #{e.class}: #{e}"
			next
		end

		if (param['imagebase'])
			pe.image_base = param['imagebase'];
		end

		if not worker
			$stderr.puts("A mode could not be set for this file.")
			next
		end

		o = worker.new(pe)
		o.scan(param)

		pe.close

	elsif bin.kind_of?(Metasm::ELF)
		case mode
		when "jump"
			worker = Rex::ElfScan::Scanner::JmpRegScanner
		when "pop"
			worker = Rex::Elfscan::Scanner::PopPopRetScanner
		when "regex"
			worker = Rex::ElfScan::Scanner::RegexScanner
		when "analyze-address"
			worker = Rex::ElfScan::Search::DumpRVA
		when "analyze-offset"
			worker = Rex::ElfScan::Search::DumpOffset
		else
			$stderr.puts("Mode unsupported by file format")
		end
		
		begin
			elf = Rex::ElfParsey::Elf.new_from_file(file, true)
		rescue Rex::ElfParsey::ElfHeaderError
			if $!.message == 'Invalid magic number'
				$stderr.puts("Skipping #{file}: #{$!}")
				next
			end
			raise $!
		rescue Errno::ENOENT
			$stderr.puts("File does not exist: #{file}")
			next
		end

		if (param['imagebase'])
			elf.base_addr = param['imagebase'];
		end

		if not worker
			$stderr.puts("A mode could not be set for this file.")
			next
		end
		
		o = worker.new(elf)
		o.scan(param)

		elf.close

	elsif bin.kind_of?(Metasm::MachO)
		case mode
		when "jump"
			worker = Rex::MachScan::Scanner::JmpRegScanner
		when "pop"
			worker = Rex::MachScan::Scanner::PopPopRetScanner
		when "regex"
			worker = Rex::MachScan::Scanner::RegexScanner
		else
			$stderr.puts("Mode unsupported by file format")
		end

		begin
			mach = Rex::MachParsey::Mach.new_from_file(file, true)
			o = worker.new(mach)
			o.scan(param)
			mach.close
		rescue Rex::MachParsey::MachHeaderError
			$stderr.puts("File is not a Mach-O binary, trying Fat..\n")
			begin
				fat = Rex::MachParsey::Fat.new_from_file(file, true)
				o = worker.new(fat)
				o.scan(param)
				fat.close
			rescue
				$stderr.puts("Error: " + $!.to_s)
				$stderr.puts("Skipping #{file}")
			end
		rescue Errno::ENOENT
			$stderr.puts("File does not exist: #{file}")
			next
		end
	end

	if not worker
		$stderr.puts("Unsupported file format")
		$stderr.puts("Skipping #{file}")
		next
	end
end
