# -*- coding: binary -*-
###
#
#
###

module Msf
module Util

#
# The class provides helper methods for verifying and updating the embedded CachedSize
# constant within payload modules.
#

class PayloadCachedSize

  OPTS = {
    'Format'      => 'raw',
    'Options'     => {
      'CPORT' => 4444,
      'LPORT' => 4444,
      'CMD' => '/bin/sh',
      'URL' => 'http://a.com',
      'PATH' => '/',
      'BUNDLE' => 'data/isight.bundle',
      'DLL' => 'external/source/byakugan/bin/XPSP2/detoured.dll',
      'RC4PASSWORD' => 'Metasploit',
      'DNSZONE' => 'corelan.eu',
      'PEXEC' => '/bin/sh',
      'HttpUserAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0',
      'StagerURILength' => 5
    },
    'Encoder'     => nil,
    'DisableNops' => true
  }

  OPTS_ARCH_X64 = {
    'DLL' => 'data/vncdll.x64.dll',
    'PE' => 'data/vncdll.x64.dll'
  }.freeze

  OPTS_ARCH_X86 = {
    'DLL' => 'data/vncdll.x86.dll',
    'PE' => 'data/vncdll.x86.dll'
  }.freeze

  OPTS_IPV4 = {
    'LHOST' => '255.255.255.255',
    'KHOST' => '255.255.255.255',
    'AHOST' => '255.255.255.255'
  }.freeze

  OPTS_IPV6 = {
    'LHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
    'KHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
    'AHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'
  }.freeze

  # Insert a new CachedSize value into the text of a payload module
  #
  # @param data [String] The source code of a payload module
  # @param cached_size [String] The new value for cached_size, which
  #   which should be either numeric or the string :dynamic
  # @return [String]
  def self.update_cache_constant(data, cached_size)
    data.
      gsub(/^\s*CachedSize\s*=\s*(\d+|:dynamic).*/, '').
      gsub(/^(module MetasploitModule)\s*\n/) do |m|
        "#{m.strip}\n\n  CachedSize = #{cached_size}\n\n"
      end
  end

  # Insert a new CachedSize value into a payload module file
  #
  # @param mod [Msf::Payload] The class of the payload module to update
  # @param cached_size [String] The new value for cached_size, which
  #   which should be either numeric or the string :dynamic
  # @return [void]
  def self.update_cached_size(mod, cached_size)
    mod_data = ""

    ::File.open(mod.file_path, 'rb') do |fd|
      mod_data = fd.read(fd.stat.size)
    end

    ::File.open(mod.file_path, 'wb') do |fd|
      fd.write update_cache_constant(mod_data, cached_size)
    end
  end

  # Updates the payload module specified with the current CachedSize
  #
  # @param mod [Msf::Payload] The class of the payload module to update
  # @return [void]
  def self.update_module_cached_size(mod)
    update_cached_size(mod, compute_cached_size(mod))
  end

  # Calculates the CachedSize value for a payload module
  #
  # @param mod [Msf::Payload] The class of the payload module to update
  # @return [Integer]
  def self.compute_cached_size(mod)
    return ":dynamic" if is_dynamic?(mod)

    mod.generate_simple(module_options(mod)).size
  end

  # Determines whether a payload generates a static sized output
  #
  # @param mod [Msf::Payload] The class of the payload module to update
  # @param generation_count [Integer] The number of iterations to use to
  #   verify that the size is static.
  # @return [Integer]
  def self.is_dynamic?(mod, generation_count=5)
    opts = module_options(mod)
    [*(1..generation_count)].map do |x|
      mod.generate_simple(opts).size
    end.uniq.length != 1
  end

  # Determines whether a payload's CachedSize is up to date
  #
  # @param mod [Msf::Payload] The class of the payload module to update
  # @return [Boolean]
  def self.is_cached_size_accurate?(mod)
    return true if mod.dynamic_size? && is_dynamic?(mod)
    return false if mod.cached_size.nil?

    mod.cached_size == mod.generate_simple(module_options(mod)).size
  end

  # Get a set of sane default options for the module so it can generate a
  # payload for size analysis.
  #
  # @param mod [Msf::Payload] The class of the payload module to get options for
  # @return [Hash]
  def self.module_options(mod)
    opts = OPTS.clone
    # Assign this way to overwrite the Options key of the newly cloned hash
    opts['Options'] = opts['Options'].merge(mod.shortname =~ /6/ ? OPTS_IPV6 : OPTS_IPV4)
    # Extract the AdaptedArch for adaptor payloads, note `mod.adapted_arch` is not part of the public API
    # at this time, but could be in the future. The use of send is safe for now as it is an internal tool
    # with automated tests if the API were to change in the future
    adapted_arch = mod.send(:module_info)['AdaptedArch']
    if adapted_arch == ARCH_X64 || mod.arch_to_s == ARCH_X64
      opts['Options'].merge!(OPTS_ARCH_X64)
    elsif adapted_arch == ARCH_X86 || mod.arch_to_s == ARCH_X86
      opts['Options'].merge!(OPTS_ARCH_X86)
    end
    opts
  end
end

end
end
