#!/usr/bin/env ruby ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## msfbase = __FILE__ while File.symlink?(msfbase) msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) end $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib'))) $LOAD_PATH.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB'] gem 'rex-text' require 'optparse' module PatternOffset class OptsConsole def self.parse(args) options = {} parser = OptionParser.new do |opt| opt.banner = "Usage: #{__FILE__} [options]\nExample: #{__FILE__} -q Aa3A\n[*] Exact match at offset 9" opt.separator '' opt.separator 'Options:' opt.on('-q', '--query Aa0A', String, "Query to Locate") do |query| options[:query] = query end opt.on('-l', '--length ', Integer, "The length of the pattern") do |len| options[:length] = len end opt.on('-s', '--sets ', Array, "Custom Pattern Sets") do |sets| options[:sets] = sets end opt.on_tail('-h', '--help', 'Show this message') do $stdout.puts opt exit end end parser.parse!(args) if options.empty? raise OptionParser::MissingArgument, 'No options set, try -h for usage' elsif options[:query].nil? raise OptionParser::MissingArgument, '-q is required' elsif options[:length].nil? && options[:sets] raise OptionParser::MissingArgument, '-l is required' end options[:sets] = nil unless options[:sets] options[:length] = 8192 unless options[:length] options end end class Driver def initialize begin @opts = OptsConsole.parse(ARGV) rescue OptionParser::ParseError => e $stderr.puts "[x] #{e.message}" exit end end def run require 'msfenv' require 'msf/core' require 'msf/base' require 'rex/text' query = (@opts[:query]) if query.length >= 8 && query.hex > 0 query = query.hex # However, you can also specify a four-byte string elsif query.length == 4 query = query.unpack("V").first else # Or even a hex query that isn't 8 bytes long query = query.to_i(16) end buffer = Rex::Text.pattern_create(@opts[:length], @opts[:sets]) offset = Rex::Text.pattern_offset(buffer, query) # Handle cases where there is no match by looking for "close" matches unless offset found = false $stderr.puts "[*] No exact matches, looking for likely candidates..." # Look for shifts by a single byte 0.upto(3) do |idx| 0.upto(255) do |c| nvb = [query].pack("V") nvb[idx, 1] = [c].pack("C") nvi = nvb.unpack("V").first off = Rex::Text.pattern_offset(buffer, nvi) if off mle = query - buffer[off, 4].unpack("V").first mbe = query - buffer[off, 4].unpack("N").first puts "[+] Possible match at offset #{off} (adjusted [ little-endian: #{mle} | big-endian: #{mbe} ] ) byte offset #{idx}" found = true end end end exit! if found # Look for 16-bit offsets [0, 2].each do |idx| 0.upto(65535) do |c| nvb = [query].pack("V") nvb[idx, 2] = [c].pack("v") nvi = nvb.unpack("V").first off = Rex::Text.pattern_offset(buffer, nvi) if off mle = query - buffer[off, 4].unpack("V").first mbe = query - buffer[off, 4].unpack("N").first puts "[+] Possible match at offset #{off} (adjusted [ little-endian: #{mle} | big-endian: #{mbe} ] )" found = true end end end end while offset puts "[*] Exact match at offset #{offset}" offset = Rex::Text.pattern_offset(buffer, query, offset + 1) end end end end if __FILE__ == $PROGRAM_NAME driver = PatternOffset::Driver.new begin driver.run rescue ::StandardError => e $stderr.puts "[x] #{e.class}: #{e.message}" $stderr.puts "[*] If necessary, please refer to framework.log for more details." end end