Path: blob/master/lib/msf/util/payload_cached_size.rb
57888 views
# -*- coding: binary -*-1###2#3#4###56module Msf7module Util89#10# The class provides helper methods for verifying and updating the embedded CachedSize11# constant within payload modules.12#1314class PayloadCachedSize1516OPTS = {17'Format' => 'raw',18'Options' => {19'VERBOSE' => false,20'CPORT' => 4444,21'LPORT' => 4444,22'RPORT' => 4444,23'CMD' => '/bin/sh',24'URL' => 'http://a.com',25'PATH' => '/',26'BUNDLE' => 'data/isight.bundle',27'DLL' => 'external/source/byakugan/bin/XPSP2/detoured.dll',28'RC4PASSWORD' => 'Metasploit',29'DNSZONE' => 'corelan.eu',30'PEXEC' => '/bin/sh',31'HttpUserAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0',32'StagerURILength' => 533},34'Encoder' => nil,35'DisableNops' => true36}3738OPTS_ARCH_X64 = {39'DLL' => 'data/vncdll.x64.dll',40'PE' => 'data/vncdll.x64.dll'41}.freeze4243OPTS_ARCH_X86 = {44'DLL' => 'data/vncdll.x86.dll',45'PE' => 'data/vncdll.x86.dll'46}.freeze4748OPTS_IPV4 = {49'LHOST' => '223.255.255.255',50'RHOST' => '255.255.255.255',51'KHOST' => '255.255.255.255',52'AHOST' => '255.255.255.255'53}.freeze5455OPTS_IPV6 = {56'LHOST' => 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',57'RHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',58'KHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',59'AHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'60}.freeze6162# Inserts or updates the CachedSize constant in the text of a payload module.63#64# @param data [String] The source code of a payload module65# @param cached_size [String, Integer] The new value for CachedSize, which should be either an integer or the string ":dynamic"66# @return [String] The updated source code with the new CachedSize value67def self.update_cache_constant(data, cached_size)68data.69gsub(/^\s*CachedSize\s*=\s*(\d+|:dynamic).*/, '').70gsub(/^(module MetasploitModule)\s*\n/) do |m|71"#{m.strip}\n CachedSize = #{cached_size}\n\n"72end73end7475# Inserts or updates the CachedSizeOverrides constant in the text of a payload module,76# removing any previous CachedSizeStages, # Other stager sizes, or CachedSizeOverrides lines.77#78# @param data [String] The source code of a payload module79# @param stages_with_sizes [Array<{:stage => Msf::Payload::Stager, :size => Integer}>] Array of hashes with :stage (an Msf::Payload::Stager instance) and :size (Integer)80# @return [String] The updated source code with the new CachedSizeOverrides value81def self.update_stage_sizes_constant(data, stages_with_sizes)82sizes = stages_with_sizes.sort_by { |stage_with_size| stage_with_size[:stage].refname }.map do |stage_with_size|83[stage_with_size[:stage].refname, stage_with_size[:size]]84end85data_without_other_stages = data.gsub(/^\s*CachedSizeOverrides\s*=.*\n/, '')86return data_without_other_stages if sizes.empty?8788data_without_other_stages.gsub(/^\s*(CachedSize\s*=\s*(\d+|:dynamic))\s*\n/) do |m|89" #{m.strip}\n CachedSizeOverrides = {#{sizes.map { |(k, v)| %Q{"#{k}" => #{v}}}.join(', ')}}\n\n"90end91end9293# Insert or update the CachedSize value into a payload module file94#95# @param mod [Msf::Payload] The class of the payload module to update96# @param cached_size [String, Integer] The new value for cached_size, which97# should be either an integer or the string ":dynamic"98# @return [void]99def self.update_cached_size(mod, cached_size)100mod_data = ""101102file_path = mod.file_path103104::File.open(file_path, 'rb') do |fd|105mod_data = fd.read(fd.stat.size)106end107108::File.open(file_path, 'wb') do |fd|109fd.write update_cache_constant(mod_data, cached_size)110end111end112113# Insert or update the CachedSize value into a payload module file114#115# @param mod [Msf::Payload] The class of the payload module to update116# @param stages_with_sizes [Array<{:stage => Msf::Payload::Stager, :size => Integer}>] Array of hashes with :stage (an Msf::Payload::Stager instance) and :size (Integer)117# @return [void]118def self.update_stager_cached_sizes(mod, stages_with_sizes)119mod_data = ""120121file_path = mod.file_path122123::File.open(file_path, 'rb') do |fd|124mod_data = fd.read(fd.stat.size)125end126127::File.open(file_path, 'wb') do |fd|128fd.write update_stage_sizes_constant( mod_data, stages_with_sizes)129end130end131132# Updates the payload module specified with the current CachedSize133#134# @param framework [Msf::Framework] The Metasploit framework instance used for payload generation135# @param mod [Msf::Payload] The class of the payload module to update136# @return [String, Integer] The updated CachedSize value137def self.update_module_cached_size(framework, mod)138cached_size = compute_cached_size(framework, mod)139update_cached_size(mod, cached_size)140cached_size141end142143# Updates the stager payload module with the most frequent CachedSize value and sets CachedSizeOverrides for other stages.144#145# @param framework [Msf::Framework] The Metasploit framework instance used for payload generation146# @param stages [Array<Msf::Payload>] Array of stager modules to update147# @return [Integer, String] The new CachedSize value set for the stager148def self.update_stager_module_cached_size(framework, stages)149stages_with_sizes = stages.map do |stage|150{ stage: stage, size: compute_cached_size(framework, stage) }151end152most_frequent_cached_size = stages_with_sizes.map { |stage_with_size| stage_with_size[:size] }153.select { |size| size.is_a?(Numeric) }.tally.sort_by(&:last).to_h.keys.last154155new_size = most_frequent_cached_size || stages_with_sizes.first[:size]156other_sizes = stages_with_sizes.select { |stage_with_size| stage_with_size[:size] != new_size }157158update_cached_size(stages.first, new_size)159update_stager_cached_sizes(stages.first, other_sizes)160161new_size162end163164# Calculates the CachedSize value for a payload module165#166# @param mod [Msf::Payload] The class of the payload module to update167# @return [Integer, String]168def self.compute_cached_size(framework, mod)169return ":dynamic" if is_dynamic?(framework, mod)170171mod.replicant.generate_simple(module_options(mod)).bytesize172end173174# Determines whether a payload generates a static sized output175#176# @param mod [Msf::Payload] The class of the payload module to update177# @param generation_count [Integer] The number of iterations to use to178# verify that the size is static.179# @return [Boolean]180def self.is_dynamic?(framework, mod, generation_count=10)181return true if mod.class.const_defined?('ForceDynamicCachedSize') && mod.class::ForceDynamicCachedSize182opts = module_options(mod)183last_bytesize = nil184generation_count.times do185# Ensure a new module instance is created for each attempt, as some options are randomized on load - such as tmp file path names etc186new_mod = framework.payloads.create(mod.refname)187bytesize = new_mod.generate_simple(opts).bytesize188last_bytesize ||= bytesize189if last_bytesize != bytesize190return true191end192end193194false195end196197# Determines whether a payload's CachedSize is up to date198#199# @param mod [Msf::Payload] The class of the payload module to update200# @return [Boolean]201def self.is_cached_size_accurate?(framework, mod)202return true if mod.dynamic_size? && is_dynamic?(framework, mod)203return false if mod.cached_size.nil?204205mod.cached_size == mod.replicant.generate_simple(module_options(mod)).bytesize206end207208# Checks for errors or inconsistencies in the CachedSize value for a payload module.209# Returns nil if the cache is correct, or a string describing the error if not.210#211# @param framework [Msf::Framework] The Metasploit framework instance used for payload generation212# @param mod [Msf::Payload] The payload module to check213# @return [String, nil] Error message if there is a problem, or nil if the cache is correct214def self.cache_size_errors_for(framework, mod)215is_payload_size_different_on_each_generation = is_dynamic?(framework,mod)216module_marked_as_dynamic = mod.dynamic_size?217payload_cached_static_size = mod.cached_size218219# Validate dynamic scenario220return if is_payload_size_different_on_each_generation && module_marked_as_dynamic221222if is_payload_size_different_on_each_generation && !module_marked_as_dynamic223return 'Module generated different sizes for each generation attempt. CacheSize must be set to :dynamic'224end225226if payload_cached_static_size.nil?227return 'Module missing CachedSize and not marked as dynamic'228end229230payload_size_after_one_generation = mod.replicant.generate_simple(module_options(mod)).bytesize231232# Validate static scenario233return if payload_cached_static_size == payload_size_after_one_generation234235if payload_cached_static_size != payload_size_after_one_generation236return "Module marked as having size #{payload_cached_static_size} but after one generation was #{payload_size_after_one_generation}"237end238239raise "unhandled scenario"240end241242# Get a set of sane default options for the module so it can generate a243# payload for size analysis.244#245# @param mod [Msf::Payload] The class of the payload module to get options for246# @return [Hash]247def self.module_options(mod)248opts = OPTS.clone249# Assign this way to overwrite the Options key of the newly cloned hash250opts['Options'] = opts['Options'].merge(mod.shortname =~ /6/ ? OPTS_IPV6 : OPTS_IPV4)251# Extract the AdaptedArch for adaptor payloads, note `mod.adapted_arch` is not part of the public API252# at this time, but could be in the future. The use of send is safe for now as it is an internal tool253# with automated tests if the API were to change in the future254adapted_arch = mod.send(:module_info)['AdaptedArch']255if adapted_arch == ARCH_X64 || mod.arch_to_s == ARCH_X64256opts['Options'].merge!(OPTS_ARCH_X64)257elsif adapted_arch == ARCH_X86 || mod.arch_to_s == ARCH_X86258opts['Options'].merge!(OPTS_ARCH_X86)259end260opts261end262end263264end265end266267268