diff --git a/lib/msf/Shared.rb b/lib/msf/Shared.rb new file mode 100644 index 0000000000..b2b343d45f --- /dev/null +++ b/lib/msf/Shared.rb @@ -0,0 +1,17 @@ +### +# +# framework-shared +# ---------------- +# +# The shared library in the framework contains classes that are +# used by various framework subsystems. +# +### + +# Shared single purpose classes +require 'Shared/ReadWriteLock' +require 'Shared/Transformer' + +# Logging facilities +require 'Shared/Logging/LogSink' +require 'Shared/Logging/LogDispatcher' diff --git a/lib/msf/core.rb b/lib/msf/core.rb new file mode 100644 index 0000000000..b57e5ea6f9 --- /dev/null +++ b/lib/msf/core.rb @@ -0,0 +1,35 @@ +### +# +# framework-core +# -------------- +# +# The core library provides all of the means by which to interact +# with the framework insofar as maniuplating encoders, nops, +# payloads, exploits, recon, and sessions. +# +### + +# Unit testing +require 'test/unit' +require 'Core/UnitTestSuite' + +# framework-core depends on framework-shared +require 'Shared' + +# General +require 'Core/Constants' +require 'Core/Exceptions' +require 'Core/DataTypes' +require 'Core/EventDispatcher' +require 'Core/DataStore' + +# Framework context and core classes +require 'Core/Framework' +require 'Core/Session' + +# Modules +require 'Core/Module' +require 'Core/Encoder' +require 'Core/Exploit' +require 'Core/Nop' +require 'Core/Recon' diff --git a/lib/msf/core/DataTypes.rb b/lib/msf/core/DataTypes.rb new file mode 100644 index 0000000000..f9e0a56e79 --- /dev/null +++ b/lib/msf/core/DataTypes.rb @@ -0,0 +1,177 @@ +module Msf + +### +# +# Author +# ------ +# +# This data type represents an author of a piece of code in either +# the framework, a module, a script, or something entirely unrelated. +# +### +class Author + + # Class method that translates a string to an instance of the Author class, + # if it's of the right format, and returns the Author class instance + def Author.from_s(str) + instance = Author.new + + # If the serialization fails... + if (instance.from_s(str) == false) + return nil + end + + return instance + end + + def initialize(name = nil, email = nil) + self.name = name + self.email = email + end + + # Serialize the author object to a string in form: + # + # name + def to_s + str = "#{name}" + + if (email != nil) + str += " <#{email}>" + end + + return str + end + + # Translate the author from the supplied string which may + # have either just a name or also an email address + def from_s(str) + + # List of known framework authors that can be referred by just name + known_authors = + { + 'H D Moore' => 'hdm@metasploit.com', + 'spoonm' => 'spoonm@gmail.com', + 'skape' => 'mmiller@hick.org', + 'vlad902' => 'vlad902@gmail.com' + } + + # Make fix up this regex to be a bit better...I suck at regex + m = /^([A-Za-z0-9 _]*?) <(.*?)>/.match(str) + + if (m != nil) + self.name = m[1] + self.email = m[2] + else + self.email = known_authors[str] + + if (self.email != nil) + self.name = str + else + return false + end + end + + return true + end + + attr_accessor :name, :email + +end + +### +# +# Reference +# --------- +# +# A reference to some sort of information. +# +### +class Reference + + def Reference.from_s(str) + return Reference.new(str) + end + + def initialize(in_str) + self.str = in_str + end + + def to_s + return self.str + end + + def from_s(in_str) + self.str = in_str + end + + attr_reader :str + +protected + + attr_writer :str + +end + +### +# +# SiteReference +# ------------- +# +# A reference to a website. +# +### +class SiteReference < Reference + + # Class method that translates a URL into a site reference instance + def SiteReference.from_s(str) + instance = SiteReference.new + + if (instance.from_s(str) == false) + return nil + end + + return instance + end + + # Initialize the site reference + def initialize(in_site = nil, in_ctx_id = nil) + self.ctx_id = in_ctx_id + + if (in_site == 'OSVDB') + self.site = 'http://www.osvdb.org/' + in_ctx_id.to_s + elsif (in_site == 'CVE') + self.site = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=' + in_ctx_id.to_s + elsif (in_site == 'BID') + self.site = 'http://www.securityfocus.com/bid/' + in_ctx_id.to_s + elsif (in_site == 'MSB') + self.site = 'http://www.microsoft.com/technet/security/bulletin/' + in_ctx_id.to_s + '.mspx' + else + self.site = in_site + end + end + + # Returns the absolute site URL + def to_s + return site || '' + end + + # Serializes a site URL string + def from_s(str) + if (/(http:\/\/|https:\/\/|ftp:\/\/)/.match(str)) + self.site = str + else + return false + end + + return true + end + + attr_reader :site, :ctx_id + +protected + + attr_writer :site, :ctx_id + +end + +end diff --git a/lib/msf/core/ModuleLoader.rb b/lib/msf/core/ModuleLoader.rb new file mode 100644 index 0000000000..c1d8df455c --- /dev/null +++ b/lib/msf/core/ModuleLoader.rb @@ -0,0 +1,88 @@ +module Msf +class ModuleLoader + + + attr_accessor :ext, :base, :namespace, :recursive, :history + + def initialize(namespace, base, opts = { }) + + # merge in the defaults + opts = { + 'ext' => '.rb', + 'recursive' => true + }.update(opts) + + self.ext = opts['ext'] + self.base = base + self.namespace = namespace + self.recursive = opts['recursive'] + self.history = { } + end + + def mod_from_name(name) + obj = Object + + name.split('::').each { |m| + obj = obj.const_get(m) + } + + return obj + end + + def error(msg) + puts '[!] ' + msg + end + + def clear_history + self.history = { } + end + + def modload() + + loaded = { } + + mod = mod_from_name(namespace) + + # build the glob to search on + glob = base + glob += '/**' if(recursive) + glob += '/*' + ext + + Dir[glob].each { |file| + modold = mod.constants + + begin + if !load(file) + error('Load failed for ' + file) + next + end + rescue LoadError + error('LoadError: ' + $!) + next + end + + added = mod.constants - modold + + if added.length > 1 + error('More than one class added in ' + file) + next + end + + if added.empty? + if history[file] + added = history[file] + else + error('Loaded ' + file + ' but no class added') + next + end + else + added = mod.const_get(added[0]) + end + + loaded[file] = added + } + + self.history.update(loaded) + return loaded.values + end +end end # ModuleLoader/Msf diff --git a/lib/msf/core/UnitTestSuite.rb b/lib/msf/core/UnitTestSuite.rb new file mode 100644 index 0000000000..c8dceadb0e --- /dev/null +++ b/lib/msf/core/UnitTestSuite.rb @@ -0,0 +1,26 @@ +require 'Core' + +module Msf +module Test + +### +# +# FrameworkCoreTestSuite +# ---------------------- +# +# This test suite is used to test the various core components of +# framework-core. +# +### +class FrameworkCoreTestSuite + def self.suite + suite = ::Test::Unit::TestSuite.new + + suite << Msf::Test::DataStoreTestCase.suite + + return suite; + end +end + +end +end diff --git a/lib/msf/core/UserOptions.rb b/lib/msf/core/UserOptions.rb new file mode 100644 index 0000000000..b93cce9f75 --- /dev/null +++ b/lib/msf/core/UserOptions.rb @@ -0,0 +1,387 @@ +require 'resolv' +require 'Core' + +module Msf + +### +# +# DataStoreOption +# --------------- +# +# A data store option is an option that is stored in a data store! It contains +# meta information about the type of option being stored, such as type, as +# well as the option's actual value +# +### +class DataStoreOption + + def initialize(in_name, attrs = []) + self.name = in_name + self.advanced = false + self.required = attrs[0] || false + self.desc = attrs[1] || nil + self.default_value = attrs[2] || nil + self.value = self.default_value + end + + def empty? + return (value == nil) + end + + def required? + return required + end + + def advanced? + return advanced + end + + def valid? + return (empty? and required?) ? false : true + end + + def type?(in_type) + return (type == in_type) + end + + def reset + value = default_value + end + + attr_reader :name, :required, :desc, :default_value, :value + attr_writer :name, :value + attr_accessor :advanced + +protected + + attr_writer :required, :desc, :default_value +end + +### +# +# Core data store option types. The core supported option types are: +# +# OptString - Multi-byte character string +# OptRaw - Multi-byte raw string +# OptBool - Boolean true or false indication +# OptPort - TCP/UDP service port +# OptAddress - IP address or hostname +# OptPath - Path name on disk +# +### + +class OptString < DataStoreOption + def type + return 'string' + end +end + +class OptRaw < DataStoreOption + def type + return 'raw' + end +end + +class OptBool < DataStoreOption + def type + return 'bool' + end + + def valid? + if ((empty? == false) and + (value.match(/^(y|n|t|f|0|1)$/i) == nil)) + return false + end + end + + def is_true? + return (value.match(/^(y|t|1)$/i) != nil) ? true : false + end + + def is_false? + return !is_true? + end +end + +class OptPort < DataStoreOption + def type + return 'port' + end + + def valid? + if ((empty? == false) and + ((value.to_i < 0 or value.to_i > 65535))) + return false + end + + return super + end +end + +class OptAddress < DataStoreOption + def type + return 'address' + end + + def valid? + if (empty? == false) + begin + Resolv.getaddress(value) + rescue + return false + end + end + + return super + end +end + +class OptPath < DataStoreOption + def type + return 'path' + end + + def valid? + if ((empty? == false) and + (File.exists?(value) == false)) + return false + end + + return super + end +end + +### +# +# DataStore +# --------- +# +# The data store's purpose in life is to associate named options +# with arbitrary values at the most simplistic level. Each +# module contains a DataStore that is used to hold the +# various options that the module depends on. Example of options +# that are stored in the DataStore are RHOST and RPORT for +# payloads or exploits that need to connect to a host and +# port, for instance. +# +### +class DataStore < Hash + + # Merges in the supplied options and converts them to a DataStoreOption + # as necessary. + def initialize(opts = {}) + add_options(opts) + end + + # Return the value associated with the supplied name + def [](name) + return get(name) + end + + # Set the value associated with the supplied name + def set(name, value) + option = fetch(name) + + if (option == nil) + return false + end + + option.value = value + + return true + end + + # Return the option associated with the supplied name + def get(name) + return fetch(name) + end + + # Return the value associated with the supplied name + def get_value(name) + return fetch(name).value + end + + # Adds one or more options + def add_options(opts) + return false if (opts == nil) + + opts.each_key { |name| + option = opts[name] + + # Skip flags + next if (name.match(/^_Flag/)) + + if (option.kind_of?(Array)) + option = option.shift.new(name, option) + elsif (!option.kind_of?(DataStoreOption)) + raise ArgumentError, + "The option named #{name} did not come in a compatible format.", + caller + end + + option.name = name + + # If the advanced flag was supplied, flag the new option as being + # an advanced option + if (opts['_FlagAdvanced'] == true) + option.advanced = true + end + + self.store(name, option) + } + end + + # Alias to add advanced options that sets the proper state flag + def add_advanced_options(opts = {}) + opts['_FlagAdvanced'] = true if (opts) + + add_options(opts) + end + + # Make sures that each of the options has a value of a compatible + # format and that all the required options are set + def validate + errors = [] + + each_pair { |name, option| + if (!option.valid?) + errors << name + end + } + + if (errors.empty? == false) + raise OptionValidateError.new(errors), + "One or more options failed to validate", caller + end + + return true + end + + # Enumerates each option name + def each_option(&block) + each_pair(&block) + end + +end + +module Test + +### +# +# DataStoreTestCase +# ----------------- +# +# This class implements some testing routines for ensuring that the data +# store is operating correctly. +# +### +class DataStoreTestCase < ::Test::Unit::TestCase + # Tests the initialization of the DataStore object + def test_initialize + # Make sure initialization works + ds = nil + + assert_block("initialize failed") { + ds = DataStore.new( + 'RHOST' => [ OptAddress, true, 'host.com' ], + 'RPORT' => [ OptPort, true, 1234 ]) + + if (ds == nil) + false + end + + true + } + + # Make sure there are 2 options + assert_equal(2, ds.length, "invalid number of options #{ds.length}") + + # Make sure that the constructor raises an argument error when + # an invalid option is supplied + assert_raise(ArgumentError, "initialize invalid failed") { + DataStore.new( + 'RHOST' => 'invalid'); + } + end + + # Tests getting the value of an option + def test_get + ds = DataStore.new( + 'RPORT' => [ OptPort, true, nil, 1234 ]) + + assert_equal(1234, ds.get_value('RPORT'), + "RPORT does not match") + + ds.set('RPORT', 1235) + + assert_equal(1235, ds.get_value('RPORT'), + "RPORT does not match (2)") + + assert_equal('RPORT', ds['RPORT'].name, + "option name does not match") + end + + # Tests setting the value of an option + def test_set + assert_block("set failed") { + ds = DataStore.new( + 'RHOST' => [ OptAddress ]) + + ds.set('RHOST', 'host.com') + + if (ds.get_value('RHOST') != 'host.com') + false + else + true + end + } + end + + # Tests validation + def test_validate + # Test validating required options + ds = DataStore.new( + 'RHOST' => [ OptAddress, true ], + 'RPORT' => [ OptPort, true ], + 'LIB' => [ OptString ]) + + assert_raise(OptionValidateError, "required validation failed") { + ds.validate + } + + # Test validating the form of individual options + ds.set('RHOST', 'www.invalid.host.tldinvalid') + ds.set('RPORT', 1234) + + assert_raise(OptionValidateError, "host validation failed") { + ds.validate + } + + # Make sure address validation does work + ds.set('RHOST', 'www.google.com') + + assert_equal(true, ds.validate, "overall validation failed") + + # Make sure port validation does work + ds.set('RPORT', 123452) + + assert_raise(OptionValidateError, "port validation failed") { + ds.validate + } + end + + # Make sure advanced additions work + def test_advanced + ds = DataStore.new + + ds.add_advanced_options( + 'DONKEY' => [ OptString, false ]) + + assert_equal(true, ds.get('DONKEY').advanced?, + "advanced option failed") + end +end + +end + +end diff --git a/lib/msf/core/constants.rb b/lib/msf/core/constants.rb new file mode 100644 index 0000000000..ec7e80be70 --- /dev/null +++ b/lib/msf/core/constants.rb @@ -0,0 +1,26 @@ +### +# +# This file contains constants that are referenced by the core +# framework and by framework modules. +# +### + +module Msf + +# +# Architecture constants +# +ARCH_IA32 = 'ia32' +ARCH_MIPS = 'mips' +ARCH_PPC = 'ppc' +ARCH_SPARC = 'sparc' + +# +# Module types +# +MODULE_ENCODER = 'encoder' +MODULE_EXPLOIT = 'exploit' +MODULE_NOP = 'nop' +MODULE_RECON = 'recon' + +end diff --git a/lib/msf/core/data_store.rb b/lib/msf/core/data_store.rb new file mode 100644 index 0000000000..f6b4e1925d --- /dev/null +++ b/lib/msf/core/data_store.rb @@ -0,0 +1,14 @@ +module Msf + +### +# +# DataStore +# --------- +# +# The data store is just a bitbucket that holds keyed values. +# +### +class DataStore < Hash +end + +end diff --git a/lib/msf/core/encoder.rb b/lib/msf/core/encoder.rb new file mode 100644 index 0000000000..98a7c50f5e --- /dev/null +++ b/lib/msf/core/encoder.rb @@ -0,0 +1,264 @@ +require 'Core' + +module Msf + +### +# +# EncoderState +# ------------ +# +# This class is used to track the state of a single encoding operation +# from start to finish. +# +### +class EncoderState + + def initialize(key = nil) + reset(key) + end + + # Reset the encoder state + def reset(key = nil) + init_key(key) + + self.encoded = '' + end + + # Set the initial encoding key + def init_key(key) + self.key = key + self.orig_key = key + end + + attr_accessor :key + attr_accessor :orig_key + attr_accessor :encoded + attr_accessor :context + +end + +### +# +# Encoder +# ------- +# +# This class is the base class that all encoders inherit from. +# +### +class Encoder < Module + + def initialize(info) + super(info) + end + + # + # Encoder information accessors that can be overriden + # by derived classes + # + + def type + return MODULE_ENCODER + end + + def decoder_stub + return module_info['DecoderStub'] + end + + def decoder_key_offset + return module_info['DecoderKeyOffset'] + end + + def decoder_key_size + return module_info['DecoderKeySize'] + end + + def decoder_block_size + return module_info['DecoderBlockSize'] + end + + def decoder_key_pack + return module_info['DecoderKeyPack'] || 'V' + end + + # + # Encoding + # + + def encode(buf, badchars, state = nil) + # Initialize the encoding state and key as necessary + if (state == nil) + state = EncoderState.new + end + + # Prepend data to the buffer as necessary + buf = prepend_buf + buf + + # If this encoder is key-based and we don't already have a key, find one + if ((decoder_key_size) and + (state.key == nil)) + # Find a key that doesn't contain and wont generate any bad + # characters + state.init_key(find_key(buf, badchars)) + + if (state.key == nil) + raise NoKeyException, "A key could not be found for the #{self.name} encoder.", caller + end + end + + # Call encode_begin to do any encoder specific pre-processing + encode_begin(state) + + # Perform the actual encoding operation with the determined state + do_encode(buf, badchars, state) + + # Call encoded_end to do any encoder specific post-processing + encode_end(state) + + # Return the encoded buffer to the caller + return state.encoded + end + + def do_encode(buf, badchars, state) + # Copy the decoder stub since we may need to modify it + stub = decoder_stub.dup + + if (state.key != nil) + # Substitute the decoder key in the copy of the decoder stub with the + # one that we found + stub[decoder_key_offset,decoder_key_size] = [ state.key.to_i ].pack(decoder_key_pack) + end + + # Walk the buffer encoding each block along the way + offset = 0 + + while (offset < buf.length) + block = buf[offset, decoder_block_size] + + state.encoded += encode_block(state, + block + ("\x00" * (decoder_block_size - block.length))) + + offset += decoder_block_size + end + + # Prefix the decoder stub to the encoded buffer + state.encoded = stub + state.encoded + + # Last but not least, do one last badchar pass to see if the stub + + # encoded payload leads to any bad char issues... + if ((badchar_idx = has_badchars?(state.encoded, badchars)) != nil) + raise BadcharException.new(state.encoded, badchar_idx, stub.length, badchars[badchar_idx]), + "The #{self.name} encoder failed to encode without bad characters.", + caller + end + + return true + end + + # + # Buffer management + # + + def prepend_buf + return '' + end + + # + # Pre-processing, post-processing, and block encoding stubs + # + + def encode_begin(state) + return nil + end + + def encode_end(state) + return nil + end + + def encode_block(state, block) + return block + end + +protected + + def find_key(buf, badchars) + key_bytes = [ ] + cur_key = [ ] + bad_keys = find_bad_keys(buf, badchars) + found = false + + # Keep chugging until we find something...right + while (!found) + # Scan each byte position + 0.upto(decoder_key_size - 1) { |index| + cur_key[index] = rand(255) + + # Scan all 255 bytes (wrapping around as necessary) + for cur_char in (cur_key[index] .. (cur_key[index] + 255)) + cur_char = (cur_char % 255) + 1 + + # If this is a known bad character at this location in the + # key or it doesn't pass the bad character check... + if (((bad_keys != nil) and + (bad_keys[index][cur_char] == true)) or + (badchars.index(cur_char) != nil)) + next + end + + key_bytes[index] = cur_char + end + } + + # Assume that we're going to rock this shit... + found = true + + # Scan each byte and see what we've got going on to make sure + # no funny business is happening + key_bytes.each { |byte| + if (badchars.index(byte) != nil) + found = false + end + } + end + + # Do we have all the key bytes accounted for? + if (key_bytes.length != decoder_key_size) + return nil + end + + return key_bytes_to_integer(key_bytes) + end + + def find_bad_keys + return [ {}, {}, {}, {} ] + end + + def has_badchars?(buf, badchars) + badchars.each_byte { |badchar| + idx = buf.index(badchar) + + if (idx != nil) + return idx + end + } + + return nil + end + + # Convert individual key bytes into a single integer based on the + # decoder's key size and packing requirements + def key_bytes_to_integer(key_bytes) + return key_bytes.pack('C' + decoder_key_size.to_s).unpack(decoder_key_pack)[0] + end + + # Convert an integer into the individual key bytes based on the + # decoder's key size and packing requirements + def integer_to_key_bytes(integer) + return [ integer.to_i ].pack(decoder_key_pack).unpack('C' + decoder_key_size.to_s) + end + +end + +end + +require 'Core/Encoder/Xor' +require 'Core/Encoder/XorAdditiveFeedback' diff --git a/lib/msf/core/encoder/xor.rb b/lib/msf/core/encoder/xor.rb new file mode 100644 index 0000000000..27a86ac729 --- /dev/null +++ b/lib/msf/core/encoder/xor.rb @@ -0,0 +1,36 @@ +require 'Core' + +### +# +# Xor +# --- +# +# This class provides basic XOR encoding of buffers. +# +### +class Msf::Encoder::Xor < Msf::Encoder + + def encode_block(state, block) + return Msf::Encoding::Xor.encode_block(state.key, block, decoder_block_size, decoder_key_pack) + end + + def find_bad_keys(buf, badchars) + bad_keys = [ {}, {}, {}, {} ] + byte_idx = 0 + + # Scan through all the badchars and build out the bad_keys array + # based on the XOR'd combinations that can occur at certain bytes + # to produce bad characters + badchars.each_byte { |badchar| + buf.each_byte { |byte| + bad_keys[byte_idx % decoder_key_size][byte ^ badchar] = true + + byte_idx += 1 + } + } + + return bad_keys + end + + +end diff --git a/lib/msf/core/encoder/xor_additive_feedback.rb b/lib/msf/core/encoder/xor_additive_feedback.rb new file mode 100644 index 0000000000..20c44998f9 --- /dev/null +++ b/lib/msf/core/encoder/xor_additive_feedback.rb @@ -0,0 +1,82 @@ +require 'Core' + +### +# +# XorAdditiveFeedback +# ------------------- +# +# This class performs per-block XOR additive feedback encoding. +# +### +class Msf::Encoder::XorAdditiveFeedback < Msf::Encoder::Xor + + def initialize(info) + super(info) + end + + def encode_block(state, block) + # XOR the key with the current block + orig = block.unpack(decoder_key_pack)[0] + oblock = orig ^ state.key + + # Add the original block contents to the key + state.key = (state.key + orig) % (1 << (decoder_key_size * 8)) + + # Return the XOR'd block + return [ oblock ].pack(decoder_key_pack) + end + + def find_key(buf, badchars) + key_bytes = integer_to_key_bytes(super(buf, badchars)) + state = Msf::EncoderState.new + valid = false + + # Save the original key_bytes so we can tell if we loop around + orig_key_bytes = key_bytes.dup + + # While we haven't found a valid key, keep trying the encode operation + while (!valid) + begin + # Reset the encoder state's key to the current set of key bytes + state.reset(key_bytes_to_integer(key_bytes)) + + # If the key itself contains a bad character, throw the bad + # character exception with the index of the bad character in the + # key. Use a stub_size of zero to bypass the check to in the + # rescue block. + if ((idx = has_badchars?([state.key.to_i].pack(decoder_key_pack), badchars)) != nil) + raise Msf::BadcharException.new(nil, idx, 0, nil) + end + + # Perform the encode operation...if it encounters a bad character + # an exception will be thrown + valid = do_encode(buf, badchars, state) + rescue Msf::BadcharException => info + # If the decoder stub contains a bad character, then there's not + # much we can do about it + if (info.index < info.stub_size) + raise info, "The #{self.name} decoder stub contains a bad character.", caller + end + + # Determine the actual index to the bad character inside the + # encoded payload by removing the decoder stub from the index and + # modulus off the decoder's key size + idx = (info.index - info.stub_size) % (decoder_key_size) + + # Increment the key byte at the index that the bad character was + # detected + key_bytes[idx] = ((key_bytes[idx] + 1) % 255) + + # If we looped around, then give up. + if (key_bytes[idx] == orig_key_bytes[idx]) + raise info, "The #{self.name} encoder failed to encode without bad characters.", + caller + end + end + end + + # Return the original key + return state.orig_key + end + +end diff --git a/lib/msf/core/encoding/xor.rb b/lib/msf/core/encoding/xor.rb new file mode 100644 index 0000000000..41412e77d9 --- /dev/null +++ b/lib/msf/core/encoding/xor.rb @@ -0,0 +1,30 @@ +module Msf +module Encoding + +### +# +# Xor +# --- +# +# This class provides basic XOR encoding facilities and is used +# by XOR encoders. +# +### +class Xor + + def Xor.encode_block(key, block, block_size = 4, block_pack = 'V') + offset = 0 + oblock = '' + + while (offset < block.length) + cblock = block[offset, block_size].unpack(block_pack)[0] + cblock ^= key + oblock += [ cblock ].pack(block_pack) + end + + return oblock + end + +end + +end end diff --git a/lib/msf/core/event_dispatcher.rb b/lib/msf/core/event_dispatcher.rb new file mode 100644 index 0000000000..ddcadd62b5 --- /dev/null +++ b/lib/msf/core/event_dispatcher.rb @@ -0,0 +1,120 @@ +require 'Core' + +module Msf + +### +# +# EventDispatcher +# --------------- +# +# This class manages subscriber registration and is the entry point +# for dispatching various events that occur for modules, such as +# recon discovery and exploit success or failure. The framework +# and external modules can register themselves as subscribers to +# various events such that they can perform custom actions when +# a specific event or events occur. +# +### +class EventDispatcher + + def initialize + self.exploit_event_subscribers = [] + self.session_event_subscribers = [] + self.recon_event_subscribers = [] + self.subscribers_rwlock = ReadWriteLock.new + end + + # + # Subscriber registration + # + + def add_recon_subscriber(subscriber) + add_event_subscriber(recon_event_subscribers, subscriber) + end + + def remove_recon_subscriber(subscriber) + remove_event_subscriber(recon_event_subscribers, subscriber) + end + + def add_exploit_subscriber(subscriber) + add_event_subscriber(exploit_event_subscribers, subscriber) + end + + def remove_exploit_subscriber(subscriber) + remove_event_subscriber(exploit_event_subscribers, subscriber) + end + + def add_session_subscriber(subscriber) + add_event_subscriber(session_event_subscribers, subscriber) + end + + def remove_session_subscriber(subscriber) + remove_event_subscriber(session_event_subscribers, subscriber) + end + + # + # Event dispatching entry point + # + + def on_recon_discovery(group, info) + subscribers_rwlock.synchronize_read { + recon_event_subscribers.each { |subscriber| + subscriber.on_recon_discovery(group, info) + } + } + end + + def on_exploit_success(exploit, target) + subscribers_rwlock.synchronize_read { + exploit_event_subscribers.each { |subscriber| + subscriber.on_exploit_success(exploit, target) + } + } + end + + def on_exploit_failure(exploit, target) + subscribers_rwlock.synchronize_read { + exploit_event_subscribers.each { |subscriber| + subscriber.on_exploit_failure(exploit, target) + } + } + end + + def on_session_open(session) + subscribers_rwlock.synchronize_read { + session_event_subscribers.each { |subscriber| + subscriber.on_session_open(session) + } + } + end + + def on_session_close(session) + subscribers_rwlock.synchronize_read { + session_event_subscribers.each { |subscriber| + subscriber.on_session_close(session) + } + } + end + +protected + + def add_event_subscriber(array, subscriber) + subscribers_rwlock.synchronize_write { + array << subscriber + } + end + + def remove_event_subscriber(array, subscriber) + subscribers_rwlock.synchronize_write { + array.delete(subscriber) + } + end + + attr_accessor :exploit_event_subscribers + attr_accessor :session_event_subscribers + attr_accessor :recon_event_subscribers + attr_accessor :subscribers_rwlock + +end + +end diff --git a/lib/msf/core/exceptions.rb b/lib/msf/core/exceptions.rb new file mode 100644 index 0000000000..12c4fbb1d8 --- /dev/null +++ b/lib/msf/core/exceptions.rb @@ -0,0 +1,54 @@ +module Msf + +class EncodingException < RuntimeError +end + +### +# +# NoKeyException +# -------------- +# +# Thrown when an encoder fails to find a viable encoding key. +# +### +class NoKeyException < EncodingException +end + +### +# +# BadcharException +# ---------------- +# +# Thrown when an encoder fails to encode a buffer due to a bad character. +# +### +class BadcharException < EncodingException + def initialize(buf, index, stub_size, char) + @buf = buf + @index = index + @stub_size = stub_size + @char = char + end + + attr_reader :buf, :index, :stub_size, :char +end + +### +# +# MissingOptionError +# ------------------ +# +# This exception is thrown when one or more options failed +# to pass data store validation. The list of option names +# can be obtained through the options attribute. +# +### +class OptionValidateError < ArgumentError + def initialize(options) + @options = options + end + + attr_reader :options +end + +end diff --git a/lib/msf/core/exploit.rb b/lib/msf/core/exploit.rb new file mode 100644 index 0000000000..3841cfe902 --- /dev/null +++ b/lib/msf/core/exploit.rb @@ -0,0 +1,15 @@ +require 'Core' + +module Msf + +module ExploitEvents + + def on_exploit_success(exploit, target) + end + + def on_exploit_failure(exploit, target) + end + +end + +end diff --git a/lib/msf/core/framework.rb b/lib/msf/core/framework.rb new file mode 100644 index 0000000000..dc90389501 --- /dev/null +++ b/lib/msf/core/framework.rb @@ -0,0 +1,49 @@ +require 'Core' + +module Msf + +### +# +# Framework +# --------- +# +# This class is the primary context that modules, scripts, and user +# interfaces interact with. It ties everything together. +# +### +class Framework + + include Msf::Logging::LogDispatcher + + def initialize() + self.events = EventDispatcher.new +# self.encoders = EncoderManager.new +# self.exploits = ExploitManager.new +# self.nops = NopManager.new +# self.payloads = PayloadManager.new +# self.recon = ReconManager.new + + super + end + + attr_reader :events + attr_reader :ui + attr_reader :encoders + attr_reader :exploits + attr_reader :nops + attr_reader :payloads + attr_reader :recon + +protected + + attr_writer :events + attr_writer :ui + attr_writer :encoders + attr_writer :exploits + attr_writer :nops + attr_writer :payloads + attr_writer :recon + +end + +end diff --git a/lib/msf/core/module.rb b/lib/msf/core/module.rb new file mode 100644 index 0000000000..ec150f0891 --- /dev/null +++ b/lib/msf/core/module.rb @@ -0,0 +1,115 @@ +require 'Core' + +module Msf + +### +# +# Module +# ------ +# +# The module base class is responsible for providing the common interface +# that is used to interact with modules at the most basic levels, such as +# by inspecting a given module's attributes (name, dsecription, version, +# authors, etc) and by managing the module's data store. +# +### +class Module + + def initialize(info) + self.module_info = info || {} + + set_defaults + + # Transform some of the fields to arrays as necessary + self.author = Transformer.transform(module_info['Author'], Array, + [ Author ], 'Author') + self.arch = Transformer.transform(module_info['Arch'], Array, + [ String ], 'Arch') + self.platform = Transformer.transform(module_info['Platform'], Array, + [ String ], 'Platform') + self.refs = Transformer.transform(module_info['Ref'], Array, + [ SiteReference, Reference ], 'Ref') + + # Create and initialize the datastore for this module + self.datastore = DataStore.new + self.datastore.add_options(info['Options']) + self.datastore.add_advanced_options(info['AdvancedOptions']) + end + + # Return the module's name + def name + return module_info['Name'] + end + + # Return the module's description + def description + return module_info['Description'] + end + + # Return the module's version information + def version + return module_info['Version'] + end + + # Return the module's abstract type + def type + raise NotImplementedError + end + + # Return a comma separated list of author for this module + def author_to_s + return author.collect { |author| author.to_s }.join(", ") + end + + # Enumerate each author + def each_author(&block) + author.each(&block) + end + + # Return a comma separated list of supported architectures, if any + def arch_to_s + return arch.join(", ") + end + + # Enumerate each architecture + def each_arch(&block) + arch.each(&block) + end + + # Return whether or not the module supports the supplied architecture + def arch?(what) + return arch.index(what) != nil + end + + # Return a comma separated list of supported platforms, if any + def platform_to_s + return platform.join(", ") + end + + attr_reader :author, :arch, :platform, :refs, :datastore + +protected + + # Sets the modules unsupplied info fields to their default values + def set_defaults + { + 'Name' => 'No module name', + 'Description' => 'No module description', + 'Version' => '0', + 'Author' => nil, + 'Arch' => nil, + 'Platform' => nil, + 'Ref' => nil + }.each_pair { |field, value| + if (module_info[field] == nil) + module_info[field] = value + end + } + end + + attr_accessor :module_info + attr_writer :author, :arch, :platform, :refs, :datastore + +end + +end diff --git a/lib/msf/core/nop.rb b/lib/msf/core/nop.rb new file mode 100644 index 0000000000..4716fac358 --- /dev/null +++ b/lib/msf/core/nop.rb @@ -0,0 +1,40 @@ +require 'Core' + +module Msf + +### +# +# Nop +# --- +# +# This class acts as the base class for all nop generators. +# +### +class Nop < Msf::Module + + # + # Stub method for generating a sled with the provided arguments. Derived + # Nop implementations must supply a length and can supply one or more of + # the following options: + # + # - Random (true/false) + # Indicates that the caller desires random NOPs (if supported). + # - SaveRegisters (array) + # The list of registers that should not be clobbered by the NOP + # generator. + # - Badchars (string) + # The list of characters that should be avoided by the NOP + # generator. + # + def generate_sled(length, opts) + return nil + end + + # Default repetition threshold when find nop characters + def nop_repeat_threshold + return 10000 + end + +end + +end diff --git a/lib/msf/core/recon.rb b/lib/msf/core/recon.rb new file mode 100644 index 0000000000..90cda04e5a --- /dev/null +++ b/lib/msf/core/recon.rb @@ -0,0 +1,21 @@ +module Msf + +### +# +# ReconEvents +# ----------- +# +# This interface is called by recon modules to notify the framework when +# network elements, services, or other types of things recon modules +# might discovery. +# +### +module ReconEvents + + def on_recon_discovery(group, info) + return nil + end + +end + +end diff --git a/lib/msf/core/session.rb b/lib/msf/core/session.rb new file mode 100644 index 0000000000..251af02691 --- /dev/null +++ b/lib/msf/core/session.rb @@ -0,0 +1,25 @@ +require 'Core' + +module Msf + +module SessionEvents + + def on_session_open(session) + end + + def on_session_close(session) + end + +end + +### +# +# Session +# ------- +# +# +### +class Session +end + +end diff --git a/lib/msf/shared/Constants.rb b/lib/msf/shared/Constants.rb new file mode 100644 index 0000000000..75cb3c4b03 --- /dev/null +++ b/lib/msf/shared/Constants.rb @@ -0,0 +1,20 @@ +module Msf + +# +# Log severities +# +LOG_ERROR = 'error' +LOG_DEBUG = 'debug' +LOG_INFO = 'info' +LOG_WARN = 'warn' +LOG_RAW = 'raw' + +# +# Log levels +# +LEV_0 = 0 +LEV_1 = 1 +LEV_2 = 2 +LEV_3 = 3 + +end diff --git a/lib/msf/shared/ReadWriteLock.rb b/lib/msf/shared/ReadWriteLock.rb new file mode 100644 index 0000000000..8c540d2886 --- /dev/null +++ b/lib/msf/shared/ReadWriteLock.rb @@ -0,0 +1,163 @@ +require 'thread' + +module Msf + +### +# +# ReadWriteLock +# ------------- +# +# This class implements a read/write lock synchronization +# primitive. It is meant to allow for more efficient access to +# resources that are more often read from than written to and many +# times can have concurrent reader threads. By allowing the reader +# threads to lock the resource concurrently rather than serially, +# a large performance boost can be seen. Acquiring a write lock +# results in exclusive access to the resource and thereby prevents +# any read operations during the time that a write lock is acquired. +# Only one write lock may be acquired at a time. +# +### +class ReadWriteLock + + def initialize + @read_sync_mutex = Mutex.new + @write_sync_mutex = Mutex.new + @exclusive_mutex = Mutex.new + @readers = 0 + @writer = false + end + + # Acquires the read lock for the calling thread + def lock_read + read_sync_mutex.lock + + begin + # If there are a non-zero number of readers and a + # writer is waiting to acquire the exclusive lock, + # free up the sync mutex temporarily and lock/unlock + # the exclusive lock. This is to give the writer + # thread a chance to acquire the lock and prevents + # it from being constantly starved. + if ((@readers > 0) and + (@writer)) + read_sync_mutex.unlock + exclusive_mutex.lock + exclusive_mutex.unlock + read_sync_mutex.lock + end + + # Increment the active reader count + @readers += 1 + + # If we now have just one reader, acquire the exclusive + # lock. Track the thread owner so that we release the + # lock from within the same thread context later on. + if (@readers == 1) + exclusive_mutex.lock + + @owner = Thread.current + end + ensure + read_sync_mutex.unlock + end + end + + # Releases the read lock for the calling thread + def unlock_read + read_sync_mutex.lock + + begin + unlocked = false + + # Keep looping until we've lost this thread's reader + # lock + while (!unlocked) + # If there are no more readers left after this one + if (@readers - 1 == 0) + # If the calling thread is the owner of the exclusive + # reader lock, then let's release that shit! + if (Thread.current == @owner) + @owner = nil + + exclusive_mutex.unlock + end + # If there is more than one reader left and this thread is + # the owner of the exclusive lock, then keep looping so that + # we can eventually unlock the exclusive mutex in this thread's + # context + elsif (Thread.current == @owner) + read_sync_mutex.unlock + + next + end + + # Unlocked! + unlocked = true + + # Decrement the active reader count + @readers -= 1 + end + ensure + read_sync_mutex.unlock + end + end + + # Acquire the exclusive write lock + def lock_write + write_sync_mutex.lock + + begin + @writer = true + + exclusive_mutex.lock + + @owner = Thread.current + ensure + write_sync_mutex.unlock + end + end + + # Release the exclusive write lock + def unlock_write + # If the caller is not the owner of the write lock, then someone is + # doing something broken, let's let them know. + if (Thread.current != @owner) + raise RuntimeError, "Non-owner calling thread attempted to release write lock", caller + end + + # Otherwise, release the exclusive write lock + @writer = false + + exclusive_mutex.unlock + end + + # Synchronize a block for read access + def synchronize_read + lock_read + begin + yield + ensure + unlock_read + end + end + + # Synchronize a block for write access + def synchronize_write + lock_write + begin + yield + ensure + unlock_write + end + end + +protected + + attr_accessor :read_sync_mutex + attr_accessor :write_sync_mutex + attr_accessor :exclusive_mutex + +end + +end diff --git a/lib/msf/shared/Transformer.rb b/lib/msf/shared/Transformer.rb new file mode 100644 index 0000000000..2c28d58ae1 --- /dev/null +++ b/lib/msf/shared/Transformer.rb @@ -0,0 +1,87 @@ +module Msf + +### +# +# Transformer - more than meets the eye! +# ----------- +# +# This class, aside from having a kickass name, is responsible for translating +# object instances of one or more types into a single list instance of one or +# more types. This is useful for translating object instances that be can +# either strings or an array of strings into an array of strings, for +# instance. It lets you make things take a uniform structure in an abstract +# manner. +# +### +class Transformer + + # Translates the object instance supplied in src_instance to an instance of + # dst_class. The dst_class parameter's instance must support the << + # operator. An example call to this method looks something like: + # + # Transformer.transform(string, Array, [ String ], target) + def Transformer.transform(src_instance, dst_class, supported_classes, + target = nil) + dst_instance = dst_class.new + + if (src_instance.kind_of?(Array)) + src_instance.each { |src_inst| + Transformer.transform_single(src_inst, dst_instance, + supported_classes, target) + } + elsif (!src_instance.kind_of?(NilClass)) + Transformer.transform_single(src_instance, dst_instance, + supported_classes, target) + end + + return dst_instance + end + +protected + + # Transform a single source instance. + def Transformer.transform_single(src_instance, dst_instance, + supported_classes, target) + # If the src instance's class is supported, just add it to the dst + # instance + if (supported_classes.include?(src_instance.class)) + dst_instance << src_instance + # If the source instance is a string, query each of the supported + # classes to see if they can serialize it to their particular data + # type. + elsif (src_instance.kind_of?(String)) + new_src_instance = nil + + # Walk each supported class calling from_s if exported + supported_classes.each { |sup_class| + new_src_instance = sup_class.from_s(src_instance) + + if (new_src_instance != nil) + dst_instance << new_src_instance + break + end + } + + # If we don't have a valid new src instance, then we suck + if (new_src_instance == nil) + bomb_translation(src_instance, target) + end + # Otherwise, bomb translation + else + bomb_translation(src_instance, target) + end + end + + def Transformer.bomb_translation(src_instance, target) + error = "Invalid source class (#{src_instance.class})" + + if (target != nil) + error += " for #{target}" + end + + raise ArgumentError, error, caller + end + +end + +end diff --git a/lib/msf/shared/logging/LogDispatcher.rb b/lib/msf/shared/logging/LogDispatcher.rb new file mode 100644 index 0000000000..9b95e5c3ca --- /dev/null +++ b/lib/msf/shared/logging/LogDispatcher.rb @@ -0,0 +1,68 @@ +require 'Shared' + +module Msf +module Logging + +### +# +# LogDispatcher +# ------------- +# +# This interface is included in the Framework class and is used to provide a +# common interface for dispatching logs to zero or more registered log sinks. +# Log sinks are typically backed against arbitrary storage mediums, such as a +# flatfile, a database, or the console. The log dispatcher itself is really +# just a log sink that backs itself against zero or more log sinks rather than +# a file, database, or other persistent storage. +# +### +module LogDispatcher + + include Msf::Logging::LogSink + + def initialize() + initialize_log_dispatcher + end + + # + # Log sink registration + # + + def add_log_sink(sink) + log_sinks_rwlock.synchronize_write { + log_sinks << sink + } + end + + def remove_log_sink(sink) + log_sinks_rwlock.synchronize_write { + sink.cleanup + + log_sinks.delete(sink) + } + end + + # + # Log dispatching + # +protected + + def initialize_log_dispatcher + self.log_sinks = [] + self.log_sinks_rwlock = ReadWriteLock.new + end + + def log(sev, level, msg, from) + log_sinks_rwlock.synchronize_read { + log_sinks.each { |sink| + sink.dispatch_log(sev, level, msg, from) + } + } + end + + attr_accessor :log_sinks + attr_accessor :log_sinks_rwlock + +end + +end; end diff --git a/lib/msf/shared/logging/LogSink.rb b/lib/msf/shared/logging/LogSink.rb new file mode 100644 index 0000000000..a9357e418c --- /dev/null +++ b/lib/msf/shared/logging/LogSink.rb @@ -0,0 +1,55 @@ +require 'Shared/Constants' + +module Msf +module Logging + +### +# +# LogSink +# ------- +# +# This abstract interface is what must be implemented by any class +# that would like to register as a log sink on a given LogDispatcher +# instance, such as the Framework object. +# +### +module LogSink + + def cleanup + end + + def dlog(level, msg, from = caller) + log(LOG_DEBUG, level, msg, from) + end + + def elog(level, msg, from = caller) + log(LOG_ERROR, level, msg, from) + end + + def wlog(level, msg, from = caller) + log(LOG_WARN, level, msg, from) + end + + def ilog(level, msg, from = caller) + log(LOG_INFO, level, msg, from) + end + + def rlog(msg) + log(LOG_RAW, 0, msg, nil) + end + +protected + + def log(sev, level, msg, from) + raise NotImplementedError + end + + def get_current_timestamp + return Time.now.strftime("%m/%d/%Y %H:%M:%S") + end + +end + +end; end + +require 'Shared/Logging/Sinks/Flatfile' diff --git a/lib/msf/shared/logging/sinks/Flatfile.rb b/lib/msf/shared/logging/sinks/Flatfile.rb new file mode 100644 index 0000000000..30af7f301a --- /dev/null +++ b/lib/msf/shared/logging/sinks/Flatfile.rb @@ -0,0 +1,44 @@ +module Msf +module Logging +module Sinks + +### +# +# Flatfile +# -------- +# +# This class implements the LogSink interface and backs it against a +# file on disk. +# +### +class Flatfile + + include Msf::Logging::LogSink + + def initialize(file) + self.fd = File.new(file, "a") + + ilog(0, "Logging initialized.") + end + + def cleanup + ilog(0, "Logging finished.") + + fd.close + end + +protected + + def log(sev, level, msg, from) + if (sev == LOG_RAW) + fd.write(msg) + else + fd.write("[#{get_current_timestamp}] (sev=#{sev},level=#{level}): #{msg}\n") + end + end + + attr_accessor :fd + +end + +end; end; end