rapid7/metasploit-framework

View on GitHub
modules/post/windows/gather/enum_prefetch.rb

Summary

Maintainability
C
1 day
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Registry

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather Prefetch File Information',
        'Description' => %q{
          This module gathers prefetch file information from WinXP, Win2k3 and Win7 systems
          and current values of related registry keys. From each prefetch file we'll collect
          filetime (converted to utc) of the last execution, file path hash, run count, filename
          and the execution path.
        },
        'License' => MSF_LICENSE,
        'Author' => ['TJ Glad <tjglad[at]cmail.nu>'],
        'Platform' => ['win'],
        'SessionType' => ['meterpreter'],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_search
              stdapi_sys_config_getenv
            ]
          }
        }
      )
    )
  end

  def print_prefetch_key_value
    # Checks if Prefetch registry key exists and what value it has.
    prefetch_key_value = registry_getvaldata('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management\\PrefetchParameters', 'EnablePrefetcher')
    if prefetch_key_value == 0
      print_error('EnablePrefetcher Value: (0) = Disabled (Non-Default).')
    elsif prefetch_key_value == 1
      print_good('EnablePrefetcher Value: (1) = Application launch prefetching enabled (Non-Default).')
    elsif prefetch_key_value == 2
      print_good('EnablePrefetcher Value: (2) = Boot prefetching enabled (Non-Default, excl. Win2k3).')
    elsif prefetch_key_value == 3
      print_good('EnablePrefetcher Value: (3) = Applaunch and boot enabled (Default Value, excl. Win2k3).')
    else
      print_error('No value or unknown value. Results might vary.')
    end
  end

  def print_timezone_key_values(key_value)
    # Looks for timezone information from registry.
    timezone = registry_getvaldata('HKLM\\SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation', key_value)
    tz_bias = registry_getvaldata('HKLM\\SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation', 'Bias')
    if timezone.nil? || tz_bias.nil?
      print_line("Couldn't find key/value for timezone from registry.")
    else
      print_good('Remote: Timezone is %s.' % timezone)
      if tz_bias < 0xfff
        print_good('Remote: Localtime bias to UTC: -%s minutes.' % tz_bias)
      else
        offset = 0xffffffff
        bias = offset - tz_bias
        print_good('Remote: Localtime bias to UTC: +%s minutes.' % bias)
      end
    end
  end

  def gather_pf_info(name_offset, hash_offset, runcount_offset, filetime_offset, filename)
    # Collects the desired information from each prefetch file found
    # from the system.

    prefetch_file = read_file(filename)
    if prefetch_file.blank?
      print_error("Couldn't read file: #{filename}")
      return nil
    else
      # First we extract the saved filename
      pf_filename = prefetch_file[name_offset, 60]
      idx = pf_filename.index("\x00\x00")
      name = Rex::Text.to_ascii(pf_filename.slice(0..idx))

      # Then we get the runcount
      run_count = prefetch_file[runcount_offset, 4].unpack('v')[0]

      # Then the filepath hash
      path_hash = prefetch_file[hash_offset, 4].unpack('h*')[0].upcase.reverse

      # Last we get the latest execution time
      filetime_a = prefetch_file[filetime_offset, 16].unpack('q*')
      filetime = filetime_a[0] + filetime_a[1]
      last_exec = Time.at((filetime - 116444736000000000) / 10000000).utc.to_s

      # This is for reading file paths of the executable from
      # the prefetch file. We'll use this to find out from where the
      # file was executed.

      # First we'll use specific offsets for finding out the location
      # and length of the filepath so that we can find it.
      filepath = []
      fpath_offset = prefetch_file[0x64, 2].unpack('v').first
      fpath_length = prefetch_file[0x68, 2].unpack('v').first
      filepath_data = prefetch_file[fpath_offset, fpath_length]

      # This part will extract the filepath so that we can find and
      # compare its contents to the filename we found previously. This
      # allows us to find the filepath (if it can be found inside the
      # prefetch file) used to execute the program
      # referenced in the prefetch-file.
      unless filepath_data.blank?
        fpath_data_array = filepath_data.split("\\\x00D\x00E\x00V\x00I\x00C\x00E")
        fpath_data_array.each do |path|
          next if path.blank?

          fpath_name = path.split('\\').last.gsub(/\0/, '')
          if fpath_name == name
            filepath << path
          end
        end
      end
    end
    if filepath.blank?
      filepath << '*** Filepath not found ***'
    end

    return [last_exec, path_hash, run_count, name, filepath[0]]
  end

  def run
    print_status('Prefetch Gathering started.')

    # Check to see what Windows Version is running.
    # Needed for offsets.
    # Tested on WinXP, Win2k3 and Win7 systems.
    # http://www.forensicswiki.org/wiki/Prefetch
    # http://www.forensicswiki.org/wiki/Windows_Prefetch_File_Format

    error_msg = "You don't have enough privileges. Try getsystem."

    version = get_version_info
    if version.xp_or_2003?

      if !is_admin?
        print_error(error_msg)
        return nil
      end

      # Offsets for WinXP & Win2k3
      print_good("Detected #{version.product_name} (max 128 entries)")
      name_offset = 0x10
      hash_offset = 0x4C
      runcount_offset = 0x90
      filetime_offset = 0x78
      # Registry key for timezone
      key_value = 'StandardName'

    elsif version.win7_or_2008r2? && !version.windows_server?
      if !is_admin?
        print_error(error_msg)
        return nil
      end

      # Offsets for Win7
      print_good("Detected #{version.product_name} (max 128 entries)")
      name_offset = 0x10
      hash_offset = 0x4C
      runcount_offset = 0x98
      filetime_offset = 0x78
      # Registry key for timezone
      key_value = 'TimeZoneKeyName'
    else
      print_error('No offsets for the target Windows version. Currently works only on WinXP, Win2k3 and Win7.')
      return nil
    end

    table = Rex::Text::Table.new(
      'Header' => 'Prefetch Information',
      'Indent' => 1,
      'Columns' =>
      [
        'Last execution (filetime)',
        'Run Count',
        'Hash',
        'Filename',
        'Filepath'
      ]
    )

    print_prefetch_key_value
    print_timezone_key_values(key_value)
    print_good('Current UTC Time: %s' % Time.now.utc)
    sys_root = session.sys.config.getenv('SYSTEMROOT')
    full_path = sys_root + '\\Prefetch\\'
    file_type = '*.pf'
    print_status('Gathering information from remote system. This will take awhile..')

    # Goes through the files in Prefetch directory, creates file paths for the
    # gather_pf_info function that enumerates all the pf info

    getfile_prefetch_filenames = client.fs.file.search(full_path, file_type)
    if getfile_prefetch_filenames.empty? || getfile_prefetch_filenames.nil?
      print_error("Could not find/access any .pf files. Can't continue. (Might be temporary error..)")
      return nil
    else
      getfile_prefetch_filenames.each do |file|
        if file.empty? || file.nil?
          next
        else
          filename = ::File.join(file['path'], file['name'])
          pf_entry = gather_pf_info(name_offset, hash_offset, runcount_offset, filetime_offset, filename)
          if !pf_entry.nil?
            table << pf_entry
          end
        end
      end
    end

    # Stores and prints out results
    results = table.to_s
    loot = store_loot('prefetch_info', 'text/plain', session, results, nil, 'Prefetch Information')
    print_line("\n" + results + "\n")
    print_status('Finished gathering information from prefetch files.')
    print_status("Results stored in: #{loot}")
  end
end