820 lines
18 KiB
Ruby
820 lines
18 KiB
Ruby
# -*- coding: binary -*-
|
|
require 'rexml/rexml'
|
|
require 'rexml/source'
|
|
require 'rexml/document'
|
|
require 'rexml/parsers/treeparser'
|
|
require 'rex/proto/http'
|
|
require 'uri'
|
|
|
|
module Rex
|
|
module Exploitation
|
|
module OpcodeDb
|
|
|
|
module OpcodeResult # :nodoc:
|
|
def initialize(hash)
|
|
@hash = hash
|
|
end
|
|
attr_reader :hash
|
|
end
|
|
|
|
###
|
|
#
|
|
# A cachable entry.
|
|
#
|
|
###
|
|
module Cachable
|
|
|
|
def create(hash) # :nodoc:
|
|
@Cache = {} unless (@Cache)
|
|
if (hash_key(hash) and @Cache[hash_key(hash)])
|
|
@Cache[hash_key(hash)]
|
|
else
|
|
@Cache[hash_key(hash)] = self.new(hash)
|
|
end
|
|
end
|
|
|
|
def hash_key(hash) # :nodoc:
|
|
hash['id'] || nil
|
|
end
|
|
|
|
def flush_cache # :nodoc:
|
|
@Cache.clear
|
|
end
|
|
|
|
end
|
|
|
|
###
|
|
#
|
|
# This class provides a general interface to items that come from that opcode
|
|
# database that have a symbolic entry identifier and name.
|
|
#
|
|
###
|
|
module DbEntry
|
|
include OpcodeResult
|
|
|
|
def initialize(hash)
|
|
super
|
|
|
|
@id = hash['id'].to_i
|
|
@name = hash['name']
|
|
end
|
|
|
|
#
|
|
# Fields that could possibly be filtered on for this database entry.
|
|
#
|
|
def filter_hash
|
|
{
|
|
"id" => id,
|
|
"name" => name
|
|
}
|
|
end
|
|
|
|
#
|
|
# The unique server identifier.
|
|
#
|
|
attr_reader :id
|
|
#
|
|
# The unique name for this entry.
|
|
#
|
|
attr_reader :name
|
|
end
|
|
|
|
###
|
|
#
|
|
# This class represents a particular image module including its name,
|
|
# segments, imports, exports, base address, and so on.
|
|
#
|
|
###
|
|
class ImageModule
|
|
include DbEntry
|
|
|
|
###
|
|
#
|
|
# This class contains information about a module-associated segment.
|
|
#
|
|
###
|
|
class Segment
|
|
def initialize(hash)
|
|
@type = hash['type']
|
|
@base_address = hash['base_address'].to_i
|
|
@size = hash['segment_size'].to_i
|
|
@writable = hash['writable'] == "true" ? true : false
|
|
@readable = hash['readable'] == "true" ? true : false
|
|
@executable = hash['executable'] == "true" ? true : false
|
|
end
|
|
|
|
#
|
|
# The type of the segment, such as ".text".
|
|
#
|
|
attr_reader :type
|
|
#
|
|
# The base address of the segment.
|
|
#
|
|
attr_reader :base_address
|
|
#
|
|
# The size of the segment in bytes.
|
|
#
|
|
attr_reader :size
|
|
#
|
|
# Boolean that indicates whether or not the segment is writable.
|
|
#
|
|
attr_reader :writable
|
|
#
|
|
# Boolean that indicates whether or not the segment is readable.
|
|
#
|
|
attr_reader :readable
|
|
#
|
|
# Boolean that indicates whether or not the segment is executable.
|
|
#
|
|
attr_reader :executable
|
|
end
|
|
|
|
###
|
|
#
|
|
# This class contains information about a module-associated import.
|
|
#
|
|
###
|
|
class Import
|
|
def initialize(hash)
|
|
@name = hash['name']
|
|
@address = hash['address'].to_i
|
|
@ordinal = hash['ordinal'].to_i
|
|
end
|
|
|
|
#
|
|
# The name of the imported function.
|
|
#
|
|
attr_reader :name
|
|
#
|
|
# The address of the function pointer in the IAT.
|
|
#
|
|
attr_reader :address
|
|
#
|
|
# The ordinal of the imported symbol.
|
|
#
|
|
attr_reader :ordinal
|
|
end
|
|
|
|
###
|
|
#
|
|
# This class contains information about a module-associated export.
|
|
#
|
|
###
|
|
class Export
|
|
def initialize(hash)
|
|
@name = hash['name']
|
|
@address = hash['address'].to_i
|
|
@ordinal = hash['ordinal'].to_i
|
|
end
|
|
|
|
#
|
|
# The name of the exported function.
|
|
#
|
|
attr_reader :name
|
|
#
|
|
# The address of the exported function.
|
|
#
|
|
attr_reader :address
|
|
#
|
|
# The ordinal of the exported symbol.
|
|
#
|
|
attr_reader :ordinal
|
|
end
|
|
|
|
class <<self
|
|
include Cachable
|
|
def hash_key(hash) # :nodoc:
|
|
(hash['id'] || '') +
|
|
(hash['segments'] || '').to_s +
|
|
(hash['exports'] || '').to_s +
|
|
(hash['imports'] || '').to_s
|
|
end
|
|
end
|
|
|
|
def initialize(hash)
|
|
super
|
|
|
|
@locale = Locale.create(hash['locale'])
|
|
@maj_maj_ver = hash['maj_maj_ver'].to_i
|
|
@maj_min_ver = hash['maj_min_ver'].to_i
|
|
@min_maj_ver = hash['min_maj_ver'].to_i
|
|
@min_min_ver = hash['min_min_ver'].to_i
|
|
@timestamp = Time.at(hash['timestamp'].to_i)
|
|
@vendor = hash['vendor']
|
|
@base_address = hash['base_address'].to_i
|
|
@image_size = hash['image_size'].to_i
|
|
|
|
@segments = hash['segments'].map { |ent|
|
|
Segment.new(ent)
|
|
} if (hash['segments'])
|
|
@imports = hash['imports'].map { |ent|
|
|
Import.new(ent)
|
|
} if (hash['imports'])
|
|
@exports = hash['exports'].map { |ent|
|
|
Export.new(ent)
|
|
} if (hash['exports'])
|
|
@platforms = hash['platforms'].map { |ent|
|
|
OsVersion.create(ent)
|
|
} if (hash['platforms'])
|
|
|
|
@segments = [] unless(@segments)
|
|
@imports = [] unless(@imports)
|
|
@exports = [] unless(@exports)
|
|
@platforms = [] unless(@platforms)
|
|
end
|
|
|
|
#
|
|
# An instance of a Locale class that is associated with this module.
|
|
#
|
|
attr_reader :locale
|
|
#
|
|
# The module's major major version number (X.x.x.x).
|
|
#
|
|
attr_reader :maj_maj_ver
|
|
#
|
|
# The module's major minor version number (x.X.x.x).
|
|
#
|
|
attr_reader :maj_min_ver
|
|
#
|
|
# The module's minor major version number (x.x.X.x).
|
|
#
|
|
attr_reader :min_maj_ver
|
|
#
|
|
# The module's minor minor version number (x.x.x.X).
|
|
#
|
|
attr_reader :min_min_ver
|
|
#
|
|
# The timestamp that the image was compiled (as a Time instance).
|
|
#
|
|
attr_reader :timestamp
|
|
#
|
|
# The vendor that created the module.
|
|
#
|
|
attr_reader :vendor
|
|
#
|
|
# The preferred base address at which the module will load.
|
|
#
|
|
attr_reader :base_address
|
|
#
|
|
# The size of the image mapping associated with the module in bytes.
|
|
#
|
|
attr_reader :image_size
|
|
#
|
|
# An array of Segment instances.
|
|
#
|
|
attr_reader :segments
|
|
#
|
|
# An array of Import instances.
|
|
#
|
|
attr_reader :imports
|
|
#
|
|
# An array of Export instances.
|
|
#
|
|
attr_reader :exports
|
|
#
|
|
# An array of OsVersion instances.
|
|
#
|
|
attr_reader :platforms
|
|
end
|
|
|
|
###
|
|
#
|
|
# This class contains information about a specific locale, such as English.
|
|
#
|
|
###
|
|
class Locale
|
|
include DbEntry
|
|
class <<self
|
|
include Cachable
|
|
end
|
|
end
|
|
|
|
###
|
|
#
|
|
# This class contains information about a platform (operating system) version.
|
|
#
|
|
###
|
|
class OsVersion
|
|
include DbEntry
|
|
|
|
class <<self
|
|
include Cachable
|
|
def hash_key(hash)
|
|
hash['id'] + (hash['modules'] || '')
|
|
end
|
|
end
|
|
|
|
def initialize(hash)
|
|
super
|
|
|
|
@modules = (hash['modules']) ? hash['modules'].to_i : 0
|
|
@desc = hash['desc']
|
|
@arch = hash['arch']
|
|
@maj_ver = hash['maj_ver'].to_i
|
|
@min_ver = hash['min_ver'].to_i
|
|
@maj_patch_level = hash['maj_patch_level'].to_i
|
|
@min_patch_level = hash['min_patch_level'].to_i
|
|
end
|
|
|
|
#
|
|
# The number of modules that exist in this operating system version.
|
|
#
|
|
attr_reader :modules
|
|
#
|
|
# The operating system version description, such as Windows XP 5.2.0.0
|
|
# (IA32).
|
|
#
|
|
attr_reader :desc
|
|
#
|
|
# The architecture that the operating system version runs on, such as IA32.
|
|
#
|
|
attr_reader :arch
|
|
#
|
|
# The major version of the operating system version.
|
|
#
|
|
attr_reader :maj_ver
|
|
#
|
|
# The minor version of the operating system version.
|
|
#
|
|
attr_reader :min_ver
|
|
#
|
|
# The major patch level of the operating system version, such as a service
|
|
# pack.
|
|
#
|
|
attr_reader :maj_patch_level
|
|
#
|
|
# The minor patch level of the operating system version.
|
|
#
|
|
attr_reader :min_patch_level
|
|
end
|
|
|
|
###
|
|
#
|
|
# An opcode group (esp => eip).
|
|
#
|
|
###
|
|
class Group
|
|
include DbEntry
|
|
class <<self
|
|
include Cachable
|
|
end
|
|
end
|
|
|
|
###
|
|
#
|
|
# An opcode type (jmp esp).
|
|
#
|
|
###
|
|
class Type
|
|
include DbEntry
|
|
|
|
class <<self
|
|
include Cachable
|
|
end
|
|
|
|
def initialize(hash)
|
|
super
|
|
|
|
@opcodes = (hash['opcodes']) ? hash['opcodes'].to_i : 0
|
|
@meta_type = MetaType.create(hash['meta_type']) if (hash['meta_type'])
|
|
@group = Group.create(hash['group']) if (hash['group'])
|
|
@arch = hash['arch']
|
|
end
|
|
|
|
#
|
|
# The number of opcodes associated with this type, or 0 if this information
|
|
# is not available.
|
|
#
|
|
attr_reader :opcodes
|
|
#
|
|
# An instance of the MetaType to which this opcode type belongs, or nil.
|
|
#
|
|
attr_reader :meta_type
|
|
#
|
|
# An instance of the Group to which this opcode type belongs, or nil.
|
|
#
|
|
attr_reader :group
|
|
#
|
|
# The architecture that this opcode type is associated with.
|
|
#
|
|
attr_reader :arch
|
|
end
|
|
|
|
###
|
|
#
|
|
# An opcode meta type (jmp reg).
|
|
#
|
|
###
|
|
class MetaType
|
|
include DbEntry
|
|
class <<self
|
|
include Cachable
|
|
end
|
|
end
|
|
|
|
###
|
|
#
|
|
# An opcode that has a specific address and is associated with one or more
|
|
# modules.
|
|
#
|
|
###
|
|
class Opcode
|
|
include DbEntry
|
|
|
|
def initialize(hash)
|
|
super
|
|
|
|
@address = hash['address'].to_i
|
|
@type = Type.create(hash['type'])
|
|
@group = @type.group
|
|
@modules = hash['modules'].map { |ent|
|
|
ImageModule.create(ent)
|
|
} if (hash['modules'])
|
|
|
|
@modules = [] unless(@modules)
|
|
end
|
|
|
|
#
|
|
# The address of the opcode.
|
|
#
|
|
attr_reader :address
|
|
#
|
|
# The type of the opcode indicating which instruction is found at the
|
|
# address. This is an instance of the Type class.
|
|
#
|
|
attr_reader :type
|
|
#
|
|
# A Group instance that reflects the group to which the opcode type found
|
|
# at the instance's address belongs.
|
|
#
|
|
attr_reader :group
|
|
#
|
|
# An array of ImageModule instances that show the modules that contain this
|
|
# address.
|
|
#
|
|
attr_reader :modules
|
|
end
|
|
|
|
###
|
|
#
|
|
# Current statistics of the opcode database.
|
|
#
|
|
###
|
|
class Statistics
|
|
def initialize(hash)
|
|
@modules = hash['modules'].to_i
|
|
@opcodes = hash['opcodes'].to_i
|
|
@opcode_types = hash['opcode_types'].to_i
|
|
@platforms = hash['platforms'].to_i
|
|
@architectures = hash['architectures'].to_i
|
|
@module_segments = hash['module_segments'].to_i
|
|
@module_imports = hash['module_imports'].to_i
|
|
@module_exports = hash['module_exports'].to_i
|
|
@last_update = Time.at(hash['last_update'].to_i)
|
|
end
|
|
|
|
#
|
|
# The number of modules found within the opcode database.
|
|
#
|
|
attr_reader :modules
|
|
#
|
|
# The number of opcodes supported by the opcode database.
|
|
#
|
|
attr_reader :opcodes
|
|
#
|
|
# The number of opcode types supported by the database.
|
|
#
|
|
attr_reader :opcode_types
|
|
#
|
|
# The number of platforms supported by the database.
|
|
#
|
|
attr_reader :platforms
|
|
#
|
|
# The number of architectures supported by the database.
|
|
#
|
|
attr_reader :architectures
|
|
#
|
|
# The number of module segments supported by the database.
|
|
#
|
|
attr_reader :module_segments
|
|
#
|
|
# The number of module imports supported by the database.
|
|
#
|
|
attr_reader :module_imports
|
|
#
|
|
# The number of module exports supported by the database.
|
|
#
|
|
attr_reader :module_exports
|
|
#
|
|
# The time at which the last database update occurred.
|
|
#
|
|
attr_reader :last_update
|
|
end
|
|
|
|
###
|
|
#
|
|
# This class implements a client interface to the Metasploit Opcode Database.
|
|
# It is intended to be used as a method of locating reliable return addresses
|
|
# given a set of executable files and a set of usable opcodes.
|
|
#
|
|
###
|
|
class Client
|
|
|
|
DefaultServerHost = "www.metasploit.com"
|
|
DefaultServerPort = 80
|
|
DefaultServerUri = "/users/opcode/msfopcode_server.cgi"
|
|
|
|
#
|
|
# Returns an instance of an initialized client that will use the supplied
|
|
# server values.
|
|
#
|
|
def initialize(host = DefaultServerHost, port = DefaultServerPort, uri = DefaultServerUri)
|
|
self.server_host = host
|
|
self.server_port = port
|
|
self.server_uri = uri
|
|
end
|
|
|
|
#
|
|
# Disables response parsing.
|
|
#
|
|
def disable_parse
|
|
@disable_parse = true
|
|
end
|
|
|
|
#
|
|
# Enables response parsing.
|
|
#
|
|
def enable_parse
|
|
@disable_parse = false
|
|
end
|
|
|
|
#
|
|
# Returns an array of MetaType instances.
|
|
#
|
|
def meta_types
|
|
request('meta_types').map { |ent| MetaType.create(ent) }
|
|
end
|
|
|
|
#
|
|
# Returns an array of Group instances.
|
|
#
|
|
def groups
|
|
request('groups').map { |ent| Group.create(ent) }
|
|
end
|
|
|
|
#
|
|
# Returns an array of Type instances. Opcode types are specific opcodes,
|
|
# such as a jmp esp. Optionally, a filter hash can be passed to include
|
|
# extra information in the results.
|
|
#
|
|
# Statistics (Bool)
|
|
#
|
|
# If this hash element is set to true, the number of opcodes currently in
|
|
# the database of this type will be returned.
|
|
#
|
|
def types(filter = {})
|
|
request('types', filter).map { |ent| Type.create(ent) }
|
|
end
|
|
|
|
#
|
|
# Returns an array of OsVersion instances. OS versions are associated with
|
|
# a particular operating system release (including service packs).
|
|
# Optionally, a filter hash can be passed to limit the number of results
|
|
# returned. If no filter hash is supplied, all results are returned.
|
|
#
|
|
# Names (Array)
|
|
#
|
|
# If this hash element is specified, only the operating systems that
|
|
# contain one or more of the names specified will be returned.
|
|
#
|
|
# Statistics (Bool)
|
|
#
|
|
# If this hash element is set to true, the number of modules associated
|
|
# with this matched operating system versions will be returned.
|
|
#
|
|
def platforms(filter = {})
|
|
request('platforms', filter).map { |ent| OsVersion.create(ent) }
|
|
end
|
|
|
|
#
|
|
# Returns an array of ImageModule instances. Image modules are
|
|
# version-specific, locale-specific, and operating system version specific
|
|
# image files. Modules have opcodes, segments, imports and exports
|
|
# associated with them. Optionally, a filter hash can be specified to
|
|
# limit the number of results returned from the database. If no filter
|
|
# hash is supplied, all modules will be returned.
|
|
#
|
|
# LocaleNames (Array)
|
|
#
|
|
# This hash element limits results to one or more specific locale by name.
|
|
#
|
|
# PlatformNames (Array)
|
|
#
|
|
# This hash element limits results to one or more specific platform by
|
|
# name.
|
|
#
|
|
# ModuleNames (Array)
|
|
#
|
|
# This hash element limits results to one or more specific module by name.
|
|
#
|
|
# Segments (Bool)
|
|
#
|
|
# If this hash element is set to true, the segments associated with each
|
|
# resulting module will be returned by the server.
|
|
#
|
|
# Imports (Bool)
|
|
#
|
|
# If this hash element is set to true, the imports associated with each
|
|
# resulting module will be returned by the server.
|
|
#
|
|
# Exports (Bool)
|
|
#
|
|
# If this hash element is set to true, the exports associated with each
|
|
# resulting module will be returned by the server.
|
|
#
|
|
def modules(filter = {})
|
|
request('modules', filter).map { |ent| ImageModule.create(ent) }
|
|
end
|
|
|
|
#
|
|
# Returns an array of Locale instances that are supported by the server.
|
|
#
|
|
def locales
|
|
request('locales').map { |ent| Locale.create(ent) }
|
|
end
|
|
|
|
#
|
|
# Returns an array of Opcode instances that match the filter limitations
|
|
# specified in the supplied filter hash. If no filter hash is specified,
|
|
# all opcodes will be returned (but are most likely going to be limited by
|
|
# the server). The filter hash limiters that can be specified are:
|
|
#
|
|
# ModuleNames (Array)
|
|
#
|
|
# This hash element limits results to one or more specific modules by
|
|
# name.
|
|
#
|
|
# GroupNames (Array)
|
|
#
|
|
# This hash element limits results to one or more specific opcode group by
|
|
# name.
|
|
#
|
|
# TypeNames (Array)
|
|
#
|
|
# This hash element limits results to one or more specific opcode type by
|
|
# name.
|
|
#
|
|
# MetaTypeNames (Array)
|
|
#
|
|
# This hash element limits results to one or more specific opcode meta
|
|
# type by name.
|
|
#
|
|
# LocaleNames (Array)
|
|
#
|
|
# Limits results to one or more specific locale by name.
|
|
#
|
|
# PlatformNames (Array)
|
|
#
|
|
# Limits reslts to one or more specific operating system version by name.
|
|
#
|
|
# Addresses (Array)
|
|
#
|
|
# Limits results to a specific set of addresses.
|
|
#
|
|
# Portable (Bool)
|
|
#
|
|
# If this hash element is true, opcode results will be limited to ones
|
|
# that span more than one operating system version.
|
|
#
|
|
def search(filter = {})
|
|
request('search', filter).map { |ent| Opcode.new(ent) }
|
|
end
|
|
|
|
#
|
|
# Returns an instance of the Statistics class that holds information about
|
|
# the server's database stats.
|
|
#
|
|
def statistics
|
|
Statistics.new(request('statistics'))
|
|
end
|
|
|
|
#
|
|
# These attributes convey information about the remote server and can be
|
|
# changed in order to point it to a locate copy as necessary.
|
|
#
|
|
attr_accessor :server_host, :server_port, :server_uri
|
|
|
|
#
|
|
# Retrieves the last raw XML response to be processed.
|
|
#
|
|
attr_reader :last_xml
|
|
|
|
protected
|
|
|
|
#
|
|
# Transmits a request to the Opcode database server and translates the
|
|
# response into a native general ruby datatype.
|
|
#
|
|
def request(method, opts = {})
|
|
client = Rex::Proto::Http::Client.new(server_host, server_port)
|
|
|
|
begin
|
|
|
|
# Create the CGI parameter list
|
|
vars = { 'method' => method }
|
|
|
|
opts.each_pair do |k, v|
|
|
vars[k] = xlate_param(v)
|
|
end
|
|
|
|
client.set_config('uri_encode_mode' => 'none')
|
|
|
|
# Initialize the request with the POST body.
|
|
request = client.request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => server_uri,
|
|
'vars_post' => vars
|
|
)
|
|
|
|
# Send the request and grab the response.
|
|
response = client.send_recv(request, 300)
|
|
|
|
# Non-200 return code?
|
|
if (response.code != 200)
|
|
raise RuntimeError, "Invalid response received from server."
|
|
end
|
|
|
|
# Convert the return value to the native type.
|
|
parse_response(response.body)
|
|
rescue ::SocketError
|
|
raise RuntimeError, "Could not communicate with the opcode service: #{$!.class} #{$!}"
|
|
ensure
|
|
client.close
|
|
end
|
|
end
|
|
|
|
#
|
|
# Translates a parameter into a flat CGI parameter string.
|
|
#
|
|
def xlate_param(v)
|
|
if (v.kind_of?(Array))
|
|
v.map { |ent|
|
|
xlate_param(ent)
|
|
}.join(',,')
|
|
elsif (v.kind_of?(Hash))
|
|
v.map { |k,v|
|
|
"#{URI.escape(k)}:#{xlate_param(v)}" if (v)
|
|
}.join(',,')
|
|
else
|
|
URI.escape(v.to_s)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Translate the data type from a flat string to a ruby native type.
|
|
#
|
|
def parse_response(xml)
|
|
@last_xml = xml
|
|
|
|
if (!@disable_parse)
|
|
source = REXML::Source.new(xml)
|
|
doc = REXML::Document.new
|
|
|
|
REXML::Parsers::TreeParser.new(source, doc).parse
|
|
|
|
translate_element(doc.root)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Translate elements conveyed as data types.
|
|
#
|
|
def translate_element(element)
|
|
case element.name
|
|
when "Array"
|
|
return element.elements.map { |child| translate_element(child) }
|
|
when "Hash"
|
|
hsh = {}
|
|
|
|
element.each_element { |child|
|
|
if (e = child.elements[1])
|
|
v = translate_element(e)
|
|
else
|
|
v = child.text
|
|
end
|
|
|
|
hsh[child.attributes['name']] = v
|
|
}
|
|
|
|
return hsh
|
|
else
|
|
return element.text
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|